Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
52bfce4
feat(telemetry): propagate W3C traceparent on outbound LLM requests
doudouOUC May 21, 2026
0607c57
fix(telemetry): harden OTLP feedback-loop guard + slim lockfile diff
doudouOUC May 21, 2026
e1fd6b4
feat(telemetry): propagate X-Qwen-Code-Session-Id on outbound LLM req…
doudouOUC May 21, 2026
a1a8a5a
fix(telemetry): R2 review fixes — critical correctness + tsc + bounda…
doudouOUC May 21, 2026
cb34526
chore(deps): allow patch updates for @opentelemetry/instrumentation-u…
doudouOUC May 21, 2026
d91eb9a
test(telemetry): stub getTelemetryEnabled + getSessionId in Gemini fa…
doudouOUC May 22, 2026
e1cd295
fix(telemetry): R3 review fixes — port + protocol + quote + safety
doudouOUC May 22, 2026
dacea05
docs(telemetry): fix misleading "BOTH" wording in wrapFetchWithCorrel…
doudouOUC May 22, 2026
fc6c13a
fix(telemetry): strip port from req.host fallback + document undici s…
doudouOUC May 22, 2026
1c8528a
feat(telemetry): scope X-Qwen-Code-Session-Id to first-party hosts by…
doudouOUC May 22, 2026
cb162e7
fix(telemetry): R5 review fixups — Vertex destination + ["*"] trim + …
doudouOUC May 22, 2026
40e1efc
chore: regenerate settings.schema.json for sessionIdHeaderHosts
doudouOUC May 22, 2026
106598c
docs(design): update telemetry-outbound-propagation design for R3 hos…
doudouOUC May 22, 2026
7a1b4f8
fix(telemetry): defensive allowlist normalization + positive proxy test
doudouOUC May 24, 2026
9bdd3bd
refactor(telemetry): split outbound correlation out of telemetry scop…
doudouOUC May 25, 2026
0be0df2
docs(telemetry): disclose telemetry.enabled dependency on propagateTr…
doudouOUC May 25, 2026
c0352fd
test(config): cover getOutboundCorrelationPropagateTraceContext defaults
doudouOUC May 25, 2026
62cf6b4
docs(design): reflect post-R4 polish commits in §12
doudouOUC May 25, 2026
6518e6d
refactor: simplify post-R4 polish per /simplify review
doudouOUC May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
878 changes: 878 additions & 0 deletions docs/design/telemetry-outbound-propagation-design.md

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions docs/developers/development/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,99 @@ and logs still carry `session.id`, and trace / log backends (Jaeger, Tempo,
Loki, Aliyun SLS / ARMS Tracing) handle per-session slicing natively without
cardinality pressure.

### Client-side HTTP span on outbound fetch

When telemetry is enabled, Qwen Code registers `UndiciInstrumentation`
which creates a client-side HTTP span for every outbound `fetch()`
request originated by the process — including the LLM SDKs (`openai`,
`@google/genai`, `@anthropic-ai/sdk`), the MCP StreamableHTTP client, the
`WebFetch` tool, and any IDE-extension out-of-process calls. The span
lets you see network latency (TTFB / response body transfer) separately
from upstream model processing time, which the existing
`api.generateContent` span alone can't distinguish.

