From 9c41e520d54c49950d9837ad24e33781fe503c1f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 17 Apr 2024 12:41:45 +0200 Subject: [PATCH 01/11] WIP INP in v8?? --- .../interactions/init.js | 1 + .../interactions/test.ts | 76 +++++---- packages/browser-utils/src/index.ts | 1 + .../src/metrics/browserMetrics.ts | 19 +-- packages/browser-utils/src/metrics/inp.ts | 152 ++++++++++++++++++ .../browser-utils/src/metrics/instrument.ts | 33 +++- packages/browser-utils/src/metrics/utils.ts | 18 ++- packages/browser/src/profiling/utils.ts | 3 + .../src/tracing/browserTracingIntegration.ts | 14 ++ packages/core/src/semanticAttributes.ts | 7 + packages/core/src/tracing/sentrySpan.ts | 4 + packages/types/src/span.ts | 2 + 12 files changed, 280 insertions(+), 50 deletions(-) create mode 100644 packages/browser-utils/src/metrics/inp.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js index 846538e7f3f0..97bbfdafdabb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js @@ -11,6 +11,7 @@ Sentry.init({ _experiments: { enableInteractions: true, }, + enableInp: true, }), ], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index 5b032ace7f8c..c8723b8319f7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -1,6 +1,6 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Contexts, Event, SpanJSON } from '@sentry/types'; +import type { Contexts, Event as SentryEvent, Measurements, SpanJSON } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -30,7 +30,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); @@ -70,12 +70,12 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + 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); + const envelope = await getMultipleSentryEnvelopeRequests(page, 1); expect(envelope[0].spans).toHaveLength(1); } }, @@ -97,7 +97,7 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); await page.locator('[data-test-id=annotated-button]').click(); @@ -113,34 +113,50 @@ sentryTest( }, ); -sentryTest( - 'should use the element name for a clicked element when no component name', - async ({ browserName, getLocalTestPath, page }) => { - const supportedBrowsers = ['chromium', 'firefox']; - - if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { - sentryTest.skip(); - } - - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); +sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; - const url = await getLocalTestPath({ testDir: __dirname }); + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } - await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); - await page.locator('[data-test-id=styled-button]').click(); + const url = await getLocalTestPath({ testDir: __dirname }); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); - expect(envelopes).toHaveLength(1); - const eventData = envelopes[0]; + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); - expect(eventData.spans).toHaveLength(1); + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); - const interactionSpan = eventData.spans![0]; - expect(interactionSpan.op).toBe('ui.interaction.click'); - expect(interactionSpan.description).toBe('body > StyledButton'); - }, -); + // Wait for the interaction transaction from the enableInteractions experiment + await getMultipleSentryEnvelopeRequests(page, 1); + + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests< + SpanJSON & { exclusive_time: number; measurements: Measurements } + >(page, 1, { + envelopeType: 'span', + }); + // Page hide to trigger INP + await page.evaluate(() => { + window.dispatchEvent(new Event('pagehide')); + }); + + // Get the INP span envelope + const spanEnvelopes = await spanEnvelopesPromise; + + expect(spanEnvelopes).toHaveLength(1); + expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); + expect(spanEnvelopes[0].description).toBe('body > button.clicked'); + expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); + expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0); + expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond'); +}); diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index f65c78a6d3bf..53e2f24068d6 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -11,6 +11,7 @@ export { startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals, + startTrackingINP, } from './metrics/browserMetrics'; export { addClickKeypressInstrumentationHandler } from './instrument/dom'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 7c6620ff7911..deca3765e404 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -14,7 +14,7 @@ import { addTtfbInstrumentationHandler, } from './instrument'; import { WINDOW } from './types'; -import { isMeasurementValue, startAndEndSpan } from './utils'; +import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; @@ -58,19 +58,6 @@ interface NavigatorDeviceMemory { const MAX_INT_AS_BYTES = 2147483647; -/** - * Converts from milliseconds to seconds - * @param time time in ms - */ -function msToSec(time: number): number { - return time / 1000; -} - -function getBrowserPerformanceAPI(): Performance | undefined { - // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are - return WINDOW && WINDOW.addEventListener && WINDOW.performance; -} - let _performanceCursor: number = 0; let _measurements: Measurements = {}; @@ -170,6 +157,8 @@ export function startTrackingInteractions(): void { }); } +export { startTrackingINP } from './inp'; + /** Starts tracking the Cumulative Layout Shift on the current page. */ function _trackCLS(): () => void { return addClsInstrumentationHandler(({ metric }) => { @@ -226,7 +215,7 @@ function _trackTtfb(): () => void { }); } -/** Add performance related spans to a span */ +/** Add performance related spans to a transaction */ export function addPerformanceEntries(span: Span): void { const performance = getBrowserPerformanceAPI(); if (!performance || !WINDOW.performance.getEntries || !browserPerformanceTimeOrigin) { diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts new file mode 100644 index 000000000000..bd1a04b4f427 --- /dev/null +++ b/packages/browser-utils/src/metrics/inp.ts @@ -0,0 +1,152 @@ +import { + SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, + SentrySpan, + createSpanEnvelope, + getActiveSpan, + getClient, + getCurrentScope, + getRootSpan, + sampleSpan, + spanIsSampled, + spanToJSON, +} from '@sentry/core'; +import type { Integration, SpanAttributes } from '@sentry/types'; +import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString, logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../debug-build'; +import { addInpInstrumentationHandler } from './instrument'; +import { getBrowserPerformanceAPI, msToSec } from './utils'; + +/** + * Start tracking INP webvital events. + */ +export function startTrackingINP(): () => void { + const performance = getBrowserPerformanceAPI(); + if (performance && browserPerformanceTimeOrigin) { + const inpCallback = _trackINP(); + + return (): void => { + inpCallback(); + }; + } + + return () => undefined; +} + +const INP_ENTRY_MAP: Record = { + click: 'click', + pointerdown: 'click', + pointerup: 'click', + mousedown: 'click', + mouseup: 'click', + touchstart: 'click', + touchend: 'click', + mouseover: 'hover', + mouseout: 'hover', + mouseenter: 'hover', + mouseleave: 'hover', + pointerover: 'hover', + pointerout: 'hover', + pointerenter: 'hover', + pointerleave: 'hover', + dragstart: 'drag', + dragend: 'drag', + drag: 'drag', + dragenter: 'drag', + dragleave: 'drag', + dragover: 'drag', + drop: 'drag', + keydown: 'press', + keyup: 'press', + keypress: 'press', + input: 'press', +}; + +/** Starts tracking the Interaction to Next Paint on the current page. */ +function _trackINP(): () => void { + return addInpInstrumentationHandler(({ metric }) => { + const client = getClient(); + if (!client || metric.value == undefined) { + return; + } + + const entry = metric.entries.find(entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name]); + + if (!entry) { + return; + } + + const interactionType = INP_ENTRY_MAP[entry.name]; + + const options = client.getOptions(); + /** Build the INP span, create an envelope from the span, and then send the envelope */ + const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const duration = msToSec(metric.value); + const scope = getCurrentScope(); + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + + const routeName = rootSpan ? spanToJSON(rootSpan).description : undefined; + const user = scope.getUser(); + + // We need to get the replay, user, and activeTransaction from the current scope + // so that we can associate replay id, profile id, and a user display to the span + const replay = client.getIntegrationByName string }>('Replay'); + + const replayId = replay && replay.getReplayId(); + + const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined; + const profileId = scope.getScopeData().contexts?.profile?.profile_id as string | undefined; + + const name = htmlTreeAsString(entry.target); + const parentSampled = activeSpan ? spanIsSampled(activeSpan) : undefined; + const attributes: SpanAttributes = dropUndefinedKeys({ + release: options.release, + environment: options.environment, + transaction: routeName, + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: metric.value, + user: userDisplay || undefined, + profile_id: profileId || undefined, + replay_id: replayId || undefined, + }); + + /** Check to see if the span should be sampled */ + const [sampled] = sampleSpan(options, { + name, + parentSampled, + attributes, + transactionContext: { + name, + parentSampled, + }, + }); + + // Nothing to do + if (!sampled) { + return; + } + + const span = new SentrySpan({ + startTimestamp: startTime, + endTimestamp: startTime + duration, + op: `ui.interaction.${interactionType}`, + name, + attributes, + }); + + span.addEvent('inp', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, + }); + + const envelope = span ? createSpanEnvelope([span]) : undefined; + const transport = client && client.getTransport(); + if (transport && envelope) { + transport.send(envelope).then(null, reason => { + DEBUG_BUILD && logger.error('Error while sending interaction:', reason); + }); + } + return; + }); +} diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 535f584dddb1..ad390191266f 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -3,13 +3,14 @@ import { getFunctionName, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { onCLS } from './web-vitals/getCLS'; import { onFID } from './web-vitals/getFID'; +import { onINP } from './web-vitals/getINP'; import { onLCP } from './web-vitals/getLCP'; import { observe } from './web-vitals/lib/observe'; import { onTTFB } from './web-vitals/onTTFB'; type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource'; -type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb'; +type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp'; // We provide this here manually instead of relying on a global, as this is not available in non-browser environements // And we do not want to expose such types @@ -20,6 +21,14 @@ interface PerformanceEntry { readonly startTime: number; toJSON(): Record; } +interface PerformanceEventTiming extends PerformanceEntry { + processingStart: number; + processingEnd: number; + duration: number; + cancelable?: boolean; + target?: unknown | null; + interactionId?: number; +} interface Metric { /** @@ -88,6 +97,7 @@ let _previousCls: Metric | undefined; let _previousFid: Metric | undefined; let _previousLcp: Metric | undefined; let _previousTtfb: Metric | undefined; +let _previousInp: Metric | undefined; /** * Add a callback that will be triggered when a CLS metric is available. @@ -132,9 +142,19 @@ export function addTtfbInstrumentationHandler(callback: (data: { metric: Metric return addMetricObserver('ttfb', callback, instrumentTtfb, _previousTtfb); } +/** + * Add a callback that will be triggered when a INP metric is available. + * Returns a cleanup callback which can be called to remove the instrumentation handler. + */ +export function addInpInstrumentationHandler( + callback: (data: { metric: Omit & { entries: PerformanceEventTiming[] } }) => void, +): CleanupHandlerCallback { + return addMetricObserver('inp', callback, instrumentInp, _previousInp); +} + export function addPerformanceInstrumentationHandler( type: 'event', - callback: (data: { entries: (PerformanceEntry & { target?: unknown | null })[] }) => void, + callback: (data: { entries: ((PerformanceEntry & { target?: unknown | null }) | PerformanceEventTiming)[] }) => void, ): CleanupHandlerCallback; export function addPerformanceInstrumentationHandler( type: InstrumentHandlerTypePerformanceObserver, @@ -217,6 +237,15 @@ function instrumentTtfb(): StopListening { }); } +function instrumentInp(): void { + return onINP(metric => { + triggerHandlers('inp', { + metric, + }); + _previousInp = metric; + }); +} + function addMetricObserver( type: InstrumentHandlerTypeMetric, callback: InstrumentHandlerCallback, diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index facb3f429e3a..1927f0e9a971 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -1,6 +1,7 @@ import type { SentrySpan } from '@sentry/core'; import { spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core'; import type { Span, SpanTimeInput, StartSpanOptions } from '@sentry/types'; +import { WINDOW } from './types'; /** * Checks if a given value is a valid measurement value. @@ -13,9 +14,6 @@ export function isMeasurementValue(value: unknown): value is number { * Helper function to start child on transactions. This function will make sure that the transaction will * use the start timestamp of the created child span if it is earlier than the transactions actual * start timestamp. - * - * Note: this will not be possible anymore in v8, - * unless we do some special handling for browser here... */ export function startAndEndSpan( parentSpan: Span, @@ -45,3 +43,17 @@ export function startAndEndSpan( return span; }); } + +/** Get the browser performance API. */ +export function getBrowserPerformanceAPI(): Performance | undefined { + // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are + return WINDOW && WINDOW.addEventListener && WINDOW.performance; +} + +/** + * Converts from milliseconds to seconds + * @param time time in ms + */ +export function msToSec(time: number): number { + return time / 1000; +} diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 4682633d101c..e13c731644b5 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -592,6 +592,9 @@ export function createProfilingEvent( return createProfilePayload(profile_id, start_timestamp, profile, event); } +// TODO (v8): We need to obtain profile ids in @sentry-internal/tracing, +// but we don't have access to this map because importing this map would +// cause a circular dependancy. We need to resolve this in v8. const PROFILE_MAP: Map = new Map(); /** * diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 7d1561b20995..9485a2118bc4 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -1,6 +1,8 @@ +/* eslint-disable max-lines */ import { addHistoryInstrumentationHandler, addPerformanceEntries, + startTrackingINP, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals, @@ -94,6 +96,13 @@ export interface BrowserTracingOptions { */ enableLongTask: boolean; + /** + * If true, Sentry will capture first input delay and add it to the corresponding transaction. + * + * Default: true + */ + enableInp: boolean; + /** * Flag to disable patching all together for fetch requests. * @@ -145,6 +154,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { instrumentPageLoad: true, markBackgroundSpan: true, enableLongTask: true, + enableInp: true, _experiments: {}, ...defaultRequestInstrumentationOptions, }; @@ -168,6 +178,10 @@ export const browserTracingIntegration = ((_options: Partial>; + profile_id?: string; + exclusive_time?: number; } // These are aligned with OpenTelemetry trace flags From 1811e5daea58e520044d9aa3bd9ef16afac4ffd7 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 18 Apr 2024 11:03:23 +0200 Subject: [PATCH 02/11] add measurements to spanJSON --- packages/core/src/envelope.ts | 16 +++++ packages/core/src/index.ts | 3 +- packages/core/src/span.ts | 22 ------- packages/core/src/tracing/sentrySpan.ts | 3 +- packages/opentelemetry/src/spanExporter.ts | 1 + packages/types/src/envelope.ts | 4 +- packages/types/src/span.ts | 2 + packages/utils/src/envelope.ts | 13 +++++ packages/utils/test/envelope.test.ts | 68 +++++++++++++++++++++- 9 files changed, 104 insertions(+), 28 deletions(-) delete mode 100644 packages/core/src/span.ts diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 3189cdc5278a..bc29c6fa8b01 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -11,6 +11,7 @@ import type { SessionAggregates, SessionEnvelope, SessionItem, + SpanEnvelope, } from '@sentry/types'; import { createAttachmentEnvelopeItem, @@ -19,6 +20,9 @@ import { dsnToString, getSdkMetadataForEnvelopeHeader, } from '@sentry/utils'; +import { createSpanEnvelopeItem } from '@sentry/utils'; +import type { SentrySpan } from './tracing'; +import { spanToJSON } from './utils/spanUtils'; /** * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. @@ -117,3 +121,15 @@ export function createAttachmentEnvelope( } return createEnvelope(envelopeHeaders, attachmentItems); } + +/** + * Create envelope from Span item. + */ +export function createSpanEnvelope(spans: SentrySpan[]): SpanEnvelope { + const headers: SpanEnvelope[0] = { + sent_at: new Date().toISOString(), + }; + + const items = spans.map(span => createSpanEnvelopeItem(spanToJSON(span))); + return createEnvelope(headers, items); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a863c6ee271d..df889035b7c4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,7 @@ export type { IntegrationIndex } from './integration'; export * from './tracing'; export * from './semanticAttributes'; -export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope } from './envelope'; +export { createEventEnvelope, createSessionEnvelope, createAttachmentEnvelope, createSpanEnvelope } from './envelope'; export { captureCheckIn, withMonitor, @@ -61,7 +61,6 @@ export { export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; -export { createSpanEnvelope } from './span'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; diff --git a/packages/core/src/span.ts b/packages/core/src/span.ts deleted file mode 100644 index 405ceeddab30..000000000000 --- a/packages/core/src/span.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { SpanEnvelope, SpanItem } from '@sentry/types'; -import type { Span } from '@sentry/types'; -import { createEnvelope } from '@sentry/utils'; - -/** - * Create envelope from Span item. - */ -export function createSpanEnvelope(spans: Span[]): SpanEnvelope { - const headers: SpanEnvelope[0] = { - sent_at: new Date().toISOString(), - }; - - const items = spans.map(createSpanItem); - return createEnvelope(headers, items); -} - -function createSpanItem(span: Span): SpanItem { - const spanHeaders: SpanItem[0] = { - type: 'span', - }; - return [spanHeaders, span]; -} diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 7d968d5d680a..31fd35bc4b68 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -187,6 +187,7 @@ export class SentrySpan implements Span { _metrics_summary: getMetricSummaryJsonForSpan(this), profile_id: this._attributes[SEMANTIC_ATTRIBUTE_PROFILE_ID] as string | undefined, exclusive_time: this._attributes[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME] as number | undefined, + measurements: timedEventsToMeasurements(this._events), }); } @@ -297,7 +298,7 @@ export class SentrySpan implements Span { }; const measurements = timedEventsToMeasurements(this._events); - const hasMeasurements = Object.keys(measurements).length; + const hasMeasurements = measurements && Object.keys(measurements).length; if (hasMeasurements) { DEBUG_BUILD && diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index abf82ba8a3f0..52a900661cad 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -281,6 +281,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remai op, origin, _metrics_summary: getMetricSummaryJsonForSpan(span as unknown as Span), + measurements: timedEventsToMeasurements(span.events), }); spans.push(spanJSON); diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index c72406b4539f..874edd8d2eda 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -8,7 +8,7 @@ import type { Profile } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; import type { SerializedSession, Session, SessionAggregates } from './session'; -import type { Span } from './span'; +import type { SpanJSON } from './span'; // Based on: https://develop.sentry.dev/sdk/envelopes/ @@ -98,7 +98,7 @@ type ReplayRecordingItem = BaseEnvelopeItem; export type FeedbackItem = BaseEnvelopeItem; export type ProfileItem = BaseEnvelopeItem; -export type SpanItem = BaseEnvelopeItem; +export type SpanItem = BaseEnvelopeItem>; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 799f718877d4..d27175276402 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -1,3 +1,4 @@ +import type { Measurements } from './measurement'; import type { Primitive } from './misc'; import type { HrTime } from './opentelemetry'; import type { SpanStatus } from './spanStatus'; @@ -56,6 +57,7 @@ export interface SpanJSON { _metrics_summary?: Record>; profile_id?: string; exclusive_time?: number; + measurements: Measurements; } // These are aligned with OpenTelemetry trace flags diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 8b1345b112d7..17c40bed92ad 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -11,6 +11,8 @@ import type { EventEnvelopeHeaders, SdkInfo, SdkMetadata, + SpanItem, + SpanJSON, } from '@sentry/types'; import { dsnToString } from './dsn'; @@ -177,6 +179,17 @@ export function parseEnvelope(env: string | Uint8Array): Envelope { return [envelopeHeader, items]; } +/** + * Creates envelope item for a single span + */ +export function createSpanEnvelopeItem(spanJson: Partial): SpanItem { + const spanHeaders: SpanItem[0] = { + type: 'span', + }; + + return [spanHeaders, spanJson]; +} + /** * Creates attachment envelope items */ diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts index efb3e873a0a6..d399d7d72a52 100644 --- a/packages/utils/test/envelope.test.ts +++ b/packages/utils/test/envelope.test.ts @@ -1,16 +1,82 @@ -import type { Event, EventEnvelope } from '@sentry/types'; +import type { Event, EventEnvelope, SpanAttributes } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, + SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, + spanToJSON, +} from '@sentry/core'; +import { SentrySpan } from '@sentry/core'; import { addItemToEnvelope, createEnvelope, + createSpanEnvelopeItem, forEachEnvelopeItem, parseEnvelope, serializeEnvelope, } from '../src/envelope'; +import { dropUndefinedKeys } from '../src/object'; import type { InternalGlobal } from '../src/worldwide'; import { GLOBAL_OBJ } from '../src/worldwide'; describe('envelope', () => { + describe('createSpanEnvelope()', () => { + it('span-envelope-item of INP event has the correct object structure', () => { + const attributes: SpanAttributes = dropUndefinedKeys({ + release: 'releaseString', + environment: 'dev', + transaction: '/test-route', + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 80, + user: 10, + profile_id: 'test-profile-id', + replay_id: 'test-replay-id', + }); + + const startTime = 1713365480; + + const span = new SentrySpan({ + startTimestamp: startTime, + endTimestamp: startTime + 2, + op: 'ui.interaction.click', + name: '', + attributes, + }); + + span.addEvent('inp', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: 100, + }); + + const spanEnvelopeItem = createSpanEnvelopeItem(spanToJSON(span)); + + const expectedObj = { + data: { + 'sentry.origin': expect.any(String), + 'sentry.op': expect.any(String), + release: expect.any(String), + environment: expect.any(String), + transaction: expect.any(String), + 'sentry.exclusive_time': expect.any(Number), + user: expect.any(Number), + profile_id: expect.any(String), + replay_id: expect.any(String), + }, + description: expect.any(String), + op: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: expect.any(String), + exclusive_time: expect.any(Number), + measurements: { inp: { value: expect.any(Number), unit: expect.any(String) } }, + }; + + expect(spanEnvelopeItem[0].type).toBe('span'); + expect(spanEnvelopeItem[1]).toMatchObject(expectedObj); + }); + }); + describe('createEnvelope()', () => { const testTable: Array<[string, Parameters[0], Parameters[1]]> = [ ['creates an empty envelope', {}, []], From 6f6cf538041a5db4e5c42eab10786a22e2608045 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 18 Apr 2024 11:33:02 +0200 Subject: [PATCH 03/11] make measurements undefined --- .../browserTracingIntegration/interactions/test.ts | 8 +++----- packages/core/src/tracing/measurement.ts | 6 +++++- packages/opentelemetry/src/spanExporter.ts | 2 +- packages/types/src/span.ts | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index c8723b8319f7..f62b96395131 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -140,9 +140,7 @@ sentryTest('should capture an INP click event span. @firefox', async ({ browserN // Wait for the interaction transaction from the enableInteractions experiment await getMultipleSentryEnvelopeRequests(page, 1); - const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests< - SpanJSON & { exclusive_time: number; measurements: Measurements } - >(page, 1, { + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span', }); // Page hide to trigger INP @@ -157,6 +155,6 @@ sentryTest('should capture an INP click event span. @firefox', async ({ browserN expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); expect(spanEnvelopes[0].description).toBe('body > button.clicked'); expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); - expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0); - expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond'); + expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0); + expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); }); diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index a8328387c209..817569a03930 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -23,7 +23,11 @@ export function setMeasurement(name: string, value: number, unit: MeasurementUni /** * Convert timed events to measurements. */ -export function timedEventsToMeasurements(events: TimedEvent[]): Measurements { +export function timedEventsToMeasurements(events: TimedEvent[]): Measurements | undefined { + if (!events || events.length === 0) { + return undefined; + } + const measurements: Measurements = {}; events.forEach(event => { const attributes = event.attributes || {}; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 52a900661cad..f44a5e49e9df 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -143,7 +143,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { transactionEvent.spans = spans; const measurements = timedEventsToMeasurements(span.events); - if (Object.keys(measurements).length) { + if (measurements && Object.keys(measurements).length) { transactionEvent.measurements = measurements; } diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index d27175276402..066d8e7bf1f3 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -57,7 +57,7 @@ export interface SpanJSON { _metrics_summary?: Record>; profile_id?: string; exclusive_time?: number; - measurements: Measurements; + measurements?: Measurements; } // These are aligned with OpenTelemetry trace flags From 7b00a3a4c7e71ec74e257333c0d7ba70ff6fefb8 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 18 Apr 2024 14:38:31 +0200 Subject: [PATCH 04/11] try fix test --- .size-limit.js | 2 +- .../interactions/test.ts | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index db032dc4a3ac..e45b2717673d 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -12,7 +12,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '34 KB', + limit: '36 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index f62b96395131..68b49e762026 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -1,6 +1,6 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Contexts, Event as SentryEvent, Measurements, SpanJSON } from '@sentry/types'; +import type { Contexts, Event as SentryEvent, SpanJSON } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -132,17 +132,21 @@ sentryTest('should capture an INP click event span. @firefox', async ({ browserN const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); // waiting for page load + + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { + envelopeType: 'span', + }); await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); - // Wait for the interaction transaction from the enableInteractions experiment + // eslint-disable-next-line no-console + console.log('buttons clicked'); + + // Wait for the interaction transaction from the experimental enableInteractions (in init.js) await getMultipleSentryEnvelopeRequests(page, 1); - const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - envelopeType: 'span', - }); // Page hide to trigger INP await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); @@ -151,7 +155,9 @@ sentryTest('should capture an INP click event span. @firefox', async ({ browserN // Get the INP span envelope const spanEnvelopes = await spanEnvelopesPromise; - expect(spanEnvelopes).toHaveLength(1); + // eslint-disable-next-line no-console + console.log('spanevents', spanEnvelopes); + expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); expect(spanEnvelopes[0].description).toBe('body > button.clicked'); expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); From f0a609439a49012a34001c52aef7f1a5bbfbf84b Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 18 Apr 2024 14:49:55 +0200 Subject: [PATCH 05/11] add logs --- .size-limit.js | 4 ++-- .../tracing/browserTracingIntegration/interactions/test.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index e45b2717673d..e47155a078dc 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -12,7 +12,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '36 KB', + limit: '34 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -52,7 +52,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '83 KB', + limit: '85 KB', }, { name: '@sentry/browser (incl. Feedback)', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index 68b49e762026..711ac1a56802 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -147,11 +147,17 @@ sentryTest('should capture an INP click event span. @firefox', async ({ browserN // Wait for the interaction transaction from the experimental enableInteractions (in init.js) await getMultipleSentryEnvelopeRequests(page, 1); + // eslint-disable-next-line no-console + console.log('waited for interaction ev'); + // Page hide to trigger INP await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); }); + // eslint-disable-next-line no-console + console.log('pagehide done'); + // Get the INP span envelope const spanEnvelopes = await spanEnvelopesPromise; From eab0354357652e4e8e8645a9c8ce7120a65f8053 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 18 Apr 2024 18:39:28 +0200 Subject: [PATCH 06/11] try tests in CI - new INP tests --- .../interactions/init.js | 1 - .../interactions/test.ts | 72 ++++------ .../metrics/web-vitals-inp/assets/script.js | 29 ++++ .../tracing/metrics/web-vitals-inp/init.js | 15 ++ .../metrics/web-vitals-inp/template.html | 16 +++ .../tracing/metrics/web-vitals-inp/test.ts | 129 ++++++++++++++++++ 6 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js index 97bbfdafdabb..846538e7f3f0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js @@ -11,7 +11,6 @@ Sentry.init({ _experiments: { enableInteractions: true, }, - enableInp: true, }), ], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index 711ac1a56802..daec29e15df5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -113,60 +113,34 @@ sentryTest( }, ); -sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => { - const supportedBrowsers = ['chromium', 'firefox']; - - if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { - sentryTest.skip(); - } - - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - - const url = await getLocalTestPath({ testDir: __dirname }); - - await page.goto(url); - await getFirstSentryEnvelopeRequest(page); // waiting for page load - - const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - envelopeType: 'span', - }); - - await page.locator('[data-test-id=interaction-button]').click(); - await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); +sentryTest( + 'should use the element name for a clicked element when no component name', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; - // eslint-disable-next-line no-console - console.log('buttons clicked'); + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } - // Wait for the interaction transaction from the experimental enableInteractions (in init.js) - await getMultipleSentryEnvelopeRequests(page, 1); + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); - // eslint-disable-next-line no-console - console.log('waited for interaction ev'); + const url = await getLocalTestPath({ testDir: __dirname }); - // Page hide to trigger INP - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); - // eslint-disable-next-line no-console - console.log('pagehide done'); + await page.locator('[data-test-id=styled-button]').click(); - // Get the INP span envelope - const spanEnvelopes = await spanEnvelopesPromise; + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); + const eventData = envelopes[0]; - // eslint-disable-next-line no-console - console.log('spanevents', spanEnvelopes); + expect(eventData.spans).toHaveLength(1); - expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); - expect(spanEnvelopes[0].description).toBe('body > button.clicked'); - expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); - expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0); - expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); -}); + const interactionSpan = eventData.spans![0]; + expect(interactionSpan.op).toBe('ui.interaction.click'); + expect(interactionSpan.description).toBe('body > StyledButton'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js new file mode 100644 index 000000000000..90825f95d3bb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js @@ -0,0 +1,29 @@ +const delay = (delay = 70) => e => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < delay) { + // + } + + e.target.classList.add('clicked'); +}; + +document.querySelector('[data-test-id=slow-interaction-button]').addEventListener('click', delay(200)); +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay()); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay()); +document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay()); + +document.querySelector('[data-test-id=click-me-button]').addEventListener('click', function (e) { + this.textContent = 'Clicked!'; + requestAnimationFrame(() => { + e.target.classList.add('clicked'); + requestAnimationFrame(() => { + this.textContent = 'Click me'; + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js new file mode 100644 index 000000000000..b558562e4cd4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + enableLongTask: false, + enableInp: true, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html new file mode 100644 index 000000000000..a35ec49327eb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html @@ -0,0 +1,16 @@ + + + + + + +
Rendered Before Long Task
+ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts new file mode 100644 index 000000000000..cc903e49c8bd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -0,0 +1,129 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event as SentryEvent, SpanJSON } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest('should capture an INP click event span.', async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); // wait for page load + + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { + // envelopeType: 'span', // todo: does not work with envelopType + }); + + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + // eslint-disable-next-line no-console + console.log('buttons clicked'); + + // Page hide to trigger INP + await page.evaluate(() => { + // eslint-disable-next-line no-console + console.log('dispatching event'); + window.dispatchEvent(new Event('pagehide')); + }); + + // eslint-disable-next-line no-console + console.log('event dispatched'); + + // Get the INP span envelope + const spanEnvelopes = await spanEnvelopesPromise; + + // eslint-disable-next-line no-console + console.log('waited for envelope'); + + // expect(spanEnvelopes).toBe(1); + + expect(spanEnvelopes).toHaveLength(1); + expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); + expect(spanEnvelopes[0].description).toBe('body > button.clicked'); + expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); + expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0); + expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); +}); + +sentryTest( + 'should choose the slowest interaction click event when INP is triggered.', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { + // envelopeType: 'span', + }); + + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + // eslint-disable-next-line no-console + console.log('2 - clicked first time'); + + await page.locator('[data-test-id=slow-interaction-button]').click(); + await page.locator('.clicked[data-test-id=slow-interaction-button]').isVisible(); + + // eslint-disable-next-line no-console + console.log('2 - clicked second time'); + + // Page hide to trigger INP + await page.evaluate(() => { + window.dispatchEvent(new Event('pagehide')); + }); + + // eslint-disable-next-line no-console + console.log('2 - dispatched event'); + + // Get the INP span envelope + const spanEnvelopes = await spanEnvelopesPromise; + + // expect(spanEnvelopes).toBe(2); + expect(spanEnvelopes).toHaveLength(1); + expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); + expect(spanEnvelopes[0].description).toBe('body > button.clicked'); + expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(150); + expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(150); + expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); + }, +); From e1ffe5239aa57ab2cb5b86c9d55a0187cb773dda Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 19 Apr 2024 11:19:58 +0200 Subject: [PATCH 07/11] try fix tests --- .../interactions/template.html | 1 - .../metrics/web-vitals-inp/assets/script.js | 19 ++------ .../metrics/web-vitals-inp/template.html | 10 ++--- .../tracing/metrics/web-vitals-inp/test.ts | 43 +++++-------------- 4 files changed, 17 insertions(+), 56 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html index d3bda54d7443..0b32a75fc3b0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html @@ -8,6 +8,5 @@ - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js index 90825f95d3bb..3e385f192f9f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js @@ -1,4 +1,4 @@ -const delay = (delay = 70) => e => { +const blockUI = (delay = 70) => e => { const startTime = Date.now(); function getElasped() { @@ -13,17 +13,6 @@ const delay = (delay = 70) => e => { e.target.classList.add('clicked'); }; -document.querySelector('[data-test-id=slow-interaction-button]').addEventListener('click', delay(200)); -document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay()); -document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay()); -document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay()); - -document.querySelector('[data-test-id=click-me-button]').addEventListener('click', function (e) { - this.textContent = 'Clicked!'; - requestAnimationFrame(() => { - e.target.classList.add('clicked'); - requestAnimationFrame(() => { - this.textContent = 'Click me'; - }); - }); -}); +document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(200)); +document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(500)); +document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html index a35ec49327eb..5c247b0b0ff1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html @@ -5,12 +5,8 @@
Rendered Before Long Task
- - - - - - - + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index cc903e49c8bd..4fb1817a5104 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -31,33 +31,20 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL await getFirstSentryEnvelopeRequest(page); // wait for page load const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - // envelopeType: 'span', // todo: does not work with envelopType + // envelopeType: 'span', // todo: does not work with envelopeType }); - await page.locator('[data-test-id=interaction-button]').click(); - await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); - - // eslint-disable-next-line no-console - console.log('buttons clicked'); + await page.locator('[data-test-id=normal-button]').click(); + await page.locator('.clicked[data-test-id=normal-button]').isVisible(); // Page hide to trigger INP await page.evaluate(() => { - // eslint-disable-next-line no-console - console.log('dispatching event'); window.dispatchEvent(new Event('pagehide')); }); - // eslint-disable-next-line no-console - console.log('event dispatched'); - // Get the INP span envelope const spanEnvelopes = await spanEnvelopesPromise; - // eslint-disable-next-line no-console - console.log('waited for envelope'); - - // expect(spanEnvelopes).toBe(1); - expect(spanEnvelopes).toHaveLength(1); expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); expect(spanEnvelopes[0].description).toBe('body > button.clicked'); @@ -92,38 +79,28 @@ sentryTest( await getFirstSentryEnvelopeRequest(page); const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - // envelopeType: 'span', + // envelopeType: 'span', // todo: does not work with envelopeType }); - await page.locator('[data-test-id=interaction-button]').click(); - await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + await page.locator('[data-test-id=normal-button]').click(); + await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - // eslint-disable-next-line no-console - console.log('2 - clicked first time'); - - await page.locator('[data-test-id=slow-interaction-button]').click(); - await page.locator('.clicked[data-test-id=slow-interaction-button]').isVisible(); - - // eslint-disable-next-line no-console - console.log('2 - clicked second time'); + await page.locator('[data-test-id=slow-button]').click(); + await page.locator('.clicked[data-test-id=slow-button]').isVisible(); // Page hide to trigger INP await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); }); - // eslint-disable-next-line no-console - console.log('2 - dispatched event'); - // Get the INP span envelope const spanEnvelopes = await spanEnvelopesPromise; - // expect(spanEnvelopes).toBe(2); expect(spanEnvelopes).toHaveLength(1); expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); expect(spanEnvelopes[0].description).toBe('body > button.clicked'); - expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(150); - expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(150); + expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(400); + expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(400); expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); }, ); From 5202e76286c48371a733435460309c7f605c39a5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 19 Apr 2024 14:29:47 +0200 Subject: [PATCH 08/11] await envelopePromise after clicking --- .../interactions/test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index daec29e15df5..ff323423d1c9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -32,10 +32,13 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN await page.goto(url); await getFirstSentryEnvelopeRequest(page); + const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); + 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); + const envelopes = await envelopesPromise; + expect(envelopes).toHaveLength(1); const eventData = envelopes[0]; @@ -73,9 +76,10 @@ sentryTest( await getFirstSentryEnvelopeRequest(page); for (let i = 0; i < 4; i++) { + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 1); await wait(100); await page.locator('[data-test-id=interaction-button]').click(); - const envelope = await getMultipleSentryEnvelopeRequests(page, 1); + const envelope = await envelopePromise; expect(envelope[0].spans).toHaveLength(1); } }, @@ -99,9 +103,11 @@ sentryTest( await page.goto(url); await getFirstSentryEnvelopeRequest(page); + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 1); + await page.locator('[data-test-id=annotated-button]').click(); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + const envelopes = await envelopePromise; expect(envelopes).toHaveLength(1); const eventData = envelopes[0]; @@ -131,9 +137,11 @@ sentryTest( await page.goto(url); await getFirstSentryEnvelopeRequest(page); + const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); + await page.locator('[data-test-id=styled-button]').click(); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + const envelopes = await envelopesPromise; expect(envelopes).toHaveLength(1); const eventData = envelopes[0]; From 7a5636b55e4d68f533bf59aed1d84b131160e149 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 19 Apr 2024 16:04:47 +0200 Subject: [PATCH 09/11] fix flaky tests --- .../{assets/script.js => subject.js} | 8 ++-- .../interactions/test.ts | 37 +++---------------- .../{assets/script.js => subject.js} | 4 +- .../metrics/web-vitals-inp/template.html | 4 +- .../tracing/metrics/web-vitals-inp/test.ts | 25 +++++++------ 5 files changed, 27 insertions(+), 51 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/{assets/script.js => subject.js} (77%) rename dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/{assets/script.js => subject.js} (87%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/subject.js similarity index 77% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/subject.js index fecb5ca8a758..f9503ef6f261 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/subject.js @@ -1,4 +1,4 @@ -const delay = e => { +const blockUI = e => { const startTime = Date.now(); function getElasped() { @@ -13,6 +13,6 @@ const delay = e => { e.target.classList.add('clicked'); }; -document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); -document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay); -document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay); +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', blockUI); +document.querySelector('[data-test-id=styled-button]').addEventListener('click', blockUI); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index ff323423d1c9..bb219eda38c7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -1,6 +1,5 @@ -import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Contexts, Event as SentryEvent, SpanJSON } from '@sentry/types'; +import type { Event as SentryEvent } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -9,30 +8,18 @@ import { shouldSkipTracingTest, } from '../../../../utils/helpers'; -type TransactionJSON = SpanJSON & { - spans: SpanJSON[]; - contexts: Contexts; - platform: string; - type: string; -}; - -const wait = (time: number) => new Promise(res => setTimeout(res, time)); - sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestPath, page }) => { const supportedBrowsers = ['chromium', 'firefox']; if (shouldSkipTracingTest() || !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); - const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); + const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); @@ -67,17 +54,13 @@ sentryTest( 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++) { const envelopePromise = getMultipleSentryEnvelopeRequests(page, 1); - await wait(100); + await page.waitForTimeout(1000); await page.locator('[data-test-id=interaction-button]').click(); const envelope = await envelopePromise; expect(envelope[0].spans).toHaveLength(1); @@ -94,16 +77,12 @@ sentryTest( 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); - const envelopePromise = getMultipleSentryEnvelopeRequests(page, 1); + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 1); await page.locator('[data-test-id=annotated-button]').click(); @@ -128,23 +107,19 @@ sentryTest( 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); - const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); + const envelopesPromise = getMultipleSentryEnvelopeRequests(page, 1); await page.locator('[data-test-id=styled-button]').click(); const envelopes = await envelopesPromise; expect(envelopes).toHaveLength(1); - const eventData = envelopes[0]; + const eventData = envelopes[0]; expect(eventData.spans).toHaveLength(1); const interactionSpan = eventData.spans![0]; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js index 3e385f192f9f..ed6db5b5afe2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/assets/script.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js @@ -13,6 +13,6 @@ const blockUI = (delay = 70) => e => { e.target.classList.add('clicked'); }; -document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(200)); -document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(500)); +document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); +document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html index 5c247b0b0ff1..25c6920f07e2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html @@ -5,8 +5,8 @@
Rendered Before Long Task
- - + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index 4fb1817a5104..799a6df407a3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -1,4 +1,3 @@ -import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import type { Event as SentryEvent, SpanJSON } from '@sentry/types'; @@ -16,7 +15,6 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL sentryTest.skip(); } - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -31,12 +29,14 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL await getFirstSentryEnvelopeRequest(page); // wait for page load const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - // envelopeType: 'span', // todo: does not work with envelopeType + envelopeType: 'span', }); await page.locator('[data-test-id=normal-button]').click(); await page.locator('.clicked[data-test-id=normal-button]').isVisible(); + await page.waitForTimeout(500); + // Page hide to trigger INP await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); @@ -47,7 +47,7 @@ sentryTest('should capture an INP click event span.', async ({ browserName, getL expect(spanEnvelopes).toHaveLength(1); expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); - expect(spanEnvelopes[0].description).toBe('body > button.clicked'); + expect(spanEnvelopes[0].description).toBe('body > NormalButton'); expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0); expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0); expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); @@ -62,9 +62,6 @@ sentryTest( sentryTest.skip(); } - await page.route('**/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), - ); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -78,16 +75,20 @@ sentryTest( await page.goto(url); await getFirstSentryEnvelopeRequest(page); - const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { - // envelopeType: 'span', // todo: does not work with envelopeType - }); - await page.locator('[data-test-id=normal-button]').click(); await page.locator('.clicked[data-test-id=normal-button]').isVisible(); + await page.waitForTimeout(500); + await page.locator('[data-test-id=slow-button]').click(); await page.locator('.clicked[data-test-id=slow-button]').isVisible(); + await page.waitForTimeout(500); + + const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests(page, 1, { + envelopeType: 'span', + }); + // Page hide to trigger INP await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); @@ -98,7 +99,7 @@ sentryTest( expect(spanEnvelopes).toHaveLength(1); expect(spanEnvelopes[0].op).toBe('ui.interaction.click'); - expect(spanEnvelopes[0].description).toBe('body > button.clicked'); + expect(spanEnvelopes[0].description).toBe('body > SlowButton'); expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(400); expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(400); expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond'); From 527e21bdaa72fc57e360575825eda60e76a90210 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 22 Apr 2024 11:28:10 +0200 Subject: [PATCH 10/11] update size limits --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 1ce4b252963d..9ead34df50f0 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -52,7 +52,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '86 KB', + limit: '87 KB', }, { name: '@sentry/browser (incl. Feedback)', @@ -165,7 +165,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: false, brotli: false, - limit: '261 KB', + limit: '264 KB', }, // Next.js SDK (ESM) { From 4d67c6e1e3cb9be7021b5061b95fa7ed4b404db1 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 22 Apr 2024 11:55:31 +0200 Subject: [PATCH 11/11] review changes --- packages/browser-utils/src/metrics/inp.ts | 4 ++-- packages/opentelemetry/src/spanExporter.ts | 2 +- packages/utils/test/envelope.test.ts | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts index bd1a04b4f427..5c7808d264cf 100644 --- a/packages/browser-utils/src/metrics/inp.ts +++ b/packages/browser-utils/src/metrics/inp.ts @@ -140,9 +140,9 @@ function _trackINP(): () => void { [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, }); - const envelope = span ? createSpanEnvelope([span]) : undefined; + const envelope = createSpanEnvelope([span]); const transport = client && client.getTransport(); - if (transport && envelope) { + if (transport) { transport.send(envelope).then(null, reason => { DEBUG_BUILD && logger.error('Error while sending interaction:', reason); }); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index f44a5e49e9df..f570b9f51859 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -143,7 +143,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { transactionEvent.spans = spans; const measurements = timedEventsToMeasurements(span.events); - if (measurements && Object.keys(measurements).length) { + if (measurements) { transactionEvent.measurements = measurements; } diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts index d399d7d72a52..6f3a9ea3a6d6 100644 --- a/packages/utils/test/envelope.test.ts +++ b/packages/utils/test/envelope.test.ts @@ -15,14 +15,13 @@ import { parseEnvelope, serializeEnvelope, } from '../src/envelope'; -import { dropUndefinedKeys } from '../src/object'; import type { InternalGlobal } from '../src/worldwide'; import { GLOBAL_OBJ } from '../src/worldwide'; describe('envelope', () => { describe('createSpanEnvelope()', () => { it('span-envelope-item of INP event has the correct object structure', () => { - const attributes: SpanAttributes = dropUndefinedKeys({ + const attributes: SpanAttributes = { release: 'releaseString', environment: 'dev', transaction: '/test-route', @@ -30,7 +29,7 @@ describe('envelope', () => { user: 10, profile_id: 'test-profile-id', replay_id: 'test-replay-id', - }); + }; const startTime = 1713365480;