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