From 22aec43aa596b78d883e4d0a98852cad2258bb2f Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 00:36:59 +0800 Subject: [PATCH 01/11] fix(telemetry): gate request_text/response_text on logPrompts When telemetry.logPrompts is false, skip serializing API request and response content into telemetry instead of emitting it to every sink. Also add request_text to the log-to-span bridge's sensitive-attribute denylist so traces-only telemetry cannot diverge from the native logger. Co-authored-by: Qwen-Coder Patrol-Run: qwen-issue-patrol/jmtx5dam9y8 --- .../loggingContentGenerator.test.ts | 74 +++++++++++++++++++ .../loggingContentGenerator.ts | 6 +- .../telemetry/log-to-span-processor.test.ts | 4 + .../src/telemetry/log-to-span-processor.ts | 1 + 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts index e21a20f65d8..a79b0464a25 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts @@ -327,6 +327,8 @@ const createConfig = (overrides: Record = {}): Config => { getWorkingDir: () => process.cwd(), getTelemetryIncludeSensitiveSpanAttributes: () => Boolean(configContent['includeSensitiveSpanAttributes']), + getTelemetryLogPromptsEnabled: () => + Boolean(configContent['logPrompts'] ?? true), getTelemetrySensitiveSpanAttributeMaxLength: () => (configContent['sensitiveSpanAttributeMaxLength'] as number) ?? 1024 * 1024, @@ -844,6 +846,78 @@ describe('LoggingContentGenerator', () => { expect(openaiError).toBeUndefined(); }); + it('omits request_text and response_text from API telemetry when logPrompts is false', async () => { + const wrapped = createWrappedGenerator( + vi.fn().mockResolvedValue( + createResponse('resp-noprompts', 'test-model', [ + { text: 'SENSITIVE_RESPONSE_MARKER' }, + ]), + ), + vi.fn(), + ); + const generator = new LoggingContentGenerator( + wrapped, + createConfig({ logPrompts: false }), + { + model: 'test-model', + authType: AuthType.USE_OPENAI, + }, + ); + + await generator.generateContent( + { + model: 'test-model', + contents: [ + { role: 'user', parts: [{ text: 'SENSITIVE_REQUEST_MARKER' }] }, + ], + } as unknown as GenerateContentParameters, + 'prompt-noprompts', + ); + + expect(logApiRequest).toHaveBeenCalledTimes(1); + const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; + expect(requestEvent.request_text).toBeUndefined(); + + expect(logApiResponse).toHaveBeenCalledTimes(1); + const [, responseEvent] = vi.mocked(logApiResponse).mock.calls[0]; + expect(responseEvent.response_text).toBeUndefined(); + }); + + it('keeps request_text and response_text in API telemetry when logPrompts is true', async () => { + const wrapped = createWrappedGenerator( + vi.fn().mockResolvedValue( + createResponse('resp-prompts', 'test-model', [ + { text: 'KEEP_RESPONSE_MARKER' }, + ]), + ), + vi.fn(), + ); + const generator = new LoggingContentGenerator( + wrapped, + createConfig({ logPrompts: true }), + { + model: 'test-model', + authType: AuthType.USE_OPENAI, + }, + ); + + await generator.generateContent( + { + model: 'test-model', + contents: [ + { role: 'user', parts: [{ text: 'KEEP_REQUEST_MARKER' }] }, + ], + } as unknown as GenerateContentParameters, + 'prompt-prompts', + ); + + const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; + expect(requestEvent.request_text).toContain('KEEP_REQUEST_MARKER'); + + const [, responseEvent] = vi.mocked(logApiResponse).mock.calls[0]; + expect(responseEvent.response_text).toBe('KEEP_RESPONSE_MARKER'); + }); + it('creates and closes the non-stream API span on success', async () => { const wrapped = createWrappedGenerator( vi diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts index f2366a51e2c..8870b143f9f 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts @@ -235,7 +235,9 @@ export class LoggingContentGenerator implements ContentGenerator { promptId: string, sessionId: string, ): void { - const requestText = JSON.stringify(contents); + const requestText = this.config.getTelemetryLogPromptsEnabled() + ? JSON.stringify(contents) + : undefined; logApiRequest( this.config, new ApiRequestEvent( @@ -268,7 +270,7 @@ export class LoggingContentGenerator implements ContentGenerator { prompt_id, this.generatorAuthType, usageMetadata, - responseText, + this.config.getTelemetryLogPromptsEnabled() ? responseText : undefined, subagentNameContext.getStore(), ttftMs, ), diff --git a/packages/core/src/telemetry/log-to-span-processor.test.ts b/packages/core/src/telemetry/log-to-span-processor.test.ts index ea78629fa8b..7ee82be9d1e 100644 --- a/packages/core/src/telemetry/log-to-span-processor.test.ts +++ b/packages/core/src/telemetry/log-to-span-processor.test.ts @@ -173,6 +173,7 @@ describe('LogToSpanProcessor', () => { error_message: 'secret upstream error', prompt: 'secret prompt', function_args: '{"token":"secret"}', + request_text: 'secret request', response_text: 'secret response', error_type: 'RateLimitError', safe: 'visible', @@ -188,6 +189,7 @@ describe('LogToSpanProcessor', () => { expect(attrs).not.toHaveProperty('error_message'); expect(attrs).not.toHaveProperty('prompt'); expect(attrs).not.toHaveProperty('function_args'); + expect(attrs).not.toHaveProperty('request_text'); expect(attrs).not.toHaveProperty('response_text'); expect(attrs['error_type']).toBe('RateLimitError'); expect(attrs['safe']).toBe('visible'); @@ -210,6 +212,7 @@ describe('LogToSpanProcessor', () => { error_message: 'secret upstream error', prompt: 'secret prompt', function_args: '{"token":"secret"}', + request_text: 'secret request', response_text: 'secret response', safe: 'visible', }, @@ -224,6 +227,7 @@ describe('LogToSpanProcessor', () => { expect(attrs['error_message']).toBe('secret upstream error'); expect(attrs['prompt']).toBe('secret prompt'); expect(attrs['function_args']).toBe('{"token":"secret"}'); + expect(attrs['request_text']).toBe('secret request'); expect(attrs['response_text']).toBe('secret response'); expect(attrs['safe']).toBe('visible'); expect(attrs['log.bridge']).toBe(true); diff --git a/packages/core/src/telemetry/log-to-span-processor.ts b/packages/core/src/telemetry/log-to-span-processor.ts index f263e69402b..490e8afc034 100644 --- a/packages/core/src/telemetry/log-to-span-processor.ts +++ b/packages/core/src/telemetry/log-to-span-processor.ts @@ -61,6 +61,7 @@ const SENSITIVE_ATTRIBUTE_KEYS = new Set([ 'error_message', 'prompt', 'function_args', + 'request_text', 'response_text', ]); From d04bb9e96545090f1aacd556a7f0f42b9e205b8a Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 03:46:54 +0800 Subject: [PATCH 02/11] fix(core): add streaming logPrompts coverage and format logging tests Prettier-formats the two new generateContent logPrompts tests so the Lint & Static gate passes, and adds two streaming counterparts driving generateContentStream so the request_text/response_text stripping invariant is pinned on the path that carries interactive traffic. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmtxcihx2yk --- .../loggingContentGenerator.test.ts | 104 +++++++++++++++--- 1 file changed, 91 insertions(+), 13 deletions(-) diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts index a79b0464a25..d619a3d1063 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts @@ -848,11 +848,13 @@ describe('LoggingContentGenerator', () => { it('omits request_text and response_text from API telemetry when logPrompts is false', async () => { const wrapped = createWrappedGenerator( - vi.fn().mockResolvedValue( - createResponse('resp-noprompts', 'test-model', [ - { text: 'SENSITIVE_RESPONSE_MARKER' }, - ]), - ), + vi + .fn() + .mockResolvedValue( + createResponse('resp-noprompts', 'test-model', [ + { text: 'SENSITIVE_RESPONSE_MARKER' }, + ]), + ), vi.fn(), ); const generator = new LoggingContentGenerator( @@ -885,11 +887,13 @@ describe('LoggingContentGenerator', () => { it('keeps request_text and response_text in API telemetry when logPrompts is true', async () => { const wrapped = createWrappedGenerator( - vi.fn().mockResolvedValue( - createResponse('resp-prompts', 'test-model', [ - { text: 'KEEP_RESPONSE_MARKER' }, - ]), - ), + vi + .fn() + .mockResolvedValue( + createResponse('resp-prompts', 'test-model', [ + { text: 'KEEP_RESPONSE_MARKER' }, + ]), + ), vi.fn(), ); const generator = new LoggingContentGenerator( @@ -904,9 +908,7 @@ describe('LoggingContentGenerator', () => { await generator.generateContent( { model: 'test-model', - contents: [ - { role: 'user', parts: [{ text: 'KEEP_REQUEST_MARKER' }] }, - ], + contents: [{ role: 'user', parts: [{ text: 'KEEP_REQUEST_MARKER' }] }], } as unknown as GenerateContentParameters, 'prompt-prompts', ); @@ -918,6 +920,82 @@ describe('LoggingContentGenerator', () => { expect(responseEvent.response_text).toBe('KEEP_RESPONSE_MARKER'); }); + it('omits request_text and response_text from API telemetry for streaming when logPrompts is false', async () => { + const streamFn = vi.fn().mockResolvedValue( + (async function* () { + yield createResponse('resp-stream-noprompts', 'test-model', [ + { text: 'SENSITIVE_RESPONSE_MARKER' }, + ]); + })(), + ); + const wrapped = createWrappedGenerator(vi.fn(), streamFn); + const generator = new LoggingContentGenerator( + wrapped, + createConfig({ logPrompts: false }), + { + model: 'test-model', + authType: AuthType.USE_OPENAI, + }, + ); + + const stream = await generator.generateContentStream( + { + model: 'test-model', + contents: [ + { role: 'user', parts: [{ text: 'SENSITIVE_REQUEST_MARKER' }] }, + ], + } as unknown as GenerateContentParameters, + 'prompt-stream-noprompts', + ); + for await (const _ of stream) { + // Drain the stream so response logging finalizes. + } + + expect(logApiRequest).toHaveBeenCalledTimes(1); + const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; + expect(requestEvent.request_text).toBeUndefined(); + + expect(logApiResponse).toHaveBeenCalledTimes(1); + const [, responseEvent] = vi.mocked(logApiResponse).mock.calls[0]; + expect(responseEvent.response_text).toBeUndefined(); + }); + + it('keeps request_text and response_text in API telemetry for streaming when logPrompts is true', async () => { + const streamFn = vi.fn().mockResolvedValue( + (async function* () { + yield createResponse('resp-stream-prompts', 'test-model', [ + { text: 'KEEP_RESPONSE_MARKER' }, + ]); + })(), + ); + const wrapped = createWrappedGenerator(vi.fn(), streamFn); + const generator = new LoggingContentGenerator( + wrapped, + createConfig({ logPrompts: true }), + { + model: 'test-model', + authType: AuthType.USE_OPENAI, + }, + ); + + const stream = await generator.generateContentStream( + { + model: 'test-model', + contents: [{ role: 'user', parts: [{ text: 'KEEP_REQUEST_MARKER' }] }], + } as unknown as GenerateContentParameters, + 'prompt-stream-prompts', + ); + for await (const _ of stream) { + // Drain the stream so response logging finalizes. + } + + const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; + expect(requestEvent.request_text).toContain('KEEP_REQUEST_MARKER'); + + const [, responseEvent] = vi.mocked(logApiResponse).mock.calls[0]; + expect(responseEvent.response_text).toContain('KEEP_RESPONSE_MARKER'); + }); + it('creates and closes the non-stream API span on success', async () => { const wrapped = createWrappedGenerator( vi From d04fb917cd80a6f1f9e4538e94105c7891b3eb6d Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 03:46:58 +0800 Subject: [PATCH 03/11] docs: enumerate request_text in sensitive span attribute docs Adding request_text to SENSITIVE_ATTRIBUTE_KEYS makes the retained set on log-to-span bridge spans larger than what two doc statements enumerate. Update both enumerations to four fields, carry the log_prompts_enabled condition into the api_request/api_response attribute reference, and widen the logPrompts descriptions to cover API request/response text. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmtxcihx2yk --- docs/developers/development/telemetry.md | 10 +++++----- docs/users/configuration/settings.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index cee947a0857..a205d58f198 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -76,7 +76,7 @@ These settings can be overridden by environment variables or CLI flags. | `otlpLogsEndpoint` | `QWEN_TELEMETRY_OTLP_LOGS_ENDPOINT` | - | Per-signal endpoint override for logs (HTTP only) | URL string | - | | `otlpMetricsEndpoint` | `QWEN_TELEMETRY_OTLP_METRICS_ENDPOINT` | - | Per-signal endpoint override for metrics (HTTP only) | URL string | - | | `outfile` | `QWEN_TELEMETRY_OUTFILE` | `--telemetry-outfile ` | Save telemetry to file (overrides OTLP export) | file path | - | -| `logPrompts` | `QWEN_TELEMETRY_LOG_PROMPTS` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include prompts in telemetry logs | `true`/`false` | `true` | +| `logPrompts` | `QWEN_TELEMETRY_LOG_PROMPTS` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include user prompt content and API request/response text in telemetry logs | `true`/`false` | `true` | | `userId` | `QWEN_TELEMETRY_USER_ID` | - | Stable end-user identifier written to GenAI spans as the ARMS extension `gen_ai.user.id`; prefer a pseudonymous value | string | - | | `includeSensitiveSpanAttributes` | `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | - | Include standard GenAI messages, instructions, tool definitions, tool arguments, and successful tool results as native span attributes | `true`/`false` | `false` | | `sensitiveSpanAttributeMaxLength` | `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | - | Maximum compact JSON string length for each sensitive native span attribute. Set lower if your backend rejects large attributes. | `1..104857600` | `1048576` | @@ -128,8 +128,8 @@ two things happen: therefore occupy more bytes after OTLP export. 2. **Log-to-span bridge spans** (used when HTTP traces are exported without a - logs endpoint) keep their existing `prompt`, `function_args`, and - `response_text` fields, instead of being dropped. + logs endpoint) keep their existing `prompt`, `function_args`, `request_text`, + and `response_text` fields, instead of being dropped. ⚠️ **Security warning:** enabling this flag streams full conversation history, file contents read by `read_file`, shell commands and their output (including @@ -642,10 +642,10 @@ The following events are logged: #### API Events - `qwen-code.api_request`: Outgoing request to the LLM API. - - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, optional), `subagent_name` (string, optional) + - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, excluded if `log_prompts_enabled` is false), `subagent_name` (string, optional) - `qwen-code.api_response`: Response received from LLM API. - - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional), `subagent_name` (string, optional) + - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, excluded if `log_prompts_enabled` is false), `subagent_name` (string, optional) - `qwen-code.api_error`: API request failed. - **Attributes**: `model` (string), `prompt_id` (string), `duration_ms` (int), `error_message` (string), `response_id` (string, optional), `auth_type` (string, optional), `error_type` (string, optional), `status_code` (int/string, optional), `subagent_name` (string, optional) diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 790569fe2cb..de74de09b43 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -661,7 +661,7 @@ Configures logging and metrics collection for Qwen Code. For more information, s | `telemetry.target` | string | Informational label for the telemetry destination (`local` or `gcp`). Does not control exporter routing; set `telemetry.otlpEndpoint` or `telemetry.outfile` to configure where data is sent. | | | `telemetry.otlpEndpoint` | string | The endpoint for the OTLP Exporter. | | | `telemetry.otlpProtocol` | string | The protocol for the OTLP Exporter (`grpc` or `http`). | | -| `telemetry.logPrompts` | boolean | Whether or not to include the content of user prompts in the logs. | | +| `telemetry.logPrompts` | boolean | Whether or not to include user prompt content and API request/response text in the logs. | | | `telemetry.userId` | string | Stable end-user identifier written to GenAI spans as the ARMS extension `gen_ai.user.id`. Prefer a pseudonymous value. Do not set a process-wide value for a shared multi-user daemon or channel instance. | | | `telemetry.includeSensitiveSpanAttributes` | boolean | When enabled, attaches verbatim user prompts, system prompts, tool inputs/outputs, and model responses to native OTel span attributes (in addition to log-to-span bridge spans). ⚠️ Streams sensitive data — file contents, shell commands, conversation history — to your OTLP backend. | `false` | | `telemetry.sensitiveSpanAttributeMaxLength` | number | Maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be between `1` and `104857600` (100 MiB). Set lower if your collector or backend rejects large attributes. | `1048576` | @@ -804,7 +804,7 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe | `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | | `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. | | `QWEN_TELEMETRY_USER_ID` | Sets a stable end-user identifier on interaction, LLM, Tool, and Agent spans as `gen_ai.user.id`. Prefer a pseudonymous value. | Overrides `telemetry.userId` after trimming. A blank value falls back to settings. This is process-wide and must not be used as per-request identity in a shared multi-user process. | -| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `prompt` / `function_args` / `response_text` on log-to-span bridge spans). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | +| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `prompt` / `function_args` / `request_text` / `response_text` on log-to-span bridge spans). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | | `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | Sets the maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be a positive integer no greater than `104857600` (100 MiB). | Overrides the `telemetry.sensitiveSpanAttributeMaxLength` setting. Default is `1048576` (1 MiB); lower it if your collector or backend rejects large span attributes. | | `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to. When set, overrides OTLP export. | Overrides the `telemetry.outfile` setting. | | `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | From 7f9f68b0115d8112fa531a864d5d534b6854a026 Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 06:38:54 +0800 Subject: [PATCH 04/11] docs(telemetry): correct request_text/response_text gating wording The prior "excluded if log_prompts_enabled is false" phrasing over-promised presence for response_text (it is also absent for internal prompt ids and for thought-only/tool-call-only turns) and misdescribed request_text (the key ships empty on native OTLP log export, and is only absent from telemetry.outfile and log-to-span bridge spans). Qualify the includeSensitiveSpanAttributes bridge-span list so prompt / request_text / response_text note they additionally require telemetry.logPrompts. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmtxixz7myu --- docs/developers/development/telemetry.md | 4 ++-- docs/users/configuration/settings.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index a205d58f198..cafce88decb 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -642,10 +642,10 @@ The following events are logged: #### API Events - `qwen-code.api_request`: Outgoing request to the LLM API. - - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, excluded if `log_prompts_enabled` is false), `subagent_name` (string, optional) + - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, present when `log_prompts_enabled` is true; when false the key is empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) - `qwen-code.api_response`: Response received from LLM API. - - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, excluded if `log_prompts_enabled` is false), `subagent_name` (string, optional) + - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — absent for internal prompt ids and for responses with no visible text such as tool-call-only or thought-only turns; when `log_prompts_enabled` is false it is empty rather than absent on native OTLP log export, and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) - `qwen-code.api_error`: API request failed. - **Attributes**: `model` (string), `prompt_id` (string), `duration_ms` (int), `error_message` (string), `response_id` (string, optional), `auth_type` (string, optional), `error_type` (string, optional), `status_code` (int/string, optional), `subagent_name` (string, optional) diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index de74de09b43..8c2ab4f7169 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -804,7 +804,7 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe | `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | | `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. | | `QWEN_TELEMETRY_USER_ID` | Sets a stable end-user identifier on interaction, LLM, Tool, and Agent spans as `gen_ai.user.id`. Prefer a pseudonymous value. | Overrides `telemetry.userId` after trimming. A blank value falls back to settings. This is process-wide and must not be used as per-request identity in a shared multi-user process. | -| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `prompt` / `function_args` / `request_text` / `response_text` on log-to-span bridge spans). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | +| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `function_args` on log-to-span bridge spans, plus `prompt` / `request_text` / `response_text` when `telemetry.logPrompts` is also enabled). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | | `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | Sets the maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be a positive integer no greater than `104857600` (100 MiB). | Overrides the `telemetry.sensitiveSpanAttributeMaxLength` setting. Default is `1048576` (1 MiB); lower it if your collector or backend rejects large span attributes. | | `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to. When set, overrides OTLP export. | Overrides the `telemetry.outfile` setting. | | `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | From 8fb0f8d91a3764ab2e746edaa746256b60a1f70c Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 09:42:05 +0800 Subject: [PATCH 05/11] docs+test(telemetry): resolve request_text/response_text sink-contract findings Address four review findings on the logPrompts gating of request_text/response_text: - R1-1/R2-1: rewrite the api_request/api_response attribute docs so the three suppression causes (internal prompt id, no visible text, logPrompts off) all collapse to "key present but empty on native OTLP log export, absent from telemetry.outfile and log-to-span bridge spans", and note that bridge spans additionally require includeSensitiveSpanAttributes for both keys. - R2-2: record that opaque thoughtSignature provider payload is exported verbatim with request_text, with the policy decision tracked in #11682. - R3-1: add loggers.test.ts assertions that request_text/response_text are present-but-undefined keys when logPrompts is off, and a FileLogExporter test that undefined-valued keys are dropped from the outfile JSON. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmtxpdgjtz5 --- docs/developers/development/telemetry.md | 4 +- .../core/src/telemetry/file-exporters.test.ts | 25 ++++++++++- packages/core/src/telemetry/loggers.test.ts | 41 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index cafce88decb..d1deb285d2c 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -642,10 +642,10 @@ The following events are logged: #### API Events - `qwen-code.api_request`: Outgoing request to the LLM API. - - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, present when `log_prompts_enabled` is true; when false the key is empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) + - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, optional — present on native OTLP log export and in `telemetry.outfile` when `log_prompts_enabled` is true; on log-to-span bridge spans it additionally requires `includeSensitiveSpanAttributes`; when `log_prompts_enabled` is false the key is present but empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans; opaque `thoughtSignature` provider payload is exported verbatim as part of `request_text`, with the policy decision tracked in #11682), `subagent_name` (string, optional) - `qwen-code.api_response`: Response received from LLM API. - - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — absent for internal prompt ids and for responses with no visible text such as tool-call-only or thought-only turns; when `log_prompts_enabled` is false it is empty rather than absent on native OTLP log export, and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) + - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — present on native OTLP log export and in `telemetry.outfile` when the response has visible text and `log_prompts_enabled` is true; on log-to-span bridge spans it additionally requires `includeSensitiveSpanAttributes`; it carries no value for internal prompt ids, for responses with no visible text such as tool-call-only or thought-only turns, and whenever `log_prompts_enabled` is false — in every one of those cases the key is present but empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) - `qwen-code.api_error`: API request failed. - **Attributes**: `model` (string), `prompt_id` (string), `duration_ms` (int), `error_message` (string), `response_id` (string, optional), `auth_type` (string, optional), `error_type` (string, optional), `status_code` (int/string, optional), `subagent_name` (string, optional) diff --git a/packages/core/src/telemetry/file-exporters.test.ts b/packages/core/src/telemetry/file-exporters.test.ts index 9cca4175fd3..2f04548a1ca 100644 --- a/packages/core/src/telemetry/file-exporters.test.ts +++ b/packages/core/src/telemetry/file-exporters.test.ts @@ -9,7 +9,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ExportResultCode } from '@opentelemetry/core'; -import { FileSpanExporter } from './file-exporters.js'; +import { FileLogExporter, FileSpanExporter } from './file-exporters.js'; type SerializeAccess = { serialize: (data: unknown) => string }; @@ -55,6 +55,29 @@ describe('FileExporter.serialize', () => { expect(out).toContain('"[Circular]"'); expect(out.endsWith('\n')).toBe(true); }); + + // Pins the outfile sink's key-absent contract: when logPrompts is off the + // producer emits request_text/response_text as undefined, and safeJsonStringify + // (JSON.stringify) drops undefined-valued keys instead of writing `null` or an + // empty string. FileLogExporter inherits FileExporter.serialize unchanged. + it('FileLogExporter omits undefined-valued attributes (logPrompts off)', () => { + const logExporter = new FileLogExporter(path.join(tmpDir, 'logs.jsonl')); + const logSerialize = ( + logExporter as unknown as SerializeAccess + ).serialize.bind(logExporter); + try { + const out = logSerialize({ + model: 'm', + request_text: undefined, + response_text: 'visible', + }); + + expect(out).not.toContain('request_text'); + expect(out).toContain('"response_text": "visible"'); + } finally { + void logExporter.shutdown(); + } + }); }); // Telemetry is best-effort and must never crash the agent. The outfile is often diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 5102eccbbf1..a284009004b 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -876,6 +876,27 @@ describe('loggers', () => { tokenUsageService.recordTokenUsageFromApiResponseBestEffort, ).not.toHaveBeenCalled(); }); + + it('emits response_text as a present-but-empty key when logPrompts is off', () => { + const configWithPromptsOff = { + ...mockConfig, + getTelemetryLogPromptsEnabled: () => false, + } as unknown as Config; + const event = new ApiResponseEvent( + 'test-response-id', + 'test-model', + 100, + 'prompt-id-1', + ); + + logApiResponse(configWithPromptsOff, event); + + const attributes = mockLogger.emit.mock.calls[0]![0].attributes; + expect( + Object.prototype.hasOwnProperty.call(attributes, 'response_text'), + ).toBe(true); + expect(attributes.response_text).toBeUndefined(); + }); }); describe('logApiResponse skips chatRecordingService for internal prompt IDs', () => { @@ -1100,6 +1121,26 @@ describe('loggers', () => { }); }); + it('emits request_text as a present-but-empty key when logPrompts is off', () => { + const configWithPromptsOff = { + ...mockConfig, + getTelemetryLogPromptsEnabled: () => false, + } as unknown as Config; + const event = new ApiRequestEvent( + 'test-model', + 'prompt-id-7', + undefined, + ); + + logApiRequest(configWithPromptsOff, event); + + const attributes = mockLogger.emit.mock.calls[0]![0].attributes; + expect( + Object.prototype.hasOwnProperty.call(attributes, 'request_text'), + ).toBe(true); + expect(attributes.request_text).toBeUndefined(); + }); + it('uses the request session snapshot when provided', () => { const event = new ApiRequestEvent('test-model', 'prompt-id'); From 3e2fd8e373482ae9ad0fbb994f2eaa22bf2946f7 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sat, 12 Sep 2026 11:56:11 +0800 Subject: [PATCH 06/11] style(core): format loggers.test.ts --- packages/core/src/telemetry/loggers.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index a284009004b..ca6cd39600f 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -1126,11 +1126,7 @@ describe('loggers', () => { ...mockConfig, getTelemetryLogPromptsEnabled: () => false, } as unknown as Config; - const event = new ApiRequestEvent( - 'test-model', - 'prompt-id-7', - undefined, - ); + const event = new ApiRequestEvent('test-model', 'prompt-id-7', undefined); logApiRequest(configWithPromptsOff, event); From b51e0af0b80a254f306ebbb6d8e142f548795c4e Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 12:44:50 +0800 Subject: [PATCH 07/11] test+docs(telemetry): pin logPrompts presence matrix and correct bridge gating docs - loggers.test.ts: drop the inert getTelemetryLogPromptsEnabled overrides; the two present-but-empty tests now name and pin the native OTLP log export leg (key present with an empty value) instead of a flag neither function reads. - file-exporters.test.ts: drive FileLogExporter.export() with a real ReadableLogRecord rather than poking the inherited private serialize. - telemetry.md: log-to-span bridge spans keep function_args under includeSensitiveSpanAttributes alone, plus prompt/request_text/response_text only when logPrompts is also enabled. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmtxvsxv1ze --- docs/developers/development/telemetry.md | 5 +-- .../core/src/telemetry/file-exporters.test.ts | 33 +++++++++++-------- packages/core/src/telemetry/loggers.test.ts | 18 +++------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index d1deb285d2c..8c9937b209c 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -128,8 +128,9 @@ two things happen: therefore occupy more bytes after OTLP export. 2. **Log-to-span bridge spans** (used when HTTP traces are exported without a - logs endpoint) keep their existing `prompt`, `function_args`, `request_text`, - and `response_text` fields, instead of being dropped. + logs endpoint) keep their existing `function_args` field, plus `prompt`, + `request_text`, and `response_text` when `logPrompts` is also enabled, + instead of being dropped. ⚠️ **Security warning:** enabling this flag streams full conversation history, file contents read by `read_file`, shell commands and their output (including diff --git a/packages/core/src/telemetry/file-exporters.test.ts b/packages/core/src/telemetry/file-exporters.test.ts index 2f04548a1ca..712f29494f0 100644 --- a/packages/core/src/telemetry/file-exporters.test.ts +++ b/packages/core/src/telemetry/file-exporters.test.ts @@ -9,6 +9,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ExportResultCode } from '@opentelemetry/core'; +import type { ReadableLogRecord } from '@opentelemetry/sdk-logs'; import { FileLogExporter, FileSpanExporter } from './file-exporters.js'; type SerializeAccess = { serialize: (data: unknown) => string }; @@ -60,23 +61,29 @@ describe('FileExporter.serialize', () => { // producer emits request_text/response_text as undefined, and safeJsonStringify // (JSON.stringify) drops undefined-valued keys instead of writing `null` or an // empty string. FileLogExporter inherits FileExporter.serialize unchanged. - it('FileLogExporter omits undefined-valued attributes (logPrompts off)', () => { - const logExporter = new FileLogExporter(path.join(tmpDir, 'logs.jsonl')); - const logSerialize = ( - logExporter as unknown as SerializeAccess - ).serialize.bind(logExporter); - try { - const out = logSerialize({ - model: 'm', + it('FileLogExporter omits undefined-valued attributes (logPrompts off)', async () => { + const outfile = path.join(tmpDir, 'logs.jsonl'); + const logExporter = new FileLogExporter(outfile); + const logRecord = { + body: 'api response', + hrTime: [1000, 0] as [number, number], + attributes: { request_text: undefined, response_text: 'visible', + }, + } as unknown as ReadableLogRecord; + + await new Promise((resolve) => { + logExporter.export([logRecord], (result) => { + expect(result.code).toBe(ExportResultCode.SUCCESS); + resolve(); }); + }); + await logExporter.shutdown(); - expect(out).not.toContain('request_text'); - expect(out).toContain('"response_text": "visible"'); - } finally { - void logExporter.shutdown(); - } + const out = fs.readFileSync(outfile, 'utf8'); + expect(out).not.toContain('request_text'); + expect(out).toContain('"response_text": "visible"'); }); }); diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index ca6cd39600f..03d8fdd2580 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -877,11 +877,7 @@ describe('loggers', () => { ).not.toHaveBeenCalled(); }); - it('emits response_text as a present-but-empty key when logPrompts is off', () => { - const configWithPromptsOff = { - ...mockConfig, - getTelemetryLogPromptsEnabled: () => false, - } as unknown as Config; + it('emits response_text as a present-but-empty key when the event has no response text', () => { const event = new ApiResponseEvent( 'test-response-id', 'test-model', @@ -889,7 +885,7 @@ describe('loggers', () => { 'prompt-id-1', ); - logApiResponse(configWithPromptsOff, event); + logApiResponse(mockConfig, event); const attributes = mockLogger.emit.mock.calls[0]![0].attributes; expect( @@ -1121,14 +1117,10 @@ describe('loggers', () => { }); }); - it('emits request_text as a present-but-empty key when logPrompts is off', () => { - const configWithPromptsOff = { - ...mockConfig, - getTelemetryLogPromptsEnabled: () => false, - } as unknown as Config; - const event = new ApiRequestEvent('test-model', 'prompt-id-7', undefined); + it('emits request_text as a present-but-empty key when the event has no request text', () => { + const event = new ApiRequestEvent('test-model', 'prompt-id-7'); - logApiRequest(configWithPromptsOff, event); + logApiRequest(mockConfig, event); const attributes = mockLogger.emit.mock.calls[0]![0].attributes; expect( From 76de1e239874cb1c1622eeb1df240c4a1e561c0e Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sat, 12 Sep 2026 13:01:24 +0800 Subject: [PATCH 08/11] test(telemetry): avoid pinning exporter empty-key internals --- docs/developers/development/telemetry.md | 4 +-- packages/core/src/telemetry/loggers.test.ts | 29 --------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index 8c9937b209c..2c797e2a4c5 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -643,10 +643,10 @@ The following events are logged: #### API Events - `qwen-code.api_request`: Outgoing request to the LLM API. - - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, optional — present on native OTLP log export and in `telemetry.outfile` when `log_prompts_enabled` is true; on log-to-span bridge spans it additionally requires `includeSensitiveSpanAttributes`; when `log_prompts_enabled` is false the key is present but empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans; opaque `thoughtSignature` provider payload is exported verbatim as part of `request_text`, with the policy decision tracked in #11682), `subagent_name` (string, optional) + - **Attributes**: `model` (string), `prompt_id` (string), `request_text` (string, optional — contains request content only when `log_prompts_enabled` is true; log-to-span bridge spans additionally require `includeSensitiveSpanAttributes`; opaque `thoughtSignature` provider payload is included, with its policy decision tracked in #11682), `subagent_name` (string, optional) - `qwen-code.api_response`: Response received from LLM API. - - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — present on native OTLP log export and in `telemetry.outfile` when the response has visible text and `log_prompts_enabled` is true; on log-to-span bridge spans it additionally requires `includeSensitiveSpanAttributes`; it carries no value for internal prompt ids, for responses with no visible text such as tool-call-only or thought-only turns, and whenever `log_prompts_enabled` is false — in every one of those cases the key is present but empty on native OTLP log export and absent from `telemetry.outfile` and log-to-span bridge spans), `subagent_name` (string, optional) + - **Attributes**: `response_id` (string), `model` (string), `status_code` (int/string, optional), `duration_ms` (int), `input_token_count` (int), `output_token_count` (int), `cached_content_token_count` (int), `thoughts_token_count` (int), `total_token_count` (int), `prompt_id` (string), `auth_type` (string, optional), `response_text` (string, optional — contains visible response content only when `log_prompts_enabled` is true; log-to-span bridge spans additionally require `includeSensitiveSpanAttributes`; it carries no content for internal prompt ids or responses with no visible text), `subagent_name` (string, optional) - `qwen-code.api_error`: API request failed. - **Attributes**: `model` (string), `prompt_id` (string), `duration_ms` (int), `error_message` (string), `response_id` (string, optional), `auth_type` (string, optional), `error_type` (string, optional), `status_code` (int/string, optional), `subagent_name` (string, optional) diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 03d8fdd2580..5102eccbbf1 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -876,23 +876,6 @@ describe('loggers', () => { tokenUsageService.recordTokenUsageFromApiResponseBestEffort, ).not.toHaveBeenCalled(); }); - - it('emits response_text as a present-but-empty key when the event has no response text', () => { - const event = new ApiResponseEvent( - 'test-response-id', - 'test-model', - 100, - 'prompt-id-1', - ); - - logApiResponse(mockConfig, event); - - const attributes = mockLogger.emit.mock.calls[0]![0].attributes; - expect( - Object.prototype.hasOwnProperty.call(attributes, 'response_text'), - ).toBe(true); - expect(attributes.response_text).toBeUndefined(); - }); }); describe('logApiResponse skips chatRecordingService for internal prompt IDs', () => { @@ -1117,18 +1100,6 @@ describe('loggers', () => { }); }); - it('emits request_text as a present-but-empty key when the event has no request text', () => { - const event = new ApiRequestEvent('test-model', 'prompt-id-7'); - - logApiRequest(mockConfig, event); - - const attributes = mockLogger.emit.mock.calls[0]![0].attributes; - expect( - Object.prototype.hasOwnProperty.call(attributes, 'request_text'), - ).toBe(true); - expect(attributes.request_text).toBeUndefined(); - }); - it('uses the request session snapshot when provided', () => { const event = new ApiRequestEvent('test-model', 'prompt-id'); From 4db3f55ae0e08cfbffe0b1266bbd70b3fdf69fb7 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sat, 12 Sep 2026 15:42:46 +0800 Subject: [PATCH 09/11] docs(telemetry): list error attributes retained by the bridge --- docs/developers/development/telemetry.md | 6 +++--- docs/users/configuration/settings.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developers/development/telemetry.md b/docs/developers/development/telemetry.md index 2c797e2a4c5..f145738131e 100644 --- a/docs/developers/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -128,9 +128,9 @@ two things happen: therefore occupy more bytes after OTLP export. 2. **Log-to-span bridge spans** (used when HTTP traces are exported without a - logs endpoint) keep their existing `function_args` field, plus `prompt`, - `request_text`, and `response_text` when `logPrompts` is also enabled, - instead of being dropped. + logs endpoint) retain `function_args`, `error`, `error.message`, and + `error_message`, plus `prompt`, `request_text`, and `response_text` when + `logPrompts` is also enabled, instead of dropping these attributes. ⚠️ **Security warning:** enabling this flag streams full conversation history, file contents read by `read_file`, shell commands and their output (including diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 8c2ab4f7169..fb0c5e271ad 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -804,7 +804,7 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe | `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | | `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. | | `QWEN_TELEMETRY_USER_ID` | Sets a stable end-user identifier on interaction, LLM, Tool, and Agent spans as `gen_ai.user.id`. Prefer a pseudonymous value. | Overrides `telemetry.userId` after trimming. A blank value falls back to settings. This is process-wide and must not be used as per-request identity in a shared multi-user process. | -| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `function_args` on log-to-span bridge spans, plus `prompt` / `request_text` / `response_text` when `telemetry.logPrompts` is also enabled). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | +| `QWEN_TELEMETRY_INCLUDE_SENSITIVE_SPAN_ATTRIBUTES` | Set to `true` or `1` to attach verbatim user prompts, system prompts, tool I/O, and model responses to native OTel span attributes (and keep `function_args`, `error`, `error.message`, and `error_message` on log-to-span bridge spans, plus `prompt` / `request_text` / `response_text` when `telemetry.logPrompts` is also enabled). Any other value disables it. | Overrides the `telemetry.includeSensitiveSpanAttributes` setting. ⚠️ Streams sensitive data to your OTLP backend. | | `QWEN_TELEMETRY_SENSITIVE_SPAN_ATTRIBUTE_MAX_LENGTH` | Sets the maximum JavaScript string length for each sensitive native OTel span attribute content payload. Must be a positive integer no greater than `104857600` (100 MiB). | Overrides the `telemetry.sensitiveSpanAttributeMaxLength` setting. Default is `1048576` (1 MiB); lower it if your collector or backend rejects large span attributes. | | `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to. When set, overrides OTLP export. | Overrides the `telemetry.outfile` setting. | | `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | From ac00d3ee46400af2c47d85123ab64b0c53531883 Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 15:53:31 +0800 Subject: [PATCH 10/11] test(logging): pin opaque thoughtSignature survival in request_text Add a regression test asserting the thoughtSignature provider payload survives toContents serialization into request_text. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmty28f6zzp --- .../loggingContentGenerator.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts index d619a3d1063..9c64078f09c 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts @@ -920,6 +920,39 @@ describe('LoggingContentGenerator', () => { expect(responseEvent.response_text).toBe('KEEP_RESPONSE_MARKER'); }); + it('preserves opaque thoughtSignature provider payload in request_text', async () => { + const wrapped = createWrappedGenerator( + vi + .fn() + .mockResolvedValue( + createResponse('resp-sig', 'test-model', [ + { text: 'RESPONSE_MARKER' }, + ]), + ), + vi.fn(), + ); + const generator = new LoggingContentGenerator(wrapped, createConfig(), { + model: 'test-model', + authType: AuthType.USE_OPENAI, + }); + + await generator.generateContent( + { + model: 'test-model', + contents: [ + { + role: 'user', + parts: [{ thought: true, thoughtSignature: 'OPAQUE_SIG_MARKER' }], + }, + ], + } as unknown as GenerateContentParameters, + 'prompt-thought-signature', + ); + + const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; + expect(requestEvent.request_text).toContain('OPAQUE_SIG_MARKER'); + }); + it('omits request_text and response_text from API telemetry for streaming when logPrompts is false', async () => { const streamFn = vi.fn().mockResolvedValue( (async function* () { From 34c8eb1cc2373550d5681e9feb48f8b0dec8230f Mon Sep 17 00:00:00 2001 From: "jinjing.zzj" Date: Sat, 12 Sep 2026 15:58:45 +0800 Subject: [PATCH 11/11] Revert "test(logging): pin opaque thoughtSignature survival in request_text" R5-1 is declined and deferred to #11682, which owns the enabled-policy thoughtSignature decision. This PR only changes the logPrompts=false privacy gate; a test that freezes the opposite policy widens scope. Co-authored-by: Qwen-Coder Patrol-Run: qwen-pr-closeout/jmty28f6zzp --- .../loggingContentGenerator.test.ts | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts index 9c64078f09c..d619a3d1063 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts @@ -920,39 +920,6 @@ describe('LoggingContentGenerator', () => { expect(responseEvent.response_text).toBe('KEEP_RESPONSE_MARKER'); }); - it('preserves opaque thoughtSignature provider payload in request_text', async () => { - const wrapped = createWrappedGenerator( - vi - .fn() - .mockResolvedValue( - createResponse('resp-sig', 'test-model', [ - { text: 'RESPONSE_MARKER' }, - ]), - ), - vi.fn(), - ); - const generator = new LoggingContentGenerator(wrapped, createConfig(), { - model: 'test-model', - authType: AuthType.USE_OPENAI, - }); - - await generator.generateContent( - { - model: 'test-model', - contents: [ - { - role: 'user', - parts: [{ thought: true, thoughtSignature: 'OPAQUE_SIG_MARKER' }], - }, - ], - } as unknown as GenerateContentParameters, - 'prompt-thought-signature', - ); - - const [, requestEvent] = vi.mocked(logApiRequest).mock.calls[0]; - expect(requestEvent.request_text).toContain('OPAQUE_SIG_MARKER'); - }); - it('omits request_text and response_text from API telemetry for streaming when logPrompts is false', async () => { const streamFn = vi.fn().mockResolvedValue( (async function* () {