Skip to content

Commit aef0ddd

Browse files
authored
Merge branch 'develop' into sig/console-aws-lambda-fix
2 parents a93810e + fa9fea2 commit aef0ddd

18 files changed

Lines changed: 596 additions & 9 deletions

File tree

.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/browser/src/integrations/graphqlClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption
6767
return;
6868
}
6969

70-
const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'];
70+
// Fall back to `url` because fetch instrumentation only sets `http.url` for absolute URLs;
71+
// relative URLs end up only in `url` (see `getFetchSpanAttributes` in packages/core/src/fetch.ts).
72+
const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'] || spanAttributes['url'];
7173
const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method'];
7274

7375
if (!isString(httpUrl) || !isString(httpMethod)) {

packages/browser/test/integrations/graphqlClient.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
* @vitest-environment jsdom
33
*/
44

5+
import type { Client } from '@sentry/core';
6+
import { SentrySpan, spanToJSON } from '@sentry/core';
57
import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils';
68
import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils';
79
import { describe, expect, test } from 'vitest';
810
import {
911
_getGraphQLOperation,
1012
getGraphQLRequestPayload,
1113
getRequestPayloadXhrOrFetch,
14+
graphqlClientIntegration,
1215
parseGraphQLQuery,
1316
} from '../../src/integrations/graphqlClient';
1417

@@ -308,4 +311,114 @@ describe('GraphqlClient', () => {
308311
expect(_getGraphQLOperation(requestBody as any)).toBe('unknown');
309312
});
310313
});
314+
315+
describe('beforeOutgoingRequestSpan handler', () => {
316+
function setupHandler(endpoints: Array<string | RegExp>): (span: SentrySpan, hint: FetchHint | XhrHint) => void {
317+
let capturedListener: ((span: SentrySpan, hint: FetchHint | XhrHint) => void) | undefined;
318+
const mockClient = {
319+
on: (eventName: string, cb: (span: SentrySpan, hint: FetchHint | XhrHint) => void) => {
320+
if (eventName === 'beforeOutgoingRequestSpan') {
321+
capturedListener = cb;
322+
}
323+
},
324+
} as unknown as Client;
325+
326+
const integration = graphqlClientIntegration({ endpoints });
327+
integration.setup?.(mockClient);
328+
329+
if (!capturedListener) {
330+
throw new Error('beforeOutgoingRequestSpan listener was not registered');
331+
}
332+
return capturedListener;
333+
}
334+
335+
function makeFetchHint(url: string, body: unknown): FetchHint {
336+
return {
337+
input: [url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }],
338+
response: new Response(null, { status: 200 }),
339+
startTimestamp: Date.now(),
340+
endTimestamp: Date.now() + 1,
341+
};
342+
}
343+
344+
const requestBody = {
345+
query: 'query GetHello { hello }',
346+
operationName: 'GetHello',
347+
variables: {},
348+
extensions: {},
349+
};
350+
351+
test('enriches http.client span for absolute URLs (http.url attribute)', () => {
352+
const handler = setupHandler([/\/graphql$/]);
353+
const span = new SentrySpan({
354+
name: 'POST http://localhost:4000/graphql',
355+
op: 'http.client',
356+
attributes: {
357+
'http.method': 'POST',
358+
'http.url': 'http://localhost:4000/graphql',
359+
url: 'http://localhost:4000/graphql',
360+
},
361+
});
362+
363+
handler(span, makeFetchHint('http://localhost:4000/graphql', requestBody));
364+
365+
const json = spanToJSON(span);
366+
expect(json.description).toBe('POST http://localhost:4000/graphql (query GetHello)');
367+
expect(json.data['graphql.document']).toBe(requestBody.query);
368+
});
369+
370+
test('enriches http.client span for relative URLs (only url attribute)', () => {
371+
const handler = setupHandler([/\/graphql$/]);
372+
// Fetch instrumentation does NOT set http.url for relative URLs — only `url`.
373+
const span = new SentrySpan({
374+
name: 'POST /graphql',
375+
op: 'http.client',
376+
attributes: {
377+
'http.method': 'POST',
378+
url: '/graphql',
379+
},
380+
});
381+
382+
handler(span, makeFetchHint('/graphql', requestBody));
383+
384+
const json = spanToJSON(span);
385+
expect(json.description).toBe('POST /graphql (query GetHello)');
386+
expect(json.data['graphql.document']).toBe(requestBody.query);
387+
});
388+
389+
test('does nothing when no URL attribute is present', () => {
390+
const handler = setupHandler([/\/graphql$/]);
391+
const span = new SentrySpan({
392+
name: 'POST',
393+
op: 'http.client',
394+
attributes: {
395+
'http.method': 'POST',
396+
},
397+
});
398+
399+
handler(span, makeFetchHint('/graphql', requestBody));
400+
401+
const json = spanToJSON(span);
402+
expect(json.description).toBe('POST');
403+
expect(json.data['graphql.document']).toBeUndefined();
404+
});
405+
406+
test('does nothing when span op is not http.client', () => {
407+
const handler = setupHandler([/\/graphql$/]);
408+
const span = new SentrySpan({
409+
name: 'custom span',
410+
op: 'custom',
411+
attributes: {
412+
'http.method': 'POST',
413+
url: '/graphql',
414+
},
415+
});
416+
417+
handler(span, makeFetchHint('/graphql', requestBody));
418+
419+
const json = spanToJSON(span);
420+
expect(json.description).toBe('custom span');
421+
expect(json.data['graphql.document']).toBeUndefined();
422+
});
423+
});
311424
});

0 commit comments

Comments
 (0)