diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f5d0364b7cda..6004a046f126 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -321,8 +321,10 @@ jobs:
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
- - name: Run linter
+ - name: Lint source files
run: yarn lint
+ - name: Validate ES5 builds
+ run: yarn validate:es5
job_circular_dep_check:
name: Circular Dependency Check
diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml
index bb414b553abd..9412da630775 100644
--- a/.github/workflows/flaky-test-detector.yml
+++ b/.github/workflows/flaky-test-detector.yml
@@ -77,5 +77,5 @@ jobs:
working-directory: packages/browser-integration-tests
env:
CHANGED_TEST_PATHS: ${{ steps.changed.outputs.browser_integration_files }}
- # Run 100 times when detecting changed test(s), else run all tests 5x
- TEST_RUN_COUNT: ${{ steps.changed.outputs.browser_integration == 'true' && 100 || 5 }}
+ # Run 50 times when detecting changed test(s), else run all tests 5x
+ TEST_RUN_COUNT: ${{ steps.changed.outputs.browser_integration == 'true' && 50 || 5 }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19b48530e607..3d8824213ccd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,23 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 7.45.0
+
+- build(cdn): Ensure ES5 bundles do not use non-ES5 code (#7550)
+- feat(core): Add trace function (#7556)
+- feat(hub): Make scope always defined on the hub (#7551)
+- feat(replay): Add `replay_id` to transaction DSC (#7571)
+- feat(replay): Capture fetch body size for replay events (#7524)
+- feat(sveltekit): Add performance monitoring for client load (#7537)
+- feat(sveltekit): Add performance monitoring to Sveltekit server handle (#7532)
+- feat(sveltekit): Add SvelteKit routing instrumentation (#7565)
+- fix(browser): Ensure keepalive flag is correctly set for parallel requests (#7553)
+- fix(core): Ensure `ignoreErrors` only applies to error events (#7573)
+- fix(node): Consider tracing error handler for process exit (#7558)
+- fix(otel): Make sure we use correct hub on finish (#7577)
+- fix(react): Handle case where error.cause already defined (#7557)
+- fix(tracing): Account for case where startTransaction returns undefined (#7566)
+
## 7.44.2
- fix(cdn): Fix ES5 CDN bundles (#7544)
diff --git a/package.json b/package.json
index 2296a10dfede..e6a54d7d032c 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"link:yarn": "lerna exec yarn link",
"lint": "lerna run lint",
"lint:eslint": "lerna run lint:eslint",
+ "validate:es5": "lerna run validate:es5",
"postpublish": "lerna run --stream --concurrency 1 postpublish",
"test": "lerna run --ignore @sentry-internal/* test",
"test:unit": "lerna run --ignore @sentry-internal/* test:unit",
@@ -89,6 +90,7 @@
"chai": "^4.1.2",
"codecov": "^3.6.5",
"deepmerge": "^4.2.2",
+ "es-check": "7.1.0",
"eslint": "7.32.0",
"jest": "^27.5.1",
"jest-environment-node": "^27.5.1",
diff --git a/packages/browser-integration-tests/scripts/detectFlakyTests.ts b/packages/browser-integration-tests/scripts/detectFlakyTests.ts
index 22977fa3ed83..af0a5c86a18e 100644
--- a/packages/browser-integration-tests/scripts/detectFlakyTests.ts
+++ b/packages/browser-integration-tests/scripts/detectFlakyTests.ts
@@ -6,52 +6,72 @@ import { promisify } from 'util';
const exec = promisify(childProcess.exec);
async function run(): Promise {
- let testPaths = getTestPaths();
- let failed = [];
+ let testPaths: string[] = [];
- try {
- const changedPaths: string[] = process.env.CHANGED_TEST_PATHS ? JSON.parse(process.env.CHANGED_TEST_PATHS) : [];
+ const changedPaths: string[] = process.env.CHANGED_TEST_PATHS ? JSON.parse(process.env.CHANGED_TEST_PATHS) : [];
- if (changedPaths.length > 0) {
- console.log(`Detected changed test paths:
+ if (changedPaths.length > 0) {
+ console.log(`Detected changed test paths:
${changedPaths.join('\n')}
`);
- testPaths = testPaths.filter(p => changedPaths.some(changedPath => changedPath.includes(p)));
+ testPaths = getTestPaths().filter(p => changedPaths.some(changedPath => changedPath.includes(p)));
+ if (testPaths.length === 0) {
+ console.log('Could not find matching tests, aborting...');
+ process.exit(1);
}
- } catch {
- console.log('Could not detect changed test paths, running all tests.');
}
const cwd = path.join(__dirname, '../');
const runCount = parseInt(process.env.TEST_RUN_COUNT || '10');
- for (const testPath of testPaths) {
- console.log(`Running test: ${testPath}`);
- const start = Date.now();
+ try {
+ await new Promise((resolve, reject) => {
+ const cp = childProcess.spawn(
+ `yarn playwright test ${
+ testPaths.length ? testPaths.join(' ') : './suites'
+ } --browser='all' --reporter='line' --repeat-each ${runCount}`,
+ { shell: true, cwd },
+ );
+
+ let error: Error | undefined;
+
+ cp.stdout.on('data', data => {
+ console.log(data ? (data as object).toString() : '');
+ });
- try {
- await exec(`yarn playwright test ${testPath} --browser='all' --repeat-each ${runCount}`, {
- cwd,
+ cp.stderr.on('data', data => {
+ console.log(data ? (data as object).toString() : '');
});
- const end = Date.now();
- console.log(` ☑️ Passed ${runCount} times, avg. duration ${Math.ceil((end - start) / runCount)}ms`);
- } catch (error) {
- logError(error);
- failed.push(testPath);
- }
- }
- console.log('');
- console.log('');
+ cp.on('error', e => {
+ console.error(e);
+ error = e;
+ });
- if (failed.length > 0) {
- console.error(`⚠️ ${failed.length} test(s) failed.`);
+ cp.on('close', status => {
+ const err = error || (status !== 0 ? new Error(`Process exited with status ${status}`) : undefined);
+
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.log('');
+ console.log('');
+
+ console.error(`⚠️ Some tests failed.`);
+ console.error(error);
process.exit(1);
- } else {
- console.log(`☑️ ${testPaths.length} test(s) passed.`);
}
+
+ console.log('');
+ console.log('');
+ console.log(`☑️ All tests passed.`);
}
function getTestPaths(): string[] {
diff --git a/packages/browser-integration-tests/suites/replay/dsc/init.js b/packages/browser-integration-tests/suites/replay/dsc/init.js
new file mode 100644
index 000000000000..c43f001779eb
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/dsc/init.js
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/browser';
+import { Integrations } from '@sentry/tracing';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ useCompression: false,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [new Integrations.BrowserTracing({ tracingOrigins: [/.*/] }), window.Replay],
+ environment: 'production',
+ tracesSampleRate: 1,
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 1.0,
+});
+
+Sentry.configureScope(scope => {
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
+});
diff --git a/packages/browser-integration-tests/suites/replay/dsc/test.ts b/packages/browser-integration-tests/suites/replay/dsc/test.ts
new file mode 100644
index 000000000000..0819e9f7bf71
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/dsc/test.ts
@@ -0,0 +1,33 @@
+import { expect } from '@playwright/test';
+import type { EventEnvelopeHeaders } from '@sentry/types';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
+import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers';
+
+sentryTest('should add replay_id to dsc of transactions', async ({ getLocalTestPath, page, browserName }) => {
+ // This is flaky on webkit, so skipping there...
+ if (shouldSkipReplayTest() || browserName === 'webkit') {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+
+ const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser);
+
+ await waitForReplayRunning(page);
+ const replay = await getReplaySnapshot(page);
+
+ expect(replay.session?.id).toBeDefined();
+
+ expect(envHeader.trace).toBeDefined();
+ expect(envHeader.trace).toEqual({
+ environment: 'production',
+ user_segment: 'segmentB',
+ sample_rate: '1',
+ trace_id: expect.any(String),
+ public_key: 'public',
+ replay_id: replay.session?.id,
+ });
+});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
index 1ffeb360c650..27c429c9be98 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('parses response_body_size from Content-Length header if available', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -22,7 +26,17 @@ sentryTest('parses response_body_size from Content-Length header if available',
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -58,4 +72,20 @@ sentryTest('parses response_body_size from Content-Length header if available',
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 789,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
index 8248b4799480..31f8d65bc7e7 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('does not capture response_body_size without Content-Length header', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -22,7 +26,17 @@ sentryTest('does not capture response_body_size without Content-Length header',
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -57,4 +71,20 @@ sentryTest('does not capture response_body_size without Content-Length header',
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 29,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
index a293df49b366..d2c167110a8a 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -19,7 +23,17 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -60,4 +74,21 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 26,
+ responseBodySize: 24,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
index baac9005fd35..0f77394b6e5d 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('captures request_body_size when body is sent', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -12,16 +16,23 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
await page.route('**/foo', route => {
return route.fulfill({
status: 200,
- body: JSON.stringify({
- userNames: ['John', 'Jane'],
- }),
headers: {
'Content-Type': 'application/json',
},
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -48,5 +59,32 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
expect(eventData.exception?.values).toHaveLength(1);
expect(eventData?.breadcrumbs?.length).toBe(1);
+ expect(eventData!.breadcrumbs![0]).toEqual({
+ timestamp: expect.any(Number),
+ category: 'fetch',
+ type: 'http',
+ data: {
+ method: 'POST',
+ request_body_size: 13,
+ status_code: 200,
+ url: 'http://localhost:7654/foo',
+ },
+ });
expect(eventData!.breadcrumbs![0].data!.request_body_size).toEqual(13);
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 13,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
index b5f517f77352..4ee170939530 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest(
'parses response_body_size from Content-Length header if available',
@@ -25,7 +29,17 @@ sentryTest(
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -65,5 +79,21 @@ sentryTest(
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 789,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
},
);
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
index 9ea10831afab..9a9bd633c71f 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest(
'captures response_body_size without Content-Length header',
@@ -25,7 +29,17 @@ sentryTest(
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -65,5 +79,21 @@ sentryTest(
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 29,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
},
);
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
index 5142f2e6be82..0210283fea60 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
@@ -20,7 +24,17 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -63,4 +77,21 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 26,
+ responseBodySize: 24,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
index fd3cc426f9fd..470fe57c51ba 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('captures request_body_size when body is sent', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
@@ -13,9 +17,6 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
await page.route('**/foo', route => {
return route.fulfill({
status: 200,
- body: JSON.stringify({
- userNames: ['John', 'Jane'],
- }),
headers: {
'Content-Type': 'application/json',
'Content-Length': '',
@@ -23,7 +24,17 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -52,5 +63,31 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
expect(eventData.exception?.values).toHaveLength(1);
expect(eventData?.breadcrumbs?.length).toBe(1);
- expect(eventData!.breadcrumbs![0].data!.request_body_size).toEqual(13);
+ expect(eventData!.breadcrumbs![0]).toEqual({
+ timestamp: expect.any(Number),
+ category: 'xhr',
+ type: 'http',
+ data: {
+ method: 'POST',
+ request_body_size: 13,
+ status_code: 200,
+ url: 'http://localhost:7654/foo',
+ },
+ });
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 13,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
index f95e857d5637..dc071b9bf487 100644
--- a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
+++ b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
@@ -24,10 +24,34 @@ sentryTest(
sentryTest.skip();
}
+ // We want to ensure to check the correct event payloads
+ const inputMutationSegmentIds: number[] = [];
const reqPromise0 = waitForReplayRequest(page, 0);
- const reqPromise1 = waitForReplayRequest(page, 1);
- const reqPromise2 = waitForReplayRequest(page, 2);
- const reqPromise3 = waitForReplayRequest(page, 3);
+ const reqPromise1 = waitForReplayRequest(page, (event, res) => {
+ const check = inputMutationSegmentIds.length === 0 && getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise2 = waitForReplayRequest(page, (event, res) => {
+ const check =
+ inputMutationSegmentIds.length === 1 &&
+ inputMutationSegmentIds[0] < event.segment_id &&
+ getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise3 = waitForReplayRequest(page, event => {
+ // This one should not have any input mutations
+ return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id;
+ });
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
@@ -72,10 +96,34 @@ sentryTest(
sentryTest.skip();
}
+ // We want to ensure to check the correct event payloads
+ const inputMutationSegmentIds: number[] = [];
const reqPromise0 = waitForReplayRequest(page, 0);
- const reqPromise1 = waitForReplayRequest(page, 1);
- const reqPromise2 = waitForReplayRequest(page, 2);
- const reqPromise3 = waitForReplayRequest(page, 3);
+ const reqPromise1 = waitForReplayRequest(page, (event, res) => {
+ const check = inputMutationSegmentIds.length === 0 && getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise2 = waitForReplayRequest(page, (event, res) => {
+ const check =
+ inputMutationSegmentIds.length === 1 &&
+ inputMutationSegmentIds[0] < event.segment_id &&
+ getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise3 = waitForReplayRequest(page, event => {
+ // This one should not have any input mutations
+ return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id;
+ });
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts
index cf21ce7b9c7b..bd7696ebd927 100644
--- a/packages/browser-integration-tests/utils/replayHelpers.ts
+++ b/packages/browser-integration-tests/utils/replayHelpers.ts
@@ -99,6 +99,16 @@ function isCustomSnapshot(event: RecordingEvent): event is RecordingEvent & { da
return event.type === EventType.Custom;
}
+/** Wait for replay to be running & available. */
+export async function waitForReplayRunning(page: Page): Promise {
+ await page.waitForFunction(() => {
+ const replayIntegration = (window as unknown as Window & { Replay: { _replay: ReplayContainer } }).Replay;
+ const replay = replayIntegration._replay;
+
+ return replay.isEnabled() && replay.session?.id !== undefined;
+ });
+}
+
/**
* This returns the replay container (assuming it exists).
* Note that due to how this works with playwright, this is a POJO copy of replay.
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 5d93e763b309..fb2162449405 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -66,6 +66,7 @@
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "validate:es5": "es-check es5 build/bundles/bundle.es5.js",
"size:check": "run-p size:check:es5 size:check:es6",
"size:check:es5": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'",
"size:check:es6": "cat build/bundles/bundle.es6.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES6: \",$1,\"kB\";}'",
diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts
index 8cafc77a68c3..1d0bd091cf19 100644
--- a/packages/browser/src/client.ts
+++ b/packages/browser/src/client.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, SDK_VERSION } from '@sentry/core';
+import { BaseClient, SDK_VERSION } from '@sentry/core';
import type {
BrowserClientReplayOptions,
ClientOptions,
@@ -9,7 +9,7 @@ import type {
Severity,
SeverityLevel,
} from '@sentry/types';
-import { createClientReportEnvelope, dsnToString, getSDKSource, logger, serializeEnvelope } from '@sentry/utils';
+import { createClientReportEnvelope, dsnToString, getSDKSource, logger } from '@sentry/utils';
import { eventFromException, eventFromMessage } from './eventbuilder';
import { WINDOW } from './helpers';
@@ -132,24 +132,7 @@ export class BrowserClient extends BaseClient {
__DEBUG_BUILD__ && logger.log('Sending outcomes:', outcomes);
- const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, this._options);
const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn));
-
- try {
- const isRealNavigator = Object.prototype.toString.call(WINDOW && WINDOW.navigator) === '[object Navigator]';
- const hasSendBeacon = isRealNavigator && typeof WINDOW.navigator.sendBeacon === 'function';
- // Make sure beacon is not used if user configures custom transport options
- if (hasSendBeacon && !this._options.transportOptions) {
- // Prevent illegal invocations - https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
- const sendBeacon = WINDOW.navigator.sendBeacon.bind(WINDOW.navigator);
- sendBeacon(url, serializeEnvelope(envelope));
- } else {
- // If beacon is not supported or if they are using the tunnel option
- // use our regular transport to send client reports to Sentry.
- void this._sendEnvelope(envelope);
- }
- } catch (e) {
- __DEBUG_BUILD__ && logger.error(e);
- }
+ void this._sendEnvelope(envelope);
}
}
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 59d814ac9412..dca32f05b35d 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -21,8 +21,8 @@ const INTEGRATIONS = {
export { INTEGRATIONS as Integrations };
export { Replay } from '@sentry/replay';
-export { BrowserTracing } from '@sentry-internal/tracing';
-export { addTracingExtensions } from '@sentry/core';
+export { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry-internal/tracing';
+export { addTracingExtensions, getActiveTransaction } from '@sentry/core';
export { makeBrowserOfflineTransport } from './transports/offline';
export { onProfilingStartRouteTransaction } from './profiling/hubextensions';
export { BrowserProfilingIntegration } from './profiling/integration';
diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts
index 83b75c82ba39..e996b6f8277a 100644
--- a/packages/browser/src/transports/fetch.ts
+++ b/packages/browser/src/transports/fetch.ts
@@ -13,7 +13,14 @@ export function makeFetchTransport(
options: BrowserTransportOptions,
nativeFetch: FetchImpl = getNativeFetchImplementation(),
): Transport {
+ let pendingBodySize = 0;
+ let pendingCount = 0;
+
function makeRequest(request: TransportRequest): PromiseLike {
+ const requestSize = request.body.length;
+ pendingBodySize += requestSize;
+ pendingCount++;
+
const requestOptions: RequestInit = {
body: request.body,
method: 'POST',
@@ -25,23 +32,31 @@ export function makeFetchTransport(
// frequently sending events right before the user is switching pages (eg. whenfinishing navigation transactions).
// Gotchas:
// - `keepalive` isn't supported by Firefox
- // - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch), a request with `keepalive: true`
- // and a content length of > 64 kibibytes returns a network error. We will therefore only activate the flag when
- // we're below that limit.
- keepalive: request.body.length <= 65536,
+ // - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch):
+ // If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
+ // We will therefore only activate the flag when we're below that limit.
+ // There is also a limit of requests that can be open at the same time, so we also limit this to 15
+ // See https://github.com/getsentry/sentry-javascript/pull/7553 for details
+ keepalive: pendingBodySize <= 60_000 && pendingCount < 15,
...options.fetchOptions,
};
try {
- return nativeFetch(options.url, requestOptions).then(response => ({
- statusCode: response.status,
- headers: {
- 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
- 'retry-after': response.headers.get('Retry-After'),
- },
- }));
+ return nativeFetch(options.url, requestOptions).then(response => {
+ pendingBodySize -= requestSize;
+ pendingCount--;
+ return {
+ statusCode: response.status,
+ headers: {
+ 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
+ 'retry-after': response.headers.get('Retry-After'),
+ },
+ };
+ });
} catch (e) {
clearCachedFetchImplementation();
+ pendingBodySize -= requestSize;
+ pendingCount--;
return rejectedSyncPromise(e);
}
}
diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts
index 53ed5c34e21b..e6b58eeaa110 100644
--- a/packages/browser/test/unit/transports/fetch.test.ts
+++ b/packages/browser/test/unit/transports/fetch.test.ts
@@ -16,6 +16,11 @@ const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
]);
+const LARGE_ERROR_ENVELOPE = createEnvelope(
+ { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' },
+ [[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', message: 'x'.repeat(10 * 900) }] as EventItem],
+);
+
class Headers {
headers: { [key: string]: string } = {};
get(key: string) {
@@ -107,4 +112,54 @@ describe('NewFetchTransport', () => {
await expect(() => transport.send(ERROR_ENVELOPE)).not.toThrow();
expect(mockFetch).toHaveBeenCalledTimes(1);
});
+
+ it('correctly sets keepalive flag', async () => {
+ const mockFetch = jest.fn(() =>
+ Promise.resolve({
+ headers: new Headers(),
+ status: 200,
+ text: () => Promise.resolve({}),
+ }),
+ ) as unknown as FetchImpl;
+
+ const REQUEST_OPTIONS: RequestInit = {
+ referrerPolicy: 'strict-origin',
+ referrer: 'http://example.org',
+ };
+
+ const transport = makeFetchTransport(
+ { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, fetchOptions: REQUEST_OPTIONS },
+ mockFetch,
+ );
+
+ const promises: PromiseLike[] = [];
+ for (let i = 0; i < 30; i++) {
+ promises.push(transport.send(LARGE_ERROR_ENVELOPE));
+ }
+
+ await Promise.all(promises);
+
+ for (let i = 1; i <= 30; i++) {
+ // After 7 requests, we hit the total limit of >64kb of size
+ // Starting there, keepalive should be false
+ const keepalive = i < 7;
+ expect(mockFetch).toHaveBeenNthCalledWith(i, expect.any(String), expect.objectContaining({ keepalive }));
+ }
+
+ (mockFetch as jest.Mock).mockClear();
+
+ // Limit resets when requests have resolved
+ // Now try based on # of pending requests
+ const promises2 = [];
+ for (let i = 0; i < 20; i++) {
+ promises2.push(transport.send(ERROR_ENVELOPE));
+ }
+
+ await Promise.all(promises2);
+
+ for (let i = 1; i <= 20; i++) {
+ const keepalive = i < 15;
+ expect(mockFetch).toHaveBeenNthCalledWith(i, expect.any(String), expect.objectContaining({ keepalive }));
+ }
+ });
});
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index a6db2788aa52..80be9dc037bd 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -6,6 +6,7 @@ import type {
ClientOptions,
DataCategory,
DsnComponents,
+ DynamicSamplingContext,
Envelope,
ErrorEvent,
Event,
@@ -378,6 +379,9 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
+ /** @inheritdoc */
+ public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
+
/** @inheritdoc */
public on(hook: string, callback: unknown): void {
if (!this._hooks[hook]) {
@@ -400,6 +404,9 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public emit(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
+ /** @inheritdoc */
+ public emit(hook: 'createDsc', dsc: DynamicSamplingContext): void;
+
/** @inheritdoc */
public emit(hook: string, ...rest: unknown[]): void {
if (this._hooks[hook]) {
diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts
index 203241a35389..f67ae3773325 100644
--- a/packages/core/src/hub.ts
+++ b/packages/core/src/hub.ts
@@ -56,7 +56,7 @@ const DEFAULT_BREADCRUMBS = 100;
*/
export interface Layer {
client?: Client;
- scope?: Scope;
+ scope: Scope;
}
/**
@@ -87,7 +87,7 @@ export interface Carrier {
*/
export class Hub implements HubInterface {
/** Is a {@link Layer}[] containing the client and scope */
- private readonly _stack: Layer[] = [{}];
+ private readonly _stack: Layer[];
/** Contains the last event id of a captured event. */
private _lastEventId?: string;
@@ -101,7 +101,7 @@ export class Hub implements HubInterface {
* @param version number, higher number means higher priority.
*/
public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) {
- this.getStackTop().scope = scope;
+ this._stack = [{ scope }];
if (client) {
this.bindClient(client);
}
@@ -166,7 +166,7 @@ export class Hub implements HubInterface {
}
/** Returns the scope of the top stack. */
- public getScope(): Scope | undefined {
+ public getScope(): Scope {
return this.getStackTop().scope;
}
@@ -256,7 +256,7 @@ export class Hub implements HubInterface {
public addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void {
const { scope, client } = this.getStackTop();
- if (!scope || !client) return;
+ if (!client) return;
const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } =
(client.getOptions && client.getOptions()) || {};
@@ -282,40 +282,35 @@ export class Hub implements HubInterface {
* @inheritDoc
*/
public setUser(user: User | null): void {
- const scope = this.getScope();
- if (scope) scope.setUser(user);
+ this.getScope().setUser(user);
}
/**
* @inheritDoc
*/
public setTags(tags: { [key: string]: Primitive }): void {
- const scope = this.getScope();
- if (scope) scope.setTags(tags);
+ this.getScope().setTags(tags);
}
/**
* @inheritDoc
*/
public setExtras(extras: Extras): void {
- const scope = this.getScope();
- if (scope) scope.setExtras(extras);
+ this.getScope().setExtras(extras);
}
/**
* @inheritDoc
*/
public setTag(key: string, value: Primitive): void {
- const scope = this.getScope();
- if (scope) scope.setTag(key, value);
+ this.getScope().setTag(key, value);
}
/**
* @inheritDoc
*/
public setExtra(key: string, extra: Extra): void {
- const scope = this.getScope();
- if (scope) scope.setExtra(key, extra);
+ this.getScope().setExtra(key, extra);
}
/**
@@ -323,8 +318,7 @@ export class Hub implements HubInterface {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public setContext(name: string, context: { [key: string]: any } | null): void {
- const scope = this.getScope();
- if (scope) scope.setContext(name, context);
+ this.getScope().setContext(name, context);
}
/**
@@ -332,7 +326,7 @@ export class Hub implements HubInterface {
*/
public configureScope(callback: (scope: Scope) => void): void {
const { scope, client } = this.getStackTop();
- if (scope && client) {
+ if (client) {
callback(scope);
}
}
@@ -395,17 +389,15 @@ export class Hub implements HubInterface {
*/
public endSession(): void {
const layer = this.getStackTop();
- const scope = layer && layer.scope;
- const session = scope && scope.getSession();
+ const scope = layer.scope;
+ const session = scope.getSession();
if (session) {
closeSession(session);
}
this._sendSessionUpdate();
// the session is over; take it off of the scope
- if (scope) {
- scope.setSession();
- }
+ scope.setSession();
}
/**
@@ -421,22 +413,20 @@ export class Hub implements HubInterface {
const session = makeSession({
release,
environment,
- ...(scope && { user: scope.getUser() }),
+ user: scope.getUser(),
...(userAgent && { userAgent }),
...context,
});
- if (scope) {
- // End existing session if there's one
- const currentSession = scope.getSession && scope.getSession();
- if (currentSession && currentSession.status === 'ok') {
- updateSession(currentSession, { status: 'exited' });
- }
- this.endSession();
-
- // Afterwards we set the new session on the scope
- scope.setSession(session);
+ // End existing session if there's one
+ const currentSession = scope.getSession && scope.getSession();
+ if (currentSession && currentSession.status === 'ok') {
+ updateSession(currentSession, { status: 'exited' });
}
+ this.endSession();
+
+ // Afterwards we set the new session on the scope
+ scope.setSession(session);
return session;
}
@@ -472,7 +462,7 @@ export class Hub implements HubInterface {
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
- private _withClient(callback: (client: Client, scope: Scope | undefined) => void): void {
+ private _withClient(callback: (client: Client, scope: Scope) => void): void {
const { scope, client } = this.getStackTop();
if (client) {
callback(client, scope);
diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts
index 047060ae4961..e790e3daf4b5 100644
--- a/packages/core/src/integrations/inboundfilters.ts
+++ b/packages/core/src/integrations/inboundfilters.ts
@@ -103,7 +103,8 @@ export function _shouldDropEvent(event: Event, options: Partial): boolean {
- if (!ignoreErrors || !ignoreErrors.length) {
+ // If event.type, this is not an error
+ if (event.type || !ignoreErrors || !ignoreErrors.length) {
return false;
}
diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts
index 7d2df2115ddd..5dc58a5fc944 100644
--- a/packages/core/src/sdk.ts
+++ b/packages/core/src/sdk.ts
@@ -28,9 +28,7 @@ export function initAndBind(
}
const hub = getCurrentHub();
const scope = hub.getScope();
- if (scope) {
- scope.update(options.initialScope);
- }
+ scope.update(options.initialScope);
const client = new clientClass(options);
hub.bindClient(client);
diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts
index 9b4579ade486..4ef196819504 100644
--- a/packages/core/src/sessionflusher.ts
+++ b/packages/core/src/sessionflusher.ts
@@ -72,15 +72,13 @@ export class SessionFlusher implements SessionFlusherLike {
return;
}
const scope = getCurrentHub().getScope();
- const requestSession = scope && scope.getRequestSession();
+ const requestSession = scope.getRequestSession();
if (requestSession && requestSession.status) {
this._incrementSessionStatusCount(requestSession.status, new Date());
// This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in
// case captureRequestSession is called more than once to prevent double count
- if (scope) {
- scope.setRequestSession(undefined);
- }
+ scope.setRequestSession(undefined);
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
}
diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts
index 3351f428fecf..8785bd86d448 100644
--- a/packages/core/src/tracing/errors.ts
+++ b/packages/core/src/tracing/errors.ts
@@ -29,3 +29,7 @@ function errorCallback(): void {
activeTransaction.setStatus(status);
}
}
+
+// The function name will be lost when bundling but we need to be able to identify this listener later to maintain the
+// node.js default exit behaviour
+errorCallback.tag = 'sentry_tracingErrorCallback';
diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts
index b5518f95e04b..ad6858d41e4a 100644
--- a/packages/core/src/tracing/hubextensions.ts
+++ b/packages/core/src/tracing/hubextensions.ts
@@ -11,15 +11,13 @@ import { Transaction } from './transaction';
/** Returns all trace headers that are currently on the top scope. */
function traceHeaders(this: Hub): { [key: string]: string } {
const scope = this.getScope();
- if (scope) {
- const span = scope.getSpan();
- if (span) {
- return {
+ const span = scope.getSpan();
+
+ return span
+ ? {
'sentry-trace': span.toTraceparent(),
- };
- }
- }
- return {};
+ }
+ : {};
}
/**
diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts
index f4604a312519..77b7d5317da1 100644
--- a/packages/core/src/tracing/idletransaction.ts
+++ b/packages/core/src/tracing/idletransaction.ts
@@ -346,10 +346,7 @@ export class IdleTransaction extends Transaction {
*/
function clearActiveTransaction(hub: Hub): void {
const scope = hub.getScope();
- if (scope) {
- const transaction = scope.getTransaction();
- if (transaction) {
- scope.setSpan(undefined);
- }
+ if (scope.getTransaction()) {
+ scope.setSpan(undefined);
}
}
diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts
index fd4949257ceb..1afb556bce4d 100644
--- a/packages/core/src/tracing/index.ts
+++ b/packages/core/src/tracing/index.ts
@@ -6,3 +6,4 @@ export { extractTraceparentData, getActiveTransaction, stripUrlQueryAndFragment,
// eslint-disable-next-line deprecation/deprecation
export { SpanStatus } from './spanstatus';
export type { SpanStatusType } from './span';
+export { trace } from './trace';
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
new file mode 100644
index 000000000000..8e7844d23988
--- /dev/null
+++ b/packages/core/src/tracing/trace.ts
@@ -0,0 +1,68 @@
+import type { TransactionContext } from '@sentry/types';
+import { isThenable } from '@sentry/utils';
+
+import { getCurrentHub } from '../hub';
+import type { Span } from './span';
+
+/**
+ * Wraps a function with a transaction/span and finishes the span after the function is done.
+ *
+ * Note that if you have not enabled tracing extensions via `addTracingExtensions`, this function
+ * will not generate spans, and the `span` returned from the callback may be undefined.
+ *
+ * This function is meant to be used internally and may break at any time. Use at your own risk.
+ *
+ * @internal
+ * @private
+ */
+export function trace(
+ context: TransactionContext,
+ callback: (span?: Span) => T,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onError: (error: unknown) => void = () => {},
+): T {
+ const ctx = { ...context };
+ // If a name is set and a description is not, set the description to the name.
+ if (ctx.name !== undefined && ctx.description === undefined) {
+ ctx.description = ctx.name;
+ }
+
+ const hub = getCurrentHub();
+ const scope = hub.getScope();
+
+ const parentSpan = scope.getSpan();
+ const activeSpan = parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
+ scope.setSpan(activeSpan);
+
+ function finishAndSetSpan(): void {
+ activeSpan && activeSpan.finish();
+ hub.getScope().setSpan(parentSpan);
+ }
+
+ let maybePromiseResult: T;
+ try {
+ maybePromiseResult = callback(activeSpan);
+ } catch (e) {
+ activeSpan && activeSpan.setStatus('internal_error');
+ onError(e);
+ finishAndSetSpan();
+ throw e;
+ }
+
+ if (isThenable(maybePromiseResult)) {
+ Promise.resolve(maybePromiseResult).then(
+ () => {
+ finishAndSetSpan();
+ },
+ e => {
+ activeSpan && activeSpan.setStatus('internal_error');
+ onError(e);
+ finishAndSetSpan();
+ },
+ );
+ } else {
+ finishAndSetSpan();
+ }
+
+ return maybePromiseResult;
+}
diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts
index eba498b7e654..9dcee22fb888 100644
--- a/packages/core/src/tracing/transaction.ts
+++ b/packages/core/src/tracing/transaction.ts
@@ -256,8 +256,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
const maybeSampleRate = this.metadata.sampleRate;
const sample_rate = maybeSampleRate !== undefined ? maybeSampleRate.toString() : undefined;
- const scope = hub.getScope();
- const { segment: user_segment } = (scope && scope.getUser()) || {};
+ const { segment: user_segment } = hub.getScope().getUser() || {};
const source = this.metadata.source;
@@ -277,6 +276,8 @@ export class Transaction extends SpanClass implements TransactionInterface {
// Uncomment if we want to make DSC immutable
// this._frozenDynamicSamplingContext = dsc;
+ client.emit && client.emit('createDsc', dsc);
+
return dsc;
}
}
diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts
index 5dc64f98d400..624fff153270 100644
--- a/packages/core/src/tracing/utils.ts
+++ b/packages/core/src/tracing/utils.ts
@@ -21,7 +21,7 @@ export { TRACEPARENT_REGEXP, extractTraceparentData } from '@sentry/utils';
export function getActiveTransaction(maybeHub?: Hub): T | undefined {
const hub = maybeHub || getCurrentHub();
const scope = hub.getScope();
- return scope && (scope.getTransaction() as T | undefined);
+ return scope.getTransaction() as T | undefined;
}
// so it can be used in manual instrumentation without necessitating a hard dependency on @sentry/utils
diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts
index ff9aca20270a..7537f19d4d5d 100644
--- a/packages/core/test/lib/integrations/inboundfilters.test.ts
+++ b/packages/core/test/lib/integrations/inboundfilters.test.ts
@@ -177,6 +177,11 @@ const MALFORMED_EVENT: Event = {
},
};
+const TRANSACTION_EVENT: Event = {
+ message: 'transaction message',
+ type: 'transaction',
+};
+
describe('InboundFilters', () => {
describe('_isSentryError', () => {
it('should work as expected', () => {
@@ -202,6 +207,13 @@ describe('InboundFilters', () => {
expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null);
});
+ it('ignores transaction event for filtering', () => {
+ const eventProcessor = createInboundFiltersEventProcessor({
+ ignoreErrors: ['transaction'],
+ });
+ expect(eventProcessor(TRANSACTION_EVENT, {})).toBe(TRANSACTION_EVENT);
+ });
+
it('string filter with exact match', () => {
const eventProcessor = createInboundFiltersEventProcessor({
ignoreErrors: ['captureMessage'],
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
new file mode 100644
index 000000000000..064c41dc123a
--- /dev/null
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -0,0 +1,189 @@
+import { addTracingExtensions, Hub, makeMain } from '../../../src';
+import { trace } from '../../../src/tracing';
+import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
+const enum Type {
+ Sync = 'sync',
+ Async = 'async',
+}
+
+let hub: Hub;
+let client: TestClient;
+
+describe('trace', () => {
+ beforeEach(() => {
+ const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 });
+ client = new TestClient(options);
+ hub = new Hub(client);
+ makeMain(hub);
+ });
+
+ describe.each([
+ // isSync, isError, callback, expectedReturnValue
+ [Type.Async, false, () => Promise.resolve('async good'), 'async good'],
+ [Type.Sync, false, () => 'sync good', 'sync good'],
+ [Type.Async, true, () => Promise.reject('async bad'), 'async bad'],
+ [
+ Type.Sync,
+ true,
+ () => {
+ throw 'sync bad';
+ },
+ 'sync bad',
+ ],
+ ])('with %s callback and error %s', (_type, isError, callback, expected) => {
+ it('should return the same value as the callback', async () => {
+ try {
+ const result = await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ expect(result).toEqual(expected);
+ } catch (e) {
+ expect(e).toEqual(expected);
+ }
+ });
+
+ it('should return the same value as the callback if transactions are undefined', async () => {
+ // @ts-ignore we are force overriding the transaction return to be undefined
+ // The `startTransaction` types are actually wrong - it can return undefined
+ // if tracingExtensions are not enabled
+ jest.spyOn(hub, 'startTransaction').mockReturnValue(undefined);
+ try {
+ const result = await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ expect(result).toEqual(expected);
+ } catch (e) {
+ expect(e).toEqual(expected);
+ }
+ });
+
+ it('creates a transaction', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ } catch (e) {
+ //
+ }
+ expect(ref).toBeDefined();
+
+ expect(ref.name).toEqual('GET users/[id]');
+ expect(ref.status).toEqual(isError ? 'internal_error' : undefined);
+ });
+
+ it('allows traceparent information to be overriden', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace(
+ {
+ name: 'GET users/[id]',
+ parentSampled: true,
+ traceId: '12345678901234567890123456789012',
+ parentSpanId: '1234567890123456',
+ },
+ () => {
+ return callback();
+ },
+ );
+ } catch (e) {
+ //
+ }
+ expect(ref).toBeDefined();
+
+ expect(ref.sampled).toEqual(true);
+ expect(ref.traceId).toEqual('12345678901234567890123456789012');
+ expect(ref.parentSpanId).toEqual('1234567890123456');
+ });
+
+ it('allows for transaction to be mutated', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]' }, span => {
+ if (span) {
+ span.op = 'http.server';
+ }
+ return callback();
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.op).toEqual('http.server');
+ });
+
+ it('creates a span with correct description', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
+ return trace({ name: 'SELECT * from users' }, () => {
+ return callback();
+ });
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.spanRecorder.spans).toHaveLength(2);
+ expect(ref.spanRecorder.spans[1].description).toEqual('SELECT * from users');
+ expect(ref.spanRecorder.spans[1].parentSpanId).toEqual(ref.spanId);
+ expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined);
+ });
+
+ it('allows for span to be mutated', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
+ return trace({ name: 'SELECT * from users' }, childSpan => {
+ if (childSpan) {
+ childSpan.op = 'db.query';
+ }
+ return callback();
+ });
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.spanRecorder.spans).toHaveLength(2);
+ expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
+ });
+
+ it('calls `onError` hook', async () => {
+ const onError = jest.fn();
+ try {
+ await trace(
+ { name: 'GET users/[id]' },
+ () => {
+ return callback();
+ },
+ onError,
+ );
+ } catch (e) {
+ expect(onError).toHaveBeenCalledTimes(1);
+ expect(onError).toHaveBeenCalledWith(e);
+ }
+ expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
+ });
+ });
+});
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts b/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
index 109dbcd55648..ffa61ca49acc 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
@@ -1,4 +1,5 @@
interface Window {
recordedTransactions?: string[];
capturedExceptionId?: string;
+ sentryReplayId?: string;
}
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx b/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
index c6b8db266ac0..ef820ec794b3 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
@@ -14,6 +14,8 @@ import {
import Index from './pages/Index';
import User from './pages/User';
+const replay = new Sentry.Replay();
+
Sentry.init({
dsn: process.env.REACT_APP_E2E_TEST_DSN,
integrations: [
@@ -26,11 +28,22 @@ Sentry.init({
matchRoutes,
),
}),
+ replay,
],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
release: 'e2e-test',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+});
+
+Object.defineProperty(window, 'sentryReplayId', {
+ get() {
+ return replay['_replay'].session.id;
+ },
});
Sentry.addGlobalEventProcessor(event => {
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
index 795d610a4b08..fb2d291dd70d 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
+import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
const EVENT_POLLING_TIMEOUT = 30_000;
@@ -169,3 +170,85 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
expect(hadPageNavigationTransaction).toBe(true);
});
+
+test('Sends a Replay recording to Sentry', async ({ browser }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ await page.goto('/');
+
+ const replayId = await page.waitForFunction(() => {
+ return window.sentryReplayId;
+ });
+
+ // Wait for replay to be sent
+
+ if (replayId === undefined) {
+ throw new Error("Application didn't set a replayId");
+ }
+
+ console.log(`Polling for replay with ID: ${replayId}`);
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+
+ // now fetch the first recording segment
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/recording-segments/?cursor=100%3A0%3A1`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return {
+ status: response.status,
+ data: response.data,
+ };
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toEqual({
+ status: 200,
+ data: ReplayRecordingData,
+ });
+});
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
new file mode 100644
index 000000000000..318fc368f7b9
--- /dev/null
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
@@ -0,0 +1,225 @@
+import { expect } from '@playwright/test';
+
+export const ReplayRecordingData = [
+ [
+ { type: 4, data: { href: 'http://localhost:3000/', width: 1280, height: 720 }, timestamp: expect.any(Number) },
+ {
+ type: 2,
+ data: {
+ node: {
+ type: 0,
+ childNodes: [
+ { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+ {
+ type: 2,
+ tagName: 'html',
+ attributes: { lang: 'en' },
+ childNodes: [
+ {
+ type: 2,
+ tagName: 'head',
+ attributes: {},
+ childNodes: [
+ { type: 2, tagName: 'meta', attributes: { charset: 'utf-8' }, childNodes: [], id: 5 },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: { name: 'viewport', content: 'width=device-width,initial-scale=1' },
+ childNodes: [],
+ id: 6,
+ },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: { name: 'theme-color', content: '#000000' },
+ childNodes: [],
+ id: 7,
+ },
+ {
+ type: 2,
+ tagName: 'title',
+ attributes: {},
+ childNodes: [{ type: 3, textContent: '***** ***', id: 9 }],
+ id: 8,
+ },
+ ],
+ id: 4,
+ },
+ {
+ type: 2,
+ tagName: 'body',
+ attributes: {},
+ childNodes: [
+ {
+ type: 2,
+ tagName: 'noscript',
+ attributes: {},
+ childNodes: [{ type: 3, textContent: '*** **** ** ****** ********** ** *** **** ****', id: 12 }],
+ id: 11,
+ },
+ { type: 2, tagName: 'div', attributes: { id: 'root' }, childNodes: [], id: 13 },
+ ],
+ id: 10,
+ },
+ ],
+ id: 3,
+ },
+ ],
+ id: 1,
+ },
+ initialOffset: { left: 0, top: 0 },
+ },
+ timestamp: expect.any(Number),
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'memory',
+ description: 'memory',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ memory: {
+ jsHeapSizeLimit: expect.any(Number),
+ totalJSHeapSize: expect.any(Number),
+ usedJSHeapSize: expect.any(Number),
+ },
+ },
+ },
+ },
+ },
+ {
+ type: 3,
+ data: {
+ source: 0,
+ texts: [],
+ attributes: [],
+ removes: [],
+ adds: [
+ {
+ parentId: 13,
+ nextId: null,
+ node: {
+ type: 2,
+ tagName: 'a',
+ attributes: { id: 'navigation', href: 'http://localhost:3000/user/5' },
+ childNodes: [],
+ id: 14,
+ },
+ },
+ { parentId: 14, nextId: null, node: { type: 3, textContent: '********', id: 15 } },
+ {
+ parentId: 13,
+ nextId: 14,
+ node: {
+ type: 2,
+ tagName: 'input',
+ attributes: { type: 'button', id: 'exception-button', value: '******* *********' },
+ childNodes: [],
+ id: 16,
+ },
+ },
+ ],
+ },
+ timestamp: expect.any(Number),
+ },
+ {
+ type: 3,
+ data: { source: 5, text: 'Capture Exception', isChecked: false, id: 16 },
+ timestamp: expect.any(Number),
+ },
+ ],
+ [
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'navigation.navigate',
+ description: 'http://localhost:3000/',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { size: expect.any(Number), duration: expect.any(Number) },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'resource.script',
+ description: expect.stringMatching(/http:\/\/localhost:3000\/static\/js\/main.(\w+).js/),
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { size: expect.any(Number), encodedBodySize: expect.any(Number) },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'largest-contentful-paint',
+ description: 'largest-contentful-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { value: expect.any(Number), size: expect.any(Number), nodeId: 16 },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'paint',
+ description: 'first-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'paint',
+ description: 'first-contentful-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'memory',
+ description: 'memory',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ memory: {
+ jsHeapSizeLimit: expect.any(Number),
+ totalJSHeapSize: expect.any(Number),
+ usedJSHeapSize: expect.any(Number),
+ },
+ },
+ },
+ },
+ },
+ ],
+];
diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts
index ac3c08e46079..0001e6c5f97c 100644
--- a/packages/ember/addon/instance-initializers/sentry-performance.ts
+++ b/packages/ember/addon/instance-initializers/sentry-performance.ts
@@ -386,7 +386,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
// Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
- const tracing = await import('@sentry/tracing');
+ const { BrowserTracing } = await import('@sentry/browser');
const idleTimeout = config.transitionTimeout || 5000;
@@ -394,7 +394,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
sentryConfig['integrations'] = [
...existingIntegrations,
- new tracing.Integrations.BrowserTracing({
+ new BrowserTracing({
routingInstrumentation: (customStartTransaction, startTransactionOnPageLoad) => {
const routerMain = appInstance.lookup('router:main');
let routerService = appInstance.lookup('service:router') as
@@ -421,7 +421,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
];
class FakeBrowserTracingClass {
- static id = tracing.BROWSER_TRACING_INTEGRATION_ID;
+ static id = 'BrowserTracing';
public name = FakeBrowserTracingClass.id;
setupOnce() {
// noop - We're just faking this class for a lookup
diff --git a/packages/ember/package.json b/packages/ember/package.json
index 2ac858180668..5ba2b5dcfead 100644
--- a/packages/ember/package.json
+++ b/packages/ember/package.json
@@ -30,7 +30,6 @@
"dependencies": {
"@embroider/macros": "^1.9.0",
"@sentry/browser": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"ember-auto-import": "^1.12.1 || ^2.4.3",
diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js
index d09be9e2e67d..05ec68cff509 100644
--- a/packages/eslint-config-sdk/src/index.js
+++ b/packages/eslint-config-sdk/src/index.js
@@ -161,6 +161,9 @@ module.exports = {
// All imports should be accounted for
'import/no-extraneous-dependencies': 'error',
+
+ // Do not allow usage of functions we do not polyfill for ES5
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'error',
},
},
{
diff --git a/packages/eslint-plugin-sdk/src/index.js b/packages/eslint-plugin-sdk/src/index.js
index 31ac785abf5e..31d8e932d904 100644
--- a/packages/eslint-plugin-sdk/src/index.js
+++ b/packages/eslint-plugin-sdk/src/index.js
@@ -13,5 +13,6 @@ module.exports = {
'no-optional-chaining': require('./rules/no-optional-chaining'),
'no-nullish-coalescing': require('./rules/no-nullish-coalescing'),
'no-eq-empty': require('./rules/no-eq-empty'),
+ 'no-unsupported-es6-methods': require('./rules/no-unsupported-es6-methods'),
},
};
diff --git a/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js b/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js
new file mode 100644
index 000000000000..85d32fb20e66
--- /dev/null
+++ b/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * Taken and adapted from https://github.com/nkt/eslint-plugin-es5/blob/master/src/rules/no-es6-methods.js
+ */
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Forbid methods added in ES6 which are not polyfilled by Sentry.',
+ },
+ schema: [],
+ },
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (!node.callee || !node.callee.property) {
+ return;
+ }
+ const functionName = node.callee.property.name;
+
+ const es6ArrayFunctions = ['copyWithin', 'values', 'fill'];
+ const es6StringFunctions = ['repeat'];
+
+ const es6Functions = [].concat(es6ArrayFunctions, es6StringFunctions);
+ if (es6Functions.indexOf(functionName) > -1) {
+ context.report({
+ node: node.callee.property,
+ message: `ES6 methods not allowed: ${functionName}`,
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 1016f7cfa754..732c0836a08f 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -22,7 +22,6 @@
"dependencies": {
"@sentry/core": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.19.0"
diff --git a/packages/gatsby/src/index.ts b/packages/gatsby/src/index.ts
index 6957f7337700..7c603d040693 100644
--- a/packages/gatsby/src/index.ts
+++ b/packages/gatsby/src/index.ts
@@ -1,4 +1,3 @@
export * from '@sentry/react';
-export { Integrations } from '@sentry/tracing';
export { init } from './sdk';
diff --git a/packages/gatsby/src/utils/integrations.ts b/packages/gatsby/src/utils/integrations.ts
index 680ef61765cc..94ef28f21272 100644
--- a/packages/gatsby/src/utils/integrations.ts
+++ b/packages/gatsby/src/utils/integrations.ts
@@ -1,5 +1,5 @@
import { hasTracingEnabled } from '@sentry/core';
-import * as Tracing from '@sentry/tracing';
+import { BrowserTracing } from '@sentry/react';
import type { Integration } from '@sentry/types';
import type { GatsbyOptions } from './types';
@@ -31,11 +31,8 @@ export function getIntegrationsFromOptions(options: GatsbyOptions): UserIntegrat
* @param isTracingEnabled Whether the user has enabled tracing.
*/
function getIntegrationsFromArray(userIntegrations: Integration[], isTracingEnabled: boolean): Integration[] {
- if (
- isTracingEnabled &&
- !userIntegrations.some(integration => integration.name === Tracing.Integrations.BrowserTracing.name)
- ) {
- userIntegrations.push(new Tracing.Integrations.BrowserTracing());
+ if (isTracingEnabled && !userIntegrations.some(integration => integration.name === BrowserTracing.name)) {
+ userIntegrations.push(new BrowserTracing());
}
return userIntegrations;
}
diff --git a/packages/gatsby/test/gatsby-browser.test.ts b/packages/gatsby/test/gatsby-browser.test.ts
index 00be51488d5f..b67305042c71 100644
--- a/packages/gatsby/test/gatsby-browser.test.ts
+++ b/packages/gatsby/test/gatsby-browser.test.ts
@@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { onClientEntry } from '../gatsby-browser';
+import { BrowserTracing } from '../src/index';
(global as any).__SENTRY_RELEASE__ = '683f3a6ab819d47d23abfca9a914c81f0524d35b';
(global as any).__SENTRY_DSN__ = 'https://examplePublicKey@o0.ingest.sentry.io/0';
@@ -20,11 +21,11 @@ global.console.warn = jest.fn();
global.console.error = jest.fn();
let tracingAddExtensionMethods = jest.fn();
-jest.mock('@sentry/tracing', () => {
- const original = jest.requireActual('@sentry/tracing');
+jest.mock('@sentry/core', () => {
+ const original = jest.requireActual('@sentry/core');
return {
...original,
- addExtensionMethods: (...args: any[]) => {
+ addTracingExtensions: (...args: any[]) => {
tracingAddExtensionMethods(...args);
},
};
@@ -140,8 +141,7 @@ describe('onClientEntry', () => {
});
it('only defines a single `BrowserTracing` integration', () => {
- const Tracing = jest.requireActual('@sentry/tracing');
- const integrations = [new Tracing.Integrations.BrowserTracing()];
+ const integrations = [new BrowserTracing()];
onClientEntry(undefined, { tracesSampleRate: 0.5, integrations });
expect(sentryInit).toHaveBeenLastCalledWith(
diff --git a/packages/gatsby/test/sdk.test.ts b/packages/gatsby/test/sdk.test.ts
index 1c4342a13a4b..082fd771060b 100644
--- a/packages/gatsby/test/sdk.test.ts
+++ b/packages/gatsby/test/sdk.test.ts
@@ -1,5 +1,4 @@
-import { init, SDK_VERSION } from '@sentry/react';
-import { Integrations } from '@sentry/tracing';
+import { BrowserTracing, init, SDK_VERSION } from '@sentry/react';
import type { Integration } from '@sentry/types';
import { init as gatsbyInit } from '../src/sdk';
@@ -58,6 +57,8 @@ describe('Initialize React SDK', () => {
});
});
+type TestArgs = [string, Integration[], GatsbyOptions, string[]];
+
describe('Integrations from options', () => {
afterEach(() => reactInit.mockClear());
@@ -65,72 +66,39 @@ describe('Integrations from options', () => {
['tracing disabled, no integrations', [], {}, []],
['tracing enabled, no integrations', [], { tracesSampleRate: 1 }, ['BrowserTracing']],
[
- 'tracing disabled, with Integrations.BrowserTracing as an array',
+ 'tracing disabled, with BrowserTracing as an array',
[],
- { integrations: [new Integrations.BrowserTracing()] },
+ { integrations: [new BrowserTracing()] },
['BrowserTracing'],
],
[
- 'tracing disabled, with Integrations.BrowserTracing as a function',
+ 'tracing disabled, with BrowserTracing as a function',
[],
{
- integrations: () => [new Integrations.BrowserTracing()],
+ integrations: () => [new BrowserTracing()],
},
['BrowserTracing'],
],
[
- 'tracing enabled, with Integrations.BrowserTracing as an array',
+ 'tracing enabled, with BrowserTracing as an array',
[],
- { tracesSampleRate: 1, integrations: [new Integrations.BrowserTracing()] },
+ { tracesSampleRate: 1, integrations: [new BrowserTracing()] },
['BrowserTracing'],
],
[
- 'tracing enabled, with Integrations.BrowserTracing as a function',
+ 'tracing enabled, with BrowserTracing as a function',
[],
- { tracesSampleRate: 1, integrations: () => [new Integrations.BrowserTracing()] },
+ { tracesSampleRate: 1, integrations: () => [new BrowserTracing()] },
['BrowserTracing'],
],
- [
- 'tracing enabled, with another integration as an array',
- [],
- { tracesSampleRate: 1, integrations: [new Integrations.Express()] },
- ['Express', 'BrowserTracing'],
- ],
- [
- 'tracing enabled, with another integration as a function',
- [],
- { tracesSampleRate: 1, integrations: () => [new Integrations.Express()] },
- ['Express', 'BrowserTracing'],
- ],
- [
- 'tracing disabled, with another integration as an array',
- [],
- { integrations: [new Integrations.Express()] },
- ['Express'],
- ],
- [
- 'tracing disabled, with another integration as a function',
- [],
- { integrations: () => [new Integrations.Express()] },
- ['Express'],
- ],
- [
- 'merges integrations with user integrations as a function',
- [new Integrations.Mongo()],
- {
- tracesSampleRate: 1,
- integrations: (defaultIntegrations: Integration[]): Integration[] => [
- ...defaultIntegrations,
- new Integrations.Express(),
- ],
- },
- ['Mongo', 'Express', 'BrowserTracing'],
- ],
- ])('%s', (_testName, defaultIntegrations: Integration[], options: GatsbyOptions, expectedIntNames: string[]) => {
- gatsbyInit(options);
- const integrations: UserIntegrations = reactInit.mock.calls[0][0].integrations;
- const arrIntegrations = Array.isArray(integrations) ? integrations : integrations(defaultIntegrations);
- expect(arrIntegrations).toHaveLength(expectedIntNames.length);
- arrIntegrations.map((integration, idx) => expect(integration.name).toStrictEqual(expectedIntNames[idx]));
- });
+ ] as TestArgs[])(
+ '%s',
+ (_testName, defaultIntegrations: Integration[], options: GatsbyOptions, expectedIntNames: string[]) => {
+ gatsbyInit(options);
+ const integrations: UserIntegrations = reactInit.mock.calls[0][0].integrations;
+ const arrIntegrations = Array.isArray(integrations) ? integrations : integrations(defaultIntegrations);
+ expect(arrIntegrations).toHaveLength(expectedIntNames.length);
+ arrIntegrations.map((integration, idx) => expect(integration.name).toStrictEqual(expectedIntNames[idx]));
+ },
+ );
});
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index f341b0609519..8b32553b1f9d 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -22,7 +22,6 @@
"@sentry/integrations": "7.44.2",
"@sentry/node": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.20.0",
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index 79dcd5219cd4..ece6bf78db6a 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -1,8 +1,13 @@
import { hasTracingEnabled } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { BrowserOptions } from '@sentry/react';
-import { configureScope, init as reactInit, Integrations } from '@sentry/react';
-import { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/tracing';
+import {
+ BrowserTracing,
+ configureScope,
+ defaultRequestInstrumentationOptions,
+ init as reactInit,
+ Integrations,
+} from '@sentry/react';
import type { EventProcessor } from '@sentry/types';
import { addOrUpdateIntegration } from '@sentry/utils';
diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts
index a5b38d651aed..16aed66d1ca4 100644
--- a/packages/nextjs/src/edge/edgeclient.ts
+++ b/packages/nextjs/src/edge/edgeclient.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, SDK_VERSION } from '@sentry/core';
+import { addTracingExtensions, BaseClient, SDK_VERSION } from '@sentry/core';
import type { ClientOptions, Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
@@ -28,6 +28,9 @@ export class EdgeClient extends BaseClient {
version: SDK_VERSION,
};
+ // The Edge client always supports tracing
+ addTracingExtensions();
+
super(options);
}
diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts
index 6f8cd2f42cc4..f7785aaa06d6 100644
--- a/packages/nextjs/src/edge/index.ts
+++ b/packages/nextjs/src/edge/index.ts
@@ -1,5 +1,3 @@
-import '@sentry/tracing'; // Allow people to call tracing API methods without explicitly importing the tracing package.
-
import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
import type { Options } from '@sentry/types';
import {
diff --git a/packages/nextjs/src/server/utils/wrapperUtils.ts b/packages/nextjs/src/server/utils/wrapperUtils.ts
index ae05b3f16b8d..9fa91fbbee5a 100644
--- a/packages/nextjs/src/server/utils/wrapperUtils.ts
+++ b/packages/nextjs/src/server/utils/wrapperUtils.ts
@@ -1,5 +1,4 @@
-import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
-import { getActiveTransaction } from '@sentry/tracing';
+import { captureException, getActiveTransaction, getCurrentHub, startTransaction } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
import * as domain from 'domain';
diff --git a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
index 5bed5dca3135..37ef93bf30cc 100644
--- a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
@@ -1,10 +1,10 @@
import { hasTracingEnabled } from '@sentry/core';
import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import type { Transaction } from '@sentry/types';
import {
addExceptionMechanism,
baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
isString,
logger,
objectify,
diff --git a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
index eab32207236c..12ff9ceb5f72 100644
--- a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
@@ -1,4 +1,4 @@
-import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
+import { addTracingExtensions, captureException, getCurrentHub, startTransaction } from '@sentry/core';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
import * as domain from 'domain';
@@ -11,6 +11,8 @@ export function wrapServerComponentWithSentry any>
appDirComponent: F,
context: ServerComponentContext,
): F {
+ addTracingExtensions();
+
const { componentRoute, componentType } = context;
// Even though users may define server components as async functions, for the client bundles
diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts
index 5bfb29434523..ed3bb666d58d 100644
--- a/packages/nextjs/test/clientSdk.test.ts
+++ b/packages/nextjs/test/clientSdk.test.ts
@@ -1,7 +1,6 @@
import { BaseClient, getCurrentHub } from '@sentry/core';
import * as SentryReact from '@sentry/react';
-import { WINDOW } from '@sentry/react';
-import { Integrations as TracingIntegrations } from '@sentry/tracing';
+import { BrowserTracing, WINDOW } from '@sentry/react';
import type { Integration } from '@sentry/types';
import type { UserIntegrationsFunction } from '@sentry/utils';
import { logger } from '@sentry/utils';
@@ -9,8 +8,6 @@ import { JSDOM } from 'jsdom';
import { init, Integrations, nextRouterInstrumentation } from '../src/client';
-const { BrowserTracing } = TracingIntegrations;
-
const reactInit = jest.spyOn(SentryReact, 'init');
const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent');
const loggerLogSpy = jest.spyOn(logger, 'log');
diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts
index dfc06c9bcf7e..92315374836b 100644
--- a/packages/nextjs/test/config/withSentry.test.ts
+++ b/packages/nextjs/test/config/withSentry.test.ts
@@ -1,4 +1,5 @@
import * as hub from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import * as Sentry from '@sentry/node';
import type { Client, ClientOptions } from '@sentry/types';
import type { NextApiRequest, NextApiResponse } from 'next';
@@ -6,6 +7,10 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { withSentry } from '../../src/server';
import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/server/types';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
const FLUSH_DURATION = 200;
async function sleep(ms: number): Promise {
diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts
index 68c598e9707f..444d45513ccb 100644
--- a/packages/nextjs/test/config/wrappers.test.ts
+++ b/packages/nextjs/test/config/wrappers.test.ts
@@ -1,4 +1,5 @@
import * as SentryCore from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import * as SentryNode from '@sentry/node';
import type { IncomingMessage, ServerResponse } from 'http';
@@ -6,6 +7,10 @@ import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from
const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction');
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
describe('data-fetching function wrappers', () => {
const route = '/tricks/[trickName]';
let req: IncomingMessage;
diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
index 852ceb5628b4..cdc7cc4986e2 100644
--- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
+++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
@@ -1,7 +1,12 @@
import * as coreSdk from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import { withEdgeWrapping } from '../../src/edge/utils/edgeWrapperUtils';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
// @ts-ignore Request does not exist on type Global
const origRequest = global.Request;
// @ts-ignore Response does not exist on type Global
diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts
index 2ecbdf22a96e..08a91e0c5e11 100644
--- a/packages/nextjs/test/edge/withSentryAPI.test.ts
+++ b/packages/nextjs/test/edge/withSentryAPI.test.ts
@@ -2,6 +2,10 @@ import * as coreSdk from '@sentry/core';
import { wrapApiHandlerWithSentry } from '../../src/edge';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
+// constructor but the client isn't used in these tests.
+coreSdk.addTracingExtensions();
+
// @ts-ignore Request does not exist on type Global
const origRequest = global.Request;
// @ts-ignore Response does not exist on type Global
diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json
index f1661102a963..e63a6e940dca 100644
--- a/packages/node-integration-tests/package.json
+++ b/packages/node-integration-tests/package.json
@@ -9,6 +9,7 @@
"scripts": {
"clean": "rimraf -g **/node_modules",
"prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)",
+ "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)",
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"",
@@ -16,7 +17,7 @@
"fix:eslint": "eslint . --format stylish --fix",
"fix:prettier": "prettier --write \"{suites,utils}/**/*.ts\"",
"type-check": "tsc",
- "pretest": "run-s --silent prisma:init",
+ "pretest": "run-s --silent prisma:init prisma:init:new",
"test": "ts-node ./utils/run-tests.ts",
"test:watch": "yarn test --watch"
},
diff --git a/packages/node-integration-tests/suites/express/tracing/server.ts b/packages/node-integration-tests/suites/express/tracing/server.ts
index faf5a50f95ed..e857621ad22e 100644
--- a/packages/node-integration-tests/suites/express/tracing/server.ts
+++ b/packages/node-integration-tests/suites/express/tracing/server.ts
@@ -1,5 +1,4 @@
import * as Sentry from '@sentry/node';
-import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';
@@ -8,7 +7,7 @@ const app = express();
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
- integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
+ integrations: [new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});
diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts
new file mode 100644
index 000000000000..5bd8aa815cbe
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts
@@ -0,0 +1,43 @@
+import * as Sentry from '@sentry/node';
+import { ApolloServer, gql } from 'apollo-server';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [new Sentry.Integrations.GraphQL(), new Sentry.Integrations.Apollo()],
+});
+
+const typeDefs = gql`
+ type Query {
+ hello: String
+ }
+`;
+
+const resolvers = {
+ Query: {
+ hello: () => {
+ return 'Hello world!';
+ },
+ },
+};
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+});
+
+const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' });
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+void (async () => {
+ // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
+ await server.executeOperation({
+ query: '{hello}',
+ });
+
+ transaction.finish();
+})();
diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts
new file mode 100644
index 000000000000..128f8a2f164b
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts
@@ -0,0 +1,35 @@
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../utils';
+
+// Node 10 is not supported by `graphql-js`
+// Ref: https://github.com/graphql/graphql-js/blob/main/package.json
+conditionalTest({ min: 12 })('GraphQL/Apollo Tests', () => {
+ test('should instrument GraphQL and Apollo Server.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ const transaction = envelope[2];
+ const parentSpanId = (transaction as any)?.contexts?.trace?.span_id;
+ const graphqlSpanId = (transaction as any)?.spans?.[0].span_id;
+
+ expect(parentSpanId).toBeDefined();
+ expect(graphqlSpanId).toBeDefined();
+
+ assertSentryTransaction(transaction, {
+ transaction: 'test_transaction',
+ spans: [
+ {
+ description: 'execute',
+ op: 'graphql.execute',
+ parent_span_id: parentSpanId,
+ },
+ {
+ description: 'Query.hello',
+ op: 'graphql.resolve',
+ parent_span_id: graphqlSpanId,
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts
new file mode 100644
index 000000000000..31d7356765e9
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts
@@ -0,0 +1,46 @@
+import * as Sentry from '@sentry/node';
+import { MongoClient } from 'mongodb';
+
+// suppress logging of the mongo download
+global.console.log = () => null;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const client = new MongoClient(process.env.MONGO_URL || '', {
+ useUnifiedTopology: true,
+});
+
+async function run(): Promise {
+ const transaction = Sentry.startTransaction({
+ name: 'Test Transaction',
+ op: 'transaction',
+ });
+
+ Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+ });
+
+ try {
+ await client.connect();
+
+ const database = client.db('admin');
+ const collection = database.collection('movies');
+
+ await collection.insertOne({ title: 'Rick and Morty' });
+ await collection.findOne({ title: 'Back to the Future' });
+ await collection.updateOne({ title: 'Back to the Future' }, { $set: { title: 'South Park' } });
+ await collection.findOne({ title: 'South Park' });
+
+ await collection.find({ title: 'South Park' }).toArray();
+ } finally {
+ if (transaction) transaction.finish();
+ await client.close();
+ }
+}
+
+void run();
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts
new file mode 100644
index 000000000000..5664aac9422b
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts
@@ -0,0 +1,85 @@
+import { MongoMemoryServer } from 'mongodb-memory-server-global';
+
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../../utils';
+
+// This test can take longer.
+jest.setTimeout(15000);
+
+conditionalTest({ min: 12 })('MongoDB Test', () => {
+ let mongoServer: MongoMemoryServer;
+
+ beforeAll(async () => {
+ mongoServer = await MongoMemoryServer.create();
+ process.env.MONGO_URL = mongoServer.getUri();
+ }, 10000);
+
+ afterAll(async () => {
+ if (mongoServer) {
+ await mongoServer.stop();
+ }
+ });
+
+ test('should auto-instrument `mongodb` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ doc: '{"title":"Rick and Morty"}',
+ },
+ description: 'insertOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"Back to the Future"}',
+ },
+ description: 'findOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ filter: '{"title":"Back to the Future"}',
+ update: '{"$set":{"title":"South Park"}}',
+ },
+ description: 'updateOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"South Park"}',
+ },
+ description: 'findOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"South Park"}',
+ },
+ description: 'find',
+ op: 'db',
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts
new file mode 100644
index 000000000000..0f576cb793aa
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node';
+import mysql from 'mysql';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const connection = mysql.createConnection({
+ user: 'root',
+ password: 'docker',
+});
+
+connection.connect(function (err: unknown) {
+ if (err) {
+ return;
+ }
+});
+
+const transaction = Sentry.startTransaction({
+ op: 'transaction',
+ name: 'Test Transaction',
+});
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+connection.query('SELECT 1 + 1 AS solution', function () {
+ connection.query('SELECT NOW()', ['1', '2'], () => {
+ if (transaction) transaction.finish();
+ connection.end();
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts
new file mode 100644
index 000000000000..3b96f2cafec0
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts
@@ -0,0 +1,23 @@
+import { assertSentryTransaction, TestEnv } from '../../../../utils';
+
+test('should auto-instrument `mysql` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ description: 'SELECT 1 + 1 AS solution',
+ op: 'db',
+ },
+
+ {
+ description: 'SELECT NOW()',
+ op: 'db',
+ },
+ ],
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts
new file mode 100644
index 000000000000..a7859fd562a3
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts
@@ -0,0 +1,25 @@
+import * as Sentry from '@sentry/node';
+import pg from 'pg';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const transaction = Sentry.startTransaction({
+ op: 'transaction',
+ name: 'Test Transaction',
+});
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+const client = new pg.Client();
+client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () =>
+ client.query('SELECT * FROM bazz', () => {
+ client.query('SELECT NOW()', () => transaction.finish());
+ }),
+);
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts
new file mode 100644
index 000000000000..edfa67cee9d7
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts
@@ -0,0 +1,54 @@
+import { assertSentryTransaction, TestEnv } from '../../../../utils';
+
+class PgClient {
+ // https://node-postgres.com/api/client#clientquery
+ public query(_text: unknown, values: unknown, callback?: () => void) {
+ if (typeof callback === 'function') {
+ callback();
+ return;
+ }
+
+ if (typeof values === 'function') {
+ values();
+ return;
+ }
+
+ return Promise.resolve();
+ }
+}
+
+beforeAll(() => {
+ jest.mock('pg', () => {
+ return {
+ Client: PgClient,
+ native: {
+ Client: PgClient,
+ },
+ };
+ });
+});
+
+test('should auto-instrument `pg` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ description: 'SELECT * FROM foo where bar ilike "baz%"',
+ op: 'db',
+ },
+ {
+ description: 'SELECT * FROM bazz',
+ op: 'db',
+ },
+ {
+ description: 'SELECT NOW()',
+ op: 'db',
+ },
+ ],
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml b/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml
new file mode 100644
index 000000000000..45caa4bb3179
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.9'
+
+services:
+ db:
+ image: postgres:13
+ restart: always
+ container_name: integration-tests-prisma
+ ports:
+ - '5433:5432'
+ environment:
+ POSTGRES_USER: prisma
+ POSTGRES_PASSWORD: prisma
+ POSTGRES_DB: tests
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json b/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json
new file mode 100644
index 000000000000..f8b24d7d0465
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "sentry-prisma-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "engines": {
+ "node": ">=12"
+ },
+ "scripts": {
+ "db-up": "docker-compose up -d",
+ "generate": "prisma generate",
+ "migrate": "prisma migrate dev -n sentry-test",
+ "setup": "run-s --silent db-up generate migrate"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@prisma/client": "3.12.0",
+ "prisma": "^3.12.0"
+ }
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml
new file mode 100644
index 000000000000..fbffa92c2bb7
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql
new file mode 100644
index 000000000000..8619aaceb2b0
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "email" TEXT NOT NULL,
+ "name" TEXT,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma
new file mode 100644
index 000000000000..4363c97738ee
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma
@@ -0,0 +1,15 @@
+datasource db {
+ url = "postgresql://prisma:prisma@localhost:5433/tests"
+ provider = "postgresql"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ email String @unique
+ name String?
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts
new file mode 100644
index 000000000000..0eb40d9c83ee
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts
@@ -0,0 +1,47 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+import { PrismaClient } from '@prisma/client';
+import * as Sentry from '@sentry/node';
+import { randomBytes } from 'crypto';
+
+const client = new PrismaClient();
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [new Sentry.Integrations.Prisma({ client })],
+});
+
+async function run(): Promise {
+ const transaction = Sentry.startTransaction({
+ name: 'Test Transaction',
+ op: 'transaction',
+ });
+
+ Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+ });
+
+ try {
+ await client.user.create({
+ data: {
+ name: 'Tilda',
+ email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`,
+ },
+ });
+
+ await client.user.findMany();
+
+ await client.user.deleteMany({
+ where: {
+ email: {
+ contains: 'sentry.io',
+ },
+ },
+ });
+ } finally {
+ if (transaction) transaction.finish();
+ }
+}
+
+void run();
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts
new file mode 100755
index 000000000000..3c40d12f7337
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts
@@ -0,0 +1,16 @@
+import { parseSemver } from '@sentry/utils';
+import { execSync } from 'child_process';
+
+const NODE_VERSION = parseSemver(process.versions.node);
+
+if (NODE_VERSION.major && NODE_VERSION.major < 12) {
+ // eslint-disable-next-line no-console
+ console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`);
+ process.exit(0);
+}
+
+try {
+ execSync('yarn && yarn setup');
+} catch (_) {
+ process.exit(1);
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts
new file mode 100644
index 000000000000..e3393f5fe2f8
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts
@@ -0,0 +1,17 @@
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../utils';
+
+conditionalTest({ min: 12 })('Prisma ORM Integration', () => {
+ test('should instrument Prisma client for tracing.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ { description: 'User create', op: 'db.sql.prisma' },
+ { description: 'User findMany', op: 'db.sql.prisma' },
+ { description: 'User deleteMany', op: 'db.sql.prisma' },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock b/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock
new file mode 100644
index 000000000000..d228adebd621
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock
@@ -0,0 +1,27 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@prisma/client@3.12.0":
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12"
+ integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw==
+ dependencies:
+ "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+
+"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
+ version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886"
+ integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw==
+
+"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
+ version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45"
+ integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==
+
+prisma@^3.12.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd"
+ integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==
+ dependencies:
+ "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts
new file mode 100644
index 000000000000..a6197e5ab743
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts
@@ -0,0 +1,26 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import * as Sentry from '@sentry/node';
+import * as http from 'http';
+
+Sentry.addTracingExtensions();
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [new Sentry.Integrations.Http({ tracing: true })],
+});
+
+const transaction = Sentry.startTransaction({ name: 'test_transaction' });
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+http.get('http://match-this-url.com/api/v0');
+http.get('http://match-this-url.com/api/v1');
+http.get('http://dont-match-this-url.com/api/v2');
+http.get('http://dont-match-this-url.com/api/v3');
+
+transaction.finish();
diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts
new file mode 100644
index 000000000000..1209c59da46a
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts
@@ -0,0 +1,42 @@
+import nock from 'nock';
+
+import { runScenario, TestEnv } from '../../../utils';
+
+test('HttpIntegration should instrument correct requests when tracePropagationTargets option is provided', async () => {
+ const match1 = nock('http://match-this-url.com')
+ .get('/api/v0')
+ .matchHeader('baggage', val => typeof val === 'string')
+ .matchHeader('sentry-trace', val => typeof val === 'string')
+ .reply(200);
+
+ const match2 = nock('http://match-this-url.com')
+ .get('/api/v1')
+ .matchHeader('baggage', val => typeof val === 'string')
+ .matchHeader('sentry-trace', val => typeof val === 'string')
+ .reply(200);
+
+ const match3 = nock('http://dont-match-this-url.com')
+ .get('/api/v2')
+ .matchHeader('baggage', val => val === undefined)
+ .matchHeader('sentry-trace', val => val === undefined)
+ .reply(200);
+
+ const match4 = nock('http://dont-match-this-url.com')
+ .get('/api/v3')
+ .matchHeader('baggage', val => val === undefined)
+ .matchHeader('sentry-trace', val => val === undefined)
+ .reply(200);
+
+ const env = await TestEnv.init(__dirname);
+ await runScenario(env.url);
+
+ env.server.close();
+ nock.cleanAll();
+
+ await new Promise(resolve => env.server.close(resolve));
+
+ expect(match1.isDone()).toBe(true);
+ expect(match2.isDone()).toBe(true);
+ expect(match3.isDone()).toBe(true);
+ expect(match4.isDone()).toBe(true);
+});
diff --git a/packages/node/.eslintrc.js b/packages/node/.eslintrc.js
index e15b119aa31e..8e4b96002ae0 100644
--- a/packages/node/.eslintrc.js
+++ b/packages/node/.eslintrc.js
@@ -6,5 +6,6 @@ module.exports = {
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
},
};
diff --git a/packages/node/package.json b/packages/node/package.json
index 7386be3912bb..fd197f539739 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -19,13 +19,13 @@
"@sentry/core": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
+ "@sentry-internal/tracing": "7.44.2",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
"tslib": "^1.9.3"
},
"devDependencies": {
- "@sentry/tracing": "7.44.2",
"@types/cookie": "0.3.2",
"@types/express": "^4.17.14",
"@types/lru-cache": "^5.1.0",
diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts
index 0b3a925d775e..d0d0ae7424be 100644
--- a/packages/node/src/client.ts
+++ b/packages/node/src/client.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core';
+import { addTracingExtensions, BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core';
import type { Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
import { logger, resolvedSyncPromise } from '@sentry/utils';
import * as os from 'os';
@@ -40,6 +40,9 @@ export class NodeClient extends BaseClient {
...options.transportOptions,
};
+ // The Node client always supports tracing
+ addTracingExtensions();
+
super(options);
}
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 92d3cfcf835f..7f0d923e4ae2 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -30,6 +30,7 @@ export {
captureMessage,
configureScope,
createTransport,
+ getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
@@ -45,6 +46,7 @@ export {
setUser,
withScope,
} from '@sentry/core';
+export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing';
export { NodeClient } from './client';
export { makeNodeTransport } from './transports';
@@ -57,10 +59,12 @@ import * as domain from 'domain';
import * as Handlers from './handlers';
import * as NodeIntegrations from './integrations';
+import * as TracingIntegrations from './tracing/integrations';
const INTEGRATIONS = {
...CoreIntegrations,
...NodeIntegrations,
+ ...TracingIntegrations,
};
export { INTEGRATIONS as Integrations, Handlers };
diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts
index 2d10ae61d696..b4f99b419fd4 100644
--- a/packages/node/src/integrations/onuncaughtexception.ts
+++ b/packages/node/src/integrations/onuncaughtexception.ts
@@ -8,6 +8,10 @@ import { logAndExitProcess } from './utils/errorhandling';
type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void;
+type TaggedListener = NodeJS.UncaughtExceptionListener & {
+ tag?: string;
+};
+
// CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts`
interface OnUncaughtExceptionOptions {
// TODO(v8): Evaluate whether we should switch the default behaviour here.
@@ -95,18 +99,20 @@ export class OnUncaughtException implements Integration {
// exit behaviour of the SDK accordingly:
// - If other listeners are attached, do not exit.
// - If the only listener attached is ours, exit.
- const userProvidedListenersCount = global.process
- .listeners('uncaughtException')
- .reduce((acc, listener) => {
- if (
- listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
- listener === this.handler // filter the handler we registered ourselves)
- ) {
- return acc;
- } else {
- return acc + 1;
- }
- }, 0);
+ const userProvidedListenersCount = (
+ global.process.listeners('uncaughtException') as TaggedListener[]
+ ).reduce((acc, listener) => {
+ if (
+ // There are 3 listeners we ignore:
+ listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
+ (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing
+ listener === this.handler // the handler we register in this integration
+ ) {
+ return acc;
+ } else {
+ return acc + 1;
+ }
+ }, 0);
const processWouldExit = userProvidedListenersCount === 0;
const shouldApplyFatalHandlingLogic = this._options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
diff --git a/packages/node/src/tracing/index.ts b/packages/node/src/tracing/index.ts
new file mode 100644
index 000000000000..15c4e2889b3f
--- /dev/null
+++ b/packages/node/src/tracing/index.ts
@@ -0,0 +1,24 @@
+import { lazyLoadedNodePerformanceMonitoringIntegrations } from '@sentry-internal/tracing';
+import type { Integration } from '@sentry/types';
+import { logger } from '@sentry/utils';
+
+/**
+ * Automatically detects and returns integrations that will work with your dependencies.
+ */
+export function autoDiscoverNodePerformanceMonitoringIntegrations(): Integration[] {
+ const loadedIntegrations = lazyLoadedNodePerformanceMonitoringIntegrations
+ .map(tryLoad => {
+ try {
+ return tryLoad();
+ } catch (_) {
+ return undefined;
+ }
+ })
+ .filter(integration => !!integration) as Integration[];
+
+ if (loadedIntegrations.length === 0) {
+ logger.warn('Performance monitoring integrations could not be automatically loaded.');
+ }
+
+ return loadedIntegrations;
+}
diff --git a/packages/node/src/tracing/integrations.ts b/packages/node/src/tracing/integrations.ts
new file mode 100644
index 000000000000..a37bf6bfd494
--- /dev/null
+++ b/packages/node/src/tracing/integrations.ts
@@ -0,0 +1 @@
+export { Apollo, Express, GraphQL, Mongo, Mysql, Postgres, Prisma } from '@sentry-internal/tracing';
diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts
index 03cfba80ade4..028cdff98af1 100644
--- a/packages/node/test/handlers.test.ts
+++ b/packages/node/test/handlers.test.ts
@@ -1,5 +1,5 @@
import * as sentryCore from '@sentry/core';
-import { Transaction } from '@sentry/tracing';
+import { Transaction } from '@sentry/core';
import type { Event } from '@sentry/types';
import { SentryError } from '@sentry/utils';
import * as http from 'http';
diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts
index 446aa4ec0f82..7d9a4e45226b 100644
--- a/packages/node/test/integrations/http.test.ts
+++ b/packages/node/test/integrations/http.test.ts
@@ -1,9 +1,8 @@
+import type { Span, Transaction } from '@sentry/core';
import * as sentryCore from '@sentry/core';
-import { Hub } from '@sentry/core';
-import type { Span, Transaction } from '@sentry/tracing';
-import { addExtensionMethods, TRACEPARENT_REGEXP } from '@sentry/tracing';
+import { addTracingExtensions, Hub } from '@sentry/core';
import type { TransactionContext } from '@sentry/types';
-import { logger, parseSemver } from '@sentry/utils';
+import { logger, parseSemver, TRACEPARENT_REGEXP } from '@sentry/utils';
import * as http from 'http';
import * as https from 'https';
import * as HttpsProxyAgent from 'https-proxy-agent';
@@ -34,7 +33,7 @@ describe('tracing', () => {
...customOptions,
});
const hub = new Hub(new NodeClient(options));
- addExtensionMethods();
+ addTracingExtensions();
hub.configureScope(scope =>
scope.setUser({
@@ -227,7 +226,7 @@ describe('tracing', () => {
}
function createTransactionAndPutOnScope(hub: Hub) {
- addExtensionMethods();
+ addTracingExtensions();
const transaction = hub.startTransaction({ name: 'dogpark' });
hub.getScope()?.setSpan(transaction);
return transaction;
diff --git a/packages/opentelemetry-node/README.md b/packages/opentelemetry-node/README.md
index 551684a75c26..b75ca0ab0ef8 100644
--- a/packages/opentelemetry-node/README.md
+++ b/packages/opentelemetry-node/README.md
@@ -66,10 +66,9 @@ const sdk = new opentelemetry.NodeSDK({
// Sentry config
spanProcessor: new SentrySpanProcessor(),
+ textMapPropagator: new SentryPropagator(),
});
-otelApi.propagation.setGlobalPropagator(new SentryPropagator());
-
sdk.start();
```
diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json
index 84391b502c63..b19a608ef2e3 100644
--- a/packages/opentelemetry-node/package.json
+++ b/packages/opentelemetry-node/package.json
@@ -17,7 +17,6 @@
},
"dependencies": {
"@sentry/core": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2"
},
diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts
index 501c93432b44..752c2cfaa0aa 100644
--- a/packages/opentelemetry-node/src/index.ts
+++ b/packages/opentelemetry-node/src/index.ts
@@ -1,4 +1,2 @@
-import '@sentry/tracing';
-
export { SentrySpanProcessor } from './spanprocessor';
export { SentryPropagator } from './propagator';
diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts
index 08d14e9fa671..773c68ebf1ff 100644
--- a/packages/opentelemetry-node/src/spanprocessor.ts
+++ b/packages/opentelemetry-node/src/spanprocessor.ts
@@ -2,8 +2,7 @@ import type { Context } from '@opentelemetry/api';
import { SpanKind, trace } from '@opentelemetry/api';
import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
-import { Transaction } from '@sentry/tracing';
+import { addGlobalEventProcessor, addTracingExtensions, getCurrentHub, Transaction } from '@sentry/core';
import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
@@ -23,6 +22,8 @@ export const SENTRY_SPAN_PROCESSOR_MAP: Map =
*/
export class SentrySpanProcessor implements OtelSpanProcessor {
public constructor() {
+ addTracingExtensions();
+
addGlobalEventProcessor(event => {
const otelSpan = trace && trace.getActiveSpan && (trace.getActiveSpan() as OtelSpan | undefined);
if (!otelSpan) {
diff --git a/packages/opentelemetry-node/src/utils/map-otel-status.ts b/packages/opentelemetry-node/src/utils/map-otel-status.ts
index 968150852e6e..8fdc0e09ac8b 100644
--- a/packages/opentelemetry-node/src/utils/map-otel-status.ts
+++ b/packages/opentelemetry-node/src/utils/map-otel-status.ts
@@ -1,6 +1,6 @@
import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import type { SpanStatusType as SentryStatus } from '@sentry/tracing';
+import type { SpanStatusType as SentryStatus } from '@sentry/core';
// canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/
const canonicalCodesHTTPMap: Record = {
diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts
index f1914653dd0d..d5222e3103d4 100644
--- a/packages/opentelemetry-node/test/propagator.test.ts
+++ b/packages/opentelemetry-node/test/propagator.test.ts
@@ -7,8 +7,7 @@ import {
TraceFlags,
} from '@opentelemetry/api';
import { suppressTracing } from '@opentelemetry/core';
-import { Hub, makeMain } from '@sentry/core';
-import { addExtensionMethods, Transaction } from '@sentry/tracing';
+import { addTracingExtensions, Hub, makeMain, Transaction } from '@sentry/core';
import type { TransactionContext } from '@sentry/types';
import {
@@ -21,7 +20,7 @@ import { SentryPropagator } from '../src/propagator';
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';
beforeAll(() => {
- addExtensionMethods();
+ addTracingExtensions();
});
describe('SentryPropagator', () => {
diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts
index 4e3c975d00cf..94abd327314c 100644
--- a/packages/opentelemetry-node/test/spanprocessor.test.ts
+++ b/packages/opentelemetry-node/test/spanprocessor.test.ts
@@ -4,10 +4,9 @@ import { Resource } from '@opentelemetry/resources';
import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
-import { createTransport, Hub, makeMain } from '@sentry/core';
+import type { SpanStatusType } from '@sentry/core';
+import { addTracingExtensions, createTransport, Hub, makeMain, Span as SentrySpan, Transaction } from '@sentry/core';
import { NodeClient } from '@sentry/node';
-import type { SpanStatusType } from '@sentry/tracing';
-import { addExtensionMethods, Span as SentrySpan, Transaction } from '@sentry/tracing';
import { resolvedSyncPromise } from '@sentry/utils';
import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor';
@@ -24,7 +23,7 @@ const DEFAULT_NODE_CLIENT_OPTIONS = {
// Integration Test of SentrySpanProcessor
beforeAll(() => {
- addExtensionMethods();
+ addTracingExtensions();
});
describe('SentrySpanProcessor', () => {
diff --git a/packages/overhead-metrics/.eslintrc.cjs b/packages/overhead-metrics/.eslintrc.cjs
index 1cbdd66daa88..8046df7df92c 100644
--- a/packages/overhead-metrics/.eslintrc.cjs
+++ b/packages/overhead-metrics/.eslintrc.cjs
@@ -10,6 +10,7 @@ module.exports = {
'import/no-unresolved': 'off',
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
'jsdoc/require-jsdoc': 'off',
},
},
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 1553028195a2..96a88cf31e73 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -66,6 +66,25 @@ const INITIAL_STATE = {
eventId: null,
};
+function setCause(error: Error & { cause?: Error }, cause: Error): void {
+ const seenErrors = new WeakMap();
+
+ function recurse(error: Error & { cause?: Error }, cause: Error): void {
+ // If we've already seen the error, there is a recursive loop somewhere in the error's
+ // cause chain. Let's just bail out then to prevent a stack overflow.
+ if (seenErrors.has(error)) {
+ return;
+ }
+ if (error.cause) {
+ seenErrors.set(error, true);
+ return recurse(error.cause, cause);
+ }
+ error.cause = cause;
+ }
+
+ recurse(error, cause);
+}
+
/**
* A ErrorBoundary component that logs errors to Sentry. Requires React >= 16.
* NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the
@@ -93,7 +112,7 @@ class ErrorBoundary extends React.Component;
}
-const TestApp: React.FC = ({ children, ...props }) => {
+interface TestAppProps extends ErrorBoundaryProps {
+ errorComp?: JSX.Element;
+}
+
+const TestApp: React.FC = ({ children, errorComp, ...props }) => {
+ // eslint-disable-next-line no-param-reassign
+ const customErrorComp = errorComp || ;
const [isError, setError] = React.useState(false);
return (
= ({ children, ...props }) => {
}
}}
>
- {isError ? : children}
+ {isError ? customErrorComp : children}
} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ const thirdError = mockCaptureException.mock.calls[0][0];
+ const secondError = thirdError.cause;
+ const firstError = secondError.cause;
+ const cause = firstError.cause;
+ expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).toContain('React ErrorBoundary');
+ expect(cause.message).toEqual(thirdError.message);
+ });
+
+ it('handles when `error.cause` is recursive', () => {
+ const mockOnError = jest.fn();
+
+ function CustomBam(): JSX.Element {
+ const firstError = new Error('bam');
+ const secondError = new Error('bam2');
+ // @ts-ignore Need to set cause on error
+ firstError.cause = secondError;
+ // @ts-ignore Need to set cause on error
+ secondError.cause = firstError;
+ throw firstError;
+ }
+
+ render(
+ You have hit an error} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ const error = mockCaptureException.mock.calls[0][0];
+ const cause = error.cause;
+ // We need to make sure that recursive error.cause does not cause infinite loop
+ expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).not.toContain('React ErrorBoundary');
+ });
+
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();
diff --git a/packages/remix/package.json b/packages/remix/package.json
index d08489a442c9..a2d68a4e0c38 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -25,7 +25,6 @@
"@sentry/integrations": "7.44.2",
"@sentry/node": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.19.0",
diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx
index 22d9ce73fe82..5c76ee4907bf 100644
--- a/packages/remix/src/index.client.tsx
+++ b/packages/remix/src/index.client.tsx
@@ -1,14 +1,11 @@
/* eslint-disable import/export */
-import { configureScope, init as reactInit, Integrations } from '@sentry/react';
+import { configureScope, init as reactInit } from '@sentry/react';
import { buildMetadata } from './utils/metadata';
import type { RemixOptions } from './utils/remixOptions';
export { remixRouterInstrumentation, withSentry } from './performance/client';
-export { BrowserTracing } from '@sentry/tracing';
export * from '@sentry/react';
-export { Integrations };
-
export function init(options: RemixOptions): void {
buildMetadata(options, ['remix', 'react']);
options.environment = options.environment || process.env.NODE_ENV;
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index b2ad73866fde..21cfc20b17ab 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -8,7 +8,6 @@ import type { RemixOptions } from './utils/remixOptions';
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
export { remixRouterInstrumentation, withSentry } from './performance/client';
-export { BrowserTracing, Integrations } from '@sentry/tracing';
export * from '@sentry/node';
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts
index 43e4d8cd1bf0..825198426eec 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/utils/instrumentServer.ts
@@ -1,8 +1,7 @@
/* eslint-disable max-lines */
-import { hasTracingEnabled } from '@sentry/core';
+import { getActiveTransaction, hasTracingEnabled } from '@sentry/core';
import type { Hub } from '@sentry/node';
import { captureException, getCurrentHub } from '@sentry/node';
-import { getActiveTransaction } from '@sentry/tracing';
import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
import {
addExceptionMechanism,
diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts
index f4d4f9c95fe0..ec1610aee400 100644
--- a/packages/remix/test/index.server.test.ts
+++ b/packages/remix/test/index.server.test.ts
@@ -2,7 +2,7 @@ import * as SentryNode from '@sentry/node';
import { getCurrentHub } from '@sentry/node';
import { GLOBAL_OBJ } from '@sentry/utils';
-import { init } from '../src/index.server';
+import { init, Integrations } from '../src/index.server';
const nodeInit = jest.spyOn(SentryNode, 'init');
@@ -57,4 +57,9 @@ describe('Server init()', () => {
// @ts-ignore need access to protected _tags attribute
expect(currentScope._tags).toEqual({ runtime: 'node' });
});
+
+ it('has both node and tracing integrations', () => {
+ expect(Integrations.Apollo).not.toBeUndefined();
+ expect(Integrations.Http).not.toBeUndefined();
+ });
});
diff --git a/packages/replay/.eslintrc.js b/packages/replay/.eslintrc.js
index e4101e557b26..da006cf432a2 100644
--- a/packages/replay/.eslintrc.js
+++ b/packages/replay/.eslintrc.js
@@ -7,21 +7,8 @@ module.exports = {
extends: ['../../.eslintrc.js'],
overrides: [
{
- files: ['worker/**/*.ts'],
- parserOptions: {
- // TODO: figure out if we need a worker-specific tsconfig
- project: ['tsconfig.worker.json'],
- },
- rules: {
- // We cannot use backticks, as that conflicts with the stringified worker
- 'prefer-template': 'off',
- },
- },
- {
- files: ['src/worker/**/*.js'],
- parserOptions: {
- sourceType: 'module',
- },
+ files: ['src/**/*.ts'],
+ rules: {},
},
{
files: ['jest.setup.ts', 'jest.config.ts'],
diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
index 0ee72f9edc9c..e9ad5d9ea209 100644
--- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
+++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
@@ -51,7 +51,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
};
if (client && client.on) {
- client.on('beforeAddBreadcrumb', (breadcrumb, hint) => handleNetworkBreadcrumb(options, breadcrumb, hint));
+ client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
} else {
// Fallback behavior
addInstrumentationHandler('fetch', handleFetchSpanListener(replay));
@@ -63,7 +63,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
}
/** just exported for tests */
-export function handleNetworkBreadcrumb(
+export function beforeAddNetworkBreadcrumb(
options: ExtendedNetworkBreadcrumbsOptions,
breadcrumb: Breadcrumb,
hint?: BreadcrumbHint,
@@ -74,27 +74,76 @@ export function handleNetworkBreadcrumb(
try {
if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
- // Enriches the breadcrumb overall
- _enrichXhrBreadcrumb(breadcrumb, hint, options);
-
- // Create a replay performance entry from this breadcrumb
- const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
- addNetworkBreadcrumb(options.replay, result);
+ _handleXhrBreadcrumb(breadcrumb, hint, options);
}
if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) {
- // Enriches the breadcrumb overall
+ // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
+ // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
+ // So any async mutations to it will not be reflected in the final breadcrumb
_enrichFetchBreadcrumb(breadcrumb, hint, options);
- // Create a replay performance entry from this breadcrumb
- const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
- addNetworkBreadcrumb(options.replay, result);
+ void _handleFetchBreadcrumb(breadcrumb, hint, options);
}
} catch (e) {
__DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb');
}
}
+function _handleXhrBreadcrumb(
+ breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
+ hint: XhrHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): void {
+ // Enriches the breadcrumb overall
+ _enrichXhrBreadcrumb(breadcrumb, hint, options);
+
+ // Create a replay performance entry from this breadcrumb
+ const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
+ addNetworkBreadcrumb(options.replay, result);
+}
+
+async function _handleFetchBreadcrumb(
+ breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
+ hint: FetchHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): Promise {
+ const fullBreadcrumb = await _parseFetchResponse(breadcrumb, hint, options);
+
+ // Create a replay performance entry from this breadcrumb
+ const result = _makeNetworkReplayBreadcrumb('resource.fetch', fullBreadcrumb, hint);
+ addNetworkBreadcrumb(options.replay, result);
+}
+
+// This does async operations on the breadcrumb for replay
+async function _parseFetchResponse(
+ breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
+ hint: FetchBreadcrumbHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): Promise {
+ if (breadcrumb.data.response_body_size || !hint.response) {
+ return breadcrumb;
+ }
+
+ // If no Content-Length header exists, we try to get the size from the response body
+ try {
+ // We have to clone this, as the body can only be read once
+ const response = (hint.response as Response).clone();
+ const body = await response.text();
+
+ if (body.length) {
+ return {
+ ...breadcrumb,
+ data: { ...breadcrumb.data, response_body_size: getBodySize(body, options.textEncoder) },
+ };
+ }
+ } catch {
+ // just ignore if something fails here
+ }
+
+ return breadcrumb;
+}
+
function _makeNetworkReplayBreadcrumb(
type: string,
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData },
diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts
index fc68f322d090..46ba18bb9ed3 100644
--- a/packages/replay/src/util/addGlobalListeners.ts
+++ b/packages/replay/src/util/addGlobalListeners.ts
@@ -1,5 +1,6 @@
import type { BaseClient } from '@sentry/core';
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
+import type { Client, DynamicSamplingContext } from '@sentry/types';
import { addInstrumentationHandler } from '@sentry/utils';
import { handleAfterSendEvent } from '../coreHandlers/handleAfterSendEvent';
@@ -25,15 +26,23 @@ export function addGlobalListeners(replay: ReplayContainer): void {
addInstrumentationHandler('history', handleHistorySpanListener(replay));
handleNetworkBreadcrumbs(replay);
- // If a custom client has no hooks yet, we continue to use the "old" implementation
- const hasHooks = !!(client && client.on);
-
// Tag all (non replay) events that get sent to Sentry with the current
// replay ID so that we can reference them later in the UI
- addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks));
+ addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks(client)));
- if (hasHooks) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (client as BaseClient).on('afterSendEvent', handleAfterSendEvent(replay));
+ // If a custom client has no hooks yet, we continue to use the "old" implementation
+ if (hasHooks(client)) {
+ client.on('afterSendEvent', handleAfterSendEvent(replay));
+ client.on('createDsc', (dsc: DynamicSamplingContext) => {
+ const replayId = replay.getSessionId();
+ if (replayId) {
+ dsc.replay_id = replayId;
+ }
+ });
}
}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function hasHooks(client: Client | undefined): client is BaseClient {
+ return !!(client && client.on);
+}
diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
index d0a7b5cedca1..1a1c5ac13d6b 100644
--- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
+++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
@@ -9,8 +9,8 @@ import { TextEncoder } from 'util';
import { BASE_TIMESTAMP } from '../..';
import {
+ beforeAddNetworkBreadcrumb,
getBodySize,
- handleNetworkBreadcrumb,
parseContentSizeHeader,
} from '../../../src/coreHandlers/handleNetworkBreadcrumbs';
import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray';
@@ -78,7 +78,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
});
- describe('handleNetworkBreadcrumb()', () => {
+ describe('beforeAddNetworkBreadcrumb()', () => {
let options: {
replay: ReplayContainer;
textEncoder: TextEncoderInternal;
@@ -98,7 +98,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
it('ignores breadcrumb without data', () => {
const breadcrumb: Breadcrumb = {};
const hint: BreadcrumbHint = {};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({});
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([]);
@@ -110,7 +110,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
data: {},
};
const hint: BreadcrumbHint = {};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'foo',
@@ -138,7 +138,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'xhr',
@@ -192,7 +192,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'xhr',
@@ -246,7 +246,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'fetch',
@@ -260,6 +260,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
jest.runAllTimers();
+ await Promise.resolve();
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
@@ -305,7 +306,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'fetch',
@@ -316,6 +317,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
jest.runAllTimers();
+ await Promise.resolve();
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
@@ -336,5 +338,63 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
},
]);
});
+
+ it('parses fetch response body if necessary', async () => {
+ const breadcrumb: Breadcrumb = {
+ category: 'fetch',
+ data: {
+ url: 'https://example.com',
+ status_code: 200,
+ },
+ };
+
+ const mockResponse = {
+ headers: {
+ get: () => '',
+ },
+ clone: () => mockResponse,
+ text: () => Promise.resolve('test response'),
+ } as unknown as Response;
+
+ const hint: FetchBreadcrumbHint = {
+ input: [],
+ response: mockResponse,
+ startTimestamp: BASE_TIMESTAMP + 1000,
+ endTimestamp: BASE_TIMESTAMP + 2000,
+ };
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
+
+ expect(breadcrumb).toEqual({
+ category: 'fetch',
+ data: {
+ status_code: 200,
+ url: 'https://example.com',
+ },
+ });
+
+ await Promise.resolve();
+ jest.runAllTimers();
+ await Promise.resolve();
+
+ expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
+ {
+ type: 5,
+ timestamp: (BASE_TIMESTAMP + 1000) / 1000,
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ data: {
+ statusCode: 200,
+ responseBodySize: 13,
+ },
+ description: 'https://example.com',
+ endTimestamp: (BASE_TIMESTAMP + 2000) / 1000,
+ op: 'resource.fetch',
+ startTimestamp: (BASE_TIMESTAMP + 1000) / 1000,
+ },
+ },
+ },
+ ]);
+ });
});
});
diff --git a/packages/serverless/package.json b/packages/serverless/package.json
index e68ec304dbe3..522d2419814c 100644
--- a/packages/serverless/package.json
+++ b/packages/serverless/package.json
@@ -17,7 +17,6 @@
},
"dependencies": {
"@sentry/node": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@types/aws-lambda": "^8.10.62",
diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts
index c7e2199aedbb..459560a660fe 100644
--- a/packages/serverless/scripts/buildLambdaLayer.ts
+++ b/packages/serverless/scripts/buildLambdaLayer.ts
@@ -17,7 +17,7 @@ async function buildLambdaLayer(): Promise {
// Create the main SDK bundle
// TODO: Check if we can get rid of this, after the lerna 6/nx update??
await ensureBundleBuildPrereqs({
- dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/tracing', '@sentry/node'],
+ dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/node'],
});
run('yarn rollup --config rollup.aws.config.js');
diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts
index 29dd10da6602..ccfc1e191024 100644
--- a/packages/serverless/src/awslambda.ts
+++ b/packages/serverless/src/awslambda.ts
@@ -2,9 +2,15 @@
import type { Scope } from '@sentry/node';
import * as Sentry from '@sentry/node';
import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import type { Integration } from '@sentry/types';
-import { baggageHeaderToDynamicSamplingContext, dsnFromString, dsnToString, isString, logger } from '@sentry/utils';
+import {
+ baggageHeaderToDynamicSamplingContext,
+ dsnFromString,
+ dsnToString,
+ extractTraceparentData,
+ isString,
+ logger,
+} from '@sentry/utils';
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
// eslint-disable-next-line import/no-unresolved
import type { Context, Handler } from 'aws-lambda';
diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts
index fbe5882f549a..8892353fd4bf 100644
--- a/packages/serverless/src/gcpfunction/http.ts
+++ b/packages/serverless/src/gcpfunction/http.ts
@@ -1,8 +1,8 @@
import type { AddRequestDataToEventOptions } from '@sentry/node';
import { captureException, flush, getCurrentHub } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import {
baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
isString,
isThenable,
logger,
diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts
index bd552060c6dd..e0513a53cd3e 100644
--- a/packages/serverless/src/index.ts
+++ b/packages/serverless/src/index.ts
@@ -20,6 +20,7 @@ export {
captureMessage,
configureScope,
createTransport,
+ getActiveTransaction,
getCurrentHub,
getHubFromCarrier,
makeMain,
diff --git a/packages/svelte/.eslintrc.js b/packages/svelte/.eslintrc.js
index 46d8d10cc538..0714feabf8d4 100644
--- a/packages/svelte/.eslintrc.js
+++ b/packages/svelte/.eslintrc.js
@@ -3,4 +3,7 @@ module.exports = {
browser: true,
},
extends: ['../../.eslintrc.js'],
+ rules: {
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ },
};
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index ab2e9ee19ac7..2293cdcd42cb 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -30,6 +30,7 @@
},
"devDependencies": {
"@sveltejs/kit": "^1.11.0",
+ "svelte": "^3.44.0",
"typescript": "^4.9.3",
"vite": "4.0.0"
},
diff --git a/packages/sveltekit/rollup.npm.config.js b/packages/sveltekit/rollup.npm.config.js
index f1f8240d5a7a..f9dfe71fd30c 100644
--- a/packages/sveltekit/rollup.npm.config.js
+++ b/packages/sveltekit/rollup.npm.config.js
@@ -1,14 +1,10 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';
-export default
- makeNPMConfigVariants(
- makeBaseNPMConfig({
- entrypoints: [
- 'src/index.server.ts',
- 'src/index.client.ts',
- 'src/client/index.ts',
- 'src/server/index.ts',
- ],
- }),
- )
-;
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
+ packageSpecificConfig: {
+ external: ['$app/stores'],
+ },
+ }),
+);
diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts
index fbaa5f98799f..32f310e3bd2d 100644
--- a/packages/sveltekit/src/client/load.ts
+++ b/packages/sveltekit/src/client/load.ts
@@ -1,6 +1,7 @@
+import { trace } from '@sentry/core';
import { captureException } from '@sentry/svelte';
-import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
-import type { ServerLoad } from '@sveltejs/kit';
+import { addExceptionMechanism, objectify } from '@sentry/utils';
+import type { Load } from '@sveltejs/kit';
function sendErrorToSentry(e: unknown): unknown {
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
@@ -30,24 +31,24 @@ function sendErrorToSentry(e: unknown): unknown {
*
* @param origLoad SvelteKit user defined load function
*/
-export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
+export function wrapLoadWithSentry(origLoad: Load): Load {
return new Proxy(origLoad, {
- apply: (wrappingTarget, thisArg, args: Parameters) => {
- let maybePromiseResult;
-
- try {
- maybePromiseResult = wrappingTarget.apply(thisArg, args);
- } catch (e) {
- throw sendErrorToSentry(e);
- }
-
- if (isThenable(maybePromiseResult)) {
- Promise.resolve(maybePromiseResult).then(null, e => {
- sendErrorToSentry(e);
- });
- }
-
- return maybePromiseResult;
+ apply: (wrappingTarget, thisArg, args: Parameters) => {
+ const [event] = args;
+
+ const routeId = event.route.id;
+ return trace(
+ {
+ op: 'function.sveltekit.load',
+ name: routeId ? routeId : event.url.pathname,
+ status: 'ok',
+ metadata: {
+ source: routeId ? 'route' : 'url',
+ },
+ },
+ () => wrappingTarget.apply(thisArg, args),
+ sendErrorToSentry,
+ );
},
});
}
diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts
new file mode 100644
index 000000000000..64125bdbdbd4
--- /dev/null
+++ b/packages/sveltekit/src/client/router.ts
@@ -0,0 +1,124 @@
+import { getActiveTransaction } from '@sentry/core';
+import { WINDOW } from '@sentry/svelte';
+import type { Span, Transaction, TransactionContext } from '@sentry/types';
+
+import { navigating, page } from '$app/stores';
+
+const DEFAULT_TAGS = {
+ 'routing.instrumentation': '@sentry/sveltekit',
+};
+
+/**
+ * Automatically creates pageload and navigation transactions for the client-side SvelteKit router.
+ *
+ * This instrumentation makes use of SvelteKit's `page` and `navigating` stores which can be accessed
+ * anywhere on the client side.
+ *
+ * @param startTransactionFn the function used to start (idle) transactions
+ * @param startTransactionOnPageLoad controls if pageload transactions should be created (defaults to `true`)
+ * @param startTransactionOnLocationChange controls if navigation transactions should be created (defauls to `true`)
+ */
+export function svelteKitRoutingInstrumentation(
+ startTransactionFn: (context: TransactionContext) => T | undefined,
+ startTransactionOnPageLoad: boolean = true,
+ startTransactionOnLocationChange: boolean = true,
+): void {
+ if (startTransactionOnPageLoad) {
+ instrumentPageload(startTransactionFn);
+ }
+
+ if (startTransactionOnLocationChange) {
+ instrumentNavigations(startTransactionFn);
+ }
+}
+
+function instrumentPageload(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
+ const initialPath = WINDOW && WINDOW.location && WINDOW.location.pathname;
+
+ const pageloadTransaction = startTransactionFn({
+ name: initialPath,
+ op: 'pageload',
+ description: initialPath,
+ tags: {
+ ...DEFAULT_TAGS,
+ },
+ metadata: {
+ source: 'url',
+ },
+ });
+
+ page.subscribe(page => {
+ if (!page) {
+ return;
+ }
+
+ const routeId = page.route && page.route.id;
+
+ if (pageloadTransaction && routeId) {
+ pageloadTransaction.setName(routeId, 'route');
+ }
+ });
+}
+
+/**
+ * Use the `navigating` store to start a transaction on navigations.
+ */
+function instrumentNavigations(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
+ let routingSpan: Span | undefined = undefined;
+ let activeTransaction: Transaction | undefined;
+
+ navigating.subscribe(navigation => {
+ if (!navigation) {
+ // `navigating` emits a 'null' value when the navigation is completed.
+ // So in this case, we can finish the routing span. If the transaction was an IdleTransaction,
+ // it will finish automatically and if it was user-created users also need to finish it.
+ if (routingSpan) {
+ routingSpan.finish();
+ routingSpan = undefined;
+ }
+ return;
+ }
+
+ const from = navigation.from;
+ const to = navigation.to;
+
+ // for the origin we can fall back to window.location.pathname because in this emission, it still is set to the origin path
+ const rawRouteOrigin = (from && from.url.pathname) || (WINDOW && WINDOW.location && WINDOW.location.pathname);
+
+ const rawRouteDestination = to && to.url.pathname;
+
+ // We don't want to create transactions for navigations of same origin and destination.
+ // We need to look at the raw URL here because parameterized routes can still differ in their raw parameters.
+ if (rawRouteOrigin === rawRouteDestination) {
+ return;
+ }
+
+ const parameterizedRouteOrigin = from && from.route.id;
+ const parameterizedRouteDestination = to && to.route.id;
+
+ activeTransaction = getActiveTransaction();
+
+ if (!activeTransaction) {
+ activeTransaction = startTransactionFn({
+ name: parameterizedRouteDestination || rawRouteDestination || 'unknown',
+ op: 'navigation',
+ metadata: { source: parameterizedRouteDestination ? 'route' : 'url' },
+ tags: {
+ ...DEFAULT_TAGS,
+ },
+ });
+ }
+
+ if (activeTransaction) {
+ if (routingSpan) {
+ // If a routing span is still open from a previous navigation, we finish it.
+ routingSpan.finish();
+ }
+ routingSpan = activeTransaction.startChild({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+ activeTransaction.setTag('from', parameterizedRouteOrigin);
+ }
+ });
+}
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 50f44bdfa353..9bf1d2cb140b 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,17 +1,18 @@
-import { defaultRequestInstrumentationOptions } from '@sentry-internal/tracing';
import { hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions } from '@sentry/svelte';
import { BrowserTracing, configureScope, init as initSvelteSdk } from '@sentry/svelte';
import { addOrUpdateIntegration } from '@sentry/utils';
import { applySdkMetadata } from '../common/metadata';
+import { svelteKitRoutingInstrumentation } from './router';
// Treeshakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;
/**
+ * Initialize the client side of the Sentry SvelteKit SDK.
*
- * @param options
+ * @param options Configuration options for the SDK.
*/
export function init(options: BrowserOptions): void {
applySdkMetadata(options, ['sveltekit', 'svelte']);
@@ -33,14 +34,11 @@ function addClientIntegrations(options: BrowserOptions): void {
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
if (hasTracingEnabled(options)) {
const defaultBrowserTracingIntegration = new BrowserTracing({
- tracePropagationTargets: [...defaultRequestInstrumentationOptions.tracePropagationTargets],
- // TODO: Add SvelteKit router instrumentations
- // routingInstrumentation: sveltekitRoutingInstrumentation,
+ routingInstrumentation: svelteKitRoutingInstrumentation,
});
integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
- // TODO: Add SvelteKit router instrumentations
- // options.routingInstrumentation: sveltekitRoutingInstrumentation,
+ 'options.routingInstrumentation': svelteKitRoutingInstrumentation,
});
}
}
diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts
index 5835b6863b61..1c1c0576729f 100644
--- a/packages/sveltekit/src/index.types.ts
+++ b/packages/sveltekit/src/index.types.ts
@@ -9,7 +9,7 @@ export * from './server';
import type { Integration, Options, StackParser } from '@sentry/types';
// eslint-disable-next-line import/no-unresolved
-import type { HandleClientError, HandleServerError, ServerLoad } from '@sveltejs/kit';
+import type { HandleClientError, HandleServerError, Load, ServerLoad } from '@sveltejs/kit';
import type * as clientSdk from './client';
import type * as serverSdk from './server';
@@ -21,7 +21,7 @@ export declare function handleErrorWithSentry;
-export declare function wrapLoadWithSentry(origLoad: S): S;
+export declare function wrapLoadWithSentry(origLoad: S): S;
// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;
diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts
new file mode 100644
index 000000000000..aab69c085048
--- /dev/null
+++ b/packages/sveltekit/src/server/handle.ts
@@ -0,0 +1,80 @@
+/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
+import type { Span } from '@sentry/core';
+import { trace } from '@sentry/core';
+import { captureException } from '@sentry/node';
+import {
+ addExceptionMechanism,
+ baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
+ objectify,
+} from '@sentry/utils';
+import type { Handle } from '@sveltejs/kit';
+import * as domain from 'domain';
+
+function sendErrorToSentry(e: unknown): unknown {
+ // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
+ // store a seen flag on it.
+ const objectifiedErr = objectify(e);
+
+ captureException(objectifiedErr, scope => {
+ scope.addEventProcessor(event => {
+ addExceptionMechanism(event, {
+ type: 'sveltekit',
+ handled: false,
+ data: {
+ function: 'handle',
+ },
+ });
+ return event;
+ });
+
+ return scope;
+ });
+
+ return objectifiedErr;
+}
+
+/**
+ * A SvelteKit handle function that wraps the request for Sentry error and
+ * performance monitoring.
+ *
+ * Usage:
+ * ```
+ * // src/hooks.server.ts
+ * import { sentryHandle } from '@sentry/sveltekit';
+ *
+ * export const handle = sentryHandle;
+ *
+ * // Optionally use the sequence function to add additional handlers.
+ * // export const handle = sequence(sentryHandle, yourCustomHandle);
+ * ```
+ */
+export const sentryHandle: Handle = ({ event, resolve }) => {
+ return domain.create().bind(() => {
+ const sentryTraceHeader = event.request.headers.get('sentry-trace');
+ const baggageHeader = event.request.headers.get('baggage');
+ const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
+ const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
+
+ return trace(
+ {
+ op: 'http.server',
+ name: `${event.request.method} ${event.route.id}`,
+ status: 'ok',
+ ...traceparentData,
+ metadata: {
+ source: 'route',
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ },
+ async (span?: Span) => {
+ const res = await resolve(event);
+ if (span) {
+ span.setHttpStatus(res.status);
+ }
+ return res;
+ },
+ sendErrorToSentry,
+ );
+ })();
+};
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index c7784d870c56..9109f29499d4 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -3,3 +3,4 @@ export * from '@sentry/node';
export { init } from './sdk';
export { handleErrorWithSentry } from './handleError';
export { wrapLoadWithSentry } from './load';
+export { sentryHandle } from './handle';
diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts
index ef0433091a9e..6cd45704d601 100644
--- a/packages/sveltekit/src/server/load.ts
+++ b/packages/sveltekit/src/server/load.ts
@@ -1,6 +1,8 @@
+/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
import { captureException } from '@sentry/node';
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
-import type { HttpError, ServerLoad } from '@sveltejs/kit';
+import type { HttpError, Load, ServerLoad } from '@sveltejs/kit';
+import * as domain from 'domain';
function isHttpError(err: unknown): err is HttpError {
return typeof err === 'object' && err !== null && 'status' in err && 'body' in err;
@@ -41,24 +43,27 @@ function sendErrorToSentry(e: unknown): unknown {
*
* @param origLoad SvelteKit user defined load function
*/
-export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
+export function wrapLoadWithSentry(origLoad: T): T {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters) => {
- let maybePromiseResult;
+ return domain.create().bind(() => {
+ let maybePromiseResult: ReturnType;
- try {
- maybePromiseResult = wrappingTarget.apply(thisArg, args);
- } catch (e) {
- throw sendErrorToSentry(e);
- }
-
- if (isThenable(maybePromiseResult)) {
- Promise.resolve(maybePromiseResult).then(null, e => {
+ try {
+ maybePromiseResult = wrappingTarget.apply(thisArg, args);
+ } catch (e) {
sendErrorToSentry(e);
- });
- }
+ throw e;
+ }
+
+ if (isThenable(maybePromiseResult)) {
+ Promise.resolve(maybePromiseResult).then(null, e => {
+ sendErrorToSentry(e);
+ });
+ }
- return maybePromiseResult;
+ return maybePromiseResult;
+ })();
},
});
}
diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts
index 7cbfd3593c03..f4d18c9f9909 100644
--- a/packages/sveltekit/test/client/load.test.ts
+++ b/packages/sveltekit/test/client/load.test.ts
@@ -1,5 +1,5 @@
-import { Scope } from '@sentry/svelte';
-import type { ServerLoad } from '@sveltejs/kit';
+import { addTracingExtensions, Scope } from '@sentry/svelte';
+import type { Load } from '@sveltejs/kit';
import { vi } from 'vitest';
import { wrapLoadWithSentry } from '../../src/client/load';
@@ -19,6 +19,19 @@ vi.mock('@sentry/svelte', async () => {
};
});
+const mockTrace = vi.fn();
+
+vi.mock('@sentry/core', async () => {
+ const original = (await vi.importActual('@sentry/core')) as any;
+ return {
+ ...original,
+ trace: (...args: unknown[]) => {
+ mockTrace(...args);
+ return original.trace(...args);
+ },
+ };
+});
+
const mockAddExceptionMechanism = vi.fn();
vi.mock('@sentry/utils', async () => {
@@ -33,41 +46,98 @@ function getById(_id?: string) {
throw new Error('error');
}
+const MOCK_LOAD_ARGS: any = {
+ params: { id: '123' },
+ route: {
+ id: '/users/[id]',
+ },
+ url: new URL('http://localhost:3000/users/123'),
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+};
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
describe('wrapLoadWithSentry', () => {
beforeEach(() => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
+ mockTrace.mockClear();
mockScope = new Scope();
});
it('calls captureException', async () => {
- async function load({ params }: Parameters[0]): Promise> {
+ async function load({ params }: Parameters[0]): Promise> {
return {
post: getById(params.id),
};
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
+ it('calls trace function', async () => {
+ async function load({ params }: Parameters[0]): Promise> {
+ return {
+ post: params.id,
+ };
+ }
+
+ const wrappedLoad = wrapLoadWithSentry(load);
+ await wrappedLoad(MOCK_LOAD_ARGS);
+
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ {
+ op: 'function.sveltekit.load',
+ name: '/users/[id]',
+ status: 'ok',
+ metadata: {
+ source: 'route',
+ },
+ },
+ expect.any(Function),
+ expect.any(Function),
+ );
+ });
+
it('adds an exception mechanism', async () => {
const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
void callback({}, { event_id: 'fake-event-id' });
return mockScope;
});
- async function load({ params }: Parameters[0]): Promise> {
+ async function load({ params }: Parameters[0]): Promise> {
return {
post: getById(params.id),
};
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(addEventProcessorSpy).toBeCalledTimes(1);
diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts
new file mode 100644
index 000000000000..0b95a7195176
--- /dev/null
+++ b/packages/sveltekit/test/client/router.test.ts
@@ -0,0 +1,186 @@
+/* eslint-disable @typescript-eslint/unbound-method */
+import type { Transaction } from '@sentry/types';
+import { writable } from 'svelte/store';
+import type { SpyInstance } from 'vitest';
+import { vi } from 'vitest';
+
+import { navigating, page } from '$app/stores';
+
+import { svelteKitRoutingInstrumentation } from '../../src/client/router';
+
+// we have to overwrite the global mock from `vitest.setup.ts` here to reset the
+// `navigating` store for each test.
+vi.mock('$app/stores', async () => {
+ return {
+ get navigating() {
+ return navigatingStore;
+ },
+ page: writable(),
+ };
+});
+
+let navigatingStore = writable();
+
+describe('sveltekitRoutingInstrumentation', () => {
+ let returnedTransaction: (Transaction & { returnedTransaction: SpyInstance }) | undefined;
+ const mockedStartTransaction = vi.fn().mockImplementation(txnCtx => {
+ returnedTransaction = {
+ ...txnCtx,
+ setName: vi.fn(),
+ startChild: vi.fn().mockImplementation(ctx => {
+ return { ...mockedRoutingSpan, ...ctx };
+ }),
+ setTag: vi.fn(),
+ };
+ return returnedTransaction;
+ });
+
+ const mockedRoutingSpan = {
+ finish: () => {},
+ };
+
+ const routingSpanFinishSpy = vi.spyOn(mockedRoutingSpan, 'finish');
+
+ beforeEach(() => {
+ navigatingStore = writable();
+ vi.clearAllMocks();
+ });
+
+ it("starts a pageload transaction when it's called with default params", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction);
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: '/',
+ op: 'pageload',
+ description: '/',
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ metadata: {
+ source: 'url',
+ },
+ });
+
+ // We emit an update to the `page` store to simulate the SvelteKit router lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `page` as a writable store
+ page.set({ route: { id: 'testRoute' } });
+
+ // This should update the transaction name with the parameterized route:
+ expect(returnedTransaction?.setName).toHaveBeenCalledTimes(1);
+ expect(returnedTransaction?.setName).toHaveBeenCalledWith('testRoute', 'route');
+ });
+
+ it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false);
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+
+ it("doesn't start a navigation transaction when `startTransactionOnLocationChange` is false", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, false);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: '/users' }, url: { pathname: '/users' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ });
+
+ // This should update the transaction name with the parameterized route:
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+
+ it('starts a navigation transaction when `startTransactionOnLocationChange` is true', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: '/users' }, url: { pathname: '/users' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ });
+
+ // This should update the transaction name with the parameterized route:
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: '/users/[id]',
+ op: 'navigation',
+ metadata: {
+ source: 'route',
+ },
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ });
+
+ expect(returnedTransaction?.startChild).toHaveBeenCalledWith({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+
+ expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users');
+
+ // We emit `null` here to simulate the end of the navigation lifecycle
+ // @ts-ignore this is fine
+ navigating.set(null);
+
+ expect(routingSpanFinishSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe('handling same origin and destination navigations', () => {
+ it("doesn't start a navigation transaction if the raw navigation origin and destination are equal", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+
+ it('starts a navigation transaction if the raw navigation origin and destination are not equal', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // @ts-ignore This is fine
+ navigating.set({
+ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/223412' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: '/users/[id]',
+ op: 'navigation',
+ metadata: {
+ source: 'route',
+ },
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ });
+
+ expect(returnedTransaction?.startChild).toHaveBeenCalledWith({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+
+ expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users/[id]');
+ });
+
+ it('falls back to `window.location.pathname` to determine the raw origin', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // window.location.pathame is "/" in tests
+
+ // @ts-ignore This is fine
+ navigating.set({
+ to: { route: {}, url: { pathname: '/' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+ });
+});
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index 8e404578883d..a8353a73df3e 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -5,6 +5,7 @@ import { SDK_VERSION, WINDOW } from '@sentry/svelte';
import { vi } from 'vitest';
import { BrowserTracing, init } from '../../src/client';
+import { svelteKitRoutingInstrumentation } from '../../src/client/router';
const svelteInit = vi.spyOn(SentrySvelte, 'init');
@@ -87,6 +88,7 @@ describe('Sentry client SDK', () => {
// This is the closest we can get to unit-testing the `__SENTRY_TRACING__` tree-shaking guard
// IRL, the code to add the integration would most likely be removed by the bundler.
+ // @ts-ignore this is fine in the test
globalThis.__SENTRY_TRACING__ = false;
init({
@@ -100,24 +102,35 @@ describe('Sentry client SDK', () => {
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
expect(browserTracing).toBeUndefined();
+ // @ts-ignore this is fine in the test
delete globalThis.__SENTRY_TRACING__;
});
- // TODO: this test is only meaningful once we have a routing instrumentation which we always want to add
- // to a user-provided BrowserTracing integration (see NextJS SDK)
- it.skip('Merges the user-provided BrowserTracing integration with the automatically added one', () => {
+ it('Merges a user-provided BrowserTracing integration with the automatically added one', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [new BrowserTracing({ tracePropagationTargets: ['myDomain.com'] })],
+ integrations: [
+ new BrowserTracing({ tracePropagationTargets: ['myDomain.com'], startTransactionOnLocationChange: false }),
+ ],
enableTracing: true,
});
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
- const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing');
+
+ const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById(
+ 'BrowserTracing',
+ ) as BrowserTracing;
+ const options = browserTracing.options;
expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
expect(browserTracing).toBeDefined();
- expect((browserTracing as BrowserTracing).options.tracePropagationTargets).toEqual(['myDomain.com']);
+
+ // This shows that the user-configured options are still here
+ expect(options.tracePropagationTargets).toEqual(['myDomain.com']);
+ expect(options.startTransactionOnLocationChange).toBe(false);
+
+ // But we force the routing instrumentation to be ours
+ expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation);
});
});
});
diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts
new file mode 100644
index 000000000000..cf17b56aaa90
--- /dev/null
+++ b/packages/sveltekit/test/server/handle.test.ts
@@ -0,0 +1,251 @@
+import { addTracingExtensions, Hub, makeMain, Scope } from '@sentry/core';
+import { NodeClient } from '@sentry/node';
+import type { Transaction } from '@sentry/types';
+import type { Handle } from '@sveltejs/kit';
+import { vi } from 'vitest';
+
+import { sentryHandle } from '../../src/server/handle';
+import { getDefaultNodeClientOptions } from '../utils';
+
+const mockCaptureException = vi.fn();
+let mockScope = new Scope();
+
+vi.mock('@sentry/node', async () => {
+ const original = (await vi.importActual('@sentry/node')) as any;
+ return {
+ ...original,
+ captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
+ cb(mockScope);
+ mockCaptureException(err, cb);
+ return original.captureException(err, cb);
+ },
+ };
+});
+
+const mockAddExceptionMechanism = vi.fn();
+
+vi.mock('@sentry/utils', async () => {
+ const original = (await vi.importActual('@sentry/utils')) as any;
+ return {
+ ...original,
+ addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
+ };
+});
+
+function mockEvent(override: Record = {}): Parameters[0]['event'] {
+ const event: Parameters[0]['event'] = {
+ cookies: {} as any,
+ fetch: () => Promise.resolve({} as any),
+ getClientAddress: () => '',
+ locals: {},
+ params: { id: '123' },
+ platform: {},
+ request: {
+ method: 'GET',
+ headers: {
+ get: () => null,
+ append: () => {},
+ delete: () => {},
+ forEach: () => {},
+ has: () => false,
+ set: () => {},
+ },
+ } as any,
+ route: { id: '/users/[id]' },
+ setHeaders: () => {},
+ url: new URL('http://localhost:3000/users/123'),
+ isDataRequest: false,
+
+ ...override,
+ };
+
+ return event;
+}
+
+const mockResponse = { status: 200, headers: {}, body: '' } as any;
+
+const enum Type {
+ Sync = 'sync',
+ Async = 'async',
+}
+
+function resolve(type: Type, isError: boolean): Parameters[0]['resolve'] {
+ if (type === Type.Sync) {
+ return (..._args: unknown[]) => {
+ if (isError) {
+ throw new Error(type);
+ }
+
+ return mockResponse;
+ };
+ }
+
+ return (..._args: unknown[]) => {
+ return new Promise((resolve, reject) => {
+ if (isError) {
+ reject(new Error(type));
+ } else {
+ resolve(mockResponse);
+ }
+ });
+ };
+}
+
+let hub: Hub;
+let client: NodeClient;
+
+describe('handleSentry', () => {
+ beforeAll(() => {
+ addTracingExtensions();
+ });
+
+ beforeEach(() => {
+ mockScope = new Scope();
+ const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 });
+ client = new NodeClient(options);
+ hub = new Hub(client);
+ makeMain(hub);
+
+ mockCaptureException.mockClear();
+ mockAddExceptionMechanism.mockClear();
+ });
+
+ describe.each([
+ // isSync, isError, expectedResponse
+ [Type.Sync, true, undefined],
+ [Type.Sync, false, mockResponse],
+ [Type.Async, true, undefined],
+ [Type.Async, false, mockResponse],
+ ])('%s resolve with error %s', (type, isError, mockResponse) => {
+ it('should return a response', async () => {
+ let response: any = undefined;
+ try {
+ response = await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ expect(e).toBeInstanceOf(Error);
+ expect(e.message).toEqual(type);
+ }
+
+ expect(response).toEqual(mockResponse);
+ });
+
+ it('creates a transaction', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+
+ expect(ref.name).toEqual('GET /users/[id]');
+ expect(ref.op).toEqual('http.server');
+ expect(ref.status).toEqual(isError ? 'internal_error' : 'ok');
+ expect(ref.metadata.source).toEqual('route');
+
+ expect(ref.endTimestamp).toBeDefined();
+ });
+
+ it('creates a transaction from sentry-trace header', async () => {
+ const event = mockEvent({
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ return null;
+ },
+ },
+ },
+ });
+
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event, resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+ expect(ref.traceId).toEqual('1234567890abcdef1234567890abcdef');
+ expect(ref.parentSpanId).toEqual('1234567890abcdef');
+ expect(ref.sampled).toEqual(true);
+ });
+
+ it('creates a transaction with dynamic sampling context from baggage header', async () => {
+ const event = mockEvent({
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+ });
+
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event, resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+ expect(ref.metadata.dynamicSamplingContext).toEqual({
+ environment: 'production',
+ release: '1.0.0',
+ public_key: 'dogsarebadatkeepingsecrets',
+ sample_rate: '1',
+ trace_id: '1234567890abcdef1234567890abcdef',
+ transaction: 'dogpark',
+ user_segment: 'segmentA',
+ });
+ });
+
+ it('send errors to Sentry', async () => {
+ const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
+ void callback({}, { event_id: 'fake-event-id' });
+ return mockScope;
+ });
+
+ try {
+ await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ expect(mockCaptureException).toBeCalledTimes(1);
+ expect(addEventProcessorSpy).toBeCalledTimes(1);
+ expect(mockAddExceptionMechanism).toBeCalledTimes(1);
+ expect(mockAddExceptionMechanism).toBeCalledWith(
+ {},
+ { handled: false, type: 'sveltekit', data: { function: 'handle' } },
+ );
+ }
+ });
+ });
+});
diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts
index ec2503b945c4..9278215074c1 100644
--- a/packages/sveltekit/test/server/load.test.ts
+++ b/packages/sveltekit/test/server/load.test.ts
@@ -1,3 +1,4 @@
+import { addTracingExtensions } from '@sentry/core';
import { Scope } from '@sentry/node';
import type { ServerLoad } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
@@ -20,6 +21,19 @@ vi.mock('@sentry/node', async () => {
};
});
+const mockTrace = vi.fn();
+
+vi.mock('@sentry/core', async () => {
+ const original = (await vi.importActual('@sentry/core')) as any;
+ return {
+ ...original,
+ trace: (...args: unknown[]) => {
+ mockTrace(...args);
+ return original.trace(...args);
+ },
+ };
+});
+
const mockAddExceptionMechanism = vi.fn();
vi.mock('@sentry/utils', async () => {
@@ -34,10 +48,42 @@ function getById(_id?: string) {
throw new Error('error');
}
+const MOCK_LOAD_ARGS: any = {
+ params: { id: '123' },
+ route: {
+ id: '/users/[id]',
+ },
+ url: new URL('http://localhost:3000/users/123'),
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+};
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
describe('wrapLoadWithSentry', () => {
beforeEach(() => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
+ mockTrace.mockClear();
mockScope = new Scope();
});
@@ -49,12 +95,50 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
+ // TODO: enable this once we figured out how tracing the load function doesn't result in creating a new transaction
+ it.skip('calls trace function', async () => {
+ async function load({ params }: Parameters[0]): Promise> {
+ return {
+ post: params.id,
+ };
+ }
+
+ const wrappedLoad = wrapLoadWithSentry(load);
+ await wrappedLoad(MOCK_LOAD_ARGS);
+
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ {
+ op: 'function.sveltekit.load',
+ name: '/users/[id]',
+ parentSampled: true,
+ parentSpanId: '1234567890abcdef',
+ status: 'ok',
+ traceId: '1234567890abcdef1234567890abcdef',
+ metadata: {
+ dynamicSamplingContext: {
+ environment: 'production',
+ public_key: 'dogsarebadatkeepingsecrets',
+ release: '1.0.0',
+ sample_rate: '1',
+ trace_id: '1234567890abcdef1234567890abcdef',
+ transaction: 'dogpark',
+ user_segment: 'segmentA',
+ },
+ source: 'route',
+ },
+ },
+ expect.any(Function),
+ expect.any(Function),
+ );
+ });
+
describe('with error() helper', () => {
it.each([
// [statusCode, timesCalled]
@@ -75,7 +159,7 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(times);
@@ -95,7 +179,7 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(addEventProcessorSpy).toBeCalledTimes(1);
diff --git a/packages/sveltekit/test/utils.ts b/packages/sveltekit/test/utils.ts
new file mode 100644
index 000000000000..993a6bd8823d
--- /dev/null
+++ b/packages/sveltekit/test/utils.ts
@@ -0,0 +1,12 @@
+import { createTransport } from '@sentry/core';
+import type { ClientOptions } from '@sentry/types';
+import { resolvedSyncPromise } from '@sentry/utils';
+
+export function getDefaultNodeClientOptions(options: Partial = {}): ClientOptions {
+ return {
+ integrations: [],
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
+ stackParser: () => [],
+ ...options,
+ };
+}
diff --git a/packages/sveltekit/test/vitest.setup.ts b/packages/sveltekit/test/vitest.setup.ts
new file mode 100644
index 000000000000..48c9b0e33528
--- /dev/null
+++ b/packages/sveltekit/test/vitest.setup.ts
@@ -0,0 +1,13 @@
+import { writable } from 'svelte/store';
+import { vi } from 'vitest';
+
+export function setup() {
+ // mock $app/stores because vitest can't resolve this import from SvelteKit.
+ // Seems like $app/stores is only created at build time of a SvelteKit app.
+ vi.mock('$app/stores', async () => {
+ return {
+ navigating: writable(),
+ page: writable(),
+ };
+ });
+}
diff --git a/packages/sveltekit/vite.config.ts b/packages/sveltekit/vite.config.ts
index f479704b7591..c1e4297e11ea 100644
--- a/packages/sveltekit/vite.config.ts
+++ b/packages/sveltekit/vite.config.ts
@@ -1,3 +1,14 @@
+import type { UserConfig } from 'vitest';
+
import baseConfig from '../../vite/vite.config';
-export default baseConfig;
+export default {
+ ...baseConfig,
+ test: {
+ // test exists, no idea why TS doesn't recognize it
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...(baseConfig as UserConfig & { test: any }).test,
+ environment: 'jsdom',
+ setupFiles: ['./test/vitest.setup.ts'],
+ },
+};
diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts
index c735942fbe19..c4e17c25294f 100644
--- a/packages/tracing-internal/src/index.ts
+++ b/packages/tracing-internal/src/index.ts
@@ -1,6 +1,15 @@
export * from './exports';
-export { Apollo, Express, GraphQL, Mongo, Mysql, Postgres, Prisma } from './node/integrations';
+export {
+ Apollo,
+ Express,
+ GraphQL,
+ Mongo,
+ Mysql,
+ Postgres,
+ Prisma,
+ lazyLoadedNodePerformanceMonitoringIntegrations,
+} from './node';
export {
BrowserTracing,
diff --git a/packages/tracing-internal/src/node/integrations/index.ts b/packages/tracing-internal/src/node/integrations/index.ts
index 607a3e129984..0b69f4440f3a 100644
--- a/packages/tracing-internal/src/node/integrations/index.ts
+++ b/packages/tracing-internal/src/node/integrations/index.ts
@@ -5,3 +5,4 @@ export { Mongo } from './mongo';
export { Prisma } from './prisma';
export { GraphQL } from './graphql';
export { Apollo } from './apollo';
+export * from './lazy';
diff --git a/packages/tracing-internal/src/node/integrations/lazy.ts b/packages/tracing-internal/src/node/integrations/lazy.ts
new file mode 100644
index 000000000000..f53ff756cd48
--- /dev/null
+++ b/packages/tracing-internal/src/node/integrations/lazy.ts
@@ -0,0 +1,47 @@
+import type { Integration, IntegrationClass } from '@sentry/types';
+import { dynamicRequire } from '@sentry/utils';
+
+export const lazyLoadedNodePerformanceMonitoringIntegrations: (() => Integration)[] = [
+ () => {
+ const integration = dynamicRequire(module, './apollo') as {
+ Apollo: IntegrationClass;
+ };
+ return new integration.Apollo();
+ },
+ () => {
+ const integration = dynamicRequire(module, './apollo') as {
+ Apollo: IntegrationClass;
+ };
+ return new integration.Apollo({ useNestjs: true });
+ },
+ () => {
+ const integration = dynamicRequire(module, './graphql') as {
+ GraphQL: IntegrationClass;
+ };
+ return new integration.GraphQL();
+ },
+ () => {
+ const integration = dynamicRequire(module, './mongo') as {
+ Mongo: IntegrationClass;
+ };
+ return new integration.Mongo();
+ },
+ () => {
+ const integration = dynamicRequire(module, './mongo') as {
+ Mongo: IntegrationClass;
+ };
+ return new integration.Mongo({ mongoose: true });
+ },
+ () => {
+ const integration = dynamicRequire(module, './mysql') as {
+ Mysql: IntegrationClass;
+ };
+ return new integration.Mysql();
+ },
+ () => {
+ const integration = dynamicRequire(module, './postgres') as {
+ Postgres: IntegrationClass;
+ };
+ return new integration.Postgres();
+ },
+];
diff --git a/packages/tracing/package.json b/packages/tracing/package.json
index ef6d6d8f060c..24ec8fbb74d5 100644
--- a/packages/tracing/package.json
+++ b/packages/tracing/package.json
@@ -46,6 +46,7 @@
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "validate:es5": "es-check es5 build/bundles/bundle.tracing.es5.js",
"test:unit": "jest",
"test": "jest",
"test:watch": "jest --watch",
diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts
index 35b37f868d85..d455c4a7590b 100644
--- a/packages/types/src/client.ts
+++ b/packages/types/src/client.ts
@@ -2,7 +2,7 @@ import type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
import type { EventDropReason } from './clientreport';
import type { DataCategory } from './datacategory';
import type { DsnComponents } from './dsn';
-import type { Envelope } from './envelope';
+import type { DynamicSamplingContext, Envelope } from './envelope';
import type { Event, EventHint } from './event';
import type { Integration, IntegrationClass } from './integration';
import type { ClientOptions } from './options';
@@ -177,6 +177,11 @@ export interface Client {
*/
on?(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
+ /**
+ * Register a callback whena DSC (Dynamic Sampling Context) is created.
+ */
+ on?(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
+
/**
* Fire a hook event for transaction start and finish. Expects to be given a transaction as the
* second argument.
@@ -196,7 +201,12 @@ export interface Client {
emit?(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse | void): void;
/**
- * Fire a hook for when a bredacrumb is added. Expects the breadcrumb as second argument.
+ * Fire a hook for when a breadcrumb is added. Expects the breadcrumb as second argument.
*/
emit?(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
+
+ /**
+ * Fire a hook for when a DSC (Dynamic Sampling Context) is created. Expects the DSC as second argument.
+ */
+ emit?(hook: 'createDsc', dsc: DynamicSamplingContext): void;
}
diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts
index 60d67b89d0da..2234317ef8ce 100644
--- a/packages/types/src/envelope.ts
+++ b/packages/types/src/envelope.ts
@@ -18,6 +18,7 @@ export type DynamicSamplingContext = {
environment?: string;
transaction?: string;
user_segment?: string;
+ replay_id?: string;
};
export type EnvelopeItemType =
diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts
index 3de2c9baa7af..22ef3e530384 100644
--- a/packages/vue/src/components.ts
+++ b/packages/vue/src/components.ts
@@ -9,6 +9,7 @@ const ANONYMOUS_COMPONENT_NAME = '';
const repeat = (str: string, n: number): string => {
// string.repeat() is not supported by IE11, we fall back to just using the string in that case
+ // eslint-disable-next-line @sentry-internal/sdk/no-unsupported-es6-methods
return str.repeat ? str.repeat(n) : str;
};
diff --git a/rollup/bundleHelpers.js b/rollup/bundleHelpers.js
index 54cd0528f271..b2d25b58d248 100644
--- a/rollup/bundleHelpers.js
+++ b/rollup/bundleHelpers.js
@@ -17,6 +17,7 @@ import {
makeTerserPlugin,
makeTSPlugin,
makeSetSDKSourcePlugin,
+ getEs5Polyfills,
} from './plugins/index.js';
import { mergePlugins } from './utils';
@@ -25,6 +26,8 @@ const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js'];
export function makeBaseBundleConfig(options) {
const { bundleType, entrypoints, jsVersion, licenseTitle, outputFileBase, packageSpecificConfig } = options;
+ const isEs5 = jsVersion.toLowerCase() === 'es5';
+
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin();
const cleanupPlugin = makeCleanupPlugin();
@@ -42,6 +45,10 @@ export function makeBaseBundleConfig(options) {
output: {
format: 'iife',
name: 'Sentry',
+ outro: () => {
+ // Add polyfills for ES6 array/string methods at the end of the bundle
+ return isEs5 ? getEs5Polyfills() : '';
+ },
},
context: 'window',
plugins: [markAsBrowserBuildPlugin],
@@ -101,10 +108,9 @@ export function makeBaseBundleConfig(options) {
strict: false,
esModule: false,
},
- plugins:
- jsVersion === 'es5'
- ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
- : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
+ plugins: isEs5
+ ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
+ : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
treeshake: 'smallest',
};
diff --git a/rollup/plugins/bundlePlugins.js b/rollup/plugins/bundlePlugins.js
index 81c41d310950..80f5123ca371 100644
--- a/rollup/plugins/bundlePlugins.js
+++ b/rollup/plugins/bundlePlugins.js
@@ -8,6 +8,9 @@
* Typescript plugin docs: https://github.com/ezolenko/rollup-plugin-typescript2
*/
+import * as fs from 'fs';
+import * as path from 'path';
+
import commonjs from '@rollup/plugin-commonjs';
import deepMerge from 'deepmerge';
import license from 'rollup-plugin-license';
@@ -38,6 +41,11 @@ export function makeLicensePlugin(title) {
return plugin;
}
+export function getEs5Polyfills() {
+ // Note: __dirname resolves to e.g. packages/browser or packages/tracing
+ return fs.readFileSync(path.join(__dirname, '../../rollup/polyfills/es5.js'), 'utf-8');
+}
+
/**
* Create a plugin to set the value of the `__SENTRY_DEBUG__` magic string.
*
diff --git a/rollup/polyfills/es5.js b/rollup/polyfills/es5.js
new file mode 100644
index 000000000000..54bd46e62cff
--- /dev/null
+++ b/rollup/polyfills/es5.js
@@ -0,0 +1,41 @@
+// Sentry ES5 polyfills
+if (!('includes' in Array.prototype)) {
+ Array.prototype.includes = function (searchElement) {
+ return this.indexOf(searchElement) > -1;
+ };
+}
+if (!('find' in Array.prototype)) {
+ Array.prototype.find = function (callback) {
+ for (var i = 0; i < this.length; i++) {
+ if (callback(this[i])) {
+ return this[i];
+ }
+ }
+ };
+}
+if (!('findIndex' in Array.prototype)) {
+ Array.prototype.findIndex = function (callback) {
+ for (var i = 0; i < this.length; i++) {
+ if (callback(this[i])) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+if (!('includes' in String.prototype)) {
+ String.prototype.includes = function (searchElement) {
+ return this.indexOf(searchElement) > -1;
+ };
+}
+if (!('startsWith' in String.prototype)) {
+ String.prototype.startsWith = function (searchElement) {
+ return this.indexOf(searchElement) === 0;
+ };
+}
+if (!('endsWith' in String.prototype)) {
+ String.prototype.endsWith = function (searchElement) {
+ var i = this.indexOf(searchElement);
+ return i > -1 && i === this.length - searchElement.length;
+ };
+}
diff --git a/yarn.lock b/yarn.lock
index 0243a33b9b4b..7f4e13e3108b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2248,6 +2248,15 @@
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+"@dabh/diagnostics@^2.0.2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
+ integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
+ dependencies:
+ colorspace "1.1.x"
+ enabled "2.0.x"
+ kuler "^2.0.0"
+
"@discoveryjs/json-ext@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
@@ -5098,6 +5107,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
+"@types/triple-beam@^1.3.2":
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"
+ integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==
+
"@types/uglify-js@*":
version "3.13.1"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea"
@@ -5832,6 +5846,11 @@ acorn-walk@^8.0.0, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+acorn@8.8.2, acorn@^8.8.1, acorn@^8.8.2:
+ version "8.8.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+ integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
acorn@^6.0.5, acorn@^6.4.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -5847,11 +5866,6 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
-acorn@^8.8.1, acorn@^8.8.2:
- version "8.8.2"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
- integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
-
add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
@@ -9257,7 +9271,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -9289,6 +9303,14 @@ color-string@^1.5.4:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
+color-string@^1.6.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+ integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
@@ -9302,6 +9324,14 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.4"
+color@^3.1.3:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
+ integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
+ dependencies:
+ color-convert "^1.9.3"
+ color-string "^1.6.0"
+
colord@^2.9.1:
version "2.9.3"
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
@@ -9322,6 +9352,14 @@ colors@1.4.0, colors@^1.4.0:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+colorspace@1.1.x:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
+ integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
+ dependencies:
+ color "^3.1.3"
+ text-hex "1.0.x"
+
columnify@1.6.0:
version "1.6.0"
resolved "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3"
@@ -9347,6 +9385,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
+commander@10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1"
+ integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==
+
commander@2.8.x:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
@@ -11836,6 +11879,11 @@ emojis-list@^3.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+enabled@2.0.x:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
+ integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -12020,6 +12068,17 @@ es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-
string.prototype.trimstart "^1.0.5"
unbox-primitive "^1.0.2"
+es-check@7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/es-check/-/es-check-7.1.0.tgz#1015db640a7b785ff4098baf2e0791c070a25964"
+ integrity sha512-t099vm9tNqNHF28Q/mRcqYxmkbkoo/Qu2ZI5/D+eFeqNUjI3jwkIyHyexXiAtstbZ1FQELi0QCuUaYCtiffi4Q==
+ dependencies:
+ acorn "8.8.2"
+ commander "10.0.0"
+ fast-glob "^3.2.12"
+ supports-color "^8.1.1"
+ winston "^3.8.2"
+
es-module-lexer@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
@@ -12905,7 +12964,7 @@ fast-glob@3.2.7:
merge2 "^1.3.0"
micromatch "^4.0.4"
-fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.5, fast-glob@^3.2.9:
+fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.4, fast-glob@^3.2.5, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -13000,6 +13059,11 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
+fecha@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
+ integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
+
fflate@^0.4.4:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
@@ -13341,6 +13405,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+fn.name@1.x.x:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
+ integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
+
follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
@@ -16988,6 +17057,11 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
+kuler@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
+ integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
+
language-subtag-registry@~0.3.2:
version "0.3.22"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
@@ -17684,6 +17758,18 @@ log4js@^6.4.1:
rfdc "^1.3.0"
streamroller "^3.0.2"
+logform@^2.3.2, logform@^2.4.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b"
+ integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==
+ dependencies:
+ "@colors/colors" "1.5.0"
+ "@types/triple-beam" "^1.3.2"
+ fecha "^4.2.0"
+ ms "^2.1.1"
+ safe-stable-stringify "^2.3.1"
+ triple-beam "^1.3.0"
+
loglevel@^1.6.8:
version "1.8.0"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
@@ -19850,6 +19936,13 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+one-time@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
+ integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
+ dependencies:
+ fn.name "1.x.x"
+
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -24669,6 +24762,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+stack-trace@0.0.x:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+ integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
+
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@@ -25235,7 +25333,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
-supports-color@^8.0.0:
+supports-color@^8.0.0, supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
@@ -25270,6 +25368,11 @@ svelte@3.49.0:
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
+svelte@^3.44.0:
+ version "3.57.0"
+ resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.57.0.tgz#a3969cfe51f25f2a55e75f7b98dbd02c3af0980b"
+ integrity sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==
+
svgo@^1.0.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
@@ -25639,6 +25742,11 @@ text-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==
+text-hex@1.0.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
+ integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
+
text-table@0.2.0, text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -25980,6 +26088,11 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+triple-beam@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
+ integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
+
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
@@ -27553,6 +27666,32 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+winston-transport@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
+ integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
+ dependencies:
+ logform "^2.3.2"
+ readable-stream "^3.6.0"
+ triple-beam "^1.3.0"
+
+winston@^3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50"
+ integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==
+ dependencies:
+ "@colors/colors" "1.5.0"
+ "@dabh/diagnostics" "^2.0.2"
+ async "^3.2.3"
+ is-stream "^2.0.0"
+ logform "^2.4.0"
+ one-time "^1.0.0"
+ readable-stream "^3.4.0"
+ safe-stable-stringify "^2.3.1"
+ stack-trace "0.0.x"
+ triple-beam "^1.3.0"
+ winston-transport "^4.5.0"
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"