Skip to content

Commit 2f04224

Browse files
authored
feat(tracing): add long animation frame tracing (#12646)
Adds an option to trace long animation frames as per #11719. This tracing feature is disabled by default. Currently, this feature https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing is only supported by Chromium browsers. Usage is opt-in: ```js Sentry.init({ dsn: '__PUBLIC_DSN__', integrations: [ Sentry.browserTracingIntegration({ enableLongAnimationFrame: true, }), ], tracesSampleRate: 1, }); ```
1 parent 196fc09 commit 2f04224

File tree

25 files changed

+607
-2
lines changed

25 files changed

+607
-2
lines changed

Diff for: .size-limit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ module.exports = [
177177
path: createCDNPath('bundle.tracing.replay.min.js'),
178178
gzip: false,
179179
brotli: false,
180-
limit: '220 KB',
180+
limit: '221 KB',
181181
},
182182
{
183183
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(() => {
2+
const startTime = Date.now();
3+
4+
function getElasped() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElasped() < 101) {
10+
//
11+
}
12+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: false, idleTimeout: 9000 }),
9+
],
10+
tracesSampleRate: 1,
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Animation Frame</div>
8+
<script src="https://example.com/path/to/script.js"></script>
9+
</body>
10+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/types';
4+
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
7+
8+
sentryTest(
9+
'should not capture long animation frame when flag is disabled.',
10+
async ({ browserName, getLocalTestPath, page }) => {
11+
// Long animation frames only work on chrome
12+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
13+
sentryTest.skip();
14+
}
15+
16+
await page.route('**/path/to/script.js', (route: Route) =>
17+
route.fulfill({ path: `${__dirname}/assets/script.js` }),
18+
);
19+
20+
const url = await getLocalTestPath({ testDir: __dirname });
21+
22+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
23+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
24+
25+
expect(uiSpans?.length).toBe(0);
26+
},
27+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function getElapsed(startTime) {
2+
const time = Date.now();
3+
return time - startTime;
4+
}
5+
6+
function handleClick() {
7+
const startTime = Date.now();
8+
while (getElapsed(startTime) < 105) {
9+
//
10+
}
11+
}
12+
13+
function start() {
14+
const startTime = Date.now();
15+
while (getElapsed(startTime) < 105) {
16+
//
17+
}
18+
}
19+
20+
// trigger 2 long-animation-frame events
21+
// one from the top-level and the other from an event-listener
22+
start();
23+
24+
const button = document.getElementById('clickme');
25+
button.addEventListener('click', handleClick);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
idleTimeout: 9000,
10+
enableLongTask: false,
11+
enableLongAnimationFrame: true,
12+
}),
13+
],
14+
tracesSampleRate: 1,
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Animation Frame</div>
8+
<button id="clickme">
9+
click me to start the long animation!
10+
</button>
11+
<script src="https://example.com/path/to/script.js"></script>
12+
</body>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/types';
4+
5+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser';
6+
import { sentryTest } from '../../../../utils/fixtures';
7+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
8+
9+
sentryTest(
10+
'should capture long animation frame for top-level script.',
11+
async ({ browserName, getLocalTestPath, page }) => {
12+
// Long animation frames only work on chrome
13+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
14+
sentryTest.skip();
15+
}
16+
17+
await page.route('**/path/to/script.js', (route: Route) =>
18+
route.fulfill({ path: `${__dirname}/assets/script.js` }),
19+
);
20+
21+
const url = await getLocalTestPath({ testDir: __dirname });
22+
23+
const promise = getFirstSentryEnvelopeRequest<Event>(page);
24+
25+
await page.goto(url);
26+
27+
await new Promise(resolve => setTimeout(resolve, 1000));
28+
29+
const eventData = await promise;
30+
31+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame'));
32+
33+
expect(uiSpans?.length).toEqual(1);
34+
35+
const [topLevelUISpan] = uiSpans || [];
36+
expect(topLevelUISpan).toEqual(
37+
expect.objectContaining({
38+
op: 'ui.long-animation-frame',
39+
description: 'Main UI thread blocked',
40+
parent_span_id: eventData.contexts?.trace?.span_id,
41+
data: {
42+
'code.filepath': 'https://example.com/path/to/script.js',
43+
'browser.script.source_char_position': 0,
44+
'browser.script.invoker': 'https://example.com/path/to/script.js',
45+
'browser.script.invoker_type': 'classic-script',
46+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame',
47+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
48+
},
49+
}),
50+
);
51+
const start = topLevelUISpan.start_timestamp ?? 0;
52+
const end = topLevelUISpan.timestamp ?? 0;
53+
const duration = end - start;
54+
55+
expect(duration).toBeGreaterThanOrEqual(0.1);
56+
expect(duration).toBeLessThanOrEqual(0.15);
57+
},
58+
);
59+
60+
sentryTest(
61+
'should capture long animation frame for event listener.',
62+
async ({ browserName, getLocalTestPath, page }) => {
63+
// Long animation frames only work on chrome
64+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
65+
sentryTest.skip();
66+
}
67+
68+
await page.route('**/path/to/script.js', (route: Route) =>
69+
route.fulfill({ path: `${__dirname}/assets/script.js` }),
70+
);
71+
72+
const url = await getLocalTestPath({ testDir: __dirname });
73+
74+
const promise = getFirstSentryEnvelopeRequest<Event>(page);
75+
76+
await page.goto(url);
77+
78+
// trigger long animation frame function
79+
await page.getByRole('button').click();
80+
81+
await new Promise(resolve => setTimeout(resolve, 1000));
82+
83+
const eventData = await promise;
84+
85+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui.long-animation-frame'));
86+
87+
expect(uiSpans?.length).toEqual(2);
88+
89+
// ignore the first ui span (top-level long animation frame)
90+
const [, eventListenerUISpan] = uiSpans || [];
91+
92+
expect(eventListenerUISpan).toEqual(
93+
expect.objectContaining({
94+
op: 'ui.long-animation-frame',
95+
description: 'Main UI thread blocked',
96+
parent_span_id: eventData.contexts?.trace?.span_id,
97+
data: {
98+
'browser.script.invoker': 'BUTTON#clickme.onclick',
99+
'browser.script.invoker_type': 'event-listener',
100+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame',
101+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
102+
},
103+
}),
104+
);
105+
const start = eventListenerUISpan.start_timestamp ?? 0;
106+
const end = eventListenerUISpan.timestamp ?? 0;
107+
const duration = end - start;
108+
109+
expect(duration).toBeGreaterThanOrEqual(0.1);
110+
expect(duration).toBeLessThanOrEqual(0.15);
111+
},
112+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(() => {
2+
const startTime = Date.now();
3+
4+
function getElasped() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElasped() < 101) {
10+
//
11+
}
12+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({ enableLongTask: true, enableLongAnimationFrame: true, idleTimeout: 9000 }),
9+
],
10+
tracesSampleRate: 1,
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Task</div>
8+
<script src="https://example.com/path/to/script.js"></script>
9+
</body>
10+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/types';
4+
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
7+
8+
sentryTest(
9+
'should not capture long animation frame or long task when browser is non-chromium',
10+
async ({ browserName, getLocalTestPath, page }) => {
11+
// Only test non-chromium browsers
12+
if (shouldSkipTracingTest() || browserName === 'chromium') {
13+
sentryTest.skip();
14+
}
15+
16+
await page.route('**/path/to/script.js', (route: Route) =>
17+
route.fulfill({ path: `${__dirname}/assets/script.js` }),
18+
);
19+
20+
const url = await getLocalTestPath({ testDir: __dirname });
21+
22+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
23+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
24+
25+
expect(uiSpans?.length).toBe(0);
26+
},
27+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function getElapsed(startTime) {
2+
const time = Date.now();
3+
return time - startTime;
4+
}
5+
6+
function handleClick() {
7+
const startTime = Date.now();
8+
while (getElapsed(startTime) < 105) {
9+
//
10+
}
11+
}
12+
13+
function start() {
14+
const startTime = Date.now();
15+
while (getElapsed(startTime) < 105) {
16+
//
17+
}
18+
}
19+
20+
// trigger 2 long-animation-frame events
21+
// one from the top-level and the other from an event-listener
22+
start();
23+
24+
const button = document.getElementById('clickme');
25+
button.addEventListener('click', handleClick);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
idleTimeout: 9000,
10+
enableLongTask: true,
11+
enableLongAnimationFrame: true,
12+
}),
13+
],
14+
tracesSampleRate: 1,
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Animation Frame</div>
8+
<button id="clickme">
9+
click me to start the long animation!
10+
</button>
11+
<script src="https://example.com/path/to/script.js"></script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)