From 48b6da98b7b628ed9fd9065c16d16394149755ad Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 2 Oct 2025 11:38:26 +0200 Subject: [PATCH 1/3] keep property values --- .../baggage-property-values/server.ts | 37 +++++++++++++++++++ .../baggage-property-values/test.ts | 28 ++++++++++++++ packages/core/src/utils/baggage.ts | 15 ++++++-- packages/core/test/lib/utils/baggage.test.ts | 12 ++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts new file mode 100644 index 000000000000..da278ce61688 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import cors from 'cors'; +import express from 'express'; +import http from 'http'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express-property-values', (req, res) => { + const incomingBaggage = req.headers.baggage; + + // Forward the incoming baggage (which contains property values) to the outgoing request + // This tests that property values with = signs are preserved during parsing and re-serialization + const headers = http.get({ hostname: 'somewhere.not.sentry', headers: { baggage: incomingBaggage } }).getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts new file mode 100644 index 000000000000..23848d36a3df --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts @@ -0,0 +1,28 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from './server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should preserve baggage property values with equal signs (W3C spec compliance)', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + // W3C spec example: https://www.w3.org/TR/baggage/#example + const response = await runner.makeRequest('get', '/test/express-property-values', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue', + }, + }); + + expect(response).toBeDefined(); + + // The baggage should be parsed and re-serialized, preserving property values with = signs + const baggageItems = response?.test_data.baggage?.split(',').map(item => decodeURIComponent(item.trim())); + + expect(baggageItems).toContain('key1=value1;property1;property2'); + expect(baggageItems).toContain('key2=value2'); + expect(baggageItems).toContain('key3=value3; propertyKey=propertyValue'); +}); diff --git a/packages/core/src/utils/baggage.ts b/packages/core/src/utils/baggage.ts index b483207ba8f2..e94bb3d896e6 100644 --- a/packages/core/src/utils/baggage.ts +++ b/packages/core/src/utils/baggage.ts @@ -113,8 +113,15 @@ export function parseBaggageHeader( function baggageHeaderToObject(baggageHeader: string): Record { return baggageHeader .split(',') - .map(baggageEntry => - baggageEntry.split('=').map(keyOrValue => { + .map(baggageEntry => { + const eqIdx = baggageEntry.indexOf('='); + if (eqIdx === -1) { + // Likely an invalid entry + return []; + } + const key = baggageEntry.slice(0, eqIdx); + const value = baggageEntry.slice(eqIdx + 1); + return [key, value].map(keyOrValue => { try { return decodeURIComponent(keyOrValue.trim()); } catch { @@ -122,8 +129,8 @@ function baggageHeaderToObject(baggageHeader: string): Record { // This will then be skipped in the next step return; } - }), - ) + }); + }) .reduce>((acc, [key, value]) => { if (key && value) { acc[key] = value; diff --git a/packages/core/test/lib/utils/baggage.test.ts b/packages/core/test/lib/utils/baggage.test.ts index 4816a3fbf079..f3717a524bf8 100644 --- a/packages/core/test/lib/utils/baggage.test.ts +++ b/packages/core/test/lib/utils/baggage.test.ts @@ -71,4 +71,16 @@ describe('parseBaggageHeader', () => { const actual = parseBaggageHeader(input); expect(actual).toStrictEqual(expectedOutput); }); + + test('should preserve property values with equal signs', () => { + // see https://www.w3.org/TR/baggage/#example + const baggageHeader = 'key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue'; + const result = parseBaggageHeader(baggageHeader); + + expect(result).toStrictEqual({ + key1: 'value1;property1;property2', + key2: 'value2', + key3: 'value3; propertyKey=propertyValue', + }); + }); }); From 306687d337abff771e4485291ae62d108047f075 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 2 Oct 2025 11:58:59 +0200 Subject: [PATCH 2/3] add browser integration tests --- .../fetch-baggage-property-values/subject.js | 9 ++++ .../template.html | 11 +++++ .../fetch-baggage-property-values/test.ts | 41 +++++++++++++++++++ .../subject.js | 8 ++++ .../xhr-baggage-property-values copy/test.ts | 36 ++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js new file mode 100644 index 000000000000..76fafc9df148 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js @@ -0,0 +1,9 @@ +fetchButton.addEventListener('click', () => { + // W3C spec example: property values can contain = signs + // See: https://www.w3.org/TR/baggage/#example + fetch('http://sentry-test-site.example/fetch-test', { + headers: { + baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue', + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html new file mode 100644 index 000000000000..404eee952355 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html @@ -0,0 +1,11 @@ + + + + + + Fetch Baggage Property Values Test + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts new file mode 100644 index 000000000000..c191304ec8e0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import { TRACEPARENT_REGEXP } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'preserves baggage property values with equal signs in fetch requests', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requestPromise = page.waitForRequest('http://sentry-test-site.example/fetch-test'); + + await page.goto(url); + await page.click('#fetchButton'); + + const request = await requestPromise; + + const requestHeaders = request.headers(); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP), + }); + + const baggageHeader = requestHeaders.baggage; + expect(baggageHeader).toBeDefined(); + + const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim())); + + // Verify property values with = signs are preserved + expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2')); + expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2')); + expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue')); + + // Verify Sentry baggage is also present + expect(baggageHeader).toMatch(/sentry-/); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js new file mode 100644 index 000000000000..839cdf137fd7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js @@ -0,0 +1,8 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://sentry-test-site.example/1'); +// W3C spec example: property values can contain = signs +// See: https://www.w3.org/TR/baggage/#example +xhr.setRequestHeader('baggage', 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue'); + +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts new file mode 100644 index 000000000000..f2ac4edb4a67 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; +import { TRACEPARENT_REGEXP } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('preserves baggage property values with equal signs in XHR requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requestPromise = page.waitForRequest('http://sentry-test-site.example/1'); + + await page.goto(url); + + const request = await requestPromise; + + const requestHeaders = request.headers(); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP), + }); + + const baggageHeader = requestHeaders.baggage; + expect(baggageHeader).toBeDefined(); + const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim())); + + // Verify property values with = signs are preserved + expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2')); + expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2')); + expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue')); + + // Verify Sentry baggage is also present + expect(baggageHeader).toMatch(/sentry-/); +}); From 8c283c4f7957797e5527d92498def43ba9b61b10 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 2 Oct 2025 12:04:50 +0200 Subject: [PATCH 3/3] . --- .../subject.js | 0 .../test.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-baggage-property-values copy => xhr-baggage-property-values}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-baggage-property-values copy => xhr-baggage-property-values}/test.ts (100%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values copy/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts