From ac57e5340a5070f6f6348553988b63fa5a5b4204 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 5 Nov 2024 14:12:00 +0100 Subject: [PATCH] fix(browser): Avoid recording long task spans starting before their parent span (#14183) - check for the start time stamp and drops long task spans starting before a _navigation_ spans started. - refactor the start and end logic a bit to compensate for bundle size increase (see comment) - add a regression test that previously would fail --- .size-limit.js | 4 +-- .../long-tasks-before-navigation/init.js | 19 +++++++++++++ .../long-tasks-before-navigation/subject.js | 17 ++++++++++++ .../template.html | 13 +++++++++ .../long-tasks-before-navigation/test.ts | 27 +++++++++++++++++++ .../src/metrics/browserMetrics.ts | 20 +++++++++----- packages/browser/package.json | 10 +++---- 7 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts diff --git a/.size-limit.js b/.size-limit.js index 0030dc3afe7f..8b506b8f683b 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -40,7 +40,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '36 KB', + limit: '36.5 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -124,7 +124,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '39.05 KB', + limit: '39.5 KB', }, // Vue SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js new file mode 100644 index 000000000000..1f396416d855 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + enableLongAnimationFrame: false, + instrumentPageLoad: false, + instrumentNavigation: true, + enableInp: false, + enableLongTask: true, + }), + ], + tracesSampleRate: 1, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/subject.js new file mode 100644 index 000000000000..2c477161b9f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/subject.js @@ -0,0 +1,17 @@ +const longTaskButton = document.getElementById('myButton'); + +longTaskButton?.addEventListener('click', () => { + const startTime = Date.now(); + + function getElapsed() { + const time = Date.now(); + return time - startTime; + } + + while (getElapsed() < 500) { + // + } + + // trigger a navigation in the same event loop tick + window.history.pushState({}, '', '/#myHeading'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/template.html new file mode 100644 index 000000000000..1c6430a388b2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/template.html @@ -0,0 +1,13 @@ + + + + + + +
Rendered Before Long Task
+ + + +

Heading

+ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts new file mode 100644 index 000000000000..fe2efb6b3565 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts @@ -0,0 +1,27 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + "doesn't capture long task spans starting before a navigation in the navigation transaction", + async ({ browserName, getLocalTestPath, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + await page.locator('#myButton').click(); + + const navigationTransactionEvent = await getFirstSentryEnvelopeRequest(page, url); + + expect(navigationTransactionEvent.contexts?.trace?.op).toBe('navigation'); + + const longTaskSpans = navigationTransactionEvent?.spans?.filter(span => span.op === 'ui.long-task'); + expect(longTaskSpans).toHaveLength(0); + }, +); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 59a58290c502..670b1ce4ae25 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -105,24 +105,32 @@ export function startTrackingWebVitals({ recordClsStandaloneSpans }: StartTracki */ export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { - if (!getActiveSpan()) { + const parent = getActiveSpan(); + if (!parent) { return; } + + const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(parent); + for (const entry of entries) { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); - const span = startInactiveSpan({ + if (parentOp === 'navigation' && parentStartTimestamp && startTime < parentStartTimestamp) { + // Skip adding a span if the long task started before the navigation started. + // `startAndEndSpan` will otherwise adjust the parent's start time to the span's start + // time, potentially skewing the duration of the actual navigation as reported via our + // routing instrumentations + continue; + } + + startAndEndSpan(parent, startTime, startTime + duration, { name: 'Main UI thread blocked', op: 'ui.long-task', - startTime, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }); - if (span) { - span.end(startTime + duration); - } } }); } diff --git a/packages/browser/package.json b/packages/browser/package.json index 532ebe06c4ea..f72074b4c5ba 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -9,9 +9,7 @@ "engines": { "node": ">=14.18" }, - "files": [ - "/build/npm" - ], + "files": ["/build/npm"], "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", @@ -30,9 +28,7 @@ }, "typesVersions": { "<4.9": { - "build/npm/types/index.d.ts": [ - "build/npm/types-ts3.8/index.d.ts" - ] + "build/npm/types/index.d.ts": ["build/npm/types-ts3.8/index.d.ts"] } }, "publishConfig": { @@ -53,7 +49,7 @@ }, "scripts": { "build": "run-p build:transpile build:bundle build:types", - "build:dev": "yarn build", + "build:dev": "run-p build:transpile build:types", "build:bundle": "rollup -c rollup.bundle.config.mjs", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel",