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,