Skip to content

Commit 349d7d0

Browse files
authored
fix(browser): Move browserTracingIntegration code to setup hook (#16386)
This PR removes the warning for multiple browserTracingIntegrations being setup. We realised this warning was actually just a symptom of us running a bunch of the integration code in the function itself, not int `setup()`. This lead to stuff running twice when users passed in a custom integration in addition to a default one, because the logic is basically: ```js const defaultIntergations = [browserTracingIntegration()]; const usedDefinedIntegrations = [browserTracingIntegration()]; // at this point, the function was already executed twice, leading to side effects even without calling `init`! ``` Now, we move all the logic to setup/setupAfterAll, so this should be safe. This means that the integration will be deduped anyhow (I added a test to make sure this is the case), and we no longer need the warning. fixes #16369
1 parent 72057b6 commit 349d7d0

File tree

7 files changed

+118
-76
lines changed

7 files changed

+118
-76
lines changed

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [Sentry.browserTracingIntegration()],
8+
tracePropagationTargets: ['http://sentry-test-site.example'],
9+
tracesSampleRate: 1,
10+
autoSessionTracking: false,
11+
});
12+
13+
// fetch directly after init
14+
fetch('http://sentry-test-site.example/0');
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import {
4+
envelopeRequestParser,
5+
shouldSkipTracingTest,
6+
waitForTransactionRequestOnUrl,
7+
} from '../../../../utils/helpers';
8+
9+
sentryTest('should create spans for fetch requests called directly after init', async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipTracingTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
15+
16+
const url = await getLocalTestUrl({ testDir: __dirname });
17+
18+
const req = await waitForTransactionRequestOnUrl(page, url);
19+
const tracingEvent = envelopeRequestParser(req);
20+
21+
const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
22+
23+
expect(requestSpans).toHaveLength(1);
24+
25+
expect(requestSpans![0]).toMatchObject({
26+
description: 'GET http://sentry-test-site.example/0',
27+
parent_span_id: tracingEvent.contexts?.trace?.span_id,
28+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
29+
start_timestamp: expect.any(Number),
30+
timestamp: expect.any(Number),
31+
trace_id: tracingEvent.contexts?.trace?.trace_id,
32+
data: {
33+
'http.method': 'GET',
34+
'http.url': 'http://sentry-test-site.example/0',
35+
url: 'http://sentry-test-site.example/0',
36+
'server.address': 'sentry-test-site.example',
37+
type: 'fetch',
38+
},
39+
});
40+
});

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource,
33
import {
44
addNonEnumerableProperty,
55
browserPerformanceTimeOrigin,
6-
consoleSandbox,
76
generateTraceId,
87
getClient,
98
getCurrentScope,
@@ -233,8 +232,6 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
233232
...defaultRequestInstrumentationOptions,
234233
};
235234

236-
let _hasBeenInitialized = false;
237-
238235
/**
239236
* The Browser Tracing integration automatically instruments browser pageload/navigation
240237
* actions as transactions, and captures requests, metrics and errors as spans.
@@ -245,23 +242,17 @@ let _hasBeenInitialized = false;
245242
* We explicitly export the proper type here, as this has to be extended in some cases.
246243
*/
247244
export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptions> = {}) => {
248-
if (_hasBeenInitialized) {
249-
consoleSandbox(() => {
250-
// eslint-disable-next-line no-console
251-
console.warn('Multiple browserTracingIntegration instances are not supported.');
252-
});
253-
}
254-
255-
_hasBeenInitialized = true;
245+
const latestRoute: RouteInfo = {
246+
name: undefined,
247+
source: undefined,
248+
};
256249

257250
/**
258251
* This is just a small wrapper that makes `document` optional.
259252
* We want to be extra-safe and always check that this exists, to ensure weird environments do not blow up.
260253
*/
261254
const optionalWindowDocument = WINDOW.document as (typeof WINDOW)['document'] | undefined;
262255

263-
registerSpanErrorInstrumentation();
264-
265256
const {
266257
enableInp,
267258
enableLongTask,
@@ -287,31 +278,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
287278
..._options,
288279
};
289280

290-
const _collectWebVitals = startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false });
291-
292-
if (enableInp) {
293-
startTrackingINP();
294-
}
295-
296-
if (
297-
enableLongAnimationFrame &&
298-
GLOBAL_OBJ.PerformanceObserver &&
299-
PerformanceObserver.supportedEntryTypes &&
300-
PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')
301-
) {
302-
startTrackingLongAnimationFrames();
303-
} else if (enableLongTask) {
304-
startTrackingLongTasks();
305-
}
306-
307-
if (enableInteractions) {
308-
startTrackingInteractions();
309-
}
310-
311-
const latestRoute: RouteInfo = {
312-
name: undefined,
313-
source: undefined,
314-
};
281+
let _collectWebVitals: undefined | (() => void);
315282