These spans go to your **own** OTLP collector (or file outfile) just like
the rest of the telemetry — they do not affect what is written onto the
outbound HTTP request itself. Whether the W3C `traceparent` header is
also written into the outgoing request stream is controlled by a
**separate, security-relevant setting** documented in
[outbound correlation](#outbound-correlation-security-relevant) below.

**Feedback-loop avoidance.** OTel SDK uses `fetch` internally to upload OTLP
data. Without protection, instrumenting `fetch` would trace those uploads,
which would themselves be uploaded, causing an infinite loop. Qwen Code's
undici instrumentation is configured with an `ignoreRequestHook` that skips
URLs matching the configured `telemetry.otlpEndpoint` /
`telemetry.otlpTracesEndpoint` / `telemetry.otlpLogsEndpoint` /
`telemetry.otlpMetricsEndpoint` prefixes. In file-outfile mode there are no
outbound HTTP uploads, so the hook is a no-op.

## Outbound correlation (SECURITY-RELEVANT)

These settings live in a **separate top-level namespace** from `telemetry.*`
on purpose: telemetry controls data flow into the operator's own
observability backend, while `outboundCorrelation.*` controls what
client-side correlation data qwen-code writes **into outbound LLM API
request streams** that reach third-party LLM provider endpoints
(DashScope, OpenAI, Anthropic, etc.). Different recipients, different
consent decision. **All values default to off.** See PR #4390 review
discussion for the framing rationale.

### `outboundCorrelation.propagateTraceContext`

```jsonc
"outboundCorrelation": {
"propagateTraceContext": false // default
}
```

When `false` (default), Qwen Code installs a no-op `TextMapPropagator` on
the OTel SDK. UndiciInstrumentation still creates client HTTP spans for
your OTLP collector, but `propagation.inject()` is a no-op so **no
`traceparent` is written onto outbound requests**. Trace IDs stay
internal to the operator's collector.

When `true`, the SDK's default W3C composite propagator
(`tracecontext` + `baggage`) is installed and the standard `traceparent`
header is written on every outbound `fetch`:

```
traceparent: 00-<32-hex traceId>-<16-hex parentSpanId>-<01-sampled | 00-not-sampled>
```

Opt in only when the LLM provider also reports into your OTel collector
for cross-process trace stitching — e.g. ARMS Tracing serving DashScope.
For most operators the value is `false`; cross-vendor trace continuation
is niche.

**Depends on `telemetry.enabled: true`.** The OTel SDK only initializes
when telemetry is enabled, so `propagateTraceContext` only takes effect
in that state. Setting it to `true` while telemetry is disabled is a
silent no-op — no SDK, no propagator, no `traceparent` on the wire.
Verify both flags when wiring an ARMS+DashScope correlation setup:

```jsonc
{
"telemetry": {
"enabled": true,
"otlpTracesEndpoint": "http://tracing-analysis-...",
},
"outboundCorrelation": {
"propagateTraceContext": true,
},
}
```

### Other outbound correlation headers

`X-Qwen-Code-Session-Id` and `X-Qwen-Code-Request-Id` are **not part of
this PR**. They will be designed and proposed in their own follow-up
PR(s) under the same `outboundCorrelation.*` namespace, each with its
own threat model and operator-consent flow. PR #4390 review (LaZzyMan)
established the principle: "telemetry's scope of work doesn't include
sending identifiers to LLM providers"; correlation-header work moves to
its own design discussion rather than landing under telemetry.

## Aliyun Telemetry

### Manual OTLP Export
Expand Down
23 changes: 20 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,7 @@ export async function loadCliConfig(
screenReader,
},
telemetry: telemetrySettings,
outboundCorrelation: settings.outboundCorrelation,
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true,
clearContextOnIdle: settings.context?.clearContextOnIdle,
fileFiltering: settings.context?.fileFiltering,
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
MCPServerConfig,
BugCommandSettings,
TelemetrySettings,
OutboundCorrelationSettings,
AuthType,
ChatCompressionSettings,
ModelProvidersConfig,
Expand Down Expand Up @@ -1037,6 +1038,29 @@ const SETTINGS_SCHEMA = {
},
},

