diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js b/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js new file mode 100644 index 000000000000..5a2aef02028d --- /dev/null +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 105) { + // + } +})(); diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js b/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js new file mode 100644 index 000000000000..5229401c2ef5 --- /dev/null +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; +import { Integrations } from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + new Integrations.BrowserTracing({ + idleTimeout: 1000, + _experiments: { + enableInteractions: true, + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html b/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html new file mode 100644 index 000000000000..becc5f50fdd1 --- /dev/null +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/template.html @@ -0,0 +1,10 @@ + + + + + +
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 new file mode 100644 index 000000000000..9f12b2e76e6d --- /dev/null +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -0,0 +1,36 @@ +import { expect, Route } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest('should capture interaction transaction.', async ({ browserName, getLocalTestPath, page }) => { + if (browserName !== 'chromium') { + 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(page, url); + + await page.locator('[data-test-id=interaction-button]').click(); + + 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', + }), + ); +}); diff --git a/packages/integration-tests/utils/generatePlugin.ts b/packages/integration-tests/utils/generatePlugin.ts index 62a91b31dc9c..acaf711946f3 100644 --- a/packages/integration-tests/utils/generatePlugin.ts +++ b/packages/integration-tests/utils/generatePlugin.ts @@ -49,6 +49,7 @@ const BUNDLE_PATHS: Record> = { function generateSentryAlias(): Record { const packageNames = readdirSync(PACKAGES_DIR, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) + .filter(dir => !['apm', 'minimal', 'next-plugin-sentry'].includes(dir.name)) .map(dir => dir.name); return Object.fromEntries( diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index 24cf9bc0459e..2b4bae572f77 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -1,10 +1,15 @@ /* eslint-disable max-lines */ import { Hub } from '@sentry/core'; -import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types'; +import { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, getDomElement, logger } from '@sentry/utils'; import { startIdleTransaction } from '../hubextensions'; -import { DEFAULT_FINAL_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_IDLE_TIMEOUT } from '../idletransaction'; +import { + DEFAULT_FINAL_TIMEOUT, + DEFAULT_HEARTBEAT_INTERVAL, + DEFAULT_IDLE_TIMEOUT, + IdleTransaction, +} from '../idletransaction'; import { extractTraceparentData } from '../utils'; import { registerBackgroundTabDetection } from './backgroundtab'; import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics'; @@ -87,7 +92,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { * * Default: undefined */ - _experiments?: Partial<{ enableLongTask: boolean }>; + _experiments?: Partial<{ enableLongTask: boolean; enableInteractions: boolean }>; /** * beforeNavigate is called before a pageload/navigation transaction is created and allows users to modify transaction @@ -120,7 +125,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { routingInstrumentation: instrumentRoutingWithDefaults, startTransactionOnLocationChange: true, startTransactionOnPageLoad: true, - _experiments: { enableLongTask: true }, + _experiments: { enableLongTask: true, enableInteractions: false }, ...defaultRequestInstrumentationOptions, }; @@ -147,6 +152,9 @@ export class BrowserTracing implements Integration { private _getCurrentHub?: () => Hub; + private _latestRouteName?: string; + private _latestRouteSource?: TransactionSource; + public constructor(_options?: Partial) { this.options = { ...DEFAULT_BROWSER_TRACING_OPTIONS, @@ -185,6 +193,7 @@ export class BrowserTracing implements Integration { traceXHR, tracePropagationTargets, shouldCreateSpanForRequest, + _experiments, } = this.options; instrumentRouting( @@ -197,6 +206,10 @@ export class BrowserTracing implements Integration { registerBackgroundTabDetection(); } + if (_experiments?.enableInteractions) { + this._registerInteractionListener(); + } + instrumentOutgoingRequests({ traceFetch, traceXHR, @@ -248,6 +261,9 @@ export class BrowserTracing implements Integration { ? { ...finalContext.metadata, source: 'custom' } : finalContext.metadata; + this._latestRouteName = finalContext.name; + this._latestRouteSource = finalContext.metadata?.source; + if (finalContext.sampled === false) { __DEBUG_BUILD__ && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); @@ -273,6 +289,57 @@ export class BrowserTracing implements Integration { return idleTransaction as Transaction; } + + /** Start listener for interaction transactions */ + private _registerInteractionListener(): void { + let inflightInteractionTransaction: IdleTransaction | undefined; + const registerInteractionTransaction = (): void => { + const { idleTimeout, finalTimeout, heartbeatInterval } = this.options; + + const op = 'ui.action.click'; + if (inflightInteractionTransaction) { + inflightInteractionTransaction.finish(); + inflightInteractionTransaction = undefined; + } + + if (!this._getCurrentHub) { + __DEBUG_BUILD__ && logger.warn(`[Tracing] Did not create ${op} transaction because _getCurrentHub is invalid.`); + return undefined; + } + + if (!this._latestRouteName) { + __DEBUG_BUILD__ && + logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); + return undefined; + } + + const hub = this._getCurrentHub(); + const { location } = WINDOW; + + const context: TransactionContext = { + name: this._latestRouteName, + op, + trimEnd: true, + metadata: { + source: this._latestRouteSource ?? 'url', + }, + }; + + inflightInteractionTransaction = startIdleTransaction( + hub, + context, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + ); + }; + + ['click'].forEach(type => { + addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); + }); + } } /** Returns the value of a meta tag */ diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts index 42b0d109dd41..3840ea75d6d6 100644 --- a/packages/tracing/test/browser/browsertracing.test.ts +++ b/packages/tracing/test/browser/browsertracing.test.ts @@ -89,6 +89,7 @@ describe('BrowserTracing', () => { expect(browserTracing.options).toEqual({ _experiments: { enableLongTask: true, + enableInteractions: false, }, idleTimeout: DEFAULT_IDLE_TIMEOUT, finalTimeout: DEFAULT_FINAL_TIMEOUT,