diff --git a/src/hooks/telemetryPrerun.ts b/src/hooks/telemetryPrerun.ts index 754197e7..83ef70b5 100644 --- a/src/hooks/telemetryPrerun.ts +++ b/src/hooks/telemetryPrerun.ts @@ -27,10 +27,26 @@ 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].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() }; +} + /** * 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 +194,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..8aace3cf 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,60 @@ 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('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({ + 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', + }); + }); +});