Skip to content

Commit 4b6cb4a

Browse files
authored
feat(browser): Send standalone fetch and XHR spans if there's no active parent span (#11783)
Enable sending standalone `http.client` spans for outgoing `fetch` and XHR requests if there's no active parent span. These spans will belong to the same trace id as a potentially previously started pageload or navigation span. Adjusted integration tests to test the newly sent spans and their trace lifetime.
1 parent 45a05c5 commit 4b6cb4a

File tree

16 files changed

+395
-204
lines changed

16 files changed

+395
-204
lines changed

dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js renamed to dev-packages/browser-integration-tests/suites/tracing/request/fetch-standalone-span/init.js

-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import * as Sentry from '@sentry/browser';
22

33
window.Sentry = Sentry;
44

5-
window._sentryTransactionsCount = 0;
6-
75
Sentry.init({
86
dsn: 'https://[email protected]/1337',
97
// disable auto span creation
@@ -16,8 +14,4 @@ Sentry.init({
1614
tracePropagationTargets: ['http://example.com'],
1715
tracesSampleRate: 1,
1816
autoSessionTracking: false,
19-
beforeSendTransaction() {
20-
window._sentryTransactionsCount++;
21-
return null;
22-
},
2317
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fetch('http://example.com/0');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { SpanEnvelope } from '@sentry/types';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import {
4+
getFirstSentryEnvelopeRequest,
5+
properFullEnvelopeRequestParser,
6+
shouldSkipTracingTest,
7+
} from '../../../../utils/helpers';
8+
9+
import { expect } from '@playwright/test';
10+
11+
sentryTest(
12+
"should create standalone span for fetch requests if there's no active span and should attach tracing headers",
13+
async ({ getLocalTestUrl, page }) => {
14+
if (shouldSkipTracingTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
let sentryTraceHeader = '';
19+
let baggageHeader = '';
20+
21+
await page.route('http://example.com/**', route => {
22+
sentryTraceHeader = route.request().headers()['sentry-trace'];
23+
baggageHeader = route.request().headers()['baggage'];
24+
return route.fulfill({
25+
status: 200,
26+
contentType: 'application/json',
27+
body: JSON.stringify({}),
28+
});
29+
});
30+
31+
const url = await getLocalTestUrl({ testDir: __dirname });
32+
33+
const spanEnvelopePromise = getFirstSentryEnvelopeRequest<SpanEnvelope>(
34+
page,
35+
undefined,
36+
properFullEnvelopeRequestParser,
37+
);
38+
39+
await page.goto(url);
40+
41+
const spanEnvelope = await spanEnvelopePromise;
42+
43+
const spanEnvelopeHeaders = spanEnvelope[0];
44+
const spanEnvelopeItem = spanEnvelope[1][0][1];
45+
46+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
47+
const spanId = spanEnvelopeItem.span_id;
48+
49+
expect(traceId).toMatch(/[a-f0-9]{32}/);
50+
expect(spanId).toMatch(/[a-f0-9]{16}/);
51+
52+
expect(spanEnvelopeHeaders).toEqual({
53+
sent_at: expect.any(String),
54+
trace: {
55+
environment: 'production',
56+
public_key: 'public',
57+
sample_rate: '1',
58+
sampled: 'true',
59+
trace_id: traceId,
60+
transaction: 'GET http://example.com/0',
61+
},
62+
});
63+
64+
expect(spanEnvelopeItem).toEqual({
65+
data: expect.objectContaining({
66+
'http.method': 'GET',
67+
'http.response.status_code': 200,
68+
'http.response_content_length': expect.any(Number),
69+
'http.url': 'http://example.com/0',
70+
'sentry.op': 'http.client',
71+
'sentry.origin': 'auto.http.browser',
72+
'sentry.sample_rate': 1,
73+
'sentry.source': 'custom',
74+
'server.address': 'example.com',
75+
type: 'fetch',
76+
url: 'http://example.com/0',
77+
}),
78+
description: 'GET http://example.com/0',
79+
op: 'http.client',
80+
origin: 'auto.http.browser',
81+
status: 'ok',
82+
trace_id: traceId,
83+
span_id: spanId,
84+
segment_id: spanId,
85+
is_segment: true,
86+
start_timestamp: expect.any(Number),
87+
timestamp: expect.any(Number),
88+
});
89+
90+
// the standalone span was sampled, so we propagate the positive sampling decision
91+
expect(sentryTraceHeader).toBe(`${traceId}-${spanId}-1`);
92+
expect(baggageHeader).toBe(
93+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-transaction=GET%20http%3A%2F%2Fexample.com%2F0,sentry-sampled=true`,
94+
);
95+
},
96+
);

dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js

-1
This file was deleted.

dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts

-41
This file was deleted.

dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js renamed to dev-packages/browser-integration-tests/suites/tracing/request/xhr-standalone-span/init.js

-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import * as Sentry from '@sentry/browser';
22

33
window.Sentry = Sentry;
44

5-
window._sentryTransactionsCount = 0;
6-
75
Sentry.init({
86
dsn: 'https://[email protected]/1337',
97
// disable auto span creation
@@ -16,8 +14,4 @@ Sentry.init({
1614
tracePropagationTargets: ['http://example.com'],
1715
tracesSampleRate: 1,
1816
autoSessionTracking: false,
19-
beforeSendTransaction() {
20-
window._sentryTransactionsCount++;
21-
return null;
22-
},
2317
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const xhr_1 = new XMLHttpRequest();
2+
xhr_1.open('GET', 'http://example.com/0');
3+
xhr_1.send();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect } from '@playwright/test';
2+
3+
import type { SpanEnvelope } from '@sentry/types';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../../utils/helpers';
10+
11+
sentryTest(
12+
"should create standalone span for XHR requests if there's no active span and should attach tracing headers",
13+
async ({ getLocalTestUrl, page }) => {
14+
if (shouldSkipTracingTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
let sentryTraceHeader = '';
19+
let baggageHeader = '';
20+
21+
await page.route('http://example.com/**', route => {
22+
sentryTraceHeader = route.request().headers()['sentry-trace'];
23+
baggageHeader = route.request().headers()['baggage'];
24+
return route.fulfill({
25+
status: 200,
26+
contentType: 'application/json',
27+
body: JSON.stringify({}),
28+
});
29+
});
30+
31+
const url = await getLocalTestUrl({ testDir: __dirname });
32+
33+
const spanEnvelopePromise = getFirstSentryEnvelopeRequest<SpanEnvelope>(
34+
page,
35+
undefined,
36+
properFullEnvelopeRequestParser,
37+
);
38+
39+
await page.goto(url);
40+
41+
const spanEnvelope = await spanEnvelopePromise;
42+
43+
const spanEnvelopeHeaders = spanEnvelope[0];
44+
const spanEnvelopeItem = spanEnvelope[1][0][1];
45+
46+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
47+
const spanId = spanEnvelopeItem.span_id;
48+
49+
expect(traceId).toMatch(/[a-f0-9]{32}/);
50+
expect(spanId).toMatch(/[a-f0-9]{16}/);
51+
52+
expect(spanEnvelopeHeaders).toEqual({
53+
sent_at: expect.any(String),
54+
trace: {
55+
environment: 'production',
56+
public_key: 'public',
57+
sample_rate: '1',
58+
sampled: 'true',
59+
trace_id: traceId,
60+
transaction: 'GET http://example.com/0',
61+
},
62+
});
63+
64+
expect(spanEnvelopeItem).toEqual({
65+
data: {
66+
'http.method': 'GET',
67+
'http.response.status_code': 200,
68+
'http.url': 'http://example.com/0',
69+
'sentry.op': 'http.client',
70+
'sentry.origin': 'auto.http.browser',
71+
'sentry.sample_rate': 1,
72+
'sentry.source': 'custom',
73+
'server.address': 'example.com',
74+
type: 'xhr',
75+
url: 'http://example.com/0',
76+
},
77+
description: 'GET http://example.com/0',
78+
op: 'http.client',
79+
origin: 'auto.http.browser',
80+
status: 'ok',
81+
trace_id: traceId,
82+
span_id: spanId,
83+
segment_id: spanId,
84+
is_segment: true,
85+
start_timestamp: expect.any(Number),
86+
timestamp: expect.any(Number),
87+
});
88+
89+
// the standalone span was sampled, so we propagate the positive sampling decision
90+
expect(sentryTraceHeader).toBe(`${traceId}-${spanId}-1`);
91+
expect(baggageHeader).toBe(
92+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-transaction=GET%20http%3A%2F%2Fexample.com%2F0,sentry-sampled=true`,
93+
);
94+
},
95+
);

dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js

-11
This file was deleted.

dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts

-41
This file was deleted.

0 commit comments

Comments
 (0)