diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/assets/script.js b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/assets/script.js
new file mode 100644
index 000000000000..9ac3d6fb33d2
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/assets/script.js
@@ -0,0 +1,12 @@
+(() => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 101) {
+ //
+ }
+})();
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/init.js b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/init.js
new file mode 100644
index 000000000000..acf98f9a3821
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/init.js
@@ -0,0 +1,10 @@
+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({ _experiments: { enableLongTasks: false }, idleTimeout: 9000 })],
+ tracesSampleRate: 1,
+});
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/template.html b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/template.html
new file mode 100644
index 000000000000..d0973c41729b
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts
new file mode 100644
index 000000000000..a8c8c2b16798
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts
@@ -0,0 +1,21 @@
+import { expect, Route } from '@playwright/test';
+import { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
+
+sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ 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 });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBe(0);
+});
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/assets/script.js b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/assets/script.js
new file mode 100644
index 000000000000..5a2aef02028d
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/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/long-tasks-enabled/init.js b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/init.js
new file mode 100644
index 000000000000..037e2dc88517
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/init.js
@@ -0,0 +1,14 @@
+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: 9000,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/template.html b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/template.html
new file mode 100644
index 000000000000..d0973c41729b
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts
new file mode 100644
index 000000000000..91a8f6087376
--- /dev/null
+++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts
@@ -0,0 +1,36 @@
+import { expect, Route } from '@playwright/test';
+import { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
+
+sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ 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 });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBe(1);
+
+ const [firstUISpan] = uiSpans || [];
+ expect(firstUISpan).toEqual(
+ expect.objectContaining({
+ op: 'ui.long-task',
+ description: 'Long Task',
+ parent_span_id: eventData.contexts?.trace.span_id,
+ }),
+ );
+ const start = firstUISpan['start_timestamp'] ?? 0;
+ const end = firstUISpan['timestamp'] ?? 0;
+ const duration = end - start;
+
+ expect(duration).toBeGreaterThanOrEqual(0.1);
+ expect(duration).toBeLessThanOrEqual(0.15);
+});
diff --git a/packages/nextjs/test/integration/test/client/tracingFetch.js b/packages/nextjs/test/integration/test/client/tracingFetch.js
index 265230286f33..2d56e802e35e 100644
--- a/packages/nextjs/test/integration/test/client/tracingFetch.js
+++ b/packages/nextjs/test/integration/test/client/tracingFetch.js
@@ -1,4 +1,9 @@
-const { expectRequestCount, isTransactionRequest, expectTransaction } = require('../utils/client');
+const {
+ expectRequestCount,
+ isTransactionRequest,
+ expectTransaction,
+ extractEnvelopeFromRequest,
+} = require('../utils/client');
module.exports = async ({ page, url, requests }) => {
await page.goto(`${url}/fetch`);
@@ -21,6 +26,5 @@ module.exports = async ({ page, url, requests }) => {
},
],
});
-
await expectRequestCount(requests, { transactions: 1 });
};
diff --git a/packages/nextjs/test/integration/test/utils/client.js b/packages/nextjs/test/integration/test/utils/client.js
index 76d88832c863..5ca5379268ca 100644
--- a/packages/nextjs/test/integration/test/utils/client.js
+++ b/packages/nextjs/test/integration/test/utils/client.js
@@ -1,4 +1,5 @@
const { strictEqual } = require('assert');
+const expect = require('expect');
const { logIf, parseEnvelope } = require('./common');
const VALID_REQUEST_PAYLOAD = {
@@ -105,8 +106,10 @@ const assertObjectMatches = (actual, expected) => {
for (const key in expected) {
const expectedValue = expected[key];
- if (Object.prototype.toString.call(expectedValue) === '[object Object]' || Array.isArray(expectedValue)) {
+ if (Object.prototype.toString.call(expectedValue) === '[object Object]') {
assertObjectMatches(actual[key], expectedValue);
+ } else if (Array.isArray(expectedValue)) {
+ expect(actual[key]).toEqual(expect.arrayContaining(expectedValue.map(expect.objectContaining)));
} else {
strictEqual(actual[key], expectedValue);
}
diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts
index 089245bfd31b..69249b5f0f96 100644
--- a/packages/tracing/src/browser/browsertracing.ts
+++ b/packages/tracing/src/browser/browsertracing.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
import { Hub } from '@sentry/hub';
import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types';
import { getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
@@ -6,7 +7,7 @@ import { startIdleTransaction } from '../hubextensions';
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
import { extractTraceparentData } from '../utils';
import { registerBackgroundTabDetection } from './backgroundtab';
-import { addPerformanceEntries, startTrackingWebVitals } from './metrics';
+import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics';
import {
defaultRequestInstrumentationOptions,
instrumentOutgoingRequests,
@@ -71,6 +72,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
*/
_metricOptions?: Partial<{ _reportAllChanges: boolean }>;
+ /**
+ * _experiments allows the user to send options to define how this integration works.
+ *
+ * Default: undefined
+ */
+ _experiments?: Partial<{ enableLongTask: boolean }>;
+
/**
* beforeNavigate is called before a pageload/navigation transaction is created and allows users to modify transaction
* context data, or drop the transaction entirely (by setting `sampled = false` in the context).
@@ -101,6 +109,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS = {
routingInstrumentation: instrumentRoutingWithDefaults,
startTransactionOnLocationChange: true,
startTransactionOnPageLoad: true,
+ _experiments: { enableLongTask: true },
...defaultRequestInstrumentationOptions,
};
@@ -148,6 +157,9 @@ export class BrowserTracing implements Integration {
const { _metricOptions } = this.options;
startTrackingWebVitals(_metricOptions && _metricOptions._reportAllChanges);
+ if (this.options._experiments?.enableLongTask) {
+ startTrackingLongTasks();
+ }
}
/**
diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts
index a48eb381c213..ee94a66d1957 100644
--- a/packages/tracing/src/browser/metrics/index.ts
+++ b/packages/tracing/src/browser/metrics/index.ts
@@ -2,12 +2,14 @@
import { Measurements } from '@sentry/types';
import { browserPerformanceTimeOrigin, getGlobalObject, htmlTreeAsString, logger } from '@sentry/utils';
+import { IdleTransaction } from '../../idletransaction';
import { Transaction } from '../../transaction';
-import { msToSec } from '../../utils';
+import { getActiveTransaction, msToSec } from '../../utils';
import { getCLS, LayoutShift } from '../web-vitals/getCLS';
import { getFID } from '../web-vitals/getFID';
import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP';
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
+import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe';
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types';
import { _startChild, isMeasurementValue } from './utils';
@@ -38,6 +40,28 @@ export function startTrackingWebVitals(reportAllChanges: boolean = false): void
}
}
+/**
+ * Start tracking long tasks.
+ */
+export function startTrackingLongTasks(): void {
+ const entryHandler: PerformanceEntryHandler = (entry: PerformanceEntry): void => {
+ const transaction = getActiveTransaction() as IdleTransaction | undefined;
+ if (!transaction) {
+ return;
+ }
+ const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
+ const duration = msToSec(entry.duration);
+ transaction.startChild({
+ description: 'Long Task',
+ op: 'ui.long-task',
+ startTimestamp: startTime,
+ endTimestamp: startTime + duration,
+ });
+ };
+
+ observe('longtask', entryHandler);
+}
+
/** Starts tracking the Cumulative Layout Shift on the current page. */
function _trackCLS(): void {
// See:
diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts
index acee76d0c966..b90b327995d5 100644
--- a/packages/tracing/test/browser/browsertracing.test.ts
+++ b/packages/tracing/test/browser/browsertracing.test.ts
@@ -89,6 +89,9 @@ describe('BrowserTracing', () => {
const browserTracing = createBrowserTracing();
expect(browserTracing.options).toEqual({
+ _experiments: {
+ enableLongTask: true,
+ },
idleTimeout: DEFAULT_IDLE_TIMEOUT,
finalTimeout: DEFAULT_FINAL_TIMEOUT,
markBackgroundTransactions: true,