316283
/** Create routing idle transaction. */
317284
function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions): void {
@@ -340,7 +307,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
340307
// should wait for finish signal if it's a pageload transaction
341308
disableAutoFinish: isPageloadTransaction,
342309
beforeSpanEnd: span => {
343-
_collectWebVitals();
310+
// This will generally always be defined here, because it is set in `setup()` of the integration
311+
// but technically, it is optional, so we guard here to be extra safe
312+
_collectWebVitals?.();
344313
addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans });
345314
setActiveIdleSpan(client, undefined);
346315

@@ -378,8 +347,29 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
378347

379348
return {
380349
name: BROWSER_TRACING_INTEGRATION_ID,
381-
afterAllSetup(client) {
382-
let startingUrl: string | undefined = getLocationHref();
350+
setup(client) {
351+
registerSpanErrorInstrumentation();
352+
353+
_collectWebVitals = startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false });
354+
355+
if (enableInp) {
356+
startTrackingINP();
357+
}
358+
359+
if (
360+
enableLongAnimationFrame &&
361+
GLOBAL_OBJ.PerformanceObserver &&
362+
PerformanceObserver.supportedEntryTypes &&
363+
PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')
364+
) {
365+
startTrackingLongAnimationFrames();
366+
} else if (enableLongTask) {
367+
startTrackingLongTasks();
368+
}
369+
370+
if (enableInteractions) {
371+
startTrackingInteractions();
372+
}
383373

384374
function maybeEndActiveSpan(): void {
385375
const activeSpan = getActiveIdleSpan(client);
@@ -440,6 +430,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
440430
...startSpanOptions,
441431
});
442432
});
433+
},
434+
afterAllSetup(client) {
435+
let startingUrl: string | undefined = getLocationHref();
443436

444437
if (linkPreviousTrace !== 'off') {
445438
linkTraces(client, { linkPreviousTrace, consistentTraceSampling });

packages/core/test/lib/integration.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ class MockIntegration implements Integration {
2222
// Only for testing - tag to keep separate instances straight when testing deduplication
2323
public tag?: string;
2424

25+
public setupOnce = vi.fn(() => {});
26+
2527
public constructor(name: string, tag?: string) {
2628
this.name = name;
2729
this.tag = tag;
2830
}
29-
30-
public setupOnce(): void {
31-
// noop
32-
}
3331
}
3432

3533
type TestCase = [
@@ -74,6 +72,31 @@ describe('getIntegrationsToSetup', () => {
7472
});
7573
expect(integrations.map(i => i.name)).toEqual(expected);
7674
});
75+
76+
test('it uses passed integration over default intergation', () => {
77+
const integrationDefault = new MockIntegration('ChaseSquirrels');
78+
const integration1 = new MockIntegration('ChaseSquirrels');
79+
80+
const integrations = getIntegrationsToSetup({
81+
defaultIntegrations: [integrationDefault],
82+
integrations: [integration1],
83+
});
84+
85+
expect(integrations).toEqual([integration1]);
86+
});
87+
88+
test('it uses last passed integration only', () => {
89+
const integrationDefault = new MockIntegration('ChaseSquirrels');
90+
const integration1 = new MockIntegration('ChaseSquirrels');
91+
const integration2 = new MockIntegration('ChaseSquirrels');
92+
93+
const integrations = getIntegrationsToSetup({
94+
defaultIntegrations: [integrationDefault],
95+
integrations: [integration1, integration2],
96+
});
97+
98+
expect(integrations).toEqual([integration2]);
99+
});
77100
});
78101

79102
describe('deduping', () => {

packages/react/src/reactrouterv6-compat-utils.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,9 @@ export function createReactRouterV6CompatibleTracingIntegration(
236236

237237
return {
238238
...integration,
239-
setup() {
239+
setup(client) {
240+
integration.setup(client);
241+
240242
_useEffect = useEffect;
241243
_useLocation = useLocation;
242244
_useNavigationType = useNavigationType;

0 commit comments

Comments
 (0)