Skip to content

Commit a611e9e

Browse files
chargomeclaude
andauthored
fix(browser): Skip browserTracingIntegration setup for bot user agents (#19708)
closes #19670 When browserTracingIntegration initializes, it creates a 30-second setTimeout (idle span final timeout), multiple PerformanceObserver instances, and various other timers. These keep the JS event loop active, which prevents Googlebot's headless Chromium renderer from considering the page "idle" — resulting in incomplete or broken page snapshots in Google Search Console. This PR detects known bot/crawler user agents and skips the tracing setup entirely, so no timers or observers are created. Error monitoring via other integrations is unaffected. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f04a416 commit a611e9e

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

.size-limit.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ module.exports = [
8282
path: 'packages/browser/build/npm/esm/prod/index.js',
8383
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
8484
gzip: true,
85-
limit: '86 KB',
85+
limit: '87 KB',
8686
},
8787
{
8888
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
@@ -255,7 +255,7 @@ module.exports = [
255255
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
256256
gzip: false,
257257
brotli: false,
258-
limit: '131 KB',
258+
limit: '132 KB',
259259
},
260260
{
261261
name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed',
@@ -269,7 +269,7 @@ module.exports = [
269269
path: createCDNPath('bundle.tracing.replay.min.js'),
270270
gzip: false,
271271
brotli: false,
272-
limit: '245 KB',
272+
limit: '246 KB',
273273
},
274274
{
275275
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
@@ -308,7 +308,7 @@ module.exports = [
308308
import: createImport('init'),
309309
ignore: ['$app/stores'],
310310
gzip: true,
311-
limit: '43 KB',
311+
limit: '44 KB',
312312
},
313313
// Node-Core SDK (ESM)
314314
{

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from
5454

5555
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
5656

57+
/**
58+
* We don't want to start a bunch of idle timers and PerformanceObservers
59+
* for web crawlers, as they may prevent the page from being seen as "idle"
60+
* by the crawler's rendering engine (e.g. Googlebot's headless Chromium).
61+
*/
62+
const BOT_USER_AGENT_RE =
63+
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|Facebot|facebookexternalhit|LinkedInBot|Twitterbot|Applebot/i;
64+
65+
function _isBotUserAgent(): boolean {
66+
const nav = WINDOW.navigator as Navigator | undefined;
67+
if (!nav?.userAgent) {
68+
return false;
69+
}
70+
return BOT_USER_AGENT_RE.test(nav.userAgent);
71+
}
72+
5773
interface RouteInfo {
5874
name: string | undefined;
5975
source: TransactionSource | undefined;
@@ -384,6 +400,8 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
384400
...options,
385401
};
386402

403+
const _isBot = _isBotUserAgent();
404+
387405
let _collectWebVitals: undefined | (() => void);
388406
let lastInteractionTimestamp: number | undefined;
389407

@@ -484,6 +502,11 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
484502
return {
485503
name: BROWSER_TRACING_INTEGRATION_ID,
486504
setup(client) {
505+
if (_isBot) {
506+
DEBUG_BUILD && debug.log('[Tracing] Skipping browserTracingIntegration setup for bot user agent.');
507+
return;
508+
}
509+
487510
registerSpanErrorInstrumentation();
488511

489512
_collectWebVitals = startTrackingWebVitals({
@@ -630,6 +653,10 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
630653
},
631654

632655
afterAllSetup(client) {
656+
if (_isBot) {
657+
return;
658+
}
659+
633660
let startingUrl: string | undefined = getLocationHref();
634661

635662
if (linkPreviousTrace !== 'off') {

packages/browser/test/tracing/browserTracingIntegration.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,68 @@ describe('browserTracingIntegration', () => {
8686
Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory });
8787
});
8888

89+
describe('bot user agent detection', () => {
90+
let originalNavigator: Navigator;
91+
92+
beforeEach(() => {
93+
originalNavigator = WINDOW.navigator;
94+
});
95+
96+
afterEach(() => {
97+
Object.defineProperty(WINDOW, 'navigator', { value: originalNavigator, writable: true, configurable: true });
98+
});
99+
100+
function setUserAgent(ua: string): void {
101+
Object.defineProperty(WINDOW, 'navigator', {
102+
value: { userAgent: ua },
103+
writable: true,
104+
configurable: true,
105+
});
106+
}
107+
108+
it.each([
109+
'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
110+
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
111+
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/W.X.Y.Z Safari/537.36',
112+
'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
113+
'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
114+
'LinkedInBot/1.0 (compatible; Mozilla/5.0)',
115+
'Twitterbot/1.0',
116+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15 (Applebot/0.1)',
117+
'Mozilla/5.0 (compatible; Google-InspectionTool/1.0)',
118+
])('skips tracing setup for bot user agent: %s', ua => {
119+
setUserAgent(ua);
120+
121+
const client = new BrowserClient(
122+
getDefaultBrowserClientOptions({
123+
tracesSampleRate: 1,
124+
integrations: [browserTracingIntegration()],
125+
}),
126+
);
127+
setCurrentClient(client);
128+
client.init();
129+
130+
expect(getActiveSpan()).toBeUndefined();
131+
});
132+
133+
it('does not skip tracing setup for normal user agents', () => {
134+
setUserAgent(
135+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
136+
);
137+
138+
const client = new BrowserClient(
139+
getDefaultBrowserClientOptions({
140+
tracesSampleRate: 1,
141+
integrations: [browserTracingIntegration()],
142+
}),
143+
);
144+
setCurrentClient(client);
145+
client.init();
146+
147+
expect(getActiveSpan()).toBeDefined();
148+
});
149+
});
150+
89151
it('works with tracing enabled', () => {
90152
const client = new BrowserClient(
91153
getDefaultBrowserClientOptions({

packages/nextjs/test/config/conflictingDebugOptions.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ describe('debug: true + removeDebugLogging warning', () => {
2020
let originalLocation: unknown;
2121
let originalAddEventListener: unknown;
2222

23-
beforeAll(() => {
23+
beforeAll(async () => {
24+
// Pre-warm V8 compilation cache for the large SDK module graphs.
25+
// Without this, the first dynamic import after vi.resetModules() can hang
26+
// because vitest needs to compile the entire module graph from scratch.
27+
await import('../../src/client/index.js');
28+
await import('../../src/server/index.js');
29+
await import('../../src/edge/index.js');
30+
vi.resetModules();
31+
2432
dom = new JSDOM('<!doctype html><html><head></head><body></body></html>', { url: 'https://example.com/' });
2533

2634
originalDocument = (globalThis as any).document;

0 commit comments

Comments
 (0)