Skip to content

Commit e9ab83a

Browse files
authored
feat(browser): Add support for streamed spans in cultureContextIntegration (#20352)
This PR adds span processing support for the `cultureContextIntegration` plus a few integration API additions to make span processing on an integration level easier: - `Integration::processSegmentSpan` - `Integration::processSpan` - register a `processSegmentSpan` callback on `cultureContextIntegration` that adds the attributes. All attributes are already registered in sentry conventions: https://getsentry.github.io/sentry-conventions/attributes/culture/ - adds a span streaming-specific integration test in addition to the already existing error/event-based integration test closes #20353 closes #20354
1 parent 313cf82 commit e9ab83a

File tree

12 files changed

+124
-8
lines changed

12 files changed

+124
-8
lines changed

.size-limit.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ module.exports = [
155155
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
156156
ignore: ['react/jsx-runtime'],
157157
gzip: true,
158-
limit: '46 KB',
158+
limit: '47 KB',
159159
},
160160
// Vue SDK (ESM)
161161
{
@@ -241,14 +241,14 @@ module.exports = [
241241
path: createCDNPath('bundle.min.js'),
242242
gzip: false,
243243
brotli: false,
244-
limit: '83.5 KB',
244+
limit: '84 KB',
245245
},
246246
{
247247
name: 'CDN Bundle (incl. Tracing) - uncompressed',
248248
path: createCDNPath('bundle.tracing.min.js'),
249249
gzip: false,
250250
brotli: false,
251-
limit: '134 KB',
251+
limit: '135 KB',
252252
},
253253
{
254254
name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed',
@@ -269,14 +269,14 @@ module.exports = [
269269
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
270270
gzip: false,
271271
brotli: false,
272-
limit: '211 KB',
272+
limit: '212 KB',
273273
},
274274
{
275275
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
276276
path: createCDNPath('bundle.tracing.replay.min.js'),
277277
gzip: false,
278278
brotli: false,
279-
limit: '251 KB',
279+
limit: '252 KB',
280280
},
281281
{
282282
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
@@ -290,7 +290,7 @@ module.exports = [
290290
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
291291
gzip: false,
292292
brotli: false,
293-
limit: '264 KB',
293+
limit: '265 KB',
294294
},
295295
{
296296
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed',
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()],
8+
tracesSampleRate: 1.0,
9+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils';
4+
import { shouldSkipTracingTest, testingCdnBundle } from '../../../utils/helpers';
5+
6+
sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => {
7+
sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
10+
const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload'));
11+
12+
await page.goto(url);
13+
14+
const spans = await spansPromise;
15+
16+
const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload');
17+
18+
expect(pageloadSpan!.attributes?.['culture.locale']).toEqual({ type: 'string', value: expect.any(String) });
19+
expect(pageloadSpan!.attributes?.['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) });
20+
expect(pageloadSpan!.attributes?.['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) });
21+
});

dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ sentryTest(
167167
},
168168
{
169169
attributes: {
170+
'culture.calendar': {
171+
type: 'string',
172+
value: expect.any(String),
173+
},
174+
'culture.locale': {
175+
type: 'string',
176+
value: expect.any(String),
177+
},
178+
'culture.timezone': {
179+
type: 'string',
180+
value: expect.any(String),
181+
},
170182
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
171183
type: 'string',
172184
value: 'test',

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
4040

4141
expect(interactionSegmentSpan).toEqual({
4242
attributes: {
43+
'culture.calendar': {
44+
type: 'string',
45+
value: expect.any(String),
46+
},
47+
'culture.locale': {
48+
type: 'string',
49+
value: expect.any(String),
50+
},
51+
'culture.timezone': {
52+
type: 'string',
53+
value: expect.any(String),
54+
},
4355
[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: {
4456
type: 'string',
4557
value: 'idleTimeout',

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ getL
6969

7070
expect(navigationSpan).toEqual({
7171
attributes: {
72+
'culture.calendar': {
73+
type: 'string',
74+
value: expect.any(String),
75+
},
76+
'culture.locale': {
77+
type: 'string',
78+
value: expect.any(String),
79+
},
80+
'culture.timezone': {
81+
type: 'string',
82+
value: expect.any(String),
83+
},
7284
'network.connection.effective_type': {
7385
type: 'string',
7486
value: expect.any(String),

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ sentryTest(
6262

6363
expect(pageloadSpan).toEqual({
6464
attributes: {
65+
'culture.calendar': {
66+
type: 'string',
67+
value: expect.any(String),
68+
},
69+
'culture.locale': {
70+
type: 'string',
71+
value: expect.any(String),
72+
},
73+
'culture.timezone': {
74+
type: 'string',
75+
value: expect.any(String),
76+
},
6577
// formerly known as 'effectiveConnectionType'
6678
'network.connection.effective_type': {
6779
type: 'string',

packages/browser/src/integrations/culturecontext.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ const _cultureContextIntegration = (() => {
1717
};
1818
}
1919
},
20+
processSegmentSpan(span) {
21+
const culture = getCultureContext();
22+
23+
if (culture) {
24+
span.attributes = {
25+
'culture.locale': culture.locale,
26+
'culture.timezone': culture.timezone,
27+
'culture.calendar': culture.calendar,
28+
...span.attributes,
29+
};
30+
}
31+
},
2032
};
2133
}) satisfies IntegrationFn;
2234

packages/core/src/integration.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DEBUG_BUILD } from './debug-build';
44
import type { Event, EventHint } from './types-hoist/event';
55
import type { Integration, IntegrationFn } from './types-hoist/integration';
66
import type { CoreOptions } from './types-hoist/options';
7+
import type { StreamedSpanJSON } from './types-hoist/span';
78
import { debug } from './utils/debug-logger';
89

910
export const installedIntegrations: string[] = [];
@@ -138,6 +139,15 @@ export function setupIntegration(client: Client, integration: Integration, integ
138139
client.addEventProcessor(processor);
139140
}
140141

142+
(['processSpan', 'processSegmentSpan'] as const).forEach(hook => {
143+
const callback = integration[hook];
144+
if (typeof callback === 'function') {
145+
// The cast is needed because TS can't resolve overloads when the discriminant is a union type.
146+
// Both overloads have the same callback signature so this is safe.
147+
client.on(hook as 'processSpan', (span: StreamedSpanJSON) => callback.call(integration, span, client));
148+
}
149+
});
150+
141151
DEBUG_BUILD && debug.log(`Integration installed: ${integration.name}`);
142152
}
143153

packages/core/src/integrations/requestdata.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { defineIntegration } from '../integration';
2-
import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled';
32
import type { Event } from '../types-hoist/event';
43
import type { IntegrationFn } from '../types-hoist/integration';
54
import type { RequestEventData } from '../types-hoist/request';

0 commit comments

Comments
 (0)