outboundCorrelation: {
type: 'object',
label: 'Outbound Correlation',
category: 'Advanced',
requiresRestart: true,
default: undefined as OutboundCorrelationSettings | undefined,
description:
"SECURITY-RELEVANT. Controls what client-side correlation data qwen-code writes into outbound LLM API requests (DashScope, OpenAI, Anthropic, etc.) — separate from `telemetry.*` which governs data flow into the operator's OWN OTLP collector. All values default to off. Opt in only when the LLM provider also reports into your OTel collector for cross-process trace stitching (e.g. ARMS Tracing + DashScope).",
showInDialog: false,
jsonSchemaOverride: {
type: 'object',
properties: {
propagateTraceContext: {
description:
"Requires `telemetry.enabled: true`. Inject W3C `traceparent` header on outbound `fetch` requests (LLM SDK calls, MCP StreamableHTTP, WebFetch, ...). Default: false — trace context stays internal to the operator's OTLP collector and is NOT written onto third-party request streams. Set true only when you want cross-process trace stitching with an OTel-aware LLM provider (e.g. ARMS+DashScope). Client HTTP spans are still emitted in either case; this flag only governs the wire `traceparent` header.",
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
},

fastModel: {
type: 'string',
label: 'Fast Model',
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
"@opentelemetry/instrumentation-http": "^0.203.0",
"@opentelemetry/instrumentation-undici": "^0.14.0",
"@opentelemetry/sdk-node": "^0.203.0",
"@types/html-to-text": "^9.0.4",
"@xterm/headless": "5.5.0",
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,37 @@ describe('Server Config (config.ts)', () => {
});
});

describe('OutboundCorrelation Configuration', () => {
// Default-to-false is security-relevant — controls whether
// `traceparent` is written onto outbound LLM/fetch request streams.
it.each<{
label: string;
outboundCorrelation: ConfigParameters['outboundCorrelation'];
expected: boolean;
}>([
{ label: 'omitted', outboundCorrelation: undefined, expected: false },
{ label: 'empty object', outboundCorrelation: {}, expected: false },
{
label: 'explicit true',
outboundCorrelation: { propagateTraceContext: true },
expected: true,
},
{
label: 'explicit false',
outboundCorrelation: { propagateTraceContext: false },
expected: false,
},
])(
'propagateTraceContext resolves to $expected when $label',
({ outboundCorrelation, expected }) => {
const config = new Config({ ...baseParams, outboundCorrelation });
expect(config.getOutboundCorrelationPropagateTraceContext()).toBe(
expected,
);
},
);
});

describe('UseRipgrep Configuration', () => {
it('should default useRipgrep to true when not provided', () => {
const config = new Config(baseParams);
Expand Down
51 changes: 51 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,42 @@ export interface TelemetryMetricsSettings {
includeSessionId?: boolean;
}

/**
* Security-relevant settings controlling what client-side correlation
* data qwen-code writes into outbound LLM API requests.
*
* **Why this is a separate namespace from `telemetry.*`:** telemetry
* controls data flow into the user's OWN observability backend (OTLP
* collector / file outfile). The settings here control data flow OUT of
* the qwen-code process and INTO third-party LLM provider request
* streams (DashScope, OpenAI, Anthropic, etc.). Different recipients =
* different consent decision, so a different settings tree. See PR
* #4390 review (LaZzyMan) for the framing rationale.
*
* All values default to off / no propagation. Operators who want to
* propagate trace context for server-side trace stitching (e.g. ARMS
* Tracing + DashScope) opt in explicitly.
*/
export interface OutboundCorrelationSettings {
/**
* Inject W3C `traceparent` header on outbound HTTP requests
* originated by undici / global `fetch` (LLM SDK calls, MCP
* StreamableHTTP clients, WebFetch tool, etc.). Default: `false`.
*
* When `false`, the SDK is configured with a no-op
* `TextMapPropagator` so trace context stays internal to the user's
* OTLP collector (operator still gets client HTTP spans, but the
* trace id is not written onto third-party request streams).
*
* When `true`, the OTel default W3C composite propagator
* (`tracecontext` + `baggage`) is installed and `traceparent` is
* written on every outbound `fetch`. Useful when the LLM provider
* also reports into the operator's OTel collector — e.g. ARMS
* Tracing + DashScope — for cross-process trace stitching.
*/
propagateTraceContext?: boolean;
}

export interface OutputSettings {
format?: OutputFormat;
}
Expand Down Expand Up @@ -565,6 +601,7 @@ export interface ConfigParameters {
contextFileName?: string | string[];
accessibility?: AccessibilitySettings;
telemetry?: TelemetrySettings;
outboundCorrelation?: OutboundCorrelationSettings;
gitCoAuthor?: GitCoAuthorParam;
usageStatisticsEnabled?: boolean;
/**
Expand Down Expand Up @@ -854,6 +891,7 @@ export class Config {
private autoModeDenialState: AutoModeDenialState = createDenialState();
private readonly accessibility: AccessibilitySettings;
private readonly telemetrySettings: TelemetrySettings;
private readonly outboundCorrelationSettings: OutboundCorrelationSettings;
private readonly gitCoAuthor: GitCoAuthorSettings;
private readonly usageStatisticsEnabled: boolean;
private readonly fileReadCacheDisabled: boolean;
Expand Down Expand Up @@ -1022,6 +1060,10 @@ export class Config {
metrics: params.telemetry?.metrics,
resourceAttributeWarnings: params.telemetry?.resourceAttributeWarnings,
};
this.outboundCorrelationSettings = {
propagateTraceContext:
params.outboundCorrelation?.propagateTraceContext ?? false,
};
this.gitCoAuthor = {
...normalizeGitCoAuthor(params.gitCoAuthor),
name: 'Qwen-Coder',
Expand Down Expand Up @@ -2853,6 +2895,15 @@ export class Config {
return this.telemetrySettings.resourceAttributeWarnings ?? [];
}

/**
* Whether to inject W3C `traceparent` on outbound `fetch` requests
* (LLM SDKs, MCP, WebFetch, etc.). Default false — see
* `OutboundCorrelationSettings` for rationale.
*/
getOutboundCorrelationPropagateTraceContext(): boolean {
Comment thread
doudouOUC marked this conversation as resolved.
return this.outboundCorrelationSettings.propagateTraceContext ?? false;
}

getTelemetryOutfile(): string | undefined {
return this.telemetrySettings.outfile;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ describe('AnthropicContentGenerator', () => {
mockConfig = {
getCliVersion: vi.fn().mockReturnValue('1.2.3'),
getProxy: vi.fn().mockReturnValue(undefined),
getTelemetryEnabled: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session'),
} as unknown as Config;
});

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/core/contentGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ describe('createContentGenerator', () => {
getUsageStatisticsEnabled: () => true,
getContentGeneratorConfig: () => ({}),
getCliVersion: () => '1.0.0',
getTelemetryEnabled: () => false,
getSessionId: () => 'test-session',
} as unknown as Config;

const mockGenerator = {
Expand Down Expand Up @@ -57,6 +59,8 @@ describe('createContentGenerator', () => {
getUsageStatisticsEnabled: () => false,
getContentGeneratorConfig: () => ({}),
getCliVersion: () => '1.0.0',
getTelemetryEnabled: () => false,
getSessionId: () => 'test-session',
} as unknown as Config;
const mockGenerator = {
models: {},
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/core/geminiContentGenerator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ describe('createGeminiContentGenerator', () => {
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
getCliVersion: vi.fn().mockReturnValue('1.0.0'),
getTelemetryEnabled: vi.fn().mockReturnValue(false),
getSessionId: vi.fn().mockReturnValue('test-session'),
} as unknown as Config;
});

Expand Down
Loading
Loading