diff --git a/.size-limit.js b/.size-limit.js index 636b9c64413a..a5ce210ef737 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -177,7 +177,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '220 KB', + limit: '221 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/assets/script.js new file mode 100644 index 000000000000..9ac3d6fb33d2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 101) { + // + } +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js new file mode 100644 index 000000000000..e1b3f6b13b01 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: false, idleTimeout: 9000 }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/template.html new file mode 100644 index 000000000000..4cd015b16f51 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/template.html @@ -0,0 +1,10 @@ + + + + + + +
Rendered Before Long Animation Frame
+ + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts new file mode 100644 index 000000000000..2527d5a67302 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled/test.ts @@ -0,0 +1,27 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should not capture long animation frame when flag is disabled.', + async ({ browserName, getLocalTestPath, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBe(0); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/assets/script.js new file mode 100644 index 000000000000..10552eeb5bd5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/assets/script.js @@ -0,0 +1,25 @@ +function getElapsed(startTime) { + const time = Date.now(); + return time - startTime; +} + +function handleClick() { + const startTime = Date.now(); + while (getElapsed(startTime) < 105) { + // + } +} + +function start() { + const startTime = Date.now(); + while (getElapsed(startTime) < 105) { + // + } +} + +// trigger 2 long-animation-frame events +// one from the top-level and the other from an event-listener +start(); + +const button = document.getElementById('clickme'); +button.addEventListener('click', handleClick); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js new file mode 100644 index 000000000000..4be408ceab7e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + enableLongTask: false, + enableLongAnimationFrame: true, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/template.html new file mode 100644 index 000000000000..ed02d1117097 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/template.html @@ -0,0 +1,13 @@ + + + + + + +
Rendered Before Long Animation Frame
+ + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts new file mode 100644 index 000000000000..850e75dbed1f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts @@ -0,0 +1,112 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should capture long animation frame for top-level script.', + async ({ browserName, getLocalTestPath, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const eventData = await promise; + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame')); + + expect(uiSpans?.length).toEqual(1); + + const [topLevelUISpan] = uiSpans || []; + expect(topLevelUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-animation-frame', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + 'code.filepath': 'https://example.com/path/to/script.js', + 'browser.script.source_char_position': 0, + 'browser.script.invoker': 'https://example.com/path/to/script.js', + 'browser.script.invoker_type': 'classic-script', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = topLevelUISpan.start_timestamp ?? 0; + const end = topLevelUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); + +sentryTest( + 'should capture long animation frame for event listener.', + async ({ browserName, getLocalTestPath, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + // trigger long animation frame function + await page.getByRole('button').click(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const eventData = await promise; + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame')); + + expect(uiSpans?.length).toEqual(2); + + // ignore the first ui span (top-level long animation frame) + const [, eventListenerUISpan] = uiSpans || []; + + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-animation-frame', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + 'browser.script.invoker': 'BUTTON#clickme.onclick', + 'browser.script.invoker_type': 'event-listener', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/assets/script.js new file mode 100644 index 000000000000..9ac3d6fb33d2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 101) { + // + } +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js new file mode 100644 index 000000000000..ca1bf10dcddd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ enableLongTask: true, enableLongAnimationFrame: true, idleTimeout: 9000 }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/template.html new file mode 100644 index 000000000000..5c3a14114991 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/template.html @@ -0,0 +1,10 @@ + + + + + + +
Rendered Before Long Task
+ + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts new file mode 100644 index 000000000000..65fb6664ac82 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-non-chromium/test.ts @@ -0,0 +1,27 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should not capture long animation frame or long task when browser is non-chromium', + async ({ browserName, getLocalTestPath, page }) => { + // Only test non-chromium browsers + if (shouldSkipTracingTest() || browserName === 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBe(0); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/assets/script.js new file mode 100644 index 000000000000..10552eeb5bd5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/assets/script.js @@ -0,0 +1,25 @@ +function getElapsed(startTime) { + const time = Date.now(); + return time - startTime; +} + +function handleClick() { + const startTime = Date.now(); + while (getElapsed(startTime) < 105) { + // + } +} + +function start() { + const startTime = Date.now(); + while (getElapsed(startTime) < 105) { + // + } +} + +// trigger 2 long-animation-frame events +// one from the top-level and the other from an event-listener +start(); + +const button = document.getElementById('clickme'); +button.addEventListener('click', handleClick); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js new file mode 100644 index 000000000000..d81b8932803c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + enableLongTask: true, + enableLongAnimationFrame: true, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/template.html new file mode 100644 index 000000000000..ed02d1117097 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/template.html @@ -0,0 +1,13 @@ + + + + + + +
Rendered Before Long Animation Frame
+ + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts new file mode 100644 index 000000000000..1949e44bd398 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts @@ -0,0 +1,114 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should capture long animation frame for top-level script.', + async ({ browserName, getLocalTestPath, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + // Long animation frame should take priority over long tasks + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const eventData = await promise; + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame')); + + expect(uiSpans?.length).toEqual(1); + + const [topLevelUISpan] = uiSpans || []; + expect(topLevelUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-animation-frame', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + 'code.filepath': 'https://example.com/path/to/script.js', + 'browser.script.source_char_position': 0, + 'browser.script.invoker': 'https://example.com/path/to/script.js', + 'browser.script.invoker_type': 'classic-script', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = topLevelUISpan.start_timestamp ?? 0; + const end = topLevelUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); + +sentryTest( + 'should capture long animation frame for event listener.', + async ({ browserName, getLocalTestPath, page }) => { + // Long animation frames only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + // trigger long animation frame function + await page.getByRole('button').click(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const eventData = await promise; + + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame')); + + expect(uiSpans?.length).toEqual(2); + + // ignore the first ui span (top-level long animation frame) + const [, eventListenerUISpan] = uiSpans || []; + + expect(eventListenerUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-animation-frame', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + data: { + 'browser.script.invoker': 'BUTTON#clickme.onclick', + 'browser.script.invoker_type': 'event-listener', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }, + }), + ); + const start = eventListenerUISpan.start_timestamp ?? 0; + const end = eventListenerUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/assets/script.js new file mode 100644 index 000000000000..5a2aef02028d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 105) { + // + } +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js new file mode 100644 index 000000000000..0e35db50764f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + enableLongTask: true, + enableLongAnimationFrame: false, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/template.html new file mode 100644 index 000000000000..5c3a14114991 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/template.html @@ -0,0 +1,10 @@ + + + + + + +
Rendered Before Long Task
+ + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts new file mode 100644 index 000000000000..6189758c0340 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-no-animation-frame/test.ts @@ -0,0 +1,37 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBeGreaterThan(0); + + const [firstUISpan] = uiSpans || []; + expect(firstUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-task', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + }), + ); + const start = firstUISpan.start_timestamp ?? 0; + const end = firstUISpan.timestamp ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); +}); diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index f59ccbf8da8f..c71b2d70e31d 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -11,6 +11,7 @@ export { addPerformanceEntries, startTrackingInteractions, startTrackingLongTasks, + startTrackingLongAnimationFrames, startTrackingWebVitals, startTrackingINP, registerInpInteractionListener, diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 4e473e42ea47..cb48c2e8b675 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -8,6 +8,7 @@ import { spanToJSON } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../types'; import { + type PerformanceLongAnimationFrameTiming, addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, @@ -120,6 +121,59 @@ export function startTrackingLongTasks(): void { }); } +/** + * Start tracking long animation frames. + */ +export function startTrackingLongAnimationFrames(): void { + // NOTE: the current web-vitals version (3.5.2) does not support long-animation-frame, so + // we directly observe `long-animation-frame` events instead of through the web-vitals + // `observe` helper function. + const observer = new PerformanceObserver(list => { + for (const entry of list.getEntries() as PerformanceLongAnimationFrameTiming[]) { + if (!getActiveSpan()) { + return; + } + if (!entry.scripts[0]) { + return; + } + + const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const duration = msToSec(entry.duration); + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + }; + const initialScript = entry.scripts[0]; + if (initialScript) { + const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; + attributes['browser.script.invoker'] = invoker; + attributes['browser.script.invoker_type'] = invokerType; + if (sourceURL) { + attributes['code.filepath'] = sourceURL; + } + if (sourceFunctionName) { + attributes['code.function'] = sourceFunctionName; + } + if (sourceCharPosition !== -1) { + attributes['browser.script.source_char_position'] = sourceCharPosition; + } + } + + const span = startInactiveSpan({ + name: 'Main UI thread blocked', + op: 'ui.long-animation-frame', + startTime, + attributes, + }); + if (span) { + span.end(startTime + duration); + } + } + }); + + observer.observe({ type: 'long-animation-frame', buffered: true }); +} + /** * Start tracking interaction events. */ diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index e22a345e3116..39292fb19b83 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -36,6 +36,17 @@ interface PerformanceEventTiming extends PerformanceEntry { interactionId?: number; } +interface PerformanceScriptTiming extends PerformanceEntry { + sourceURL: string; + sourceFunctionName: string; + sourceCharPosition: number; + invoker: string; + invokerType: string; +} +export interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { + scripts: PerformanceScriptTiming[]; +} + interface Metric { /** * The name of the metric (in acronym form). diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index c058b1930928..0423831219e2 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -5,6 +5,7 @@ import { registerInpInteractionListener, startTrackingINP, startTrackingInteractions, + startTrackingLongAnimationFrames, startTrackingLongTasks, startTrackingWebVitals, } from '@sentry-internal/browser-utils'; @@ -102,6 +103,13 @@ export interface BrowserTracingOptions { */ enableLongTask: boolean; + /** + * If true, Sentry will capture long animation frames and add them to the corresponding transaction. + * + * Default: false + */ + enableLongAnimationFrame: boolean; + /** * If true, Sentry will capture first input delay and add it to the corresponding transaction. * @@ -160,6 +168,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, + enableLongAnimationFrame: false, enableInp: true, _experiments: {}, ...defaultRequestInstrumentationOptions, @@ -180,6 +189,7 @@ export const browserTracingIntegration = ((_options: Partial