Skip to content

Commit dbd8340

Browse files
authored
feat(tracing): Add long task collection (#5529)
* rfc(perf): Add long task collection This adds long task collection to represent long running ui behaviour as spans in your frontend transactions. Can be disabled by passing an experimental flag for now.
1 parent 1718e98 commit dbd8340

File tree

13 files changed

+174
-5
lines changed

13 files changed

+174
-5
lines changed
Lines changed: 12 additions & 0 deletions
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+
})();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { Integrations } from '@sentry/tracing';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [new Integrations.BrowserTracing({ _experiments: { enableLongTasks: false }, idleTimeout: 9000 })],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<html>
2+
<head>
3+
<meta charset="utf-8" />
4+
</head>
5+
<body>
6+
<div>Rendered Before Long Task</div>
7+
<script src="https://example.com/path/to/script.js"></script>
8+
</body>
9+
</html>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect, Route } from '@playwright/test';
2+
import { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
6+
7+
sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestPath, page }) => {
8+
// Long tasks only work on chrome
9+
if (browserName !== 'chromium') {
10+
sentryTest.skip();
11+
}
12+
13+
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
14+
15+
const url = await getLocalTestPath({ testDir: __dirname });
16+
17+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
18+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
19+
20+
expect(uiSpans?.length).toBe(0);
21+
});
Lines changed: 12 additions & 0 deletions
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() < 105) {
10+
//
11+
}
12+
})();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { Integrations } from '@sentry/tracing';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [
9+
new Integrations.BrowserTracing({
10+
idleTimeout: 9000,
11+
}),
12+
],
13+
tracesSampleRate: 1,
14+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<html>
2+
<head>
3+
<meta charset="utf-8" />
4+
</head>
5+
<body>
6+
<div>Rendered Before Long Task</div>
7+
<script src="https://example.com/path/to/script.js"></script>
8+
</body>
9+
</html>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect, Route } from '@playwright/test';
2+
import { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
6+
7+
sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => {
8+
// Long tasks only work on chrome
9+
if (browserName !== 'chromium') {
10+
sentryTest.skip();
11+
}
12+
13+
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
14+
15+
const url = await getLocalTestPath({ testDir: __dirname });
16+
17+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
18+
const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
19+
20+
expect(uiSpans?.length).toBe(1);
21+
22+
const [firstUISpan] = uiSpans || [];
23+
expect(firstUISpan).toEqual(
24+
expect.objectContaining({
25+
op: 'ui.long-task',
26+
description: 'Long Task',
27+
parent_span_id: eventData.contexts?.trace.span_id,
28+
}),
29+
);
30+
const start = firstUISpan['start_timestamp'] ?? 0;
31+
const end = firstUISpan['timestamp'] ?? 0;
32+
const duration = end - start;
33+
34+
expect(duration).toBeGreaterThanOrEqual(0.1);
35+
expect(duration).toBeLessThanOrEqual(0.15);
36+
});

packages/nextjs/test/integration/test/client/tracingFetch.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
1+
const {
2+
expectRequestCount,
3+
isTransactionRequest,
4+
expectTransaction,
5+
extractEnvelopeFromRequest,
6+
} = require('../utils/client');
27

38
module.exports = async ({ page, url, requests }) => {
49
await page.goto(`${url}/fetch`);
@@ -21,6 +26,5 @@ module.exports = async ({ page, url, requests }) => {
2126
},
2227
],
2328
});
24-
2529
await expectRequestCount(requests, { transactions: 1 });
2630
};

packages/nextjs/test/integration/test/utils/client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { strictEqual } = require('assert');
2+
const expect = require('expect');
23
const { logIf, parseEnvelope } = require('./common');
34

