Skip to content

Commit 57c0287

Browse files
authored
feat(browser): Add support for streamed spans in httpContextIntegration (#20464)
This PR adds span processing support for the `httpContextIntegration `: - register a `processSegmentSpan` callback on `httpContextIntegration ` that adds the attributes. All attributes are already registered in sentry conventions: https://getsentry.github.io/sentry-conventions/attributes/url/#url-full and https://getsentry.github.io/sentry-conventions/attributes/http/#http-request-header-key - adds a span streaming-specific integration test in addition to the already existing error/event-based integration test Closes #20378
1 parent 64fc5b9 commit 57c0287

9 files changed

Lines changed: 95 additions & 10 deletions

File tree

.size-limit.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ module.exports = [
5959
path: 'packages/browser/build/npm/esm/prod/index.js',
6060
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
6161
gzip: true,
62-
limit: '83 KB',
62+
limit: '84 KB',
6363
},
6464
{
6565
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
@@ -96,7 +96,7 @@ module.exports = [
9696
path: 'packages/browser/build/npm/esm/prod/index.js',
9797
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
9898
gzip: true,
99-
limit: '100 KB',
99+
limit: '101 KB',
100100
},
101101
{
102102
name: '@sentry/browser (incl. Feedback)',
@@ -138,7 +138,7 @@ module.exports = [
138138
path: 'packages/browser/build/npm/esm/prod/index.js',
139139
import: createImport('init', 'metrics', 'logger'),
140140
gzip: true,
141-
limit: '28 KB',
141+
limit: '29 KB',
142142
},
143143
// React SDK (ESM)
144144
{
@@ -197,7 +197,7 @@ module.exports = [
197197
name: 'CDN Bundle (incl. Logs, Metrics)',
198198
path: createCDNPath('bundle.logs.metrics.min.js'),
199199
gzip: true,
200-
limit: '30 KB',
200+
limit: '31 KB',
201201
},
202202
{
203203
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
@@ -209,7 +209,7 @@ module.exports = [
209209
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
210210
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
211211
gzip: true,
212-
limit: '69 KB',
212+
limit: '70 KB',
213213
},
214214
{
215215
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -255,7 +255,7 @@ module.exports = [
255255
path: createCDNPath('bundle.logs.metrics.min.js'),
256256
gzip: false,
257257
brotli: false,
258-
limit: '88 KB',
258+
limit: '89 KB',
259259
},
260260
{
261261
name: 'CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed',
@@ -283,21 +283,21 @@ module.exports = [
283283
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
284284
gzip: false,
285285
brotli: false,
286-
limit: '258.5 KB',
286+
limit: '259 KB',
287287
},
288288
{
289289
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
290290
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
291291
gzip: false,
292292
brotli: false,
293-
limit: '268 KB',
293+
limit: '269 KB',
294294
},
295295
{
296296
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed',
297297
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
298298
gzip: false,
299299
brotli: false,
300-
limit: '271.5 KB',
300+
limit: '272 KB',
301301
},
302302
// Next.js SDK (ESM)
303303
{
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { shouldSkipTracingTest } from '../../../utils/helpers';
4+
import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils';
5+
6+
sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => {
7+
sentryTest.skip(shouldSkipTracingTest());
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, { referer: 'https://sentry.io/' });
13+
14+
const spans = await spansPromise;
15+
16+
const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload');
17+
18+
expect(pageloadSpan!.attributes?.['url.full']).toEqual({ type: 'string', value: expect.any(String) });
19+
expect(pageloadSpan!.attributes?.['http.request.header.user_agent']).toEqual({
20+
type: 'string',
21+
value: expect.any(String),
22+
});
23+
expect(pageloadSpan!.attributes?.['http.request.header.referer']).toEqual({
24+
type: 'string',
25+
value: 'https://sentry.io/',
26+
});
27+
});

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ sentryTest(
179179
type: 'string',
180180
value: expect.any(String),
181181
},
182+
'http.request.header.user_agent': {
183+
type: 'string',
184+
value: expect.any(String),
185+
},
186+
'url.full': {
187+
type: 'string',
188+
value: expect.any(String),
189+
},
182190
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
183191
type: 'string',
184192
value: 'test',

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
5252
type: 'string',
5353
value: expect.any(String),
5454
},
55+
'http.request.header.user_agent': {
56+
type: 'string',
57+
value: expect.any(String),
58+
},
59+
'url.full': {
60+
type: 'string',
61+
value: expect.any(String),
62+
},
5563
[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: {
5664
type: 'string',
5765
value: 'idleTimeout',

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
8181
type: 'string',
8282
value: expect.any(String),
8383
},
84+
'http.request.header.user_agent': {
85+
type: 'string',
86+
value: expect.any(String),
87+
},
88+
'url.full': {
89+
type: 'string',
90+
value: expect.any(String),
91+
},
8492
'device.processor_count': {
8593
type: expect.stringMatching(/^(integer)|(double)$/),
8694
value: expect.any(Number),

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ sentryTest(
7474
type: 'string',
7575
value: expect.any(String),
7676
},
77+
'http.request.header.user_agent': {
78+
type: 'string',
79+
value: expect.any(String),
80+
},
81+
'url.full': {
82+
type: 'string',
83+
value: expect.any(String),
84+
},
7785
// formerly known as 'hardwareConcurrency'
7886
'device.processor_count': {
7987
type: expect.stringMatching(/^(integer)|(double)$/),

packages/browser/src/integrations/httpcontext.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineIntegration } from '@sentry/core';
1+
import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core';
22
import { getHttpRequestData, WINDOW } from '../helpers';
33

44
/**
@@ -26,5 +26,21 @@ export const httpContextIntegration = defineIntegration(() => {
2626
headers,
2727
};
2828
},
29+
processSegmentSpan(span) {
30+
// if none of the information we want exists, don't bother
31+
if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) {
32+
return;
33+
}
34+
35+
const reqData = getHttpRequestData();
36+
37+
safeSetSpanJSONAttributes(span, {
38+
// Coerce empty string to undefined so the helper's nullish check drops it,
39+
// rather than writing an empty `url.full` attribute onto the span.
40+
'url.full': reqData.url || undefined,
41+
'http.request.header.user_agent': reqData.headers['User-Agent'],
42+
'http.request.header.referer': reqData.headers['Referer'],
43+
});
44+
},
2945
};
3046
});

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export { createCheckInEnvelope } from './checkin';
7373
export { hasSpansEnabled } from './utils/hasSpansEnabled';
7474
export { withStreamedSpan } from './tracing/spans/beforeSendSpan';
7575
export { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan';
76+
export { safeSetSpanJSONAttributes } from './tracing/spans/captureSpan';
7677
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
7778
export { handleCallbackErrors } from './utils/handleCallbackErrors';
7879
export { parameterize, fmt } from './utils/parameterize';

0 commit comments

Comments
 (0)