From 495d0d5700628be764647a824ebf2500ac8912c1 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 23 Sep 2026 13:52:33 -0600 Subject: [PATCH 1/2] feat: attach W3C trace context (TRACEPARENT) to telemetry events When a parent process (e.g., Code Builder) spawns sf with the TRACEPARENT env var set, parse the W3C trace context and attach traceId, parentSpanId, and traceFlags to every telemetry event for that CLI execution. This enables trace correlation across the process boundary. @W-24279635@ --- src/hooks/telemetryPrerun.ts | 16 +++++++++- test/hooks/telemetryPrerun.test.ts | 47 +++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/hooks/telemetryPrerun.ts b/src/hooks/telemetryPrerun.ts index 754197e7..00958e7d 100644 --- a/src/hooks/telemetryPrerun.ts +++ b/src/hooks/telemetryPrerun.ts @@ -27,10 +27,23 @@ declare const global: TelemetryGlobal; type CommonData = { nodeVersion: string; plugin?: string; - + plugin_version?: string; command?: string; + traceId?: string; + parentSpanId?: string; + traceFlags?: string; }; + +const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i; + +export function parseTraceParent(raw: string | undefined): Pick { + if (!raw) return {}; + const match = TRACEPARENT_RE.exec(raw.trim()); + if (!match || match[1] === 'ff') return {}; + return { traceId: match[2].toLowerCase(), parentSpanId: match[3].toLowerCase(), traceFlags: match[4].toLowerCase() }; +} + /** * A hook that runs before every command that: * 1. Warns the user about command usage data collection the CLI does unless they have already acknowledged the warning. @@ -178,6 +191,7 @@ const hook: Hook.Prerun = async function (options): Promise { // eslint-disable-next-line camelcase plugin_version: pluginInfo.version, command: commandExecution.getCommandName(), + ...parseTraceParent(process.env.TRACEPARENT), }; } return commonData; diff --git a/test/hooks/telemetryPrerun.test.ts b/test/hooks/telemetryPrerun.test.ts index a51a62e4..61eaab50 100644 --- a/test/hooks/telemetryPrerun.test.ts +++ b/test/hooks/telemetryPrerun.test.ts @@ -23,7 +23,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import Telemetry from '../../src/telemetry.js'; import { CommandExecution } from '../../src/commandExecution.js'; -import hook from '../../src/hooks/telemetryPrerun.js'; +import hook, { parseTraceParent } from '../../src/hooks/telemetryPrerun.js'; import { MyCommand } from '../helpers/myCommand.js'; const args = { argv: [], Command: MyCommand, config: {} as Config, context: {} as Hook.Context }; @@ -137,3 +137,48 @@ describe('telemetry prerun hook', () => { }); }); }); + +describe('parseTraceParent', () => { + it('returns trace fields for a valid traceparent', () => { + const result = parseTraceParent('00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01'); + expect(result).to.deep.equal({ + traceId: '0af7651916cd43dd8448eb211c80319c', + parentSpanId: 'b7ad6b7169203331', + traceFlags: '01', + }); + }); + + it('returns empty object for undefined', () => { + expect(parseTraceParent(undefined)).to.deep.equal({}); + }); + + it('returns empty object for empty string', () => { + expect(parseTraceParent('')).to.deep.equal({}); + }); + + it('returns empty object for invalid format', () => { + expect(parseTraceParent('not-valid')).to.deep.equal({}); + }); + + it('rejects version ff', () => { + expect(parseTraceParent('ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01')).to.deep.equal({}); + }); + + it('handles uppercase hex by normalizing to lowercase', () => { + const result = parseTraceParent('00-0AF7651916CD43DD8448EB211C80319C-B7AD6B7169203331-01'); + expect(result).to.deep.equal({ + traceId: '0af7651916cd43dd8448eb211c80319c', + parentSpanId: 'b7ad6b7169203331', + traceFlags: '01', + }); + }); + + it('trims whitespace', () => { + const result = parseTraceParent(' 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 '); + expect(result).to.deep.equal({ + traceId: '0af7651916cd43dd8448eb211c80319c', + parentSpanId: 'b7ad6b7169203331', + traceFlags: '01', + }); + }); +}); From f47128b6160006a2214a767bbabcecf9bf9f3ffa Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 23 Sep 2026 14:05:51 -0600 Subject: [PATCH 2/2] fix: harden W3C trace context validation per spec - Fix uppercase FF version bypass (case-insensitive regex vs case-sensitive check) - Reject all-zero trace-id and parent-id per W3C spec section 3.2.2.3 - Add tests for uppercase FF, all-zero trace-id, all-zero parent-id @W-24279635@ --- src/hooks/telemetryPrerun.ts | 7 +++++-- test/hooks/telemetryPrerun.test.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hooks/telemetryPrerun.ts b/src/hooks/telemetryPrerun.ts index 00958e7d..83ef70b5 100644 --- a/src/hooks/telemetryPrerun.ts +++ b/src/hooks/telemetryPrerun.ts @@ -40,8 +40,11 @@ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2 export function parseTraceParent(raw: string | undefined): Pick { if (!raw) return {}; const match = TRACEPARENT_RE.exec(raw.trim()); - if (!match || match[1] === 'ff') return {}; - return { traceId: match[2].toLowerCase(), parentSpanId: match[3].toLowerCase(), traceFlags: match[4].toLowerCase() }; + if (!match || match[1].toLowerCase() === 'ff') return {}; + const traceId = match[2].toLowerCase(); + const parentSpanId = match[3].toLowerCase(); + if (/^0+$/.test(traceId) || /^0+$/.test(parentSpanId)) return {}; + return { traceId, parentSpanId, traceFlags: match[4].toLowerCase() }; } /** diff --git a/test/hooks/telemetryPrerun.test.ts b/test/hooks/telemetryPrerun.test.ts index 61eaab50..8aace3cf 100644 --- a/test/hooks/telemetryPrerun.test.ts +++ b/test/hooks/telemetryPrerun.test.ts @@ -164,6 +164,18 @@ describe('parseTraceParent', () => { expect(parseTraceParent('ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01')).to.deep.equal({}); }); + it('rejects uppercase version FF', () => { + expect(parseTraceParent('FF-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01')).to.deep.equal({}); + }); + + it('rejects all-zero trace-id', () => { + expect(parseTraceParent('00-00000000000000000000000000000000-b7ad6b7169203331-01')).to.deep.equal({}); + }); + + it('rejects all-zero parent-id', () => { + expect(parseTraceParent('00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01')).to.deep.equal({}); + }); + it('handles uppercase hex by normalizing to lowercase', () => { const result = parseTraceParent('00-0AF7651916CD43DD8448EB211C80319C-B7AD6B7169203331-01'); expect(result).to.deep.equal({