From 361c5a43e18ca444ae9e1278cf3b7fc6c3bbfd8e Mon Sep 17 00:00:00 2001 From: Ash Anand <0Calories@users.noreply.github.com> Date: Thu, 9 Mar 2023 04:05:27 -0500 Subject: [PATCH] feat(tracing): Track PerformanceObserver interactions as spans (#7331) Co-authored-by: Abhijeet Prasad --- .../interactions/assets/script.js | 10 ++- .../browsertracing/interactions/init.js | 1 + .../browsertracing/interactions/template.html | 2 +- .../browsertracing/interactions/test.ts | 70 ++++++++++++++----- .../tracing/src/browser/browsertracing.ts | 10 ++- packages/tracing/src/browser/metrics/index.ts | 28 ++++++++ .../tracing/src/browser/web-vitals/types.ts | 1 + 7 files changed, 100 insertions(+), 22 deletions(-) diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js b/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js index 5a2aef02028d..89d814bd397d 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js @@ -1,4 +1,4 @@ -(() => { +const delay = e => { const startTime = Date.now(); function getElasped() { @@ -6,7 +6,11 @@ return time - startTime; } - while (getElasped() < 105) { + while (getElasped() < 70) { // } -})(); + + e.target.classList.add('clicked'); +}; + +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js b/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js index 5229401c2ef5..d30222b7f47e 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js @@ -10,6 +10,7 @@ Sentry.init({ idleTimeout: 1000, _experiments: { enableInteractions: true, + enableLongTask: false, }, }), ], diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html b/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html index e74a9c17eeb2..e16deb9ee519 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html @@ -5,7 +5,7 @@
Rendered Before Long Task
- + diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts b/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts index b9a70ebda3ec..faff888fc2e8 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -1,12 +1,23 @@ 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 & { + spans: ReturnType[]; + 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(); } @@ -14,24 +25,49 @@ sentryTest('should capture interaction transaction.', async ({ browserName, getL const url = await getLocalTestPath({ testDir: __dirname }); - await getFirstSentryEnvelopeRequest(page, url); + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); - const envelopes = await getMultipleSentryEnvelopeRequests(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(page); + + for (let i = 0; i < 4; i++) { + await wait(100); + await page.locator('[data-test-id=interaction-button]').click(); + const envelope = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelope[0].spans).toHaveLength(1); + } }); diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index edd2baf71d9f..28615763a949 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -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'; @@ -189,6 +194,9 @@ export class BrowserTracing implements Integration { if (this.options.enableLongTask) { startTrackingLongTasks(); } + if (this.options._experiments.enableInteractions) { + startTrackingInteractions(); + } } /** diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index 2d37ef8c1919..7f0107408502 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -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: diff --git a/packages/tracing/src/browser/web-vitals/types.ts b/packages/tracing/src/browser/web-vitals/types.ts index ef8de70c12bb..b4096b2678f6 100644 --- a/packages/tracing/src/browser/web-vitals/types.ts +++ b/packages/tracing/src/browser/web-vitals/types.ts @@ -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