45
const VALID_REQUEST_PAYLOAD = {
@@ -105,8 +106,10 @@ const assertObjectMatches = (actual, expected) => {
105106
for (const key in expected) {
106107
const expectedValue = expected[key];
107108

108-
if (Object.prototype.toString.call(expectedValue) === '[object Object]' || Array.isArray(expectedValue)) {
109+
if (Object.prototype.toString.call(expectedValue) === '[object Object]') {
109110
assertObjectMatches(actual[key], expectedValue);
111+
} else if (Array.isArray(expectedValue)) {
112+
expect(actual[key]).toEqual(expect.arrayContaining(expectedValue.map(expect.objectContaining)));
110113
} else {
111114
strictEqual(actual[key], expectedValue);
112115
}

packages/tracing/src/browser/browsertracing.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import { Hub } from '@sentry/hub';
23
import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types';
34
import { getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
@@ -6,7 +7,7 @@ import { startIdleTransaction } from '../hubextensions';
67
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
78
import { extractTraceparentData } from '../utils';
89
import { registerBackgroundTabDetection } from './backgroundtab';
9-
import { addPerformanceEntries, startTrackingWebVitals } from './metrics';
10+
import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics';
1011
import {
1112
defaultRequestInstrumentationOptions,
1213
instrumentOutgoingRequests,
@@ -71,6 +72,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
7172
*/
7273
_metricOptions?: Partial<{ _reportAllChanges: boolean }>;
7374

75+
/**
76+
* _experiments allows the user to send options to define how this integration works.
77+
*
78+
* Default: undefined
79+
*/
80+
_experiments?: Partial<{ enableLongTask: boolean }>;
81+
7482
/**
7583
* beforeNavigate is called before a pageload/navigation transaction is created and allows users to modify transaction
7684
* context data, or drop the transaction entirely (by setting `sampled = false` in the context).
@@ -101,6 +109,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS = {
101109
routingInstrumentation: instrumentRoutingWithDefaults,
102110
startTransactionOnLocationChange: true,
103111
startTransactionOnPageLoad: true,
112+
_experiments: { enableLongTask: true },
104113
...defaultRequestInstrumentationOptions,
105114
};
106115

@@ -148,6 +157,9 @@ export class BrowserTracing implements Integration {
148157

149158
const { _metricOptions } = this.options;
150159
startTrackingWebVitals(_metricOptions && _metricOptions._reportAllChanges);
160+
if (this.options._experiments?.enableLongTask) {
161+
startTrackingLongTasks();
162+
}
151163
}
152164

153165
/**

packages/tracing/src/browser/metrics/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import { Measurements } from '@sentry/types';
33
import { browserPerformanceTimeOrigin, getGlobalObject, htmlTreeAsString, logger } from '@sentry/utils';
44

5+
import { IdleTransaction } from '../../idletransaction';
56
import { Transaction } from '../../transaction';
6-
import { msToSec } from '../../utils';
7+
import { getActiveTransaction, msToSec } from '../../utils';
78
import { getCLS, LayoutShift } from '../web-vitals/getCLS';
89
import { getFID } from '../web-vitals/getFID';
910
import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP';
1011
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
12+
import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe';
1113
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types';
1214
import { _startChild, isMeasurementValue } from './utils';
1315

@@ -38,6 +40,28 @@ export function startTrackingWebVitals(reportAllChanges: boolean = false): void
3840
}
3941
}
4042

43+
/**
44+
* Start tracking long tasks.
45+
*/
46+
export function startTrackingLongTasks(): void {
47+
const entryHandler: PerformanceEntryHandler = (entry: PerformanceEntry): void => {
48+
const transaction = getActiveTransaction() as IdleTransaction | undefined;
49+
if (!transaction) {
50+
return;
51+
}
52+
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
53+
const duration = msToSec(entry.duration);
54+
transaction.startChild({
55+
description: 'Long Task',
56+
op: 'ui.long-task',
57+
startTimestamp: startTime,
58+
endTimestamp: startTime + duration,
59+
});
60+
};
61+
62+
observe('longtask', entryHandler);
63+
}
64+
4165
/** Starts tracking the Cumulative Layout Shift on the current page. */
4266
function _trackCLS(): void {
4367
// See:

packages/tracing/test/browser/browsertracing.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ describe('BrowserTracing', () => {
8989
const browserTracing = createBrowserTracing();
9090

9191
expect(browserTracing.options).toEqual({
92+
_experiments: {
93+
enableLongTask: true,
94+
},
9295
idleTimeout: DEFAULT_IDLE_TIMEOUT,
9396
finalTimeout: DEFAULT_FINAL_TIMEOUT,
9497
markBackgroundTransactions: true,

0 commit comments

Comments
 (0)