Skip to content

Commit 361c5a4

Browse files
feat(tracing): Track PerformanceObserver interactions as spans (#7331)
Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent 295ea3d commit 361c5a4

File tree

7 files changed

+100
-22
lines changed

7 files changed

+100
-22
lines changed
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
(() => {
1+
const delay = e => {
22
const startTime = Date.now();
33

44
function getElasped() {
55
const time = Date.now();
66
return time - startTime;
77
}
88

9-
while (getElasped() < 105) {
9+
while (getElasped() < 70) {
1010
//
1111
}
12-
})();
12+
13+
e.target.classList.add('clicked');
14+
};
15+
16+
document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);

packages/integration-tests/suites/tracing/browsertracing/interactions/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Sentry.init({
1010
idleTimeout: 1000,
1111
_experiments: {
1212
enableInteractions: true,
13+
enableLongTask: false,
1314
},
1415
}),
1516
],

packages/integration-tests/suites/tracing/browsertracing/interactions/template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</head>
66
<body>
77
<div>Rendered Before Long Task</div>
8-
<script src="https://example.com/path/to/script.js"></script>
98
<button data-test-id="interaction-button">Click Me</button>
9+
<script src="https://example.com/path/to/script.js"></script>
1010
</body>
1111
</html>
Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,73 @@
11
import type { Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Event } from '@sentry/types';
3+
import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
44

55
import { sentryTest } from '../../../../utils/fixtures';
66
import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers';
77

8+
type TransactionJSON = ReturnType<Transaction['toJSON']> & {
9+
spans: ReturnType<Span['toJSON']>[];
10+
contexts: SpanContext;
11+
platform: string;
12+
type: string;
13+
};
14+
15+
const wait = (time: number) => new Promise(res => setTimeout(res, time));
16+
817
sentryTest('should capture interaction transaction.', async ({ browserName, getLocalTestPath, page }) => {
9-
if (browserName !== 'chromium') {
18+
const supportedBrowsers = ['chromium', 'firefox'];
19+
20+
if (!supportedBrowsers.includes(browserName)) {
1021
sentryTest.skip();
1122
}
1223

1324
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
1425

1526
const url = await getLocalTestPath({ testDir: __dirname });
1627

17-
await getFirstSentryEnvelopeRequest<Event>(page, url);
28+
await page.goto(url);
29+
await getFirstSentryEnvelopeRequest<Event>(page);
1830

1931
await page.locator('[data-test-id=interaction-button]').click();
32+
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
33+
34+
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
35+
expect(envelopes).toHaveLength(1);
2036

21-
const envelopes = await getMultipleSentryEnvelopeRequests<Event>(page, 1);
2237
const eventData = envelopes[0];
2338

24-
expect(eventData).toEqual(
25-
expect.objectContaining({
26-
contexts: expect.objectContaining({
27-
trace: expect.objectContaining({
28-
op: 'ui.action.click',
29-
}),
30-
}),
31-
platform: 'javascript',
32-
spans: [],
33-
tags: {},
34-
type: 'transaction',
35-
}),
36-
);
39+
expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } });
40+
expect(eventData.platform).toBe('javascript');
41+
expect(eventData.type).toBe('transaction');
42+
expect(eventData.spans).toHaveLength(1);
43+
44+
const interactionSpan = eventData.spans![0];
45+
expect(interactionSpan.op).toBe('ui.interaction.click');
46+
expect(interactionSpan.description).toBe('body > button.clicked');
47+
expect(interactionSpan.timestamp).toBeDefined();
48+
49+
const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000;
50+
expect(interactionSpanDuration).toBeGreaterThan(70);
51+
expect(interactionSpanDuration).toBeLessThan(200);
52+
});
53+
54+
sentryTest('should create only one transaction per interaction', async ({ browserName, getLocalTestPath, page }) => {
55+
const supportedBrowsers = ['chromium', 'firefox'];
56+
57+
if (!supportedBrowsers.includes(browserName)) {
58+
sentryTest.skip();
59+
}
60+
61+
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
62+
63+
const url = await getLocalTestPath({ testDir: __dirname });
64+
await page.goto(url);
65+
await getFirstSentryEnvelopeRequest<Event>(page);
66+
67+
for (let i = 0; i < 4; i++) {
68+
await wait(100);
69+
await page.locator('[data-test-id=interaction-button]').click();
70+
const envelope = await getMultipleSentryEnvelopeRequests<Event>(page, 1);
71+
expect(envelope[0].spans).toHaveLength(1);
72+
}
3773
});

packages/tracing/src/browser/browsertracing.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import type { EventProcessor, Integration, Transaction, TransactionContext, Tran
55
import { baggageHeaderToDynamicSamplingContext, getDomElement, logger } from '@sentry/utils';
66

77
import { registerBackgroundTabDetection } from './backgroundtab';
8-
import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics';
8+
import {
9+
addPerformanceEntries,
10+
startTrackingInteractions,
11+
startTrackingLongTasks,
12+
startTrackingWebVitals,
13+
} from './metrics';
914
import type { RequestInstrumentationOptions } from './request';
1015
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
1116
import { instrumentRoutingWithDefaults } from './router';
@@ -189,6 +194,9 @@ export class BrowserTracing implements Integration {
189194
if (this.options.enableLongTask) {
190195
startTrackingLongTasks();
191196
}
197+
if (this.options._experiments.enableInteractions) {
198+
startTrackingInteractions();
199+
}
192200
}
193201

194202
/**

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,34 @@ export function startTrackingLongTasks(): void {
7171
observe('longtask', entryHandler);
7272
}
7373

74+
/**
75+
* Start tracking interaction events.
76+
*/
77+
export function startTrackingInteractions(): void {
78+
const entryHandler = (entries: PerformanceEventTiming[]): void => {
79+
for (const entry of entries) {
80+
const transaction = getActiveTransaction() as IdleTransaction | undefined;
81+
if (!transaction) {
82+
return;
83+
}
84+
85+
if (entry.name === 'click') {
86+
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
87+
const duration = msToSec(entry.duration);
88+
89+
transaction.startChild({
90+
description: htmlTreeAsString(entry.target),
91+
op: `ui.interaction.${entry.name}`,
92+
startTimestamp: startTime,
93+
endTimestamp: startTime + duration,
94+
});
95+
}
96+
}
97+
};
98+
99+
observe('event', entryHandler, { durationThreshold: 0 });
100+
}
101+
74102
/** Starts tracking the Cumulative Layout Shift on the current page. */
75103
function _trackCLS(): void {
76104
// See:

packages/tracing/src/browser/web-vitals/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ declare global {
135135
interface PerformanceEventTiming extends PerformanceEntry {
136136
duration: DOMHighResTimeStamp;
137137
interactionId?: number;
138+
readonly target: Node | null;
138139
}
139140

140141
// https://wicg.github.io/layout-instability/#sec-layout-shift-attribution

0 commit comments

Comments
 (0)