Skip to content

Commit

Permalink
feat(tracing): Track PerformanceObserver interactions as spans (#7331)
Browse files Browse the repository at this point in the history
Co-authored-by: Abhijeet Prasad <aprasad@sentry.io>
  • Loading branch information
0Calories and AbhiPrasad committed Mar 9, 2023
1 parent 295ea3d commit 361c5a4
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
(() => {
const delay = e => {
const startTime = Date.now();

function getElasped() {
const time = Date.now();
return time - startTime;
}

while (getElasped() < 105) {
while (getElasped() < 70) {
//
}
})();

e.target.classList.add('clicked');
};

document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Sentry.init({
idleTimeout: 1000,
_experiments: {
enableInteractions: true,
enableLongTask: false,
},
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</head>
<body>
<div>Rendered Before Long Task</div>
<script src="https://example.com/path/to/script.js"></script>
<button data-test-id="interaction-button">Click Me</button>
<script src="https://example.com/path/to/script.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,37 +1,73 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';
import type { Event, Span, SpanContext, Transaction } from '@sentry/types';

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

type TransactionJSON = ReturnType<Transaction['toJSON']> & {
spans: ReturnType<Span['toJSON']>[];
contexts: SpanContext;
platform: string;
type: string;
};

const wait = (time: number) => new Promise(res => setTimeout(res, time));

sentryTest('should capture interaction transaction.', async ({ browserName, getLocalTestPath, page }) => {
if (browserName !== 'chromium') {
const supportedBrowsers = ['chromium', 'firefox'];

if (!supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

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

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

await getFirstSentryEnvelopeRequest<Event>(page, url);
await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);

await page.locator('[data-test-id=interaction-button]').click();
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();

const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
expect(envelopes).toHaveLength(1);

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

expect(eventData).toEqual(
expect.objectContaining({
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'ui.action.click',
}),
}),
platform: 'javascript',
spans: [],
tags: {},
type: 'transaction',
}),
);
expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } });
expect(eventData.platform).toBe('javascript');
expect(eventData.type).toBe('transaction');
expect(eventData.spans).toHaveLength(1);

const interactionSpan = eventData.spans![0];
expect(interactionSpan.op).toBe('ui.interaction.click');
expect(interactionSpan.description).toBe('body > button.clicked');
expect(interactionSpan.timestamp).toBeDefined();

const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000;
expect(interactionSpanDuration).toBeGreaterThan(70);
expect(interactionSpanDuration).toBeLessThan(200);
});

sentryTest('should create only one transaction per interaction', async ({ browserName, getLocalTestPath, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];

if (!supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

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

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);

for (let i = 0; i < 4; i++) {
await wait(100);
await page.locator('[data-test-id=interaction-button]').click();
const envelope = await getMultipleSentryEnvelopeRequests<Event>(page, 1);
expect(envelope[0].spans).toHaveLength(1);
}
});
10 changes: 9 additions & 1 deletion packages/tracing/src/browser/browsertracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import type { EventProcessor, Integration, Transaction, TransactionContext, Tran
import { baggageHeaderToDynamicSamplingContext, getDomElement, logger } from '@sentry/utils';

import { registerBackgroundTabDetection } from './backgroundtab';
import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics';
import {
addPerformanceEntries,
startTrackingInteractions,
startTrackingLongTasks,
startTrackingWebVitals,
} from './metrics';
import type { RequestInstrumentationOptions } from './request';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
import { instrumentRoutingWithDefaults } from './router';
Expand Down Expand Up @@ -189,6 +194,9 @@ export class BrowserTracing implements Integration {
if (this.options.enableLongTask) {
startTrackingLongTasks();
}
if (this.options._experiments.enableInteractions) {
startTrackingInteractions();
}
}

/**
Expand Down
28 changes: 28 additions & 0 deletions packages/tracing/src/browser/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ export function startTrackingLongTasks(): void {
observe('longtask', entryHandler);
}

/**
* Start tracking interaction events.
*/
export function startTrackingInteractions(): void {
const entryHandler = (entries: PerformanceEventTiming[]): void => {
for (const entry of entries) {
const transaction = getActiveTransaction() as IdleTransaction | undefined;
if (!transaction) {
return;
}

if (entry.name === 'click') {
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
const duration = msToSec(entry.duration);

transaction.startChild({
description: htmlTreeAsString(entry.target),
op: `ui.interaction.${entry.name}`,
startTimestamp: startTime,
endTimestamp: startTime + duration,
});
}
}
};

observe('event', entryHandler, { durationThreshold: 0 });
}

/** Starts tracking the Cumulative Layout Shift on the current page. */
function _trackCLS(): void {
// See:
Expand Down
1 change: 1 addition & 0 deletions packages/tracing/src/browser/web-vitals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ declare global {
interface PerformanceEventTiming extends PerformanceEntry {
duration: DOMHighResTimeStamp;
interactionId?: number;
readonly target: Node | null;
}

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

0 comments on commit 361c5a4

Please sign in to comment.