From d6dedf6e140932f22a6bf5fd857722a252187be2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 16 Apr 2026 15:47:40 +0200 Subject: [PATCH 1/7] feat(browser): Add support for streamed spans in `cultureContextIntegration` --- .../cultureContext-streamed/init.js | 9 ++++++++ .../cultureContext-streamed/test.ts | 21 +++++++++++++++++++ .../src/integrations/culturecontext.ts | 13 ++++++++++++ packages/core/src/integration.ts | 10 +++++++++ .../core/src/tracing/spans/captureSpan.ts | 4 +++- packages/core/src/types-hoist/integration.ts | 15 +++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js new file mode 100644 index 000000000000..c69a872adc77 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts new file mode 100644 index 000000000000..43dee40f093b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; +import { shouldSkipTracingTest, testingCdnBundle } from '../../../utils/helpers'; + +sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); + + await page.goto(url); + + const spans = await spansPromise; + + const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); + + expect(pageloadSpan!.attributes?.['culture.locale']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan!.attributes?.['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan!.attributes?.['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) }); +}); diff --git a/packages/browser/src/integrations/culturecontext.ts b/packages/browser/src/integrations/culturecontext.ts index 486f5dcd012b..f6e7c365c9e4 100644 --- a/packages/browser/src/integrations/culturecontext.ts +++ b/packages/browser/src/integrations/culturecontext.ts @@ -17,6 +17,19 @@ const _cultureContextIntegration = (() => { }; } }, + processSegmentSpan(span) { + const culture = getCultureContext(); + + if (culture) { + span.attributes = { + 'culture.locale': culture.locale, + 'culture.timezone': culture.timezone, + 'culture.calendar': culture.calendar, + xxxDeleteMe: undefined, + ...span.attributes, + }; + } + }, }; }) satisfies IntegrationFn; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index b8e7240cf748..f39abccdd473 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -138,6 +138,16 @@ export function setupIntegration(client: Client, integration: Integration, integ client.addEventProcessor(processor); } + if (typeof integration.processSpan === 'function') { + const callback = integration.processSpan.bind(integration) as typeof integration.processSpan; + client.on('processSpan', span => callback(span, client)); + } + + if (typeof integration.processSegmentSpan === 'function') { + const callback = integration.processSegmentSpan.bind(integration) as typeof integration.processSegmentSpan; + client.on('processSegmentSpan', span => callback(span, client)); + } + DEBUG_BUILD && debug.log(`Integration installed: ${integration.name}`); } diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts index 979c7b460af1..fe8bc31fcae7 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -54,10 +54,12 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW if (spanJSON.is_segment) { applyScopeToSegmentSpan(spanJSON, finalScopeData); // Allow hook subscribers to mutate the segment span JSON + // This also invokes the `processSegmentSpan` hook of all integrations client.emit('processSegmentSpan', spanJSON); } - // Allow hook subscribers to mutate the span JSON + // This allows hook subscribers to mutate the span JSON + // This also invokes the `processSpan` hook of all integrations client.emit('processSpan', spanJSON); const { beforeSendSpan } = client.getOptions(); diff --git a/packages/core/src/types-hoist/integration.ts b/packages/core/src/types-hoist/integration.ts index fc80cf3f524a..77fd5ebd5a29 100644 --- a/packages/core/src/types-hoist/integration.ts +++ b/packages/core/src/types-hoist/integration.ts @@ -1,5 +1,6 @@ import type { Client } from '../client'; import type { Event, EventHint } from './event'; +import { StreamedSpanJSON } from './span'; /** Integration interface */ export interface Integration { @@ -50,6 +51,20 @@ export interface Integration { * This receives the client that the integration was installed for as third argument. */ processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike; + + /** + * An optional hook that allows modifications to a span. This hook runs after the span is ended, + * during `captureSpan` and before the span is passed to users' `beforeSendSpan` callback. + * Use this hook to modify a span in-place. + */ + processSpan?(span: StreamedSpanJSON, client: Client): void; + + /** + * An optional hook that allows modifications to a segment span. This hook runs after the segment span is ended, + * during `captureSpan` and before the segment span is passed to users' `beforeSendSpan` callback. + * Use this hook to modify a segment span in-place. + */ + processSegmentSpan?(span: StreamedSpanJSON, client: Client): void; } /** From c74fa240e53a6712c96240f6dade2fe49641ffcc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 16 Apr 2026 15:57:47 +0200 Subject: [PATCH 2/7] cleanup --- packages/browser/src/integrations/culturecontext.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/browser/src/integrations/culturecontext.ts b/packages/browser/src/integrations/culturecontext.ts index f6e7c365c9e4..f2b705e3e9a9 100644 --- a/packages/browser/src/integrations/culturecontext.ts +++ b/packages/browser/src/integrations/culturecontext.ts @@ -25,7 +25,6 @@ const _cultureContextIntegration = (() => { 'culture.locale': culture.locale, 'culture.timezone': culture.timezone, 'culture.calendar': culture.calendar, - xxxDeleteMe: undefined, ...span.attributes, }; } From 420b2831c2832fd68924261b620d9c5dc4d939a5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 16 Apr 2026 17:46:19 +0200 Subject: [PATCH 3/7] fix integration tests and size limit --- .size-limit.js | 10 +++++----- .../suites/public-api/startSpan/streamed/test.ts | 12 ++++++++++++ .../interactions-streamed/test.ts | 12 ++++++++++++ .../navigation-streamed/test.ts | 12 ++++++++++++ .../pageload-streamed/test.ts | 12 ++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 86f3ef5ed87d..d511b618fbcd 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -241,14 +241,14 @@ module.exports = [ path: createCDNPath('bundle.min.js'), gzip: false, brotli: false, - limit: '83.5 KB', + limit: '84 KB', }, { name: 'CDN Bundle (incl. Tracing) - uncompressed', path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '134 KB', + limit: '135 KB', }, { name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed', @@ -269,14 +269,14 @@ module.exports = [ path: createCDNPath('bundle.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '211 KB', + limit: '212 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '251 KB', + limit: '252 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed', @@ -290,7 +290,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: false, brotli: false, - limit: '264 KB', + limit: '265 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed', diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts index b5f8f41ab4b4..9ea2197fff85 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -167,6 +167,18 @@ sentryTest( }, { attributes: { + 'culture.calendar': { + type: 'string', + value: expect.any(String), + }, + 'culture.locale': { + type: 'string', + value: expect.any(String), + }, + 'culture.timezone': { + type: 'string', + value: expect.any(String), + }, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts index fd384d0d3ff9..546d80c133ac 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts @@ -40,6 +40,18 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser expect(interactionSegmentSpan).toEqual({ attributes: { + 'culture.calendar': { + type: 'string', + value: expect.any(String), + }, + 'culture.locale': { + type: 'string', + value: expect.any(String), + }, + 'culture.timezone': { + type: 'string', + value: expect.any(String), + }, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'idleTimeout', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index 403fdd4fdc0a..b9922c178fd9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -69,6 +69,18 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ getL expect(navigationSpan).toEqual({ attributes: { + 'culture.calendar': { + type: 'string', + value: expect.any(String), + }, + 'culture.locale': { + type: 'string', + value: expect.any(String), + }, + 'culture.timezone': { + type: 'string', + value: expect.any(String), + }, 'network.connection.effective_type': { type: 'string', value: expect.any(String), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index 86882134cab4..e89e2011b100 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -62,6 +62,18 @@ sentryTest( expect(pageloadSpan).toEqual({ attributes: { + 'culture.calendar': { + type: 'string', + value: expect.any(String), + }, + 'culture.locale': { + type: 'string', + value: expect.any(String), + }, + 'culture.timezone': { + type: 'string', + value: expect.any(String), + }, // formerly known as 'effectiveConnectionType' 'network.connection.effective_type': { type: 'string', From 5c44e55e1f257cd4721164a04893d7a34c5bbe4d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 16 Apr 2026 17:47:49 +0200 Subject: [PATCH 4/7] lint --- packages/core/src/types-hoist/integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/types-hoist/integration.ts b/packages/core/src/types-hoist/integration.ts index 77fd5ebd5a29..2e3cb5d45723 100644 --- a/packages/core/src/types-hoist/integration.ts +++ b/packages/core/src/types-hoist/integration.ts @@ -1,6 +1,6 @@ import type { Client } from '../client'; import type { Event, EventHint } from './event'; -import { StreamedSpanJSON } from './span'; +import type { StreamedSpanJSON } from './span'; /** Integration interface */ export interface Integration { From c216b99381b7bdf6a8c2a6a8b5f2d0513e5eda03 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 17 Apr 2026 09:51:54 +0200 Subject: [PATCH 5/7] slightly reduce bundle size --- packages/core/src/integration.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f39abccdd473..1688b312f98f 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -4,6 +4,7 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { Integration, IntegrationFn } from './types-hoist/integration'; import type { CoreOptions } from './types-hoist/options'; +import { StreamedSpanJSON } from './types-hoist/span'; import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; @@ -138,15 +139,14 @@ export function setupIntegration(client: Client, integration: Integration, integ client.addEventProcessor(processor); } - if (typeof integration.processSpan === 'function') { - const callback = integration.processSpan.bind(integration) as typeof integration.processSpan; - client.on('processSpan', span => callback(span, client)); - } - - if (typeof integration.processSegmentSpan === 'function') { - const callback = integration.processSegmentSpan.bind(integration) as typeof integration.processSegmentSpan; - client.on('processSegmentSpan', span => callback(span, client)); - } + (['processSpan', 'processSegmentSpan'] as const).forEach(hook => { + const callback = integration[hook]; + if (typeof callback === 'function') { + // The cast is needed because TS can't resolve overloads when the discriminant is a union type. + // Both overloads have the same callback signature so this is safe. + client.on(hook as 'processSpan', (span: StreamedSpanJSON) => callback.call(integration, span, client)); + } + }); DEBUG_BUILD && debug.log(`Integration installed: ${integration.name}`); } From d7787409f1f0fdcae21d4d95e8ea71d436afbc06 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 17 Apr 2026 10:01:19 +0200 Subject: [PATCH 6/7] lint --- packages/core/src/integration.ts | 2 +- packages/core/src/integrations/requestdata.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 1688b312f98f..be8e2179bdf0 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -4,7 +4,7 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { Integration, IntegrationFn } from './types-hoist/integration'; import type { CoreOptions } from './types-hoist/options'; -import { StreamedSpanJSON } from './types-hoist/span'; +import type { StreamedSpanJSON } from './types-hoist/span'; import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 7fdd8cee1683..a72fbed70d7e 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,5 +1,4 @@ import { defineIntegration } from '../integration'; -import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled'; import type { Event } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; import type { RequestEventData } from '../types-hoist/request'; From 878ef99483d216caf79e268947830a9a2def9777 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 17 Apr 2026 14:42:42 +0200 Subject: [PATCH 7/7] size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index d511b618fbcd..718781bbd318 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -155,7 +155,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '46 KB', + limit: '47 KB', }, // Vue SDK (ESM) {