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/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js
new file mode 100644
index 000000000000..839cdf137fd7
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/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/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts
new file mode 100644
index 000000000000..f2ac4edb4a67
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/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-/);
+});
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',
+ });
+ });
});