diff --git a/.craft.yml b/.craft.yml
index deb38bf0c40d..efb18669ac9f 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -8,10 +8,13 @@ targets:
- name: npm
id: '@sentry/types'
includeNames: /^sentry-types-\d.*\.tgz$/
- ## 1.2 Core SDK
+ ## 1.2 Core SDKs
- name: npm
id: '@sentry/core'
includeNames: /^sentry-core-\d.*\.tgz$/
+ - name: npm
+ id: '@sentry/node-core'
+ includeNames: /^sentry-node-core-\d.*\.tgz$/
## 1.3 Browser Utils package
- name: npm
id: '@sentry-internal/browser-utils'
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
index 5e172393dc37..808fad64696f 100644
--- a/.github/workflows/auto-release.yml
+++ b/.github/workflows/auto-release.yml
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Get auth token
id: token
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c33391b6de73..ca8ebedfdd75 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -920,8 +920,12 @@ jobs:
env:
E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }}
+ - name: Copy to temp
+ run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
run: pnpm ${{ matrix.build-command || 'test:build' }}
@@ -929,10 +933,10 @@ jobs:
uses: ./.github/actions/install-playwright
with:
browsers: chromium
- cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ cwd: ${{ runner.temp }}/test-application
- name: Run E2E test
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 10
run: pnpm test:assert
@@ -941,7 +945,7 @@ jobs:
if: failure()
with:
name: playwright-traces-job_e2e_playwright_tests-${{ matrix.test-application}}
- path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application}}/test-results
+ path: ${{ runner.temp }}/test-application/test-results
overwrite: true
retention-days: 7
@@ -955,7 +959,7 @@ jobs:
if: always()
with:
name: E2E Test Dump (${{ matrix.label || matrix.test-application }})
- path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps
+ path: ${{ runner.temp }}/test-application/event-dumps
overwrite: true
retention-days: 7
if-no-files-found: ignore
@@ -1037,8 +1041,12 @@ jobs:
env:
E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }}
+ - name: Copy to temp
+ run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
run: pnpm ${{ matrix.build-command || 'test:build' }}
@@ -1046,10 +1054,10 @@ jobs:
uses: ./.github/actions/install-playwright
with:
browsers: chromium
- cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ cwd: ${{ runner.temp }}/test-application
- name: Run E2E test
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 10
run: pnpm ${{ matrix.assert-command || 'test:assert' }}
@@ -1063,20 +1071,19 @@ jobs:
if: always()
with:
name: E2E Test Dump (${{ matrix.label || matrix.test-application }})
- path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps
+ path: ${{ runner.temp }}/test-application/event-dumps
overwrite: true
retention-days: 7
if-no-files-found: ignore
- name: Deploy Astro to Cloudflare
- uses: cloudflare/pages-action@v1
+ uses: cloudflare/wrangler-action@v3
if: matrix.test-application == 'cloudflare-astro'
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
- directory: dist
- workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ command: pages deploy dist --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
+ workingDirectory: ${{ runner.temp }}/test-application
job_required_jobs_passed:
name: All required jobs passed or were skipped
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index 49f603e8235c..2b8f46f0c352 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -153,8 +153,12 @@ jobs:
env:
E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }}
+ - name: Copy to temp
+ run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
run: yarn ${{ matrix.build-command }}
@@ -162,10 +166,10 @@ jobs:
uses: ./.github/actions/install-playwright
with:
browsers: chromium
- cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ cwd: ${{ runner.temp }}/test-application
- name: Run E2E test
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
+ working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 15
run: yarn test:assert
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index af083427288e..2edb9fb09405 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Get auth token
id: token
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
+ uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09f630d5aedf..99bd0c1bde29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,58 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 9.36.0
+
+### Important Changes
+
+- **feat(node-core): Add node-core SDK ([#16745](https://github.com/getsentry/sentry-javascript/pull/16745))**
+
+This release adds a new SDK `@sentry/node-core` which ships without any OpenTelemetry instrumententation out of the box. All OpenTelemetry dependencies are peer dependencies and OpenTelemetry has to be set up manually.
+
+Use `@sentry/node-core` when:
+
+- You already have OpenTelemetry set up
+- You need custom OpenTelemetry configuration
+- You want minimal dependencies
+- You need fine-grained control over instrumentation
+
+Use `@sentry/node` when:
+
+- You want an automatic setup
+- You're new to OpenTelemetry
+- You want sensible defaults
+- You prefer convenience over control
+
+* **feat(node): Deprecate ANR integration ([#16832](https://github.com/getsentry/sentry-javascript/pull/16832))**
+
+The ANR integration has been deprecated and will be removed in future versions. Use `eventLoopBlockIntegration` from `@sentry/node-native` instead.
+
+- **feat(replay): Add `_experiments.ignoreMutations` option ([#16816](https://github.com/getsentry/sentry-javascript/pull/16816))**
+
+This replay option allows to configure a selector list of elements to not capture mutations for.
+
+```js
+Sentry.replayIntegration({
+ _experiments: {
+ ignoreMutations: ['.dragging'],
+ },
+});
+```
+
+### Other changes
+
+- feat(deps): bump @prisma/instrumentation from 6.10.1 to 6.11.1 ([#16833](https://github.com/getsentry/sentry-javascript/pull/16833))
+- feat(nextjs): Add flag for suppressing router transition warning ([#16823](https://github.com/getsentry/sentry-javascript/pull/16823))
+- feat(nextjs): Automatically skip middleware requests for tunnel route ([#16812](https://github.com/getsentry/sentry-javascript/pull/16812))
+- feat(replay): Export compression worker from `@sentry/replay-internal` ([#16794](https://github.com/getsentry/sentry-javascript/pull/16794))
+- fix(browser): Avoid 4xx response for succesful `diagnoseSdkConnectivity` request ([#16840](https://github.com/getsentry/sentry-javascript/pull/16840))
+- fix(browser): Guard against undefined nextHopProtocol ([#16806](https://github.com/getsentry/sentry-javascript/pull/16806))
+- fix(cloudflare): calculate retries not attempts ([#16834](https://github.com/getsentry/sentry-javascript/pull/16834))
+- fix(nuxt): Parametrize routes on the server-side ([#16785](https://github.com/getsentry/sentry-javascript/pull/16785))
+- fix(vue): Make pageload span handling more reliable ([#16799](https://github.com/getsentry/sentry-javascript/pull/16799))
+
+Work in this release was contributed by @Spice-King and @stayallive. Thank you for your contributions!
+
## 9.35.0
- feat(browser): Add ElementTiming instrumentation and spans ([#16589](https://github.com/getsentry/sentry-javascript/pull/16589))
diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js
new file mode 100644
index 000000000000..269b531b57eb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js
@@ -0,0 +1,21 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = Sentry.replayIntegration({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ minReplayDuration: 0,
+ useCompression: false,
+ _experiments: {
+ ignoreMutations: ['.moving'],
+ },
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 0,
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ integrations: [window.Replay],
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/subject.js b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/subject.js
new file mode 100644
index 000000000000..b886ea05a458
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/subject.js
@@ -0,0 +1,26 @@
+function moveElement(el, remaining) {
+ if (!remaining) {
+ el.classList.remove('moving');
+
+ setTimeout(() => {
+ el.style.transform = `translate(${remaining}0px, 0)`;
+ el.classList.add('moved');
+ });
+ return;
+ }
+
+ el.style.transform = `translate(${remaining}0px, 0)`;
+
+ setTimeout(() => {
+ moveElement(el, remaining - 1);
+ }, 10);
+}
+
+const el = document.querySelector('#mutation-target');
+const btn = document.querySelector('#button-move');
+
+btn.addEventListener('click', event => {
+ el.classList.add('moving');
+ event.preventDefault();
+ moveElement(el, 20);
+});
diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/template.html b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/template.html
new file mode 100644
index 000000000000..58cb29d50590
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ This is moved around!
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/test.ts b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/test.ts
new file mode 100644
index 000000000000..3d76a2b07b1a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/test.ts
@@ -0,0 +1,65 @@
+import { expect } from '@playwright/test';
+import type { mutationData } from '@sentry-internal/rrweb-types';
+import { sentryTest } from '../../../utils/fixtures';
+import type { RecordingSnapshot } from '../../../utils/replayHelpers';
+import { collectReplayRequests, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers';
+
+sentryTest('allows to ignore mutations via `ignoreMutations` option', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipReplayTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const reqPromise0 = waitForReplayRequest(page, 0);
+
+ await page.goto(url);
+ await reqPromise0;
+
+ const requestsPromise = collectReplayRequests(page, recordingEvents => {
+ const events = recordingEvents as (RecordingSnapshot & { data: mutationData })[];
+ return events.some(event => event.data.attributes?.some(attr => attr.attributes['class'] === 'moved'));
+ });
+
+ page.locator('#button-move').click();
+
+ const requests = await requestsPromise;
+
+ // All transform mutatinos are ignored and not captured
+ const transformMutations = requests.replayRecordingSnapshots.filter(
+ item =>
+ (item.data as mutationData)?.attributes?.some(
+ attr => attr.attributes['style'] && attr.attributes['class'] !== 'moved',
+ ),
+ );
+
+ // Should capture the final class mutation
+ const classMutations = requests.replayRecordingSnapshots.filter(
+ item => (item.data as mutationData)?.attributes?.some(attr => attr.attributes['class']),
+ );
+
+ expect(transformMutations).toEqual([]);
+ expect(classMutations).toEqual([
+ {
+ data: {
+ adds: [],
+ attributes: [
+ {
+ attributes: {
+ class: 'moved',
+ style: {
+ transform: 'translate(0px, 0px)',
+ },
+ },
+ id: expect.any(Number),
+ },
+ ],
+ removes: [],
+ source: expect.any(Number),
+ texts: [],
+ },
+ timestamp: 0,
+ type: 3,
+ },
+ ]);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts
index d0fa133f9567..9ced3b2dee07 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts
@@ -60,7 +60,6 @@ sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl,
'user_agent.original': expect.stringContaining('Chrome'),
'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
'lcp.element': 'body > img',
- 'lcp.id': '',
'lcp.loadTime': expect.any(Number),
'lcp.renderTime': expect.any(Number),
'lcp.size': expect.any(Number),
diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md
index 919d74e78542..a1eb1b559b9c 100644
--- a/dev-packages/e2e-tests/README.md
+++ b/dev-packages/e2e-tests/README.md
@@ -133,3 +133,24 @@ A standardized frontend test application has the following features:
### Standardized Backend Test Apps
TBD
+
+### Standardized Frontend-to-Backend Test Apps
+
+A standardized Meta-Framework test application has the following features:
+
+- Has a parameterized backend API route `/user/:id` that returns a JSON object with the user ID.
+- Has a parameterized frontend page (can be SSR) `/user/:id` that fetches the user data on the client-side from the API route and displays it.
+
+This setup creates the scenario where the frontend page loads, and then immediately makes an API request to the backend API.
+
+The following test cases for connected tracing should be implemented in the test app:
+
+- Capturing a distributed page load trace when a page is loaded
+ - The HTML meta-tag should include the Sentry trace data and baggage
+ - The server root span should be the parent of the client pageload span
+ - All routes (server and client) should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route
+- Capturing a distributed trace when requesting the API from the client-side
+ - There should be three transactions involved: the client pageload, the server "pageload", and the server API request
+ - The client pageload should include an `http.client` span that is the parent of the server API request span
+ - All three transactions and the `http.client` span should share the same `trace_id`
+ - All `transaction` names and the `span` description should be parameterized, e.g. `/user/5` should be captured as `/user/:id` route
diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts
new file mode 100644
index 000000000000..0ecd3999db4d
--- /dev/null
+++ b/dev-packages/e2e-tests/ciCopyToTemp.ts
@@ -0,0 +1,19 @@
+/* eslint-disable no-console */
+
+import { copyToTemp } from './lib/copyToTemp';
+
+async function run(): Promise {
+ const originalPath = process.argv[2];
+ const tmpDirPath = process.argv[3];
+
+ if (!originalPath || !tmpDirPath) {
+ throw new Error('Original path and tmp dir path are required');
+ }
+
+ console.log(`Copying ${originalPath} to ${tmpDirPath}...`);
+
+ await copyToTemp(originalPath, tmpDirPath);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.ts
new file mode 100644
index 000000000000..d6667978b924
--- /dev/null
+++ b/dev-packages/e2e-tests/lib/copyToTemp.ts
@@ -0,0 +1,61 @@
+/* eslint-disable no-console */
+import { readFileSync, writeFileSync } from 'fs';
+import { cp } from 'fs/promises';
+import { join } from 'path';
+
+export async function copyToTemp(originalPath: string, tmpDirPath: string): Promise {
+ // copy files to tmp dir
+ await cp(originalPath, tmpDirPath, { recursive: true });
+
+ fixPackageJson(tmpDirPath);
+}
+
+function fixPackageJson(cwd: string): void {
+ const packageJsonPath = join(cwd, 'package.json');
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
+ dependencies?: Record;
+ devDependencies?: Record;
+ volta?: Record;
+ };
+
+ // 1. Fix file dependencies
+ if (packageJson.dependencies) {
+ fixFileLinkDependencies(packageJson.dependencies);
+ }
+ if (packageJson.devDependencies) {
+ fixFileLinkDependencies(packageJson.devDependencies);
+ }
+
+ // 2. Fix volta extends
+ if (!packageJson.volta) {
+ throw new Error('No volta config found, please provide one!');
+ }
+
+ if (typeof packageJson.volta.extends === 'string') {
+ const extendsPath = packageJson.volta.extends;
+ // We add a virtual dir to ensure that the relative depth is consistent
+ // dirPath is relative to ./../test-applications/xxx
+ const newPath = join(__dirname, 'virtual-dir/', extendsPath);
+ packageJson.volta.extends = newPath;
+ console.log(`Fixed volta.extends to ${newPath}`);
+ } else {
+ console.log('No volta.extends found');
+ }
+
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
+}
+
+function fixFileLinkDependencies(dependencyObj: Record): void {
+ for (const [key, value] of Object.entries(dependencyObj)) {
+ if (value.startsWith('link:')) {
+ const dirPath = value.replace('link:', '');
+
+ // We add a virtual dir to ensure that the relative depth is consistent
+ // dirPath is relative to ./../test-applications/xxx
+ const newPath = join(__dirname, 'virtual-dir/', dirPath);
+
+ dependencyObj[key] = `link:${newPath}`;
+ console.log(`Fixed ${key} dependency to ${newPath}`);
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json
index c2279b69cf6e..8bd8a3fd83ec 100644
--- a/dev-packages/e2e-tests/package.json
+++ b/dev-packages/e2e-tests/package.json
@@ -16,7 +16,8 @@
"clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm",
"ci:build-matrix": "ts-node ./lib/getTestMatrix.ts",
"ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true",
- "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results}",
+ "ci:copy-to-temp": "ts-node ./ciCopyToTemp.ts",
+ "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular,event-dumps}",
"clean:pnpm": "pnpm store prune"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts
index 44f0bc06dca7..7b6efb2dd13b 100644
--- a/dev-packages/e2e-tests/run.ts
+++ b/dev-packages/e2e-tests/run.ts
@@ -1,8 +1,11 @@
/* eslint-disable no-console */
import { spawn } from 'child_process';
import * as dotenv from 'dotenv';
+import { mkdtemp, rm } from 'fs/promises';
import { sync as globSync } from 'glob';
-import { resolve } from 'path';
+import { tmpdir } from 'os';
+import { join, resolve } from 'path';
+import { copyToTemp } from './lib/copyToTemp';
import { registrySetup } from './registrySetup';
const DEFAULT_DSN = 'https://username@domain/123';
@@ -39,7 +42,7 @@ async function run(): Promise {
dotenv.config();
// Allow to run a single app only via `yarn test:run `
- const appName = process.argv[2];
+ const appName = process.argv[2] || '';
const dsn = process.env.E2E_TEST_DSN || DEFAULT_DSN;
@@ -74,13 +77,20 @@ async function run(): Promise {
console.log('');
for (const testAppPath of testAppPaths) {
- const cwd = resolve('test-applications', testAppPath);
+ const originalPath = resolve('test-applications', testAppPath);
+ const tmpDirPath = await mkdtemp(join(tmpdir(), `sentry-e2e-tests-${appName}-`));
- console.log(`Building ${testAppPath}...`);
+ await copyToTemp(originalPath, tmpDirPath);
+ const cwd = tmpDirPath;
+
+ console.log(`Building ${testAppPath} in ${tmpDirPath}...`);
await asyncExec('pnpm test:build', { env, cwd });
console.log(`Testing ${testAppPath}...`);
await asyncExec('pnpm test:assert', { env, cwd });
+
+ // clean up (although this is tmp, still nice to do)
+ await rm(tmpDirPath, { recursive: true });
}
} catch (error) {
console.error(error);
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json
index e6b1df6cd387..1382f0be6fd8 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json
@@ -5,7 +5,7 @@
"ng": "ng",
"dev": "ng serve",
"proxy": "node start-event-proxy.mjs",
- "preview": "http-server dist/angular-17/browser --port 8080",
+ "preview": "http-server dist/angular-17/browser --port 8080 --silent",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "playwright test",
@@ -31,6 +31,7 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *",
"@angular-devkit/build-angular": "^17.1.1",
"@angular/cli": "^17.1.1",
"@angular/compiler-cli": "^17.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
index 03a715ce646c..59749ff3cc78 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
+// Cannot use @sentry/angular here due to build stuff
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/angular-18/package.json b/dev-packages/e2e-tests/test-applications/angular-18/package.json
index 288b1b119912..3856f2523916 100644
--- a/dev-packages/e2e-tests/test-applications/angular-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json
@@ -5,7 +5,7 @@
"ng": "ng",
"dev": "ng serve",
"proxy": "node start-event-proxy.mjs",
- "preview": "http-server dist/angular-18/browser --port 8080",
+ "preview": "http-server dist/angular-18/browser --port 8080 --silent",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "playwright test",
@@ -31,6 +31,7 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *",
"@angular-devkit/build-angular": "^18.0.0",
"@angular/cli": "^18.0.0",
"@angular/compiler-cli": "^18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts
index af85b8ffc405..a44b454b10ef 100644
--- a/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-18/tests/performance.test.ts
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
+// Cannot use @sentry/angular here due to build stuff
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json
index f7544ccb5239..fd35576d531c 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json
@@ -5,7 +5,7 @@
"ng": "ng",
"dev": "ng serve",
"proxy": "node start-event-proxy.mjs",
- "preview": "http-server dist/angular-19/browser --port 8080",
+ "preview": "http-server dist/angular-19/browser --port 8080 --silent",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "playwright test",
@@ -34,6 +34,7 @@
"@angular/compiler-cli": "^19.0.0",
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.4.0",
@@ -43,5 +44,8 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.2"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs
index b1b4620866bd..1344499c2256 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'angular-18',
+ proxyServerName: 'angular-19',
});
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts
index 36d23bd077a5..c6e8bc43abed 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('sends an error', async ({ page }) => {
- const errorPromise = waitForError('angular-18', async errorEvent => {
+ const errorPromise = waitForError('angular-19', async errorEvent => {
return !errorEvent.type;
});
@@ -30,11 +30,11 @@ test('sends an error', async ({ page }) => {
});
test('assigns the correct transaction value after a navigation', async ({ page }) => {
- const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const pageloadTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
- const errorPromise = waitForError('angular-18', async errorEvent => {
+ const errorPromise = waitForError('angular-19', async errorEvent => {
return !errorEvent.type;
});
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
index c2cb2eca34b6..c41aa5859f82 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
@@ -1,9 +1,10 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
+// Cannot use @sentry/angular here due to build stuff
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
- const transactionPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const transactionPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
@@ -26,11 +27,11 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
});
test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
- const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const pageloadTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -55,11 +56,11 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
});
test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => {
- const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const pageloadTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -100,7 +101,7 @@ test('sends a navigation transaction even if the pageload span is still active',
});
test('groups redirects within one navigation root span', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -130,7 +131,7 @@ test('groups redirects within one navigation root span', async ({ page }) => {
test.describe('finish routing span', () => {
test('finishes routing span on navigation cancel', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -159,7 +160,7 @@ test.describe('finish routing span', () => {
});
test('finishes routing span on navigation error', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -192,7 +193,7 @@ test.describe('finish routing span', () => {
test.describe('TraceDirective', () => {
test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -237,7 +238,7 @@ test.describe('TraceDirective', () => {
test.describe('TraceClass Decorator', () => {
test('adds init span for decorated class', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -269,7 +270,7 @@ test.describe('TraceClass Decorator', () => {
test.describe('TraceMethod Decorator', () => {
test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -297,7 +298,7 @@ test.describe('TraceMethod Decorator', () => {
});
test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => {
- const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('angular-19', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json
index 34ce69c6ea44..6f4b09fb0787 100644
--- a/dev-packages/e2e-tests/test-applications/angular-20/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json
@@ -5,7 +5,7 @@
"ng": "ng",
"dev": "ng serve",
"proxy": "node start-event-proxy.mjs",
- "preview": "http-server dist/angular-20/browser --port 8080",
+ "preview": "http-server dist/angular-20/browser --port 8080 --silent",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "playwright test",
@@ -35,6 +35,7 @@
"@angular/compiler-cli": "^20.0.0",
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.4.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts
index f790cb10d180..cfdd2d95c575 100644
--- a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
+// Cannot use @sentry/angular here due to build stuff
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json
index 4e49cf150bb4..c6f3ca83d07c 100644
--- a/dev-packages/e2e-tests/test-applications/astro-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json
@@ -23,5 +23,8 @@
},
"devDependencies": {
"@astrojs/internal-helpers": "^0.4.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json
index 189c2834f3ae..88741c9f01d9 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json
@@ -22,5 +22,8 @@
"overrides": {
"esbuild": "0.24.0"
}
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
index ab822e9d669d..4ffb80821c2d 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
@@ -3,14 +3,14 @@
"version": "1.0.0",
"private": true,
"scripts": {
- "copy:layer": "cp -r ./../../../../packages/aws-serverless/build/aws/dist-serverless/nodejs/node_modules/ ./node_modules",
"start": "node src/run.js",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
- "test:build": "pnpm install && pnpm copy:layer",
+ "test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
+ "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless"
},
"devDependencies": {
"@sentry-internal/test-utils": "link:../../../test-utils",
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
index b9667aeef85f..4329f837613a 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
@@ -18,7 +18,7 @@
"@cloudflare/vitest-pool-workers": "^0.8.31",
"@cloudflare/workers-types": "^4.20250521.0",
"vitest": "3.1.0",
- "wrangler": "^4.16.0"
+ "wrangler": "4.22.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
index e926cc164121..b195ec7e2716 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
@@ -20,7 +20,7 @@
"@cloudflare/workers-types": "^4.20240725.0",
"typescript": "^5.5.2",
"vitest": "1.6.1",
- "wrangler": "^3.60.3"
+ "wrangler": "4.22.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
index 76a003b6c1de..fa09dc31d2e9 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
@@ -28,7 +28,6 @@
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^2.7.2",
- "@sentry/core": "latest || *",
"@types/compression": "^1.7.5",
"@types/express": "^4.17.20",
"@types/morgan": "^1.9.9",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts
index 2e91a73258f7..58e81eeee529 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test';
-import { uuid4 } from '@sentry/core';
import { waitForTransaction } from '@sentry-internal/test-utils';
@@ -20,7 +19,7 @@ test('Sends parameterized transaction name to Sentry', async ({ page }) => {
test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
// We use this to identify the transactions
- const testTag = uuid4();
+ const testTag = crypto.randomUUID();
const httpServerTransactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
index 1050b86850c3..46d56fb933d1 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
@@ -31,7 +31,6 @@
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^2.7.2",
- "@sentry/core": "latest || *",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.17",
"@types/morgan": "^1.9.4",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts
index 3503c0cb3c24..36c51debbb98 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts
@@ -1,6 +1,4 @@
import { expect, test } from '@playwright/test';
-import { uuid4 } from '@sentry/core';
-
import { waitForTransaction } from '@sentry-internal/test-utils';
test.describe.configure({ mode: 'serial' });
@@ -63,7 +61,7 @@ test('Sends a loader span to Sentry', async ({ page }) => {
test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => {
// We use this to identify the transactions
- const testTag = uuid4();
+ const testTag = crypto.randomUUID();
const httpServerTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
@@ -103,7 +101,7 @@ test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => {
test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
// We use this to identify the transactions
- const testTag = uuid4();
+ const testTag = crypto.randomUUID();
const httpServerTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
index 4a5070b4fe98..72563553b1c5 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
@@ -25,7 +25,6 @@
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "2.16.7",
"@remix-run/eslint-config": "2.16.7",
- "@sentry/core": "latest || *",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.34",
"@types/prop-types": "15.7.7",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts
index bb2664a0a447..69508ed67183 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts
@@ -1,6 +1,4 @@
import { expect, test } from '@playwright/test';
-import { uuid4 } from '@sentry/core';
-
import { waitForTransaction } from '@sentry-internal/test-utils';
test.describe.configure({ mode: 'serial' });
@@ -20,7 +18,7 @@ test('Sends parameterized transaction name to Sentry', async ({ page }) => {
test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
// We use this to identify the transactions
- const testTag = uuid4();
+ const testTag = crypto.randomUUID();
const httpServerTransactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
@@ -40,9 +38,8 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id;
const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id;
- const loaderSpanId = httpServerTransaction?.spans?.find(
- span => span.data && span.data['code.function'] === 'loader',
- )?.span_id;
+ const loaderSpanId = httpServerTransaction?.spans?.find(span => span.data && span.data['code.function'] === 'loader')
+ ?.span_id;
const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id;
diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
index c9b564bf1651..fbd40cebcb07 100644
--- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
+++ b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
@@ -19,5 +19,8 @@
"@sentry/node": "latest || *",
"@sentry-internal/replay": "latest || *",
"@sentry/wasm": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
index f5302ee3531e..679033bbbbfb 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
@@ -25,7 +25,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
@@ -49,5 +49,8 @@
"overrides": {
"minimatch": "10.0.1"
}
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts
index 242b4c778a0e..7dc21c733b19 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts
@@ -1,16 +1,15 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
-import type { MonitorConfig } from '@sentry/core';
import * as Sentry from '@sentry/nestjs';
import { SentryCron, SentryTraced } from '@sentry/nestjs';
-const monitorConfig: MonitorConfig = {
+const monitorConfig = {
schedule: {
type: 'crontab',
value: '* * * * *',
},
-};
+} as const;
@Injectable()
export class AppService {
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
index a4a046b9106d..a9b1a676344f 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
@@ -44,5 +44,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts
index 6b6b01411c0c..5d95dc9f0359 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts
@@ -1,16 +1,15 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
-import type { MonitorConfig } from '@sentry/core';
import * as Sentry from '@sentry/nestjs';
import { SentryCron, SentryTraced } from '@sentry/nestjs';
-const monitorConfig: MonitorConfig = {
+const monitorConfig = {
schedule: {
type: 'crontab',
value: '* * * * *',
},
-};
+} as const;
@Injectable()
export class AppService {
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
index 816c5ec0fcce..acca8d1e2234 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
@@ -46,5 +46,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
index 3d367978c6c7..aef4d5b70d57 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
@@ -44,5 +44,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
index a9f89152d56d..39495a3a7424 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
@@ -1,16 +1,15 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
-import type { MonitorConfig } from '@sentry/core';
import * as Sentry from '@sentry/nestjs';
import { SentryCron, SentryTraced } from '@sentry/nestjs';
-const monitorConfig: MonitorConfig = {
+const monitorConfig = {
schedule: {
type: 'crontab',
value: '* * * * *',
},
-};
+} as const;
@Injectable()
export class AppService {
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
index 133c56648f1f..101ab8f1cddb 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
@@ -43,5 +43,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
index 040d5d326809..78dfe680a453 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const id = crypto.randomUUID();
@@ -27,7 +26,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
@@ -141,7 +140,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
index 88092675cc98..82c9a03554a2 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
@@ -44,5 +44,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts
index 242b4c778a0e..7dc21c733b19 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts
@@ -1,16 +1,15 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
-import type { MonitorConfig } from '@sentry/core';
import * as Sentry from '@sentry/nestjs';
import { SentryCron, SentryTraced } from '@sentry/nestjs';
-const monitorConfig: MonitorConfig = {
+const monitorConfig = {
schedule: {
type: 'crontab',
value: '* * * * *',
},
-};
+} as const;
@Injectable()
export class AppService {
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
index 0f645056e025..5892c221c640 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
@@ -46,5 +46,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
index 791c3df2305b..62183df5b781 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
@@ -42,5 +42,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
index dfd015acade9..7c9071664135 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
@@ -42,5 +42,8 @@
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
index eea815ebb836..8016f60f2a81 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
@@ -24,18 +24,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry/vercel-edge": "latest || *"
+ "@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
index a7365cc3fb10..2aa054f1a965 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
@@ -25,17 +25,7 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/vercel-edge": "latest || *"
+ "@sentry/core": "latest || *"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index 416102b15da7..715bb2defa41 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -27,18 +27,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
- "@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/vercel-edge": "latest || *"
+ "@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/components/span-context.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/components/span-context.tsx
index 1375b7e9cd4c..834ccc3fadf3 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/components/span-context.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/components/span-context.tsx
@@ -1,7 +1,6 @@
'use client';
-import { Span } from '@sentry/core';
-import { startInactiveSpan } from '@sentry/nextjs';
+import { startInactiveSpan, Span } from '@sentry/nextjs';
import { PropsWithChildren, createContext, useState } from 'react';
export const SpanContext = createContext<
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
index a06b718835bf..251178afaa76 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
@@ -16,6 +16,7 @@
},
"dependencies": {
"@sentry/nextjs": "latest || *",
+ "@sentry/core": "latest || *",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
@@ -27,17 +28,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/vercel-edge": "latest || *",
"ts-node": "10.9.1"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
index 7fcad2ab0e64..04046652f51b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
@@ -37,7 +37,8 @@
"eslint-config-next": "^14.2.4",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
- "typescript": "^5.5.3"
+ "typescript": "^5.5.3",
+ "zod": "^3.24.2"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
index 36beb12cd227..6cfd5524d7f2 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
@@ -25,17 +25,7 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/vercel-edge": "latest || *"
+ "@sentry/core": "latest || *"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/node-connect/package.json b/dev-packages/e2e-tests/test-applications/node-connect/package.json
index f882e55d8797..6593bfdc6146 100644
--- a/dev-packages/e2e-tests/test-applications/node-connect/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-connect/package.json
@@ -12,9 +12,8 @@
},
"dependencies": {
"@sentry/node": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/node": "^18.19.1",
+ "@types/connect": "3.4.38",
"connect": "3.7.0",
"typescript": "~5.0.0",
"ts-node": "10.9.1"
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore
new file mode 100644
index 000000000000..1521c8b7652b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json
new file mode 100644
index 000000000000..67351f3ab187
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "node-core-express-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/node-core": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/instrumentation-http": "^0.57.1",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-node": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.30.0",
+ "@types/express": "^4.17.21",
+ "@types/node": "^18.19.1",
+ "express": "^4.21.2",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "resolutions": {
+ "@types/qs": "6.9.17"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts
new file mode 100644
index 000000000000..d5bf40067de0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts
@@ -0,0 +1,57 @@
+// Import this first!
+import './instrument';
+
+// Now import other modules
+import * as Sentry from '@sentry/node-core';
+import express from 'express';
+
+const app = express();
+const port = 3030;
+
+app.get('/test-transaction', function (req, res) {
+ Sentry.withActiveSpan(null, async () => {
+ Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => {
+ Sentry.startSpan({ name: 'test-span' }, () => undefined);
+ });
+
+ await Sentry.flush();
+
+ res.send({
+ transactionIds: global.transactionIds || [],
+ });
+ });
+});
+
+app.get('/test-exception/:id', function (req, _res) {
+ try {
+ throw new Error(`This is an exception with id ${req.params.id}`);
+ } catch (e) {
+ Sentry.captureException(e);
+ throw e;
+ }
+});
+
+app.get('/test-local-variables-caught', function (req, res) {
+ const randomVariableToRecord = Math.random();
+
+ let exceptionId: string;
+ try {
+ throw new Error('Local Variable Error');
+ } catch (e) {
+ exceptionId = Sentry.captureException(e);
+ }
+
+ res.send({ exceptionId, randomVariableToRecord });
+});
+
+// @ts-ignore
+app.use(function onError(err, req, res, next) {
+ // The error id is attached to `res.sentry` to be returned
+ // and optionally displayed to the user for support.
+ res.statusCode = 500;
+ res.end(res.sentry + '\n');
+});
+
+app.listen(port, () => {
+ console.log(`Example app listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts
new file mode 100644
index 000000000000..a3969933ea64
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts
@@ -0,0 +1,46 @@
+import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
+import * as Sentry from '@sentry/node-core';
+import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry';
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+
+declare global {
+ namespace globalThis {
+ var transactionIds: string[];
+ }
+}
+
+const sentryClient = Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ includeLocalVariables: true,
+ debug: !!process.env.DEBUG,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+ openTelemetryInstrumentations: [new HttpInstrumentation()],
+});
+
+const provider = new NodeTracerProvider({
+ sampler: sentryClient ? new SentrySampler(sentryClient) : undefined,
+ spanProcessors: [new SentrySpanProcessor()],
+});
+
+provider.register({
+ propagator: new SentryPropagator(),
+ contextManager: new Sentry.SentryContextManager(),
+});
+
+Sentry.validateOpenTelemetrySetup();
+
+Sentry.addEventProcessor(event => {
+ global.transactionIds = global.transactionIds || [];
+
+ if (event.type === 'transaction') {
+ const eventId = event.event_id;
+
+ if (eventId) {
+ global.transactionIds.push(eventId);
+ }
+ }
+
+ return event;
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs
new file mode 100644
index 000000000000..161017eab5ee
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'node-core-express-otel-v1',
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts
new file mode 100644
index 000000000000..013c622f125a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts
@@ -0,0 +1,42 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Sends correct error event', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-core-express-otel-v1', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ await fetch(`${baseURL}/test-exception/123`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
+
+ expect(errorEvent.request).toEqual({
+ method: 'GET',
+ cookies: {},
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/test-exception/123',
+ });
+
+ expect(errorEvent.transaction).toEqual('GET /test-exception/123');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
+
+test('Should record caught exceptions with local variable', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-core-express-otel-v1', event => {
+ return event.transaction === 'GET /test-local-variables-caught';
+ });
+
+ await fetch(`${baseURL}/test-local-variables-caught`);
+
+ const errorEvent = await errorEventPromise;
+
+ const frames = errorEvent.exception?.values?.[0].stacktrace?.frames;
+ expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts
new file mode 100644
index 000000000000..1628a9a03ada
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts
@@ -0,0 +1,80 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends an API route transaction', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('node-core-express-otel-v1', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-transaction'
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.source': 'url',
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'http.server',
+ 'sentry.sample_rate': 1,
+ url: 'http://localhost:3030/test-transaction',
+ 'otel.kind': 'SERVER',
+ 'http.response.status_code': 200,
+ 'http.url': 'http://localhost:3030/test-transaction',
+ 'http.host': 'localhost:3030',
+ 'net.host.name': 'localhost',
+ 'http.method': 'GET',
+ 'http.scheme': 'http',
+ 'http.target': '/test-transaction',
+ 'http.user_agent': 'node',
+ 'http.flavor': '1.1',
+ 'net.transport': 'ip_tcp',
+ 'net.host.ip': expect.any(String),
+ 'net.host.port': expect.any(Number),
+ 'net.peer.ip': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ },
+ op: 'http.server',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'manual',
+ });
+
+ expect(transactionEvent.contexts?.response).toEqual({
+ status_code: 200,
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: 'GET /test-transaction',
+ type: 'transaction',
+ transaction_info: {
+ source: 'url',
+ },
+ }),
+ );
+});
+
+test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('node-core-express-otel-v1', transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' &&
+ transactionEvent.transaction === 'GET /test-exception/777' &&
+ transactionEvent.request?.url === 'http://localhost:3030/test-exception/777'
+ );
+ });
+
+ await fetch(`${baseURL}/test-exception/777`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
+ expect(transactionEvent.transaction).toEqual('GET /test-exception/777');
+ expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error');
+ expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json
new file mode 100644
index 000000000000..0060abd94682
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "esModuleInterop": true,
+ "lib": ["es2020"],
+ "strict": true,
+ "outDir": "dist",
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore
new file mode 100644
index 000000000000..1521c8b7652b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json
new file mode 100644
index 000000000000..e7f854cb7943
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "node-core-express-otel-v2-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/node-core": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^2.0.0",
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.202.0",
+ "@opentelemetry/instrumentation-http": "^0.202.0",
+ "@opentelemetry/resources": "^2.0.0",
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.30.0",
+ "@types/express": "^4.17.21",
+ "@types/node": "^18.19.1",
+ "express": "^4.21.2",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "resolutions": {
+ "@types/qs": "6.9.17"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts
new file mode 100644
index 000000000000..d5bf40067de0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts
@@ -0,0 +1,57 @@
+// Import this first!
+import './instrument';
+
+// Now import other modules
+import * as Sentry from '@sentry/node-core';
+import express from 'express';
+
+const app = express();
+const port = 3030;
+
+app.get('/test-transaction', function (req, res) {
+ Sentry.withActiveSpan(null, async () => {
+ Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => {
+ Sentry.startSpan({ name: 'test-span' }, () => undefined);
+ });
+
+ await Sentry.flush();
+
+ res.send({
+ transactionIds: global.transactionIds || [],
+ });
+ });
+});
+
+app.get('/test-exception/:id', function (req, _res) {
+ try {
+ throw new Error(`This is an exception with id ${req.params.id}`);
+ } catch (e) {
+ Sentry.captureException(e);
+ throw e;
+ }
+});
+
+app.get('/test-local-variables-caught', function (req, res) {
+ const randomVariableToRecord = Math.random();
+
+ let exceptionId: string;
+ try {
+ throw new Error('Local Variable Error');
+ } catch (e) {
+ exceptionId = Sentry.captureException(e);
+ }
+
+ res.send({ exceptionId, randomVariableToRecord });
+});
+
+// @ts-ignore
+app.use(function onError(err, req, res, next) {
+ // The error id is attached to `res.sentry` to be returned
+ // and optionally displayed to the user for support.
+ res.statusCode = 500;
+ res.end(res.sentry + '\n');
+});
+
+app.listen(port, () => {
+ console.log(`Example app listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts
new file mode 100644
index 000000000000..a3969933ea64
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts
@@ -0,0 +1,46 @@
+import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
+import * as Sentry from '@sentry/node-core';
+import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry';
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+
+declare global {
+ namespace globalThis {
+ var transactionIds: string[];
+ }
+}
+
+const sentryClient = Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ includeLocalVariables: true,
+ debug: !!process.env.DEBUG,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+ openTelemetryInstrumentations: [new HttpInstrumentation()],
+});
+
+const provider = new NodeTracerProvider({
+ sampler: sentryClient ? new SentrySampler(sentryClient) : undefined,
+ spanProcessors: [new SentrySpanProcessor()],
+});
+
+provider.register({
+ propagator: new SentryPropagator(),
+ contextManager: new Sentry.SentryContextManager(),
+});
+
+Sentry.validateOpenTelemetrySetup();
+
+Sentry.addEventProcessor(event => {
+ global.transactionIds = global.transactionIds || [];
+
+ if (event.type === 'transaction') {
+ const eventId = event.event_id;
+
+ if (eventId) {
+ global.transactionIds.push(eventId);
+ }
+ }
+
+ return event;
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs
new file mode 100644
index 000000000000..23a724872457
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'node-core-express-otel-v2',
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts
new file mode 100644
index 000000000000..f4832729b899
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts
@@ -0,0 +1,42 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Sends correct error event', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-core-express-otel-v2', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ await fetch(`${baseURL}/test-exception/123`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
+
+ expect(errorEvent.request).toEqual({
+ method: 'GET',
+ cookies: {},
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/test-exception/123',
+ });
+
+ expect(errorEvent.transaction).toEqual('GET /test-exception/123');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
+
+test('Should record caught exceptions with local variable', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-core-express-otel-v2', event => {
+ return event.transaction === 'GET /test-local-variables-caught';
+ });
+
+ await fetch(`${baseURL}/test-local-variables-caught`);
+
+ const errorEvent = await errorEventPromise;
+
+ const frames = errorEvent.exception?.values?.[0].stacktrace?.frames;
+ expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts
new file mode 100644
index 000000000000..f3b1b680f2e9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts
@@ -0,0 +1,80 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends an API route transaction', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-transaction'
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.source': 'url',
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'http.server',
+ 'sentry.sample_rate': 1,
+ url: 'http://localhost:3030/test-transaction',
+ 'otel.kind': 'SERVER',
+ 'http.response.status_code': 200,
+ 'http.url': 'http://localhost:3030/test-transaction',
+ 'http.host': 'localhost:3030',
+ 'net.host.name': 'localhost',
+ 'http.method': 'GET',
+ 'http.scheme': 'http',
+ 'http.target': '/test-transaction',
+ 'http.user_agent': 'node',
+ 'http.flavor': '1.1',
+ 'net.transport': 'ip_tcp',
+ 'net.host.ip': expect.any(String),
+ 'net.host.port': expect.any(Number),
+ 'net.peer.ip': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ },
+ op: 'http.server',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'manual',
+ });
+
+ expect(transactionEvent.contexts?.response).toEqual({
+ status_code: 200,
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: 'GET /test-transaction',
+ type: 'transaction',
+ transaction_info: {
+ source: 'url',
+ },
+ }),
+ );
+});
+
+test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' &&
+ transactionEvent.transaction === 'GET /test-exception/777' &&
+ transactionEvent.request?.url === 'http://localhost:3030/test-exception/777'
+ );
+ });
+
+ await fetch(`${baseURL}/test-exception/777`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
+ expect(transactionEvent.transaction).toEqual('GET /test-exception/777');
+ expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error');
+ expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json
new file mode 100644
index 000000000000..0060abd94682
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "esModuleInterop": true,
+ "lib": ["es2020"],
+ "strict": true,
+ "outDir": "dist",
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
index e9989a5790a6..7d028901f0ef 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
@@ -11,7 +11,6 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
"@trpc/server": "10.45.2",
"@trpc/client": "10.45.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
index 338a7ccbd604..a3ab4dcbe28a 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
@@ -11,7 +11,6 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json
index d036d2621fa2..1831fa75a87d 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express/package.json
@@ -12,7 +12,6 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
"@trpc/server": "10.45.2",
"@trpc/client": "10.45.2",
@@ -24,7 +23,8 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
- "@sentry-internal/test-utils": "link:../../../test-utils"
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/core": "latest || *"
},
"resolutions": {
"@types/qs": "6.9.17"
diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts b/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts
index 7565e08f7c85..c5f2c24c61b8 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts
+++ b/dev-packages/e2e-tests/test-applications/node-express/src/mcp.ts
@@ -2,7 +2,7 @@ import express from 'express';
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { z } from 'zod';
-import { wrapMcpServerWithSentry } from '@sentry/core';
+import { wrapMcpServerWithSentry } from '@sentry/node';
const mcpRouter = express.Router();
diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts
index f2e125696af6..6c172479d9b7 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-express/tests/logs.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
-import type { SerializedLog, SerializedLogContainer } from '@sentry/core';
+import type { SerializedLogContainer } from '@sentry/core';
test('should send logs', async ({ baseURL }) => {
const logEnvelopePromise = waitForEnvelopeItem('node-express', envelope => {
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
index 25b5881905d2..ca6ba8c79d2c 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
@@ -12,8 +12,6 @@
},
"dependencies": {
"@sentry/node": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/node": "^18.19.1",
"fastify": "3.29.5",
"typescript": "~5.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
index ec20963d86e7..ee097817bafb 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const id = crypto.randomUUID();
@@ -27,7 +26,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
@@ -141,7 +140,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
index 56c23e38522e..a16abf8aa218 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
@@ -12,8 +12,6 @@
},
"dependencies": {
"@sentry/node": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/node": "^18.19.1",
"fastify": "4.29.1",
"typescript": "5.6.3",
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
index 965a47b9aba6..3746687b92c1 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const id = crypto.randomUUID();
@@ -27,7 +26,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
@@ -141,7 +140,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
index 08d6245f771e..9ab561dea98c 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
@@ -12,8 +12,6 @@
},
"dependencies": {
"@sentry/node": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/node": "^18.19.1",
"fastify": "5.3.2",
"typescript": "5.6.3",
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
index 1b4e0f92a97a..6de3b988c3b5 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const id = crypto.randomUUID();
@@ -27,7 +26,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
@@ -141,7 +140,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
index 00fa3ecfca9d..f7e8639b7ace 100644
--- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const id = crypto.randomUUID();
@@ -27,8 +26,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
-
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
@@ -141,7 +139,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outboundTransaction = await outboundTransactionPromise;
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
- const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as SpanJSON | undefined;
+ const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client');
expect(outgoingHttpSpan).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
index efb74e7346ad..39ff5bbeff11 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
@@ -13,9 +13,7 @@
"dependencies": {
"@opentelemetry/sdk-node": "0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "4.19.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
index b097e5b91930..e0bc082e1529 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
@@ -16,9 +16,7 @@
"@opentelemetry/instrumentation-undici": "0.6.0",
"@opentelemetry/instrumentation-http": "0.53.0",
"@opentelemetry/instrumentation": "0.53.0",
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "4.19.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json
index 3112ce669479..b7a3a98310a2 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json
@@ -13,9 +13,7 @@
"dependencies": {
"@opentelemetry/sdk-node": "0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
- "@sentry/core": "latest || *",
"@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "4.20.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
index 051127a4961f..722c567a712b 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
@@ -13,6 +13,7 @@
"@playwright/test": "~1.50.0",
"@sentry/node": "latest || *",
"@sentry/profiling-node": "latest || *",
+ "@types/node": "^18.19.1",
"esbuild": "0.25.0",
"typescript": "^5.7.3"
},
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
index 78536d6794ad..2fc24b2da6e5 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
@@ -13,6 +13,7 @@
"@playwright/test": "~1.50.0",
"@sentry/node": "latest || *",
"@sentry/profiling-node": "latest || *",
+ "@types/node": "^18.19.1",
"esbuild": "0.25.0",
"typescript": "^5.7.3"
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash
deleted file mode 100644
index 0e04d001c968..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash
+++ /dev/null
@@ -1,7 +0,0 @@
-# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules`
-# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects.
-#
-# Things we tried (that did not fix the problem):
-# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project)
-# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules`
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
index 9379acaf978a..f60139973266 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
@@ -11,12 +11,6 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
sentry: {
autoInjectServerSentry: 'experimental_dynamic-import',
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
index 1277c8a76966..02bdcd0e1583 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "nuxt build && bash ./copyIITM.bash",
+ "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
@@ -21,10 +21,7 @@
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
- "pnpm": {
- "overrides": {
- "nitropack": "~2.9.7",
- "ofetch": "^1.4.0"
- }
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue
@@ -1,13 +1,13 @@
-
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts
new file mode 100644
index 000000000000..eb268d2f3e13
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/user/[userId].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const userId = getRouterParam(event, 'userId');
+
+ return `UserId Param: ${userId}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts
index b781642c2b4f..b18da9ba0a3b 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts
index fd7a12e5e15d..272c98efff16 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts
index 0bc6ffa80b73..d3b7415e7678 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts
@@ -10,7 +10,7 @@ test.describe('distributed tracing', () => {
});
const serverTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
- return txnEvent.transaction.includes('GET /test-param/');
+ return txnEvent.transaction?.includes('GET /test-param/') || false;
});
const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});
expect(serverTxnEvent).toMatchObject({
- transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
+ transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
});
+
+ test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
+ return txnEvent.transaction?.includes('GET /api/user/') ?? false;
+ });
+
+ // Navigate to the page which will trigger an API call from the client-side
+ await page.goto(`/test-param/user/${PARAM}`);
+
+ const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
+ clientTxnEventPromise,
+ ssrTxnEventPromise,
+ serverReqTxnEventPromise,
+ ]);
+
+ const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
+
+ expect(clientTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: '/test-param/user/:userId()', // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ }),
+ }),
+ }),
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan).toEqual(
+ expect.objectContaining({
+ description: `GET /api/user/${PARAM}`, // fixme: parametrize
+ parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
+ data: expect.objectContaining({
+ url: `/api/user/${PARAM}`,
+ type: 'fetch',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.browser',
+ 'http.method': 'GET',
+ }),
+ }),
+ );
+
+ expect(ssrTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
+ transaction_info: { source: 'url' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ }),
+ }),
+ }),
+ );
+
+ expect(serverReqTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /api/user/:userId`, // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ parent_span_id: httpClientSpan?.span_id, // http.client span is parent
+ }),
+ }),
+ }),
+ );
+
+ // All 3 transactions and the http.client span should share the same trace_id
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash
deleted file mode 100644
index 0e04d001c968..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash
+++ /dev/null
@@ -1,7 +0,0 @@
-# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules`
-# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects.
-#
-# Things we tried (that did not fix the problem):
-# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project)
-# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules`
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
index 87e046ed39e9..0fcccd560af9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
@@ -11,10 +11,4 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
index 62133c9a85c3..7cb4f16d7d8d 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "nuxt build && bash ./copyIITM.bash",
+ "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
@@ -17,14 +17,22 @@
},
"dependencies": {
"@sentry/nuxt": "latest || *",
- "nuxt": "3.7.0"
+ "nuxt": "3.7.0",
+ "vue": "3.3.4",
+ "vue-router": "4.2.4"
},
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
- "overrides": {
- "nitropack": "2.10.0",
- "ofetch": "1.4.0"
+ "pnpm": {
+ "overrides": {
+ "nitropack": "2.10.0",
+ "ofetch": "1.4.0",
+ "@vercel/nft": "0.29.4"
+ }
+},
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue
@@ -1,13 +1,13 @@
-
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts
new file mode 100644
index 000000000000..eb268d2f3e13
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/user/[userId].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const userId = getRouterParam(event, 'userId');
+
+ return `UserId Param: ${userId}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts
index 8f20aa938893..40904b51b993 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
index 1f5a6aee4891..252e0daf1933 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
@@ -1,10 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import type { Span } from '@sentry/nuxt';
test('sends a pageload root span with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => {
- return transactionEvent.transaction === '/test-param/:param()';
+ return (
+ transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/test-param/:param()'
+ );
});
await page.goto(`/test-param/1234`);
@@ -39,7 +40,7 @@ test('sends component tracking spans when `trackComponents` is enabled', async (
await page.goto(`/client-error`);
const rootSpan = await transactionPromise;
- const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue ');
+ const errorButtonSpan = rootSpan.spans.find(span => span.description === 'Vue ');
const expected = {
data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.vue.mount' },
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts
index 6f2085e38cd7..e517602f4ade 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-min', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts
index cb86df11fe84..a23b1bb8f35a 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});
expect(serverTxnEvent).toMatchObject({
- transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
+ transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
});
+
+ test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes('GET /api/user/') ?? false;
+ });
+
+ // Navigate to the page which will trigger an API call from the client-side
+ await page.goto(`/test-param/user/${PARAM}`);
+
+ const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
+ clientTxnEventPromise,
+ ssrTxnEventPromise,
+ serverReqTxnEventPromise,
+ ]);
+
+ const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
+
+ expect(clientTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: '/test-param/user/:userId()', // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ }),
+ }),
+ }),
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan).toEqual(
+ expect.objectContaining({
+ description: `GET /api/user/${PARAM}`, // fixme: parametrize
+ parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
+ data: expect.objectContaining({
+ url: `/api/user/${PARAM}`,
+ type: 'fetch',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.browser',
+ 'http.method': 'GET',
+ }),
+ }),
+ );
+
+ expect(ssrTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
+ transaction_info: { source: 'url' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ }),
+ }),
+ }),
+ );
+
+ expect(serverReqTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /api/user/:userId`, // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ parent_span_id: httpClientSpan?.span_id, // http.client span is parent
+ }),
+ }),
+ }),
+ );
+
+ // All 3 transactions and the http.client span should share the same trace_id
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash
deleted file mode 100644
index 0e04d001c968..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash
+++ /dev/null
@@ -1,7 +0,0 @@
-# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules`
-# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects.
-#
-# Things we tried (that did not fix the problem):
-# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project)
-# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules`
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
index d5828016d034..fbac0aa482e9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
@@ -11,12 +11,6 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
sentry: {
autoInjectServerSentry: 'top-level-import',
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
index 0f7411d6a3d6..8ad95ba9fdcc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "nuxt build && bash ./copyIITM.bash",
+ "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
@@ -15,10 +15,14 @@
},
"dependencies": {
"@sentry/nuxt": "latest || *",
+ "@sentry/core": "latest || *",
"nuxt": "^3.14.0"
},
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue
@@ -1,13 +1,13 @@
-
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts
new file mode 100644
index 000000000000..eb268d2f3e13
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/user/[userId].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const userId = getRouterParam(event, 'userId');
+
+ return `UserId Param: ${userId}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts
index 053ec5b6ab67..551a33ee1fc1 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts
@@ -12,7 +12,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const transactionEvent = await transactionEventPromise;
const error = await errorPromise;
@@ -40,7 +40,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const transactionEvent = await transactionEventPromise;
const error = await errorPromise;
@@ -57,7 +57,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts
index 748c7f25354b..cbb0b7fceef7 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3-top-level-import', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts
index 69c4bd2833c4..62f8f9ab51e0 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});
expect(serverTxnEvent).toMatchObject({
- transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
+ transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
});
+
+ test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => {
+ return txnEvent.transaction?.includes('GET /api/user/') ?? false;
+ });
+
+ // Navigate to the page which will trigger an API call from the client-side
+ await page.goto(`/test-param/user/${PARAM}`);
+
+ const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
+ clientTxnEventPromise,
+ ssrTxnEventPromise,
+ serverReqTxnEventPromise,
+ ]);
+
+ const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
+
+ expect(clientTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: '/test-param/user/:userId()', // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ }),
+ }),
+ }),
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan).toEqual(
+ expect.objectContaining({
+ description: `GET /api/user/${PARAM}`, // fixme: parametrize
+ parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
+ data: expect.objectContaining({
+ url: `/api/user/${PARAM}`,
+ type: 'fetch',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.browser',
+ 'http.method': 'GET',
+ }),
+ }),
+ );
+
+ expect(ssrTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
+ transaction_info: { source: 'url' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ }),
+ }),
+ }),
+ );
+
+ expect(serverReqTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /api/user/:userId`, // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ parent_span_id: httpClientSpan?.span_id, // http.client span is parent
+ }),
+ }),
+ }),
+ );
+
+ // All 3 transactions and the http.client span should share the same trace_id
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3/copyIITM.bash
deleted file mode 100644
index 0e04d001c968..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/copyIITM.bash
+++ /dev/null
@@ -1,7 +0,0 @@
-# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules`
-# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects.
-#
-# Things we tried (that did not fix the problem):
-# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project)
-# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules`
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
index 87e046ed39e9..0fcccd560af9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
@@ -11,10 +11,4 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
index a49a958ff1ee..51d9c7713cd4 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "nuxt build && bash ./copyIITM.bash",
+ "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
@@ -17,6 +17,7 @@
},
"dependencies": {
"@sentry/nuxt": "latest || *",
+ "@sentry/core": "latest || *",
"nuxt": "^3.14.0"
},
"devDependencies": {
@@ -30,5 +31,8 @@
"label": "nuxt-3 (canary)"
}
]
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/fetch-server-error.vue
@@ -1,13 +1,13 @@
-
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts
new file mode 100644
index 000000000000..eb268d2f3e13
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/user/[userId].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const userId = getRouterParam(event, 'userId');
+
+ return `UserId Param: ${userId}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts
index d1556d511bf0..b62654b60c47 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.server.test.ts
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
index 5b78e235e564..2785a47802e8 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts
index 523ece4cc085..de19d6d739f9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});
expect(serverTxnEvent).toMatchObject({
- transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
+ transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
});
+
+ test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-3', txnEvent => {
+ return txnEvent.transaction?.includes('GET /api/user/') ?? false;
+ });
+
+ // Navigate to the page which will trigger an API call from the client-side
+ await page.goto(`/test-param/user/${PARAM}`);
+
+ const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
+ clientTxnEventPromise,
+ ssrTxnEventPromise,
+ serverReqTxnEventPromise,
+ ]);
+
+ const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
+
+ expect(clientTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: '/test-param/user/:userId()', // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ }),
+ }),
+ }),
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan).toEqual(
+ expect.objectContaining({
+ description: `GET /api/user/${PARAM}`, // fixme: parametrize
+ parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
+ data: expect.objectContaining({
+ url: `/api/user/${PARAM}`,
+ type: 'fetch',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.browser',
+ 'http.method': 'GET',
+ }),
+ }),
+ );
+
+ expect(ssrTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
+ transaction_info: { source: 'url' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ }),
+ }),
+ }),
+ );
+
+ expect(serverReqTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /api/user/:userId`, // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ parent_span_id: httpClientSpan?.span_id, // http.client span is parent
+ }),
+ }),
+ }),
+ );
+
+ // All 3 transactions and the http.client span should share the same trace_id
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue
index 8cb2a9997e58..0e9aeb34b4fc 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-error.vue
@@ -1,13 +1,13 @@
-
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
index e83392b37b5c..019404aaf460 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue
@@ -1,12 +1,12 @@
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-4/copyIITM.bash
deleted file mode 100644
index 0e04d001c968..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/copyIITM.bash
+++ /dev/null
@@ -1,7 +0,0 @@
-# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules`
-# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects.
-#
-# Things we tried (that did not fix the problem):
-# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project)
-# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules`
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index 568b76483c02..531d19fc90b7 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
- "build": "nuxt build && bash ./copyIITM.bash",
+ "build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
@@ -24,8 +24,8 @@
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
- "overrides": {
- "@vercel/nft": "0.27.4"
+ "volta": {
+ "extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts
new file mode 100644
index 000000000000..eb268d2f3e13
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/user/[userId].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const userId = getRouterParam(event, 'userId');
+
+ return `UserId Param: ${userId}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
index 396870d19925..1d593cb09caf 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
@@ -8,7 +8,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/fetch-server-error`);
- await page.getByText('Fetch Server Data', { exact: true }).click();
+ await page.getByText('Fetch Server API Error', { exact: true }).click();
const error = await errorPromise;
@@ -26,7 +26,7 @@ test.describe('server-side errors', async () => {
});
await page.goto(`/test-param/1234`);
- await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click();
+ await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
const error = await errorPromise;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
index ffe693422dc6..a84bd139a2de 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
index 505a912c95d5..3448851dd299 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts
@@ -10,7 +10,7 @@ test.describe('distributed tracing', () => {
});
const serverTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => {
- return txnEvent.transaction.includes('GET /test-param/');
+ return txnEvent.transaction?.includes('GET /test-param/') ?? false;
});
const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});
expect(serverTxnEvent).toMatchObject({
- transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
+ transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
@@ -66,4 +66,91 @@ test.describe('distributed tracing', () => {
expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id);
expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId);
});
+
+ test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction === '/test-param/user/:userId()';
+ });
+ const ssrTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes('GET /test-param/user') ?? false;
+ });
+ const serverReqTxnEventPromise = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes('GET /api/user/') ?? false;
+ });
+
+ // Navigate to the page which will trigger an API call from the client-side
+ await page.goto(`/test-param/user/${PARAM}`);
+
+ const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([
+ clientTxnEventPromise,
+ ssrTxnEventPromise,
+ serverReqTxnEventPromise,
+ ]);
+
+ const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`);
+
+ expect(clientTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: '/test-param/user/:userId()', // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ }),
+ }),
+ }),
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan).toEqual(
+ expect.objectContaining({
+ description: `GET /api/user/${PARAM}`, // fixme: parametrize
+ parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent
+ data: expect.objectContaining({
+ url: `/api/user/${PARAM}`,
+ type: 'fetch',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.browser',
+ 'http.method': 'GET',
+ }),
+ }),
+ );
+
+ expect(ssrTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /test-param/user/${PARAM}`, // fixme: parametrize (nitro)
+ transaction_info: { source: 'url' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ }),
+ }),
+ }),
+ );
+
+ expect(serverReqTxnEvent).toEqual(
+ expect.objectContaining({
+ type: 'transaction',
+ transaction: `GET /api/user/:userId`, // parametrized route
+ transaction_info: { source: 'route' },
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ parent_span_id: httpClientSpan?.span_id, // http.client span is parent
+ }),
+ }),
+ }),
+ );
+
+ // All 3 transactions and the http.client span should share the same trace_id
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined();
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id);
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id);
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts
index b6146838b0de..c4e6c1c6813c 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts
@@ -1,6 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-import { SpanJSON } from '@sentry/core';
test('Waits for sse streaming when creating spans', async ({ page }) => {
await page.goto('/sse');
@@ -13,8 +12,8 @@ test('Waits for sse streaming when creating spans', async ({ page }) => {
await fetchButton.click();
const rootSpan = await transactionPromise;
- const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON;
- const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON;
+ const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
+ const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!;
expect(sseFetchCall).toBeDefined();
expect(httpGet).toBeDefined();
@@ -45,8 +44,8 @@ test('Waits for sse streaming when sse has been explicitly aborted', async ({ pa
await fetchButton.click();
const rootSpan = await transactionPromise;
- const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON;
- const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON;
+ const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
+ const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!;
expect(sseFetchCall).toBeDefined();
expect(httpGet).toBeDefined();
@@ -85,10 +84,10 @@ test('Aborts when stream takes longer than 5s, by not updating the span duration
await fetchButton.click();
const rootSpan = await transactionPromise;
- const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON;
+ const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
const httpGet = rootSpan.spans?.filter(
span => span.description === 'GET http://localhost:8080/sse-timeout',
- )[0] as SpanJSON;
+ )[0]!;
expect(sseFetchCall).toBeDefined();
expect(httpGet).toBeDefined();
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
index 6f793c0d20eb..ad51a6646b84 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
@@ -10,15 +10,6 @@
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@sentry/react-router": "latest || *",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
index 5e7a2c9782ee..02115558a99f 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
@@ -10,15 +10,6 @@
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@sentry/react-router": "latest || *",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
index 6e83254597de..75c8f4409eb2 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
@@ -26,7 +26,7 @@
"react-router": "^7.1.5"
},
"devDependencies": {
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "~1.50.0",
"@react-router/dev": "^7.5.3",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@tailwindcss/vite": "^4.1.4",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
index 93562c5e6642..e92c52e6d501 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
@@ -26,7 +26,7 @@
"react-router": "^7.1.5"
},
"devDependencies": {
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "~1.50.0",
"@react-router/dev": "^7.5.3",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@tailwindcss/vite": "^4.1.4",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
index a9afbbfcd07b..cdd96f39569e 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
@@ -10,15 +10,6 @@
"@react-router/node": "^7.1.5",
"@react-router/serve": "^7.1.5",
"@sentry/react-router": "latest || *",
- "@sentry-internal/feedback": "latest || *",
- "@sentry-internal/replay-canvas": "latest || *",
- "@sentry-internal/browser-utils": "latest || *",
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
- "@sentry/react": "latest || *",
- "@sentry-internal/replay": "latest || *",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
index b2956b59aa52..e1b2e25b0f47 100644
--- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
+++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
@@ -31,7 +31,7 @@
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.2",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "~1.50.0",
"@remix-run/dev": "^2.15.2",
"@remix-run/eslint-config": "^2.15.2",
"@sentry-internal/test-utils": "link:../../../test-utils",
diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts
index 5153b57a4d90..025e843021b1 100644
--- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts
@@ -1,6 +1,4 @@
import { expect, test } from '@playwright/test';
-import { uuid4 } from '@sentry/core';
-
import { waitForTransaction } from '@sentry-internal/test-utils';
test.describe.configure({ mode: 'serial' });
@@ -20,7 +18,7 @@ test('Sends parameterized transaction name to Sentry', async ({ page }) => {
test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
// We use this to identify the transactions
- const testTag = uuid4();
+ const testTag = crypto.randomUUID();
const httpServerTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
diff --git a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
index 303c04081e24..2685e1f87085 100644
--- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
@@ -16,7 +16,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"solid-devtools": "^0.29.2",
@@ -28,5 +27,8 @@
"@solidjs/router": "^0.13.5",
"solid-js": "^1.8.18",
"@sentry/solid": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json
index 5bdcf53a9188..755841e3379e 100644
--- a/dev-packages/e2e-tests/test-applications/solid/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid/package.json
@@ -16,7 +16,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"solid-devtools": "^0.29.2",
@@ -27,5 +26,8 @@
"dependencies": {
"solid-js": "^1.8.18",
"@sentry/solid": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
index d5ed0f8a8100..3d0903c64925 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
@@ -4,7 +4,7 @@
"scripts": {
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
"dev": "vinxi dev",
- "build": "vinxi build && sh ./post_build.sh",
+ "build": "vinxi build",
"preview": "HOST=localhost PORT=3030 vinxi start",
"test:prod": "TEST_ENV=production playwright test",
"test:build": "pnpm install && pnpm build",
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -31,7 +32,7 @@
"vite-plugin-solid": "^2.11.6",
"vitest": "^1.5.0"
},
- "overrides": {
- "@vercel/nft": "0.27.4"
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh
deleted file mode 100644
index 6ed67c9afb8a..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
-
-# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
-# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
-# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
-# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
-cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts
index c300014bf012..1722970cba76 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts
@@ -4,7 +4,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
-} from '@sentry/core';
+} from '@sentry/solidstart';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
index b834045e0c60..55d8376ab0ca 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
- "build": "vinxi build && sh post_build.sh",
+ "build": "vinxi build",
"preview": "HOST=localhost PORT=3030 vinxi start",
"start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs",
"test:prod": "TEST_ENV=production playwright test",
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -31,7 +32,7 @@
"vite-plugin-solid": "^2.11.6",
"vitest": "^1.5.0"
},
- "overrides": {
- "@vercel/nft": "0.27.4"
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-spa/post_build.sh
deleted file mode 100644
index 6ed67c9afb8a..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/post_build.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
-
-# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
-# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
-# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
-# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
-cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/tests/performance.server.test.ts
index df56fe841f27..9406c4d977bb 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/tests/performance.server.test.ts
@@ -4,7 +4,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
-} from '@sentry/core';
+} from '@sentry/solidstart';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('solidstart-spa', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
index 4ab1fe36b633..03c01f35d3dc 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
@@ -4,7 +4,7 @@
"scripts": {
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
"dev": "vinxi dev",
- "build": "vinxi build && sh ./post_build.sh",
+ "build": "vinxi build",
"preview": "HOST=localhost PORT=3030 vinxi start",
"test:prod": "TEST_ENV=production playwright test",
"test:build": "pnpm install && pnpm build",
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -31,7 +32,7 @@
"vite-plugin-solid": "^2.11.6",
"vitest": "^1.5.0"
},
- "overrides": {
- "@vercel/nft": "0.27.4"
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh
deleted file mode 100644
index 6ed67c9afb8a..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
-
-# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
-# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
-# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
-# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
-cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts
index 8072a7e75181..018e3bdcedc7 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts
@@ -4,7 +4,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
-} from '@sentry/core';
+} from '@sentry/solidstart';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json
index 1746bf0f6c8c..fbd73d805b47 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
- "build": "vinxi build && sh post_build.sh",
+ "build": "vinxi build",
"preview": "HOST=localhost PORT=3030 vinxi start",
"start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs",
"test:prod": "TEST_ENV=production playwright test",
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -31,7 +32,7 @@
"vite-plugin-solid": "^2.11.6",
"vitest": "^1.5.0"
},
- "overrides": {
- "@vercel/nft": "0.27.4"
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh
deleted file mode 100644
index 6ed67c9afb8a..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
-
-# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
-# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
-# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
-# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
-cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
-cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.server.test.ts
index bfd53bbb6bfa..a86f5b5af68c 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.server.test.ts
@@ -4,7 +4,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
-} from '@sentry/core';
+} from '@sentry/solidstart';
test('sends a server action transaction on pageload', async ({ page }) => {
const transactionPromise = waitForTransaction('solidstart', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
index 39d6250f3b75..6ccc0a7a4058 100644
--- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
@@ -15,7 +15,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"svelte": "^5.0.0-next.115",
@@ -26,5 +25,8 @@
},
"dependencies": {
"@sentry/svelte": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
index 784bd0a6a341..22f97f89bedd 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
@@ -21,7 +21,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.21.3",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
@@ -31,5 +30,8 @@
"typescript": "^5.0.0",
"vite": "^5.4.11"
},
- "type": "module"
+ "type": "module",
+ "volta": {
+ "extends": "../../package.json"
+ }
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
index 0f5547728cc5..9c570887e22f 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
@@ -20,7 +20,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "2.8.3",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
@@ -30,5 +29,8 @@
"typescript": "^5.0.0",
"vite": "^5.4.11"
},
- "type": "module"
+ "type": "module",
+ "volta": {
+ "extends": "../../package.json"
+ }
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
index 84ac60c1fb14..f183bcf9827d 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
@@ -20,7 +20,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/kit": "^2.21.3",
@@ -30,5 +29,8 @@
"typescript": "^5.0.0",
"vite": "^5.4.11"
},
+ "volta": {
+ "extends": "../../package.json"
+ },
"type": "module"
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
index 230f6127d55d..688a1d2d5ab4 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
@@ -18,7 +18,7 @@
"@sentry/sveltekit": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.45.3",
+ "@playwright/test": "~1.50.0",
"@sveltejs/adapter-cloudflare": "^5.0.3",
"@sveltejs/kit": "^2.21.3",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -26,6 +26,9 @@
"svelte-check": "^4.1.4",
"typescript": "^5.0.0",
"vite": "^6.1.1",
- "wrangler": "3.105.0"
+ "wrangler": "4.22.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts
rename to dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.js
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/tsconfig.json
similarity index 93%
rename from dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json
rename to dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/tsconfig.json
index 0b2d8865f4ef..08403d6a6841 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "./.svelte-kit/tsconfig.json",
+ "extends": "../.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts
rename to dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.js
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.js
similarity index 57%
rename from dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts
rename to dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.js
index 706faf25f2b5..a217f999cc3a 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.js
@@ -4,4 +4,14 @@ import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sentrySvelteKit({ autoUploadSourceMaps: false }), sveltekit()],
+ build: {
+ rollupOptions: {
+ external: (id) => {
+ // External Node.js native modules
+ if (id === 'fsevents') return true;
+
+ return false;
+ }
+ }
+ }
});
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json
index c968f558673c..164da8cf021d 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json
@@ -23,7 +23,6 @@
"devDependencies": {
"@playwright/test": "~1.50.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
"@tsconfig/node20": "^20.1.2",
"@types/node": "^18.19.1",
"@vitejs/plugin-vue": "^5.0.3",
diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
index c143fd9ab82e..5eca9bced8b9 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
@@ -18,5 +18,8 @@
"terser-webpack-plugin": "^4.2.3",
"html-webpack-plugin": "^4.5.2",
"serve": "^14.2.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/package.json b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
index 00cff9198262..d42556bf5f54 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
@@ -15,5 +15,8 @@
"terser-webpack-plugin": "^5.3.10",
"html-webpack-plugin": "^5.6.0",
"serve": "^14.2.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml
index d96c3cc6905e..1d565dbecda2 100644
--- a/dev-packages/e2e-tests/verdaccio-config/config.yaml
+++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml
@@ -104,6 +104,12 @@ packages:
unpublish: $all
# proxy: npmjs # Don't proxy for E2E tests!
+ '@sentry/node-core':
+ access: $all
+ publish: $all
+ unpublish: $all
+ # proxy: npmjs # Don't proxy for E2E tests!
+
'@sentry/node-native':
access: $all
publish: $all
diff --git a/dev-packages/node-core-integration-tests/.eslintrc.js b/dev-packages/node-core-integration-tests/.eslintrc.js
new file mode 100644
index 000000000000..0598ba3f5ca1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/.eslintrc.js
@@ -0,0 +1,40 @@
+module.exports = {
+ env: {
+ node: true,
+ },
+ extends: ['../../.eslintrc.js'],
+ overrides: [
+ {
+ files: ['utils/**/*.ts', 'src/**/*.ts'],
+ parserOptions: {
+ project: ['tsconfig.json'],
+ sourceType: 'module',
+ },
+ },
+ {
+ files: ['suites/**/*.ts', 'suites/**/*.mjs'],
+ parserOptions: {
+ project: ['tsconfig.test.json'],
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ },
+ globals: {
+ fetch: 'readonly',
+ },
+ rules: {
+ '@typescript-eslint/typedef': 'off',
+ // Explicitly allow ts-ignore with description for Node integration tests
+ // Reason: We run these tests on TS3.8 which doesn't support `@ts-expect-error`
+ '@typescript-eslint/ban-ts-comment': [
+ 'error',
+ {
+ 'ts-ignore': 'allow-with-description',
+ 'ts-expect-error': true,
+ },
+ ],
+ // We rely on having imports after init() is called for OTEL
+ 'import/first': 'off',
+ },
+ },
+ ],
+};
diff --git a/dev-packages/node-core-integration-tests/.gitignore b/dev-packages/node-core-integration-tests/.gitignore
new file mode 100644
index 000000000000..365cb959a94c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/.gitignore
@@ -0,0 +1 @@
+suites/**/tmp_*
diff --git a/dev-packages/node-core-integration-tests/README.md b/dev-packages/node-core-integration-tests/README.md
new file mode 100644
index 000000000000..2e49b2ee4a2e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/README.md
@@ -0,0 +1,66 @@
+# Integration Tests for Sentry Node.JS Core SDK with OpenTelemetry v2 dependencies
+
+## Structure
+
+```
+suites/
+|---- public-api/
+ |---- captureMessage/
+ |---- test.ts [assertions]
+ |---- scenario.ts [Sentry initialization and test subject]
+ |---- customTest/
+ |---- test.ts [assertions]
+ |---- scenario_1.ts [optional extra test scenario]
+ |---- scenario_2.ts [optional extra test scenario]
+ |---- server_with_mongo.ts [optional custom server]
+ |---- server_with_postgres.ts [optional custom server]
+```
+
+The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple
+folders containing test scenarios and assertions.
+
+`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but
+`runServer` also accepts an optional `scenarioPath` argument for non-standard usage.
+
+`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry
+requests, and assertions. Test server, interceptors and assertions are all run on the same Vitest thread.
+
+### Utilities
+
+`utils/` contains helpers and Sentry-specific assertions that can be used in (`test.ts`).
+
+Nock interceptors are internally used to capture envelope requests by `getEnvelopeRequest` and
+`getMultipleEnvelopeRequest` helpers. After capturing required requests, the interceptors are removed. Nock can manually
+be used inside the test cases to intercept requests but should be removed before the test ends, as not to cause
+flakiness.
+
+## Running Tests Locally
+
+Tests can be run locally with:
+
+`yarn test`
+
+To run tests with Vitest's watch mode:
+
+`yarn test:watch`
+
+To filter tests by their title:
+
+`yarn test -t "set different properties of a scope"`
+
+## Debugging Tests
+
+To enable verbose logging during test execution, set the `DEBUG` environment variable:
+
+`DEBUG=1 yarn test`
+
+When `DEBUG` is enabled, the test runner will output:
+
+- Test scenario startup information (path, flags, DSN)
+- Docker Compose output when using `withDockerCompose`
+- Child process stdout and stderr output
+- HTTP requests made during tests
+- Process errors and exceptions
+- Line-by-line output from test scenarios
+
+This is particularly useful when debugging failing tests or understanding the test execution flow.
diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json
new file mode 100644
index 000000000000..d21894f1debc
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@sentry-internal/node-core-integration-tests",
+ "version": "9.35.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "private": true,
+ "main": "build/cjs/index.js",
+ "module": "build/esm/index.js",
+ "types": "build/types/src/index.d.ts",
+ "scripts": {
+ "build": "run-s build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:types": "tsc -p tsconfig.types.json",
+ "clean": "rimraf -g **/node_modules && run-p clean:script",
+ "clean:script": "node scripts/clean.js",
+ "lint": "eslint . --format stylish",
+ "fix": "eslint . --format stylish --fix",
+ "type-check": "tsc",
+ "test": "vitest run",
+ "test:watch": "yarn test --watch"
+ },
+ "dependencies": {
+ "@nestjs/common": "11.0.16",
+ "@nestjs/core": "10.4.6",
+ "@nestjs/platform-express": "10.4.6",
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.2",
+ "@opentelemetry/instrumentation-http": "0.57.2",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.34.0",
+ "@sentry/core": "9.35.0",
+ "@sentry/node-core": "9.35.0",
+ "body-parser": "^1.20.3",
+ "cors": "^2.8.5",
+ "cron": "^3.1.6",
+ "express": "^4.21.1",
+ "http-terminator": "^3.2.0",
+ "nock": "^13.5.5",
+ "node-cron": "^3.0.3",
+ "node-schedule": "^2.1.1",
+ "proxy": "^2.1.1",
+ "reflect-metadata": "0.2.1",
+ "rxjs": "^7.8.1",
+ "winston": "^3.17.0",
+ "yargs": "^16.2.0"
+ },
+ "devDependencies": {
+ "@types/node-cron": "^3.0.11",
+ "@types/node-schedule": "^2.1.7",
+ "globby": "11"
+ },
+ "config": {
+ "mongodbMemoryServer": {
+ "preferGlobalPath": true,
+ "runtimeDownload": false
+ }
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/rollup.npm.config.mjs b/dev-packages/node-core-integration-tests/rollup.npm.config.mjs
new file mode 100644
index 000000000000..84a06f2fb64a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/rollup.npm.config.mjs
@@ -0,0 +1,3 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default makeNPMConfigVariants(makeBaseNPMConfig());
diff --git a/dev-packages/node-core-integration-tests/scripts/clean.js b/dev-packages/node-core-integration-tests/scripts/clean.js
new file mode 100644
index 000000000000..0610e39f92d4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/scripts/clean.js
@@ -0,0 +1,19 @@
+const { execSync } = require('child_process');
+const globby = require('globby');
+const { dirname, join } = require('path');
+
+const cwd = join(__dirname, '..');
+const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path)));
+
+// eslint-disable-next-line no-console
+console.log('Cleaning up docker containers and volumes...');
+
+for (const path of paths) {
+ try {
+ // eslint-disable-next-line no-console
+ console.log(`docker compose down @ ${path}`);
+ execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path });
+ } catch (_) {
+ //
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js b/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js
new file mode 100644
index 000000000000..d759179f8e06
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js
@@ -0,0 +1,39 @@
+/* eslint-disable no-console */
+const { execSync } = require('child_process');
+const { join } = require('path');
+const { readFileSync, writeFileSync } = require('fs');
+
+const cwd = join(__dirname, '../../..');
+
+// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8.
+// We'll pin to the last version of those types that are compatible.
+console.log('Pinning Express types to old versions...');
+
+const packageJsonPath = join(cwd, 'package.json');
+const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
+
+if (!packageJson.resolutions) packageJson.resolutions = {};
+packageJson.resolutions['@types/express'] = '4.17.13';
+packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30';
+
+writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
+
+const tsVersion = '3.8';
+
+console.log(`Installing typescript@${tsVersion}, and @types/node@14...`);
+
+execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, {
+ stdio: 'inherit',
+ cwd,
+});
+
+console.log('Removing unsupported tsconfig options...');
+
+const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json');
+
+const tsConfig = require(baseTscConfigPath);
+
+// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :(
+delete tsConfig.compilerOptions.noUncheckedIndexedAccess;
+
+writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2));
diff --git a/dev-packages/node-core-integration-tests/src/index.ts b/dev-packages/node-core-integration-tests/src/index.ts
new file mode 100644
index 000000000000..ed6a150bd8d6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/src/index.ts
@@ -0,0 +1,57 @@
+import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core';
+import type { Express } from 'express';
+import type { AddressInfo } from 'net';
+
+/**
+ * Debug logging transport
+ */
+export function loggingTransport(_options: BaseTransportOptions): Transport {
+ return {
+ send(request: Envelope): Promise {
+ // eslint-disable-next-line no-console
+ console.log(JSON.stringify(request));
+ return Promise.resolve({ statusCode: 200 });
+ },
+ flush(): PromiseLike {
+ return new Promise(resolve => setTimeout(() => resolve(true), 1000));
+ },
+ };
+}
+
+/**
+ * Starts an express server and sends the port to the runner
+ * @param app Express app
+ * @param port Port to start the app on. USE WITH CAUTION! By default a random port will be chosen.
+ * Setting this port to something specific is useful for local debugging but dangerous for
+ * CI/CD environments where port collisions can cause flakes!
+ */
+export function startExpressServerAndSendPortToRunner(
+ app: Pick,
+ port: number | undefined = undefined,
+): void {
+ const server = app.listen(port || 0, () => {
+ const address = server.address() as AddressInfo;
+
+ // @ts-expect-error If we write the port to the app we can read it within route handlers in tests
+ app.port = port || address.port;
+
+ // eslint-disable-next-line no-console
+ console.log(`{"port":${port || address.port}}`);
+ });
+}
+
+/**
+ * Sends the port to the runner
+ */
+export function sendPortToRunner(port: number): void {
+ // eslint-disable-next-line no-console
+ console.log(`{"port":${port}}`);
+}
+
+/**
+ * Can be used to get the port of a running app, so requests can be sent to a server from within the server.
+ */
+export function getPortAppIsRunningOn(app: Express): number | undefined {
+ // @ts-expect-error It's not defined in the types but we'd like to read it.
+ return app.port;
+}
diff --git a/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs
new file mode 100644
index 000000000000..2cf1cff1ea32
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/node-core';
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import * as path from 'path';
+import * as url from 'url';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs
new file mode 100644
index 000000000000..9c8b17b590bc
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/node-core';
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
+
+setTimeout(() => {
+ longWork();
+}, 4000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic-session.js b/dev-packages/node-core-integration-tests/suites/anr/basic-session.js
new file mode 100644
index 000000000000..541c5ee25e36
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/basic-session.js
@@ -0,0 +1,32 @@
+const crypto = require('crypto');
+const assert = require('assert');
+
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic.js b/dev-packages/node-core-integration-tests/suites/anr/basic.js
new file mode 100644
index 000000000000..738810f2fa2f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/basic.js
@@ -0,0 +1,34 @@
+const crypto = require('crypto');
+const assert = require('assert');
+
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic.mjs b/dev-packages/node-core-integration-tests/suites/anr/basic.mjs
new file mode 100644
index 000000000000..5902394e8109
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/basic.mjs
@@ -0,0 +1,38 @@
+import * as Sentry from '@sentry/node-core';
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
+
+// Ensure we only send one event even with multiple blocking events
+setTimeout(() => {
+ longWork();
+}, 4000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/forked.js b/dev-packages/node-core-integration-tests/suites/anr/forked.js
new file mode 100644
index 000000000000..be4848abee5c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/forked.js
@@ -0,0 +1,33 @@
+const crypto = require('crypto');
+const assert = require('assert');
+
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ debug: true,
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/forker.js b/dev-packages/node-core-integration-tests/suites/anr/forker.js
new file mode 100644
index 000000000000..c1ac5e1ccd1c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/forker.js
@@ -0,0 +1,7 @@
+const { fork } = require('child_process');
+const { join } = require('path');
+
+const child = fork(join(__dirname, 'forked.js'), { stdio: 'inherit' });
+child.on('exit', () => {
+ process.exit();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs b/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs
new file mode 100644
index 000000000000..db9217ec34b2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs
@@ -0,0 +1,32 @@
+import * as Sentry from '@sentry/node-core';
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ // This loop will run almost indefinitely
+ for (let i = 0; i < 2000000000; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs
new file mode 100644
index 000000000000..37e804d01b71
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs
@@ -0,0 +1,53 @@
+import * as Sentry from '@sentry/node-core';
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })],
+});
+
+setupOtel(client);
+
+async function longWork() {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+function neverResolve() {
+ return new Promise(() => {
+ //
+ });
+}
+
+const fns = [
+ neverResolve,
+ neverResolve,
+ neverResolve,
+ neverResolve,
+ neverResolve,
+ longWork, // [5]
+ neverResolve,
+ neverResolve,
+ neverResolve,
+ neverResolve,
+];
+
+for (let id = 0; id < 10; id++) {
+ Sentry.withIsolationScope(async () => {
+ Sentry.setUser({ id });
+
+ await fns[id]();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js b/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js
new file mode 100644
index 000000000000..523d7d7fce6b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js
@@ -0,0 +1,20 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+function configureSentry() {
+ const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ debug: true,
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true })],
+ });
+ setupOtel(client);
+}
+
+async function main() {
+ configureSentry();
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ process.exit(0);
+}
+
+main();
diff --git a/dev-packages/node-core-integration-tests/suites/anr/should-exit.js b/dev-packages/node-core-integration-tests/suites/anr/should-exit.js
new file mode 100644
index 000000000000..ba8d24c347d5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/should-exit.js
@@ -0,0 +1,19 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+function configureSentry() {
+ const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ debug: true,
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true })],
+ });
+ setupOtel(client);
+}
+
+async function main() {
+ configureSentry();
+ await new Promise(resolve => setTimeout(resolve, 1000));
+}
+
+main();
diff --git a/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js
new file mode 100644
index 000000000000..c377c8716814
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js
@@ -0,0 +1,55 @@
+const crypto = require('crypto');
+const assert = require('assert');
+
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+setTimeout(() => {
+ process.exit();
+}, 20000);
+
+const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 });
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ debug: true,
+ integrations: [anr],
+});
+
+setupOtel(client);
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWorkIgnored() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ anr.stopWorker();
+
+ setTimeout(() => {
+ longWorkIgnored();
+
+ setTimeout(() => {
+ anr.startWorker();
+
+ setTimeout(() => {
+ longWork();
+ });
+ }, 2000);
+ }, 2000);
+}, 2000);
diff --git a/dev-packages/node-core-integration-tests/suites/anr/test.ts b/dev-packages/node-core-integration-tests/suites/anr/test.ts
new file mode 100644
index 000000000000..08b6a6571e17
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/anr/test.ts
@@ -0,0 +1,253 @@
+import type { Event } from '@sentry/core';
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+const ANR_EVENT = {
+ // Ensure we have context
+ contexts: {
+ trace: {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ device: {
+ arch: expect.any(String),
+ },
+ app: {
+ app_start_time: expect.any(String),
+ },
+ os: {
+ name: expect.any(String),
+ },
+ culture: {
+ timezone: expect.any(String),
+ },
+ },
+ // and an exception that is our ANR
+ exception: {
+ values: [
+ {
+ type: 'ApplicationNotResponding',
+ value: 'Application Not Responding for at least 100 ms',
+ mechanism: { type: 'ANR' },
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ colno: expect.any(Number),
+ lineno: expect.any(Number),
+ filename: expect.any(String),
+ function: '?',
+ in_app: true,
+ }),
+ expect.objectContaining({
+ colno: expect.any(Number),
+ lineno: expect.any(Number),
+ filename: expect.any(String),
+ function: 'longWork',
+ in_app: true,
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+};
+
+const ANR_EVENT_WITHOUT_STACKTRACE = {
+ // Ensure we have context
+ contexts: {
+ device: {
+ arch: expect.any(String),
+ },
+ app: {
+ app_start_time: expect.any(String),
+ },
+ os: {
+ name: expect.any(String),
+ },
+ culture: {
+ timezone: expect.any(String),
+ },
+ },
+ // and an exception that is our ANR
+ exception: {
+ values: [
+ {
+ type: 'ApplicationNotResponding',
+ value: 'Application Not Responding for at least 100 ms',
+ mechanism: { type: 'ANR' },
+ stacktrace: {},
+ },
+ ],
+ },
+};
+
+const ANR_EVENT_WITH_SCOPE = {
+ ...ANR_EVENT,
+ user: {
+ email: 'person@home.com',
+ },
+ breadcrumbs: expect.arrayContaining([
+ {
+ timestamp: expect.any(Number),
+ message: 'important message!',
+ },
+ ]),
+};
+
+const ANR_EVENT_WITH_DEBUG_META: Event = {
+ ...ANR_EVENT_WITH_SCOPE,
+ debug_meta: {
+ images: [
+ {
+ type: 'sourcemap',
+ debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
+ code_file: expect.stringContaining('basic'),
+ },
+ ],
+ },
+};
+
+describe('should report ANR when event loop blocked', { timeout: 90_000 }, () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('CJS', async () => {
+ await createRunner(__dirname, 'basic.js')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .start()
+ .completed();
+ });
+
+ test('ESM', async () => {
+ await createRunner(__dirname, 'basic.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .start()
+ .completed();
+ });
+
+ test('Custom appRootPath', async () => {
+ const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = {
+ ...ANR_EVENT_WITH_SCOPE,
+ debug_meta: {
+ images: [
+ {
+ type: 'sourcemap',
+ debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
+ code_file: 'app:///app-path.mjs',
+ },
+ ],
+ },
+ };
+
+ await createRunner(__dirname, 'app-path.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META })
+ .start()
+ .completed();
+ });
+
+ test('multiple events via maxAnrEvents', async () => {
+ await createRunner(__dirname, 'basic-multiple.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .start()
+ .completed();
+ });
+
+ test('blocked indefinitely', async () => {
+ await createRunner(__dirname, 'indefinite.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT })
+ .start()
+ .completed();
+ });
+
+ test("With --inspect the debugger isn't used", async () => {
+ await createRunner(__dirname, 'basic.mjs')
+ .withMockSentryServer()
+ .withFlags('--inspect')
+ .expect({ event: ANR_EVENT_WITHOUT_STACKTRACE })
+ .start()
+ .completed();
+ });
+
+ test('should exit', async () => {
+ const runner = createRunner(__dirname, 'should-exit.js').start();
+
+ await new Promise(resolve => setTimeout(resolve, 5_000));
+
+ expect(runner.childHasExited()).toBe(true);
+ });
+
+ test('should exit forced', async () => {
+ const runner = createRunner(__dirname, 'should-exit-forced.js').start();
+
+ await new Promise(resolve => setTimeout(resolve, 5_000));
+
+ expect(runner.childHasExited()).toBe(true);
+ });
+
+ test('With session', async () => {
+ await createRunner(__dirname, 'basic-session.js')
+ .withMockSentryServer()
+ .unignore('session')
+ .expect({
+ session: {
+ status: 'abnormal',
+ abnormal_mechanism: 'anr_foreground',
+ attrs: {
+ release: '1.0.0',
+ },
+ },
+ })
+ .expect({ event: ANR_EVENT_WITH_SCOPE })
+ .start()
+ .completed();
+ });
+
+ test('from forked process', async () => {
+ await createRunner(__dirname, 'forker.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start().completed();
+ });
+
+ test('worker can be stopped and restarted', async () => {
+ await createRunner(__dirname, 'stop-and-start.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start().completed();
+ });
+
+ const EXPECTED_ISOLATED_EVENT = {
+ user: {
+ id: 5,
+ },
+ exception: {
+ values: [
+ {
+ type: 'ApplicationNotResponding',
+ value: 'Application Not Responding for at least 100 ms',
+ mechanism: { type: 'ANR' },
+ stacktrace: {
+ frames: expect.arrayContaining([
+ {
+ colno: expect.any(Number),
+ lineno: expect.any(Number),
+ filename: expect.stringMatching(/isolated.mjs$/),
+ function: 'longWork',
+ in_app: true,
+ },
+ ]),
+ },
+ },
+ ],
+ },
+ };
+
+ test('fetches correct isolated scope', async () => {
+ await createRunner(__dirname, 'isolated.mjs')
+ .withMockSentryServer()
+ .expect({ event: EXPECTED_ISOLATED_EVENT })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs
new file mode 100644
index 000000000000..7169b4824532
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs
@@ -0,0 +1,33 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { spawn } from 'child_process';
+import { join } from 'path';
+import { Worker } from 'worker_threads';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+const __dirname = new URL('.', import.meta.url).pathname;
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+(async () => {
+ await new Promise(resolve => {
+ const child = spawn('sleep', ['a']);
+ child.on('error', resolve);
+ child.on('exit', resolve);
+ });
+
+ await new Promise(resolve => {
+ const worker = new Worker(join(__dirname, 'worker.mjs'));
+ worker.on('error', resolve);
+ worker.on('exit', resolve);
+ });
+
+ throw new Error('This is a test error');
+})();
diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts
new file mode 100644
index 000000000000..a3ae49da4808
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts
@@ -0,0 +1,50 @@
+import type { Event } from '@sentry/core';
+import { afterAll, expect, test } from 'vitest';
+import { conditionalTest } from '../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+const EVENT = {
+ // and an exception that is our ANR
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'This is a test error',
+ },
+ ],
+ },
+ breadcrumbs: [
+ {
+ timestamp: expect.any(Number),
+ category: 'child_process',
+ message: "Child process exited with code '1'",
+ level: 'warning',
+ data: {
+ spawnfile: 'sleep',
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ category: 'worker_thread',
+ message: "Worker thread errored with 'Worker error'",
+ level: 'error',
+ data: {
+ threadId: expect.any(Number),
+ },
+ },
+ ],
+};
+
+conditionalTest({ min: 20 })('should capture process and thread breadcrumbs', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('ESM', async () => {
+ await createRunner(__dirname, 'app.mjs')
+ .withMockSentryServer()
+ .expect({ event: EVENT as Event })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs
new file mode 100644
index 000000000000..049063bd26b4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs
@@ -0,0 +1 @@
+throw new Error('Worker error');
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/child.js b/dev-packages/node-core-integration-tests/suites/child-process/child.js
new file mode 100644
index 000000000000..cb1937007297
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/child.js
@@ -0,0 +1,3 @@
+setTimeout(() => {
+ throw new Error('Test error');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/child.mjs b/dev-packages/node-core-integration-tests/suites/child-process/child.mjs
new file mode 100644
index 000000000000..cb1937007297
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/child.mjs
@@ -0,0 +1,3 @@
+setTimeout(() => {
+ throw new Error('Test error');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/fork.js b/dev-packages/node-core-integration-tests/suites/child-process/fork.js
new file mode 100644
index 000000000000..0cadad736d87
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/fork.js
@@ -0,0 +1,20 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const path = require('path');
+const { fork } = require('child_process');
+
+const client = Sentry.init({
+ debug: true,
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+fork(path.join(__dirname, 'child.mjs'));
+
+setTimeout(() => {
+ throw new Error('Exiting main process');
+}, 3000);
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs
new file mode 100644
index 000000000000..7aab2c78e6b6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { fork } from 'child_process';
+import * as path from 'path';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+const __dirname = new URL('.', import.meta.url).pathname;
+
+const client = Sentry.init({
+ debug: true,
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+fork(path.join(__dirname, 'child.mjs'));
+
+setTimeout(() => {
+ throw new Error('Exiting main process');
+}, 3000);
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/test.ts b/dev-packages/node-core-integration-tests/suites/child-process/test.ts
new file mode 100644
index 000000000000..798cd48d86d0
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/test.ts
@@ -0,0 +1,66 @@
+import type { Event } from '@sentry/core';
+import { afterAll, describe, expect, test } from 'vitest';
+import { conditionalTest } from '../../utils';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+const WORKER_EVENT: Event = {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Test error',
+ mechanism: {
+ type: 'instrument',
+ handled: false,
+ data: {
+ threadId: expect.any(String),
+ },
+ },
+ },
+ ],
+ },
+};
+
+const CHILD_EVENT: Event = {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Exiting main process',
+ },
+ ],
+ },
+ breadcrumbs: [
+ {
+ category: 'child_process',
+ message: "Child process exited with code '1'",
+ level: 'warning',
+ },
+ ],
+};
+
+describe('should capture child process events', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ conditionalTest({ min: 20 })('worker', () => {
+ test('ESM', async () => {
+ await createRunner(__dirname, 'worker.mjs').expect({ event: WORKER_EVENT }).start().completed();
+ });
+
+ test('CJS', async () => {
+ await createRunner(__dirname, 'worker.js').expect({ event: WORKER_EVENT }).start().completed();
+ });
+ });
+
+ conditionalTest({ min: 20 })('fork', () => {
+ test('ESM', async () => {
+ await createRunner(__dirname, 'fork.mjs').expect({ event: CHILD_EVENT }).start().completed();
+ });
+
+ test('CJS', async () => {
+ await createRunner(__dirname, 'fork.js').expect({ event: CHILD_EVENT }).start().completed();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/worker.js b/dev-packages/node-core-integration-tests/suites/child-process/worker.js
new file mode 100644
index 000000000000..34818297cff9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/worker.js
@@ -0,0 +1,20 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../utils/setupOtel.js');
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const path = require('path');
+const { Worker } = require('worker_threads');
+
+const client = Sentry.init({
+ debug: true,
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+new Worker(path.join(__dirname, 'child.js'));
+
+setTimeout(() => {
+ process.exit();
+}, 3000);
diff --git a/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs
new file mode 100644
index 000000000000..1c2037ba79e0
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as path from 'path';
+import { Worker } from 'worker_threads';
+import { setupOtel } from '../../utils/setupOtel.js';
+
+const __dirname = new URL('.', import.meta.url).pathname;
+
+const client = Sentry.init({
+ debug: true,
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+new Worker(path.join(__dirname, 'child.mjs'));
+
+setTimeout(() => {
+ process.exit();
+}, 3000);
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts
new file mode 100644
index 000000000000..fab6c8f0afdf
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts
@@ -0,0 +1,26 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+(async () => {
+ const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ beforeSend(event) {
+ return !event.type ? null : event;
+ },
+ });
+
+ setupOtel(client);
+
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+
+ await Sentry.flush();
+
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.flush();
+})();
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts
new file mode 100644
index 000000000000..73a40fd88d17
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts
@@ -0,0 +1,35 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should record client report for beforeSend', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .unignore('client_report')
+ .expect({
+ client_report: {
+ discarded_events: [
+ {
+ category: 'error',
+ quantity: 1,
+ reason: 'before_send',
+ },
+ ],
+ },
+ })
+ .expect({
+ client_report: {
+ discarded_events: [
+ {
+ category: 'error',
+ quantity: 2,
+ reason: 'before_send',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts
new file mode 100644
index 000000000000..3e50b33f0626
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts
@@ -0,0 +1,27 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+(async () => {
+ const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ });
+
+ setupOtel(client);
+
+ Sentry.addEventProcessor(event => {
+ return !event.type ? null : event;
+ });
+
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+
+ await Sentry.flush();
+
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+ Sentry.captureException(new Error('this should get dropped by the event processor'));
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.flush();
+})();
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts
new file mode 100644
index 000000000000..4e236e375c40
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts
@@ -0,0 +1,35 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should record client report for event processors', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .unignore('client_report')
+ .expect({
+ client_report: {
+ discarded_events: [
+ {
+ category: 'error',
+ quantity: 1,
+ reason: 'event_processor',
+ },
+ ],
+ },
+ })
+ .expect({
+ client_report: {
+ discarded_events: [
+ {
+ category: 'error',
+ quantity: 2,
+ reason: 'event_processor',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts
new file mode 100644
index 000000000000..3a7a1dd32181
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ clientReportFlushInterval: 5000,
+ beforeSend(event) {
+ return !event.type ? null : event;
+ },
+});
+
+setupOtel(client);
+
+Sentry.captureException(new Error('this should get dropped by before send'));
diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts
new file mode 100644
index 000000000000..69775219b784
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts
@@ -0,0 +1,24 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should flush client reports automatically after the timeout interval', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .unignore('client_report')
+ .expect({
+ client_report: {
+ discarded_events: [
+ {
+ category: 'error',
+ quantity: 1,
+ reason: 'before_send',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs
new file mode 100644
index 000000000000..0aade82dbf23
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs
new file mode 100644
index 000000000000..41618eb3fee5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs
@@ -0,0 +1,12 @@
+const Sentry = require('@sentry/node');
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+Sentry.captureException(new Error('Test Error'));
+
+// some more post context
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs
new file mode 100644
index 000000000000..e3139401e5e2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs
@@ -0,0 +1,5 @@
+import * as Sentry from '@sentry/node-core';
+
+Sentry.captureException(new Error('Test Error'));
+
+// some more post context
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts
new file mode 100644
index 000000000000..c765e4b541f2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts
@@ -0,0 +1,85 @@
+import { join } from 'path';
+import { describe, expect, test } from 'vitest';
+import { createRunner } from '../../../utils/runner';
+
+describe('ContextLines integration in ESM', () => {
+ test('reads encoded context lines from filenames with spaces', async () => {
+ expect.assertions(1);
+ const instrumentPath = join(__dirname, 'instrument.mjs');
+
+ await createRunner(__dirname, 'scenario with space.mjs')
+ .withInstrument(instrumentPath)
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ value: 'Test Error',
+ stacktrace: {
+ frames: expect.arrayContaining([
+ {
+ filename: expect.stringMatching(/\/scenario with space.mjs$/),
+ context_line: "Sentry.captureException(new Error('Test Error'));",
+ pre_context: ["import * as Sentry from '@sentry/node-core';", ''],
+ post_context: ['', '// some more post context'],
+ colno: 25,
+ lineno: 3,
+ function: '?',
+ in_app: true,
+ module: 'scenario with space',
+ },
+ ]),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+});
+
+describe('ContextLines integration in CJS', () => {
+ test('reads context lines from filenames with spaces', async () => {
+ expect.assertions(1);
+
+ await createRunner(__dirname, 'scenario with space.cjs')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ value: 'Test Error',
+ stacktrace: {
+ frames: expect.arrayContaining([
+ {
+ filename: expect.stringMatching(/\/scenario with space.cjs$/),
+ context_line: "Sentry.captureException(new Error('Test Error'));",
+ pre_context: [
+ '',
+ 'Sentry.init({',
+ " dsn: 'https://public@dsn.ingest.sentry.io/1337',",
+ " release: '1.0',",
+ ' transport: loggingTransport,',
+ '});',
+ '',
+ ],
+ post_context: ['', '// some more post context'],
+ colno: 25,
+ lineno: 10,
+ function: 'Object.?',
+ in_app: true,
+ module: 'scenario with space',
+ },
+ ]),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts
new file mode 100644
index 000000000000..bd76720a6285
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts
@@ -0,0 +1,5 @@
+import * as Sentry from '@sentry/node-core';
+
+export function captureException(i: number): void {
+ Sentry.captureException(new Error(`error in loop ${i}`));
+}
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts
new file mode 100644
index 000000000000..c48fae3e2e2e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts
@@ -0,0 +1,7 @@
+import { captureException } from './nested-file';
+
+export function runSentry(): void {
+ for (let i = 0; i < 10; i++) {
+ captureException(i);
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts
new file mode 100644
index 000000000000..cf36c8a2f613
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts
@@ -0,0 +1,32 @@
+import { execSync } from 'node:child_process';
+import * as path from 'node:path';
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+import { runSentry } from './other-file';
+
+runSentry();
+
+const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' });
+const lsofTable = lsofOutput.split('\n');
+const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, '');
+const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath));
+
+// There should only be a single entry with the main path, otherwise we are leaking file handles from the
+// context lines integration.
+if (numberOfLsofEntriesWithMainPath.length > 1) {
+ // eslint-disable-next-line no-console
+ console.error('Leaked file handles detected');
+ // eslint-disable-next-line no-console
+ console.error(lsofTable);
+ process.exit(1);
+}
diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts
new file mode 100644
index 000000000000..1a5170c05fe7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, describe, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+describe('ContextLines integration in CJS', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ // Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892
+ test('does not leak open file handles', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expectN(10, {
+ event: {},
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts
new file mode 100644
index 000000000000..4ddd9f115189
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts
@@ -0,0 +1,33 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { CronJob } from 'cron';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job');
+
+let closeNext = false;
+
+const cron = new CronJobWithCheckIn('* * * * * *', () => {
+ if (closeNext) {
+ cron.stop();
+ throw new Error('Error in cron job');
+ }
+
+ // eslint-disable-next-line no-console
+ console.log('You will see this message every second');
+ closeNext = true;
+});
+
+cron.start();
+
+setTimeout(() => {
+ process.exit();
+}, 5000);
diff --git a/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts b/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts
new file mode 100644
index 000000000000..8b9fdfd5c593
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts
@@ -0,0 +1,77 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('cron instrumentation', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'ok',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'error',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Error in cron job' }] },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts
new file mode 100644
index 000000000000..818bf7b63871
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts
@@ -0,0 +1,39 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as cron from 'node-cron';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron);
+
+let closeNext = false;
+
+const task = cronWithCheckIn.schedule(
+ '* * * * * *',
+ () => {
+ if (closeNext) {
+ // https://github.com/node-cron/node-cron/issues/317
+ setImmediate(() => {
+ task.stop();
+ });
+
+ throw new Error('Error in cron job');
+ }
+
+ // eslint-disable-next-line no-console
+ console.log('You will see this message every second');
+ closeNext = true;
+ },
+ { name: 'my-cron-job' },
+);
+
+setTimeout(() => {
+ process.exit();
+}, 5000);
diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts
new file mode 100644
index 000000000000..1c5fa515e208
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts
@@ -0,0 +1,77 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('node-cron instrumentation', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'ok',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'error',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Error in cron job' }] },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts
new file mode 100644
index 000000000000..65f4fc9ab49a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts
@@ -0,0 +1,31 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as schedule from 'node-schedule';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule);
+
+let closeNext = false;
+
+const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * * *', () => {
+ if (closeNext) {
+ job.cancel();
+ throw new Error('Error in cron job');
+ }
+
+ // eslint-disable-next-line no-console
+ console.log('You will see this message every second');
+ closeNext = true;
+});
+
+setTimeout(() => {
+ process.exit();
+}, 5000);
diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts
new file mode 100644
index 000000000000..a2019253203f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts
@@ -0,0 +1,77 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('node-schedule instrumentation', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'ok',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'in_progress',
+ release: '1.0',
+ monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } },
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ check_in: {
+ check_in_id: expect.any(String),
+ monitor_slug: 'my-cron-job',
+ status: 'error',
+ release: '1.0',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ },
+ },
+ })
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Error in cron job' }] },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs
new file mode 100644
index 000000000000..180eedbab9a5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as iitm from 'import-in-the-middle';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+new iitm.Hook((_, name) => {
+ if (name !== 'http') {
+ throw new Error(`'http' should be the only hooked modules but we just hooked '${name}'`);
+ }
+});
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+(async () => {
+ await import('./sub-module.mjs');
+ await import('http');
+ await import('os');
+})();
diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs
new file mode 100644
index 000000000000..9940c57857eb
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs
@@ -0,0 +1,2 @@
+// eslint-disable-next-line no-console
+console.assert(true);
diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts
new file mode 100644
index 000000000000..99dea0e9193a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts
@@ -0,0 +1,15 @@
+import { spawnSync } from 'child_process';
+import { join } from 'path';
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+describe('import-in-the-middle', () => {
+ test('should only instrument modules that we have instrumentation for', () => {
+ const result = spawnSync('node', [join(__dirname, 'app.mjs')], { encoding: 'utf-8' });
+ expect(result.stderr).not.toMatch('should be the only hooked modules but we just hooked');
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs
new file mode 100644
index 000000000000..ab1566c7b139
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ integrations: [Sentry.modulesIntegration()],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts
new file mode 100644
index 000000000000..94995aedb91f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts
@@ -0,0 +1,12 @@
+import { afterAll, describe, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+describe('modulesIntegration', () => {
+ test('does not crash ESM setups', async () => {
+ await createRunner(__dirname, 'app.mjs').ensureNoErrorOutput().start().completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js
new file mode 100644
index 000000000000..fc6c1aaa75f4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js
@@ -0,0 +1,22 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
+const express = require('express');
+
+const app = express();
+
+app.get('/test/success', (req, res) => {
+ res.send({ response: 'response 3' });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs
new file mode 100644
index 000000000000..b02456a34f4e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
+import express from 'express';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const app = express();
+
+app.get('/test/success', (req, res) => {
+ res.send({ response: 'response 3' });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts
new file mode 100644
index 000000000000..18eebdab6e85
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts
@@ -0,0 +1,42 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+const esmWarning = `[Sentry] You are using Node.js v${process.versions.node} in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.`;
+
+test("warns if using ESM on Node.js versions that don't support `register()`", async () => {
+ const nodeMajorVersion = Number(process.versions.node.split('.')[0]);
+ if (nodeMajorVersion >= 18) {
+ return;
+ }
+
+ const runner = createRunner(__dirname, 'server.mjs').ignore('event').start();
+
+ await runner.makeRequest('get', '/test/success');
+
+ expect(runner.getLogs()).toContain(esmWarning);
+});
+
+test('does not warn if using ESM on Node.js versions that support `register()`', async () => {
+ const nodeMajorVersion = Number(process.versions.node.split('.')[0]);
+ if (nodeMajorVersion < 18) {
+ return;
+ }
+
+ const runner = createRunner(__dirname, 'server.mjs').ignore('event').start();
+
+ await runner.makeRequest('get', '/test/success');
+
+ expect(runner.getLogs()).not.toContain(esmWarning);
+});
+
+test('does not warn if using CJS', async () => {
+ const runner = createRunner(__dirname, 'server.js').ignore('event').start();
+
+ await runner.makeRequest('get', '/test/success');
+
+ expect(runner.getLogs()).not.toContain(esmWarning);
+});
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts
new file mode 100644
index 000000000000..5a89f59e17b4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts
@@ -0,0 +1,22 @@
+import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.featureFlagsIntegration()],
+});
+
+setupOtel(client);
+
+const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags');
+for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
+ flagsIntegration?.addFeatureFlag(`feat${i}`, false);
+}
+flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction
+flagsIntegration?.addFeatureFlag('feat3', true); // update
+
+throw new Error('Test error');
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts
new file mode 100644
index 000000000000..74ff1c125b45
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts
@@ -0,0 +1,31 @@
+import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('Flags captured on error with eviction, update, and no async tasks', async () => {
+ // Based on scenario.ts.
+ const expectedFlags = [{ flag: 'feat2', result: false }];
+ for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
+ expectedFlags.push({ flag: 'feat3', result: true });
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Test error' }] },
+ contexts: {
+ flags: {
+ values: expectedFlags,
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts
new file mode 100644
index 000000000000..f45c089cdca2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts
@@ -0,0 +1,33 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../../utils/setupOtel';
+
+const flagsIntegration = Sentry.featureFlagsIntegration();
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [flagsIntegration],
+});
+
+setupOtel(client);
+
+async function run(): Promise {
+ flagsIntegration.addFeatureFlag('shared', true);
+
+ Sentry.withScope(() => {
+ flagsIntegration.addFeatureFlag('forked', true);
+ flagsIntegration.addFeatureFlag('shared', false);
+ Sentry.captureException(new Error('Error in forked scope'));
+ });
+
+ await Sentry.flush();
+
+ flagsIntegration.addFeatureFlag('main', true);
+
+ throw new Error('Error in main scope');
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts
new file mode 100644
index 000000000000..947b299923e7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts
@@ -0,0 +1,38 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('Flags captured on error are isolated by current scope', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] },
+ contexts: {
+ flags: {
+ values: [
+ { flag: 'forked', result: true },
+ { flag: 'shared', result: false },
+ ],
+ },
+ },
+ },
+ })
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Error in main scope' }] },
+ contexts: {
+ flags: {
+ values: [
+ { flag: 'shared', result: true },
+ { flag: 'main', result: true },
+ ],
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts
new file mode 100644
index 000000000000..77b97396ab5a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts
@@ -0,0 +1,28 @@
+import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.featureFlagsIntegration()],
+});
+
+setupOtel(client);
+
+const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags');
+
+Sentry.startSpan({ name: 'test-root-span' }, () => {
+ Sentry.startSpan({ name: 'test-span' }, () => {
+ Sentry.startSpan({ name: 'test-nested-span' }, () => {
+ for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
+ flagsIntegration?.addFeatureFlag(`feat${i}`, false);
+ }
+ flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag
+ flagsIntegration?.addFeatureFlag('feat3', true); // update
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts
new file mode 100644
index 000000000000..4a417a3c3959
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts
@@ -0,0 +1,33 @@
+import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('Flags captured on span attributes with max limit', async () => {
+ // Based on scenario.ts.
+ const expectedFlags: Record = {};
+ for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
+ expectedFlags[`flag.evaluation.feat${i}`] = i === 3;
+ }
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ spans: [
+ expect.objectContaining({
+ description: 'test-span',
+ data: expect.objectContaining({}),
+ }),
+ expect.objectContaining({
+ description: 'test-nested-span',
+ data: expect.objectContaining(expectedFlags),
+ }),
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/no-code/app.js b/dev-packages/node-core-integration-tests/suites/no-code/app.js
new file mode 100644
index 000000000000..cb1937007297
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/no-code/app.js
@@ -0,0 +1,3 @@
+setTimeout(() => {
+ throw new Error('Test error');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/no-code/app.mjs b/dev-packages/node-core-integration-tests/suites/no-code/app.mjs
new file mode 100644
index 000000000000..cb1937007297
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/no-code/app.mjs
@@ -0,0 +1,3 @@
+setTimeout(() => {
+ throw new Error('Test error');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/no-code/test.ts b/dev-packages/node-core-integration-tests/suites/no-code/test.ts
new file mode 100644
index 000000000000..11f37fbf4d6c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/no-code/test.ts
@@ -0,0 +1,39 @@
+import { afterAll, describe, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+const EVENT = {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Test error',
+ },
+ ],
+ },
+};
+
+describe('no-code init', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('CJS', async () => {
+ await createRunner(__dirname, 'app.js')
+ .withFlags('--require=@sentry/node-core/init')
+ .withMockSentryServer()
+ .expect({ event: EVENT })
+ .start()
+ .completed();
+ });
+
+ describe('--import', () => {
+ test('ESM', async () => {
+ await createRunner(__dirname, 'app.mjs')
+ .withFlags('--import=@sentry/node-core/init')
+ .withMockSentryServer()
+ .expect({ event: EVENT })
+ .start()
+ .completed();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/proxy/basic.js b/dev-packages/node-core-integration-tests/suites/proxy/basic.js
new file mode 100644
index 000000000000..00709a26de91
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/proxy/basic.js
@@ -0,0 +1,20 @@
+const http = require('http');
+const Sentry = require('@sentry/node-core');
+const { createProxy } = require('proxy');
+const { setupOtel } = require('../../utils/setupOtel.js');
+
+const proxy = createProxy(http.createServer());
+proxy.listen(0, () => {
+ const proxyPort = proxy.address().port;
+
+ const client = Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ transportOptions: {
+ proxy: `http://localhost:${proxyPort}`,
+ },
+ });
+
+ setupOtel(client);
+
+ Sentry.captureMessage('Hello, via proxy!');
+});
diff --git a/dev-packages/node-core-integration-tests/suites/proxy/test.ts b/dev-packages/node-core-integration-tests/suites/proxy/test.ts
new file mode 100644
index 000000000000..805b913d4814
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/proxy/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('proxies sentry requests', async () => {
+ await createRunner(__dirname, 'basic.js')
+ .withMockSentryServer()
+ .expect({
+ event: {
+ message: 'Hello, via proxy!',
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs
new file mode 100644
index 000000000000..08db0bf96acb
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs
@@ -0,0 +1,22 @@
+import { register } from 'node:module';
+
+register(
+ new URL(`data:application/javascript,
+export async function resolve(specifier, context, nextResolve) {
+ if (specifier === 'node:inspector' || specifier === 'inspector') {
+ throw new Error('Should not use node:inspector module');
+ }
+
+ return nextResolve(specifier);
+}`),
+ import.meta.url,
+);
+
+(async () => {
+ const Sentry = await import('@sentry/node-core');
+ const { setupOtel } = await import('../../../utils/setupOtel.js');
+
+ const client = Sentry.init({});
+
+ setupOtel(client);
+})();
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js
new file mode 100644
index 000000000000..17211aea77bd
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js
@@ -0,0 +1,43 @@
+/* eslint-disable no-unused-vars */
+const Sentry = require('@sentry/node-core');
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ includeLocalVariables: true,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+class Some {
+ two(name) {
+ throw new Error('Enough!');
+ }
+}
+
+function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ ty.two(name);
+}
+
+setTimeout(() => {
+ try {
+ one('some name');
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs
new file mode 100644
index 000000000000..3df12c70382b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs
@@ -0,0 +1,47 @@
+/* eslint-disable no-unused-vars */
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ includeLocalVariables: true,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+class Some {
+ async two(name) {
+ return new Promise((_, reject) => {
+ reject(new Error('Enough!'));
+ });
+ }
+}
+
+async function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ functionsShouldNotBeIncluded: () => {},
+ functionsShouldNotBeIncluded2() {},
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ await ty.two(name);
+}
+
+setTimeout(async () => {
+ try {
+ await one('some name');
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js
new file mode 100644
index 000000000000..71b6c22cf75e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js
@@ -0,0 +1,11 @@
+const Sentry = require('@sentry/node-core');
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ includeLocalVariables: true,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js
new file mode 100644
index 000000000000..08636175fa7b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js
@@ -0,0 +1,31 @@
+/* eslint-disable no-unused-vars */
+process.on('uncaughtException', () => {
+ // do nothing - this will prevent the Error below from closing this process
+});
+
+class Some {
+ two(name) {
+ throw new Error('Enough!');
+ }
+}
+
+function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ ty.two(name);
+}
+
+setTimeout(() => {
+ one('some name');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js
new file mode 100644
index 000000000000..5a533ac16867
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js
@@ -0,0 +1,48 @@
+/* eslint-disable no-unused-vars */
+const Sentry = require('@sentry/node-core');
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ includeLocalVariables: true,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+class Some {
+ two(name) {
+ throw new Error('Enough!');
+ }
+}
+
+function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ ty.two(name);
+}
+
+setTimeout(() => {
+ try {
+ try {
+ one('some name');
+ } catch (e) {
+ const more = 'here';
+ throw e;
+ }
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js
new file mode 100644
index 000000000000..ecdd5f219316
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js
@@ -0,0 +1,43 @@
+/* eslint-disable no-unused-vars */
+const Sentry = require('@sentry/node-core');
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ includeLocalVariables: true,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+process.on('uncaughtException', () => {
+ // do nothing - this will prevent the Error below from closing this process
+});
+
+class Some {
+ two(name) {
+ throw new Error('Enough!');
+ }
+}
+
+function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ ty.two(name);
+}
+
+setTimeout(() => {
+ one('some name');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js
new file mode 100644
index 000000000000..1532abc2797a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js
@@ -0,0 +1,42 @@
+/* eslint-disable no-unused-vars */
+const Sentry = require('@sentry/node-core');
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+process.on('uncaughtException', () => {
+ // do nothing - this will prevent the Error below from closing this process
+});
+
+class Some {
+ two(name) {
+ throw new Error('Enough!');
+ }
+}
+
+function one(name) {
+ const arr = [1, '2', null];
+ const obj = {
+ name,
+ num: 5,
+ };
+ const bool = false;
+ const num = 0;
+ const str = '';
+ const something = undefined;
+ const somethingElse = null;
+
+ const ty = new Some();
+
+ ty.two(name);
+}
+
+setTimeout(() => {
+ one('some name');
+}, 1000);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts
new file mode 100644
index 000000000000..e95e5a9e3767
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts
@@ -0,0 +1,104 @@
+import * as path from 'path';
+import { afterAll, describe, expect, test } from 'vitest';
+import { conditionalTest } from '../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+// This test takes some time because it connects the debugger etc.
+// So we increase the timeout here
+// vi.setTimeout(45_000);
+
+const EXPECTED_LOCAL_VARIABLES_EVENT = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: 'one',
+ vars: {
+ name: 'some name',
+ arr: [1, '2', null],
+ obj: { name: 'some name', num: 5 },
+ ty: '',
+ bool: false,
+ num: 0,
+ str: '',
+ something: '',
+ somethingElse: '',
+ },
+ }),
+ expect.objectContaining({
+ function: 'Some.two',
+ vars: { name: 'some name' },
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+};
+
+describe('LocalVariables integration', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('Should not include local variables by default', async () => {
+ await createRunner(__dirname, 'no-local-variables.js')
+ .expect({
+ event: event => {
+ for (const frame of event.exception?.values?.[0]?.stacktrace?.frames || []) {
+ expect(frame.vars).toBeUndefined();
+ }
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('Should include local variables when enabled', async () => {
+ await createRunner(__dirname, 'local-variables.js')
+ .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
+ .start()
+ .completed();
+ });
+
+ test('Should include local variables when instrumenting via --require', async () => {
+ const requirePath = path.resolve(__dirname, 'local-variables-instrument.js');
+
+ await createRunner(__dirname, 'local-variables-no-sentry.js')
+ .withFlags(`--require=${requirePath}`)
+ .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
+ .start()
+ .completed();
+ });
+
+ test('Should include local variables with ESM', async () => {
+ await createRunner(__dirname, 'local-variables-caught.mjs')
+ .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
+ .start()
+ .completed();
+ });
+
+ conditionalTest({ min: 19 })('Node v19+', () => {
+ test('Should not import inspector when not in use', async () => {
+ await createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start().completed();
+ });
+ });
+
+ conditionalTest({ min: 20 })('Node v20+', () => {
+ test('Should retain original local variables when error is re-thrown', async () => {
+ await createRunner(__dirname, 'local-variables-rethrow.js')
+ .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
+ .start()
+ .completed();
+ });
+ });
+
+ test('Includes local variables for caught exceptions when enabled', async () => {
+ await createRunner(__dirname, 'local-variables-caught.js')
+ .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js
new file mode 100644
index 000000000000..745b744da467
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js
@@ -0,0 +1,19 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+setupOtel(client);
+
+process.on('uncaughtException', () => {
+ // do nothing - this will prevent the Error below from closing this process before the timeout resolves
+});
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+throw new Error();
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js
new file mode 100644
index 000000000000..467eb127f7d1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js
@@ -0,0 +1,10 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+setupOtel(client);
+
+throw new Error('foo', { cause: 'bar' });
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js
new file mode 100644
index 000000000000..d5eb8383c188
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js
@@ -0,0 +1,24 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.onUncaughtExceptionIntegration({
+ exitEvenIfOtherHandlersAreRegistered: false,
+ }),
+ ],
+});
+
+setupOtel(client);
+
+process.on('uncaughtException', () => {
+ // do nothing - this will prevent the Error below from closing this process before the timeout resolves
+});
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+throw new Error();
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js
new file mode 100644
index 000000000000..bc8fc1f8c898
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js
@@ -0,0 +1,21 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.onUncaughtExceptionIntegration({
+ exitEvenIfOtherHandlersAreRegistered: false,
+ }),
+ ],
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ // This should not be called because the script throws before this resolves
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+throw new Error();
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js
new file mode 100644
index 000000000000..513eb3abc7cb
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js
@@ -0,0 +1,16 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ // This should not be called because the script throws before this resolves
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+throw new Error();
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts
new file mode 100644
index 000000000000..d27b08f152be
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts
@@ -0,0 +1,77 @@
+import * as childProcess from 'child_process';
+import * as path from 'path';
+import { describe, expect, test } from 'vitest';
+
+describe('OnUncaughtException integration', () => {
+ test('should close process on uncaught error with no additional listeners registered', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'no-additional-listener-test-script.js');
+
+ childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => {
+ expect(err).not.toBeNull();
+ expect(err?.code).toBe(1);
+ expect(stdout).not.toBe("I'm alive!");
+ done();
+ });
+ }));
+
+ test('should not close process on uncaught error when additional listeners are registered', () =>
+ new Promise(done => {
+ expect.assertions(2);
+
+ const testScriptPath = path.resolve(__dirname, 'additional-listener-test-script.js');
+
+ childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ done();
+ });
+ }));
+
+ test('should log entire error object to console stderr', () =>
+ new Promise(done => {
+ expect.assertions(2);
+
+ const testScriptPath = path.resolve(__dirname, 'log-entire-error-to-console.js');
+
+ childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stderr) => {
+ expect(err).not.toBeNull();
+ const errString = err?.toString() || '';
+
+ expect(errString).toContain(stderr);
+
+ done();
+ });
+ }));
+
+ describe('with `exitEvenIfOtherHandlersAreRegistered` set to false', () => {
+ test('should close process on uncaught error with no additional listeners registered', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'mimic-native-behaviour-no-additional-listener-test-script.js');
+
+ childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => {
+ expect(err).not.toBeNull();
+ expect(err?.code).toBe(1);
+ expect(stdout).not.toBe("I'm alive!");
+ done();
+ });
+ }));
+
+ test('should not close process on uncaught error when additional listeners are registered', () =>
+ new Promise(done => {
+ expect.assertions(2);
+
+ const testScriptPath = path.resolve(__dirname, 'mimic-native-behaviour-additional-listener-test-script.js');
+
+ childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ done();
+ });
+ }));
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts
new file mode 100644
index 000000000000..45aa96d3e8f7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.addBreadcrumb({});
+Sentry.captureMessage('test-empty-obj');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts
new file mode 100644
index 000000000000..c4f5145a8bbf
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts
@@ -0,0 +1,17 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should add an empty breadcrumb, when an empty object is given', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'test-empty-obj',
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts
new file mode 100644
index 000000000000..91e7670f4dfe
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.addBreadcrumb({
+ category: 'foo',
+ message: 'bar',
+ level: 'fatal',
+});
+
+Sentry.addBreadcrumb({
+ category: 'qux',
+});
+
+Sentry.captureMessage('test_multi_breadcrumbs');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts
new file mode 100644
index 000000000000..13dba000a823
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts
@@ -0,0 +1,27 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should add multiple breadcrumbs', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'test_multi_breadcrumbs',
+ breadcrumbs: [
+ {
+ category: 'foo',
+ message: 'bar',
+ level: 'fatal',
+ },
+ {
+ category: 'qux',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts
new file mode 100644
index 000000000000..27cbadbd9c22
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.addBreadcrumb({
+ category: 'foo',
+ message: 'bar',
+ level: 'fatal',
+});
+
+Sentry.captureMessage('test_simple');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts
new file mode 100644
index 000000000000..9708e00201ae
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts
@@ -0,0 +1,20 @@
+import { test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('should add a simple breadcrumb', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'test_simple',
+ breadcrumbs: [
+ {
+ category: 'foo',
+ message: 'bar',
+ level: 'fatal',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts
new file mode 100644
index 000000000000..f09a9b971e6f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+try {
+ throw new Error('catched_error');
+} catch (err) {
+ Sentry.captureException(err);
+}
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts
new file mode 100644
index 000000000000..31a5bf3d6b2e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts
@@ -0,0 +1,45 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should work inside catch block', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'catched_error',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ context_line: " throw new Error('catched_error');",
+ pre_context: [
+ " release: '1.0',",
+ ' transport: loggingTransport,',
+ '});',
+ '',
+ 'setupOtel(client);',
+ '',
+ 'try {',
+ ],
+ post_context: ['} catch (err) {', ' Sentry.captureException(err);', '}'],
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts
new file mode 100644
index 000000000000..50e651ff3f71
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+Sentry.captureException({});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts
new file mode 100644
index 000000000000..b8a6fe4f85e2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts
@@ -0,0 +1,28 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should capture an empty object', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Object captured as exception with keys: [object has no keys]',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts
new file mode 100644
index 000000000000..8fd8955e6df4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.captureException(new Error('test_simple_error'));
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts
new file mode 100644
index 000000000000..3afe450398e3
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts
@@ -0,0 +1,31 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should capture a simple error with message', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'test_simple_error',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ stacktrace: {
+ frames: expect.any(Array),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts
new file mode 100644
index 000000000000..013ba37320c6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const x = 'first';
+const y = 'second';
+
+Sentry.captureMessage(Sentry.parameterize`This is a log statement with ${x} and ${y} params`);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts
new file mode 100644
index 000000000000..15e6e76306fe
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts
@@ -0,0 +1,20 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should capture a parameterized representation of the message', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ logentry: {
+ message: 'This is a log statement with %s and %s params',
+ params: ['first', 'second'],
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts
new file mode 100644
index 000000000000..ac6d3b60b18a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.captureMessage('Message');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts
new file mode 100644
index 000000000000..e32081747f28
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should capture a simple message string', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'Message',
+ level: 'info',
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts
new file mode 100644
index 000000000000..d829d8fe100d
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ attachStacktrace: true,
+});
+
+setupOtel(client);
+
+Sentry.captureMessage('Message');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts
new file mode 100644
index 000000000000..8c79687b2bc4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts
@@ -0,0 +1,27 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('capture a simple message string with a stack trace if `attachStackTrace` is `true`', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'Message',
+ level: 'info',
+ exception: {
+ values: [
+ {
+ mechanism: { synthetic: true, type: 'generic', handled: true },
+ value: 'Message',
+ stacktrace: { frames: expect.any(Array) },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts
new file mode 100644
index 000000000000..0156dd6339c1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.captureMessage('debug_message', 'debug');
+Sentry.captureMessage('info_message', 'info');
+Sentry.captureMessage('warning_message', 'warning');
+Sentry.captureMessage('error_message', 'error');
+Sentry.captureMessage('fatal_message', 'fatal');
+Sentry.captureMessage('log_message', 'log');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts
new file mode 100644
index 000000000000..a44af6931d1f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should capture with different severity levels', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ event: { message: 'debug_message', level: 'debug' } })
+ .expect({ event: { message: 'info_message', level: 'info' } })
+ .expect({ event: { message: 'warning_message', level: 'warning' } })
+ .expect({ event: { message: 'error_message', level: 'error' } })
+ .expect({ event: { message: 'fatal_message', level: 'fatal' } })
+ .expect({ event: { message: 'log_message', level: 'log' } })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts
new file mode 100644
index 000000000000..fa0b7016626c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const scope = Sentry.getCurrentScope();
+scope.setTag('foo', 'bar');
+scope.setUser({ id: 'baz' });
+scope.setExtra('qux', 'quux');
+scope.clear();
+
+Sentry.captureMessage('cleared_scope');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts
new file mode 100644
index 000000000000..19f16417bb50
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts
@@ -0,0 +1,17 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should clear previously set properties of a scope', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'cleared_scope',
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts
new file mode 100644
index 000000000000..3ec9b740fd23
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const scope = Sentry.getCurrentScope();
+scope.setTag('foo', 'bar');
+scope.setUser({ id: 'baz' });
+scope.setExtra('qux', 'quux');
+
+Sentry.captureMessage('configured_scope');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts
new file mode 100644
index 000000000000..ecfb83c3a4a3
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts
@@ -0,0 +1,26 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set different properties of a scope', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'configured_scope',
+ tags: {
+ foo: 'bar',
+ },
+ extra: {
+ qux: 'quux',
+ },
+ user: {
+ id: 'baz',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js
new file mode 100644
index 000000000000..7974601a0ddd
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js
@@ -0,0 +1,16 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'none' })],
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+Promise.reject('test rejection');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js
new file mode 100644
index 000000000000..5a0919adabce
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js
@@ -0,0 +1,17 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })],
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ // should not be called
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+Promise.reject('test rejection');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js
new file mode 100644
index 000000000000..1e4e209a610a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js
@@ -0,0 +1,15 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+Promise.reject(new Error('test rejection'));
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js
new file mode 100644
index 000000000000..a80cada0d039
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js
@@ -0,0 +1,15 @@
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+setupOtel(client);
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+Promise.reject('test rejection');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts
new file mode 100644
index 000000000000..fa5f165582e7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts
@@ -0,0 +1,15 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })],
+});
+
+setupOtel(client);
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Promise.reject('test rejection');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts
new file mode 100644
index 000000000000..b2babced8554
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Promise.reject('test rejection');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts
new file mode 100644
index 000000000000..2f4a22c835a4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts
@@ -0,0 +1,126 @@
+import * as childProcess from 'child_process';
+import * as path from 'path';
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+describe('onUnhandledRejectionIntegration', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should show string-type promise rejection warnings by default', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'mode-warn-string.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ expect(stderr.trim())
+ .toBe(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:
+test rejection`);
+ done();
+ });
+ }));
+
+ test('should show error-type promise rejection warnings by default', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'mode-warn-error.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ expect(stderr)
+ .toContain(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:
+Error: test rejection
+ at Object.`);
+ done();
+ });
+ }));
+
+ test('should not close process on unhandled rejection in strict mode', () =>
+ new Promise(done => {
+ expect.assertions(4);
+
+ const testScriptPath = path.resolve(__dirname, 'mode-strict.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).not.toBeNull();
+ expect(err?.code).toBe(1);
+ expect(stdout).not.toBe("I'm alive!");
+ expect(stderr)
+ .toContain(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:
+test rejection`);
+ done();
+ });
+ }));
+
+ test('should not close process or warn on unhandled rejection in none mode', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'mode-none.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ expect(stderr).toBe('');
+ done();
+ });
+ }));
+
+ test('captures exceptions for unhandled rejections', async () => {
+ await createRunner(__dirname, 'scenario-warn.ts')
+ .expect({
+ event: {
+ level: 'error',
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'test rejection',
+ mechanism: {
+ type: 'onunhandledrejection',
+ handled: false,
+ },
+ stacktrace: {
+ frames: expect.any(Array),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('captures exceptions for unhandled rejections in strict mode', async () => {
+ await createRunner(__dirname, 'scenario-strict.ts')
+ .expect({
+ event: {
+ level: 'fatal',
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'test rejection',
+ mechanism: {
+ type: 'onunhandledrejection',
+ handled: false,
+ },
+ stacktrace: {
+ frames: expect.any(Array),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts
new file mode 100644
index 000000000000..b2d85041bae7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts
@@ -0,0 +1,28 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const globalScope = Sentry.getGlobalScope();
+const isolationScope = Sentry.getIsolationScope();
+const currentScope = Sentry.getCurrentScope();
+
+globalScope.setExtra('aa', 'aa');
+isolationScope.setExtra('bb', 'bb');
+currentScope.setExtra('cc', 'cc');
+
+Sentry.captureMessage('outer_before');
+
+Sentry.withScope(scope => {
+ scope.setExtra('dd', 'dd');
+ Sentry.captureMessage('inner');
+});
+
+Sentry.captureMessage('outer_after');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts
new file mode 100644
index 000000000000..8f16958cc1c9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts
@@ -0,0 +1,40 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should apply scopes correctly', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'outer_before',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'inner',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ cc: 'cc',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'outer_after',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts
new file mode 100644
index 000000000000..4bf13819f0f5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const globalScope = Sentry.getGlobalScope();
+const isolationScope = Sentry.getIsolationScope();
+const currentScope = Sentry.getCurrentScope();
+
+globalScope.setExtra('aa', 'aa');
+isolationScope.setExtra('bb', 'bb');
+currentScope.setExtra('cc', 'cc');
+
+Sentry.captureMessage('outer_before');
+
+Sentry.withScope(scope => {
+ Sentry.getIsolationScope().setExtra('dd', 'dd');
+ scope.setExtra('ee', 'ee');
+ Sentry.captureMessage('inner');
+});
+
+Sentry.withIsolationScope(() => {
+ Sentry.getIsolationScope().setExtra('ff', 'ff');
+ Sentry.getCurrentScope().setExtra('gg', 'gg');
+ Sentry.captureMessage('inner_async_context');
+});
+
+Sentry.captureMessage('outer_after');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts
new file mode 100644
index 000000000000..eb926423ef58
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts
@@ -0,0 +1,57 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should apply scopes correctly', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'outer_before',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'inner',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ cc: 'cc',
+ dd: 'dd',
+ ee: 'ee',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'inner_async_context',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ cc: 'cc',
+ dd: 'dd',
+ ff: 'ff',
+ gg: 'gg',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'outer_after',
+ extra: {
+ aa: 'aa',
+ bb: 'bb',
+ cc: 'cc',
+ dd: 'dd',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts
new file mode 100644
index 000000000000..3a67c4ec8f78
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts
@@ -0,0 +1,27 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setContext('context_1', {
+ foo: 'bar',
+ baz: {
+ qux: 'quux',
+ },
+});
+
+Sentry.setContext('context_2', {
+ 1: 'foo',
+ bar: false,
+});
+
+Sentry.setContext('context_3', null);
+
+Sentry.captureMessage('multiple_contexts');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts
new file mode 100644
index 000000000000..1cf8342e2f29
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts
@@ -0,0 +1,24 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should record multiple contexts', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'multiple_contexts',
+ contexts: {
+ context_1: {
+ foo: 'bar',
+ baz: { qux: 'quux' },
+ },
+ context_2: { 1: 'foo', bar: false },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts
new file mode 100644
index 000000000000..fdeb4cd4a121
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+type Circular = {
+ self?: Circular;
+};
+
+const objCircular: Circular = {};
+objCircular.self = objCircular;
+
+Sentry.setContext('non_serializable', objCircular);
+
+Sentry.captureMessage('non_serializable');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts
new file mode 100644
index 000000000000..34c962e5e216
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts
@@ -0,0 +1,13 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should normalize non-serializable context', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ event: { message: 'non_serializable', contexts: {} } })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts
new file mode 100644
index 000000000000..4fa05a4ae2ba
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setContext('foo', { bar: 'baz' });
+Sentry.captureMessage('simple_context_object');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts
new file mode 100644
index 000000000000..3c28a109130b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts
@@ -0,0 +1,22 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set a simple context', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'simple_context_object',
+ contexts: {
+ foo: {
+ bar: 'baz',
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts
new file mode 100644
index 000000000000..f8275d63986e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setExtra('extra_1', {
+ foo: 'bar',
+ baz: {
+ qux: 'quux',
+ },
+});
+
+Sentry.setExtra('extra_2', false);
+
+Sentry.captureMessage('multiple_extras');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts
new file mode 100644
index 000000000000..f40d56af6579
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts
@@ -0,0 +1,21 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should record multiple extras of different types', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'multiple_extras',
+ extra: {
+ extra_1: { foo: 'bar', baz: { qux: 'quux' } },
+ extra_2: false,
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts
new file mode 100644
index 000000000000..1d4eedf9ccc9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+type Circular = {
+ self?: Circular;
+};
+
+const objCircular: Circular = {};
+objCircular.self = objCircular;
+
+Sentry.setExtra('non_serializable', objCircular);
+
+Sentry.captureMessage('non_serializable');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts
new file mode 100644
index 000000000000..113c99883f32
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should normalize non-serializable extra', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'non_serializable',
+ extra: {},
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts
new file mode 100644
index 000000000000..87b1314979d1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setExtra('foo', {
+ foo: 'bar',
+ baz: {
+ qux: 'quux',
+ },
+});
+Sentry.captureMessage('simple_extra');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts
new file mode 100644
index 000000000000..115d4ca064a4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts
@@ -0,0 +1,25 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set a simple extra', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'simple_extra',
+ extra: {
+ foo: {
+ foo: 'bar',
+ baz: {
+ qux: 'quux',
+ },
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts
new file mode 100644
index 000000000000..5e60d1092008
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts
@@ -0,0 +1,22 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setExtras({ extra: [] });
+Sentry.setExtras({ null: 0 });
+Sentry.setExtras({
+ obj: {
+ foo: ['bar', 'baz', 1],
+ },
+});
+Sentry.setExtras({ [Infinity]: 2 });
+
+Sentry.captureMessage('consecutive_calls');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts
new file mode 100644
index 000000000000..da5dc31e9fea
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts
@@ -0,0 +1,18 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set extras from multiple consecutive calls', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'consecutive_calls',
+ extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts
new file mode 100644
index 000000000000..36d1d9b1de92
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setExtras({
+ extra_1: [1, ['foo'], 'bar'],
+ extra_2: 'baz',
+ extra_3: Math.PI,
+ extra_4: {
+ qux: {
+ quux: false,
+ },
+ },
+});
+
+Sentry.captureMessage('multiple_extras');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts
new file mode 100644
index 000000000000..614a157fed14
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts
@@ -0,0 +1,23 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should record an extras object', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'multiple_extras',
+ extra: {
+ extra_1: [1, ['foo'], 'bar'],
+ extra_2: 'baz',
+ extra_3: 3.141592653589793,
+ extra_4: { qux: { quux: false } },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts
new file mode 100644
index 000000000000..5f6788d0f8b8
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'some_transaction' }, () => {
+ Sentry.setMeasurement('metric.foo', 42, 'ms');
+ Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds');
+ Sentry.setMeasurement('metric.baz', 99, 's');
+ Sentry.setMeasurement('metric.baz', 1, '');
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts
new file mode 100644
index 000000000000..829e6a7ed3da
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts
@@ -0,0 +1,22 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should attach measurement to transaction', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'some_transaction',
+ measurements: {
+ 'metric.foo': { value: 42, unit: 'ms' },
+ 'metric.bar': { value: 1337, unit: 'nanoseconds' },
+ 'metric.baz': { value: 1, unit: '' },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts
new file mode 100644
index 000000000000..5717d98929f4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setTag('tag_1', 'foo');
+Sentry.setTag('tag_2', Math.PI);
+Sentry.setTag('tag_3', false);
+Sentry.setTag('tag_4', null);
+Sentry.setTag('tag_5', undefined);
+Sentry.setTag('tag_6', -1);
+
+Sentry.captureMessage('primitive_tags');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts
new file mode 100644
index 000000000000..23e22402c666
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts
@@ -0,0 +1,24 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set primitive tags', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'primitive_tags',
+ tags: {
+ tag_1: 'foo',
+ tag_2: 3.141592653589793,
+ tag_3: false,
+ tag_4: null,
+ tag_6: -1,
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts
new file mode 100644
index 000000000000..5717d98929f4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setTag('tag_1', 'foo');
+Sentry.setTag('tag_2', Math.PI);
+Sentry.setTag('tag_3', false);
+Sentry.setTag('tag_4', null);
+Sentry.setTag('tag_5', undefined);
+Sentry.setTag('tag_6', -1);
+
+Sentry.captureMessage('primitive_tags');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts
new file mode 100644
index 000000000000..23e22402c666
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts
@@ -0,0 +1,24 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should set primitive tags', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'primitive_tags',
+ tags: {
+ tag_1: 'foo',
+ tag_2: 3.141592653589793,
+ tag_3: false,
+ tag_4: null,
+ tag_6: -1,
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts
new file mode 100644
index 000000000000..c935f334275c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts
@@ -0,0 +1,25 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.captureMessage('no_user');
+
+Sentry.setUser({
+ id: 'foo',
+ ip_address: 'bar',
+ other_key: 'baz',
+});
+
+Sentry.captureMessage('user');
+
+Sentry.setUser(null);
+
+Sentry.captureMessage('unset_user');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts
new file mode 100644
index 000000000000..9b7f3e2c23be
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts
@@ -0,0 +1,24 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should unset user', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ event: { message: 'no_user' } })
+ .expect({
+ event: {
+ message: 'user',
+ user: {
+ id: 'foo',
+ ip_address: 'bar',
+ other_key: 'baz',
+ },
+ },
+ })
+ .expect({ event: { message: 'unset_user' } })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts
new file mode 100644
index 000000000000..98e25199eb4f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setUser({
+ id: 'foo',
+ ip_address: 'bar',
+});
+
+Sentry.captureMessage('first_user');
+
+Sentry.setUser({
+ id: 'baz',
+});
+
+Sentry.captureMessage('second_user');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts
new file mode 100644
index 000000000000..7a6c89f4c213
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts
@@ -0,0 +1,29 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should update user', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'first_user',
+ user: {
+ id: 'foo',
+ ip_address: 'bar',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'second_user',
+ user: {
+ id: 'baz',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts
new file mode 100644
index 000000000000..5d6d8daa517a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'test_span' }, () => undefined);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts
new file mode 100644
index 000000000000..8a72ecd7c8b3
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts
@@ -0,0 +1,45 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core';
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('sends a manually started root span with source custom', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'test_span',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
+
+test("doesn't change the name for manually started spans even if attributes triggering inference are set", async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'test_span',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts
new file mode 100644
index 000000000000..ac0892fe5f6f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts
@@ -0,0 +1,33 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.getCurrentScope().setPropagationContext({
+ parentSpanId: '1234567890123456',
+ traceId: '12345678901234567890123456789012',
+ sampleRand: Math.random(),
+});
+
+const spanIdTraceId = Sentry.startSpan(
+ {
+ name: 'test_span_1',
+ },
+ span1 => span1.spanContext().traceId,
+);
+
+Sentry.startSpan(
+ {
+ name: 'test_span_2',
+ attributes: { spanIdTraceId },
+ },
+ () => undefined,
+);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts
new file mode 100644
index 000000000000..e1b8f793d9b6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts
@@ -0,0 +1,31 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should send manually started parallel root spans in root context', async () => {
+ expect.assertions(7);
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ transaction: { transaction: 'test_span_1' } })
+ .expect({
+ transaction: transaction => {
+ expect(transaction).toBeDefined();
+ const traceId = transaction.contexts?.trace?.trace_id;
+ expect(traceId).toBeDefined();
+
+ // It ignores propagation context of the root context
+ expect(traceId).not.toBe('12345678901234567890123456789012');
+ expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
+
+ // Different trace ID than the first span
+ const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
+ expect(trace1Id).toBeDefined();
+ expect(trace1Id).not.toBe(traceId);
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts
new file mode 100644
index 000000000000..3c8a707e9919
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.withScope(scope => {
+ scope.setPropagationContext({
+ parentSpanId: '1234567890123456',
+ traceId: '12345678901234567890123456789012',
+ sampleRand: Math.random(),
+ });
+
+ const spanIdTraceId = Sentry.startSpan(
+ {
+ name: 'test_span_1',
+ },
+ span1 => span1.spanContext().traceId,
+ );
+
+ Sentry.startSpan(
+ {
+ name: 'test_span_2',
+ attributes: { spanIdTraceId },
+ },
+ () => undefined,
+ );
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts
new file mode 100644
index 000000000000..e10a1210a0c9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts
@@ -0,0 +1,27 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should send manually started parallel root spans outside of root context with parentSpanId', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ transaction: { transaction: 'test_span_1' } })
+ .expect({
+ transaction: transaction => {
+ expect(transaction).toBeDefined();
+ const traceId = transaction.contexts?.trace?.trace_id;
+ expect(traceId).toBeDefined();
+ expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
+
+ const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
+ expect(trace1Id).toBeDefined();
+
+ // Different trace ID as the first span
+ expect(trace1Id).not.toBe(traceId);
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts
new file mode 100644
index 000000000000..7b5b56d18343
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts
@@ -0,0 +1,29 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.withScope(() => {
+ const spanIdTraceId = Sentry.startSpan(
+ {
+ name: 'test_span_1',
+ },
+ span1 => span1.spanContext().traceId,
+ );
+
+ Sentry.startSpan(
+ {
+ name: 'test_span_2',
+ attributes: { spanIdTraceId },
+ },
+ () => undefined,
+ );
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts
new file mode 100644
index 000000000000..69fc2bc2774a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts
@@ -0,0 +1,29 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should send manually started parallel root spans outside of root context', async () => {
+ expect.assertions(6);
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({ transaction: { transaction: 'test_span_1' } })
+ .expect({
+ transaction: transaction => {
+ expect(transaction).toBeDefined();
+ const traceId = transaction.contexts?.trace?.trace_id;
+ expect(traceId).toBeDefined();
+ expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
+
+ const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
+ expect(trace1Id).toBeDefined();
+
+ // Different trace ID as the first span
+ expect(trace1Id).not.toBe(traceId);
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts
new file mode 100644
index 000000000000..faa3e76bcecb
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan(
+ { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
+ (span: Sentry.Span) => {
+ span.updateName('new name');
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts
new file mode 100644
index 000000000000..f54cbeb84895
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts
@@ -0,0 +1,26 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core';
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('updates the span name when calling `span.updateName`', async () => {
+ createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'new name',
+ transaction_info: { source: 'url' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' },
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts
new file mode 100644
index 000000000000..e5581473b3f1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan(
+ { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
+ (span: Sentry.Span) => {
+ Sentry.updateSpanName(span, 'new name');
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts
new file mode 100644
index 000000000000..faa6a674bfc6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts
@@ -0,0 +1,26 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core';
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('updates the span name and source when calling `updateSpanName`', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'new name',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts
new file mode 100644
index 000000000000..d1a2a44382cf
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'root_span' }, () => {
+ Sentry.startSpan(
+ {
+ name: 'span_1',
+ attributes: {
+ foo: 'bar',
+ baz: [1, 2, 3],
+ },
+ },
+ () => undefined,
+ );
+
+ // span_2 doesn't finish
+ Sentry.startInactiveSpan({ name: 'span_2' });
+
+ Sentry.startSpan({ name: 'span_3' }, () => {
+ // span_4 is the child of span_3 but doesn't finish.
+ Sentry.startInactiveSpan({ name: 'span_4', attributes: { qux: 'quux' } });
+
+ // span_5 is another child of span_3 but finishes.
+ Sentry.startSpan({ name: 'span_5' }, () => undefined);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts
new file mode 100644
index 000000000000..c01b837db5f7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts
@@ -0,0 +1,47 @@
+import type { SpanJSON } from '@sentry/core';
+import { afterAll, expect, test } from 'vitest';
+import { assertSentryTransaction } from '../../../../utils/assertions';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should report finished spans as children of the root transaction.', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: transaction => {
+ const rootSpanId = transaction.contexts?.trace?.span_id;
+ const span3Id = transaction.spans?.[1]?.span_id;
+
+ expect(rootSpanId).toEqual(expect.any(String));
+ expect(span3Id).toEqual(expect.any(String));
+
+ assertSentryTransaction(transaction, {
+ transaction: 'root_span',
+ spans: [
+ {
+ description: 'span_1',
+ data: {
+ foo: 'bar',
+ baz: [1, 2, 3],
+ },
+ parent_span_id: rootSpanId,
+ },
+ {
+ description: 'span_3',
+ parent_span_id: rootSpanId,
+ data: {},
+ },
+ {
+ description: 'span_5',
+ parent_span_id: span3Id,
+ data: {},
+ },
+ ] as SpanJSON[],
+ });
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts
new file mode 100644
index 000000000000..d1a24307172a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts
@@ -0,0 +1,30 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.setUser({ id: 'qux' });
+Sentry.captureMessage('root_before');
+
+Sentry.withScope(scope => {
+ scope.setTag('foo', false);
+ Sentry.captureMessage('outer_before');
+
+ Sentry.withScope(scope => {
+ scope.setTag('bar', 10);
+ scope.setUser(null);
+ Sentry.captureMessage('inner');
+ });
+
+ scope.setUser({ id: 'baz' });
+ Sentry.captureMessage('outer_after');
+});
+
+Sentry.captureMessage('root_after');
diff --git a/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts
new file mode 100644
index 000000000000..4e646a233443
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts
@@ -0,0 +1,59 @@
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should allow nested scoping', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ message: 'root_before',
+ user: {
+ id: 'qux',
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'outer_before',
+ user: {
+ id: 'qux',
+ },
+ tags: {
+ foo: false,
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'inner',
+ tags: {
+ foo: false,
+ bar: 10,
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'outer_after',
+ user: {
+ id: 'baz',
+ },
+ tags: {
+ foo: false,
+ },
+ },
+ })
+ .expect({
+ event: {
+ message: 'root_after',
+ user: {
+ id: 'qux',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts
new file mode 100644
index 000000000000..ba8110a62675
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts
@@ -0,0 +1,29 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should aggregate successful, crashed and erroneous sessions', async () => {
+ const runner = createRunner(__dirname, '..', 'server.ts')
+ .ignore('transaction', 'event')
+ .unignore('sessions')
+ .expect({
+ sessions: {
+ aggregates: [
+ {
+ started: expect.any(String),
+ exited: 2,
+ errored: 1,
+ },
+ ],
+ },
+ })
+ .start();
+
+ runner.makeRequest('get', '/test/success');
+ runner.makeRequest('get', '/test/error_handled');
+ runner.makeRequest('get', '/test/error_unhandled', { expectError: true });
+ await runner.completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts
new file mode 100644
index 000000000000..228ee9a98643
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts
@@ -0,0 +1,28 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should aggregate successful sessions', async () => {
+ const runner = createRunner(__dirname, '..', 'server.ts')
+ .ignore('transaction', 'event')
+ .unignore('sessions')
+ .expect({
+ sessions: {
+ aggregates: [
+ {
+ started: expect.any(String),
+ exited: 3,
+ },
+ ],
+ },
+ })
+ .start();
+
+ runner.makeRequest('get', '/test/success');
+ runner.makeRequest('get', '/test/success_next');
+ runner.makeRequest('get', '/test/success_slow');
+ await runner.completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/sessions/server.ts b/dev-packages/node-core-integration-tests/suites/sessions/server.ts
new file mode 100644
index 000000000000..5638b0946d73
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/sessions/server.ts
@@ -0,0 +1,51 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ integrations: [
+ Sentry.httpIntegration({
+ // Flush after 2 seconds (to avoid waiting for the default 60s)
+ sessionFlushingDelayMS: 2_000,
+ }),
+ ],
+});
+
+setupOtel(client);
+
+import express from 'express';
+
+const app = express();
+
+app.get('/test/success', (_req, res) => {
+ res.send('Success!');
+});
+
+app.get('/test/success_next', (_req, res, next) => {
+ res.send('Success!');
+ next();
+});
+
+app.get('/test/success_slow', async (_req, res) => {
+ await new Promise(res => setTimeout(res, 50));
+
+ res.send('Success!');
+});
+
+app.get('/test/error_unhandled', () => {
+ throw new Error('Crash!');
+});
+
+app.get('/test/error_handled', (_req, res) => {
+ try {
+ throw new Error('Crash!');
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+ res.send('Crash!');
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts
new file mode 100644
index 000000000000..034f8b1f60e4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts
@@ -0,0 +1,33 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ tracesSampleRate: 1,
+ environment: 'production',
+});
+
+setupOtel(client);
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan(
+ { name: 'initial-name', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
+ async span => {
+ Sentry.captureMessage('message-1');
+
+ span.updateName('updated-name-1');
+ span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+
+ Sentry.captureMessage('message-2');
+
+ span.updateName('updated-name-2');
+ span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
+
+ Sentry.captureMessage('message-3');
+
+ span.end();
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts
new file mode 100644
index 000000000000..1f7d68340f79
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts
@@ -0,0 +1,60 @@
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ tracesSampleRate: 1,
+ environment: 'production',
+ openTelemetryInstrumentations: [new HttpInstrumentation()],
+});
+
+setupOtel(client);
+
+import http from 'http';
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan(
+ {
+ name: 'initial-name',
+ attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' },
+ },
+ async span => {
+ const serverUrl = process.env.SERVER_URL;
+ if (!serverUrl) {
+ throw new Error('SERVER_URL environment variable not set');
+ }
+
+ await makeHttpRequest(`${serverUrl}/api/v0`);
+
+ span.updateName('updated-name-1');
+ span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+
+ await makeHttpRequest(`${serverUrl}/api/v1`);
+
+ span.updateName('updated-name-2');
+ span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
+
+ await makeHttpRequest(`${serverUrl}/api/v2`);
+
+ span.end();
+ },
+);
+
+function makeHttpRequest(url: string): Promise {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts
new file mode 100644
index 000000000000..9fe401badaa7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts
@@ -0,0 +1,138 @@
+import { expect, test } from 'vitest';
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
+import { createTestServer } from '../../../utils/server';
+
+// This test requires Node.js 22+ because it depends on the 'http.client.request.created'
+// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+
+conditionalTest({ min: 22 })('node >=22', () => {
+ test('adds current transaction name to baggage when the txn name is high-quality', async () => {
+ expect.assertions(5);
+
+ let traceId: string | undefined;
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', (headers: Record) => {
+ const baggageItems = getBaggageHeaderItems(headers);
+ traceId = baggageItems.find(item => item.startsWith('sentry-trace_id='))?.split('=')[1] as string;
+
+ expect(traceId).toMatch(/^[0-9a-f]{32}$/);
+
+ expect(baggageItems).toEqual([
+ 'sentry-environment=production',
+ 'sentry-public_key=public',
+ 'sentry-release=1.0',
+ expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/),
+ 'sentry-sample_rate=1',
+ 'sentry-sampled=true',
+ `sentry-trace_id=${traceId}`,
+ ]);
+ })
+ .get('/api/v1', (headers: Record) => {
+ expect(getBaggageHeaderItems(headers)).toEqual([
+ 'sentry-environment=production',
+ 'sentry-public_key=public',
+ 'sentry-release=1.0',
+ expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/),
+ 'sentry-sample_rate=1',
+ 'sentry-sampled=true',
+ `sentry-trace_id=${traceId}`,
+ 'sentry-transaction=updated-name-1',
+ ]);
+ })
+ .get('/api/v2', (headers: Record) => {
+ expect(getBaggageHeaderItems(headers)).toEqual([
+ 'sentry-environment=production',
+ 'sentry-public_key=public',
+ 'sentry-release=1.0',
+ expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/),
+ 'sentry-sample_rate=1',
+ 'sentry-sampled=true',
+ `sentry-trace_id=${traceId}`,
+ 'sentry-transaction=updated-name-2',
+ ]);
+ })
+ .start();
+
+ await createRunner(__dirname, 'scenario-headers.ts')
+ .withEnv({ SERVER_URL })
+ .expect({
+ transaction: {},
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+});
+
+test('adds current transaction name to trace envelope header when the txn name is high-quality', async () => {
+ expect.assertions(4);
+
+ await createRunner(__dirname, 'scenario-events.ts')
+ .expectHeader({
+ event: {
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .expectHeader({
+ event: {
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ transaction: 'updated-name-1',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .expectHeader({
+ event: {
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ transaction: 'updated-name-2',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .expectHeader({
+ transaction: {
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ transaction: 'updated-name-2',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
+
+function getBaggageHeaderItems(headers: Record) {
+ const baggage = headers['baggage'] as string;
+ const baggageItems = baggage
+ .split(',')
+ .map(b => b.trim())
+ .sort();
+ return baggageItems;
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts
new file mode 100644
index 000000000000..ee86f615d220
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'test span' }, () => {
+ Sentry.captureException(new Error('foo'));
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts
new file mode 100644
index 000000000000..bba04c788282
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for error event during active unsampled span is correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .ignore('transaction')
+ .expectHeader({
+ event: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ public_key: 'public',
+ environment: 'production',
+ release: '1.0',
+ sample_rate: '0',
+ sampled: 'false',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts
new file mode 100644
index 000000000000..72ef4f49a2f6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 1,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'test span' }, () => {
+ Sentry.startSpan({ name: 'test inner span' }, () => {
+ Sentry.captureException(new Error('foo'));
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts
new file mode 100644
index 000000000000..f11defc490c8
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for error event during active span is correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .ignore('transaction')
+ .expectHeader({
+ event: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ public_key: 'public',
+ environment: 'production',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ transaction: 'test span',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts
new file mode 100644
index 000000000000..edeee6176370
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.captureException(new Error('foo'));
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts
new file mode 100644
index 000000000000..9d39209d456f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts
@@ -0,0 +1,18 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for error events is correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expectHeader({
+ event: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ environment: 'production',
+ public_key: 'public',
+ release: '1.0',
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js
new file mode 100644
index 000000000000..d3a9be811778
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js
@@ -0,0 +1,38 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ // disable attaching headers to /test/* endpoints
+ tracePropagationTargets: [/^(?!.*test).*$/],
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const bodyParser = require('body-parser');
+const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+app.use(bodyParser.json());
+app.use(bodyParser.text());
+app.use(bodyParser.raw());
+
+app.get('/test', (req, res) => {
+ // Create a transaction to trigger trace continuation from headers
+ // because node-core doesn't create spans for http requests due to
+ // the lack of @opentelemetry/instrumentation-http
+ Sentry.startSpan({ name: 'test-transaction', op: 'http.server' }, () => {
+ res.send({ headers: req.headers });
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts
new file mode 100644
index 000000000000..63db6ff4e820
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts
@@ -0,0 +1,33 @@
+import { afterAll, describe, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('tracesSampleRate propagation', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ const traceId = '12345678123456781234567812345678';
+
+ test('uses sample rate from incoming baggage header in trace envelope item', async () => {
+ const runner = createRunner(__dirname, 'server.js')
+ .expectHeader({
+ transaction: {
+ trace: {
+ sample_rate: '0.05',
+ sampled: 'true',
+ trace_id: traceId,
+ transaction: 'myTransaction',
+ sample_rand: '0.42',
+ },
+ },
+ })
+ .start();
+ runner.makeRequest('get', '/test', {
+ headers: {
+ 'sentry-trace': `${traceId}-1234567812345678-1`,
+ baggage: `sentry-sample_rate=0.05,sentry-trace_id=${traceId},sentry-sampled=true,sentry-transaction=myTransaction,sentry-sample_rand=0.42`,
+ },
+ });
+ await runner.completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts
new file mode 100644
index 000000000000..cd28c63fc4b7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts
@@ -0,0 +1,29 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 1,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan(
+ {
+ name: 'GET /route',
+ attributes: {
+ 'http.method': 'GET',
+ 'http.route': '/route',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ },
+ },
+ () => {
+ // noop
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts
new file mode 100644
index 000000000000..f4bb6e2b4293
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for transaction event of route correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expectHeader({
+ transaction: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ public_key: 'public',
+ transaction: 'GET /route',
+ environment: 'production',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts
new file mode 100644
index 000000000000..b47f9cfc73dc
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts
@@ -0,0 +1,29 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 1,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan(
+ {
+ name: 'GET /route/1',
+ attributes: {
+ 'http.method': 'GET',
+ 'http.route': '/route',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ },
+ },
+ () => {
+ // noop
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts
new file mode 100644
index 000000000000..c4ed5ae4983f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts
@@ -0,0 +1,21 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for transaction event with source=url correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expectHeader({
+ transaction: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ public_key: 'public',
+ environment: 'production',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts
new file mode 100644
index 000000000000..0bec8720f01b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 1,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'test span' }, () => {
+ // noop
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts
new file mode 100644
index 000000000000..104761d52c86
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../../utils/runner';
+
+test('envelope header for transaction event is correct', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expectHeader({
+ transaction: {
+ trace: {
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ public_key: 'public',
+ environment: 'production',
+ release: '1.0',
+ sample_rate: '1',
+ sampled: 'true',
+ transaction: 'test span',
+ sample_rand: expect.any(String),
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts
new file mode 100644
index 000000000000..2923e2d3414f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => {
+ childSpan1.addLink({
+ context: parentSpan1.spanContext(),
+ attributes: { 'sentry.link.type': 'previous_trace' },
+ });
+
+ childSpan1.end();
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.startSpan({ name: 'child1.2' }, async childSpan2 => {
+ childSpan2.addLink({
+ context: parentSpan1.spanContext(),
+ attributes: { 'sentry.link.type': 'previous_trace' },
+ });
+
+ childSpan2.end();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts
new file mode 100644
index 000000000000..d12fb52ad748
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const span1 = Sentry.startInactiveSpan({ name: 'span1' });
+span1.end();
+
+Sentry.startSpan({ name: 'rootSpan' }, rootSpan => {
+ rootSpan.addLink({
+ context: span1.spanContext(),
+ attributes: { 'sentry.link.type': 'previous_trace' },
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts
new file mode 100644
index 000000000000..5a1d45845f85
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts
@@ -0,0 +1,34 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.startSpan({ name: 'child2.1' }, async childSpan2 => {
+ childSpan2.addLinks([
+ { context: parentSpan1.spanContext() },
+ {
+ context: childSpan1.spanContext(),
+ attributes: { 'sentry.link.type': 'previous_trace' },
+ },
+ ]);
+
+ childSpan2.end();
+ });
+
+ childSpan1.end();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts
new file mode 100644
index 000000000000..c2c1e765b3d2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts
@@ -0,0 +1,29 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const span1 = Sentry.startInactiveSpan({ name: 'span1' });
+span1.end();
+
+const span2 = Sentry.startInactiveSpan({ name: 'span2' });
+span2.end();
+
+Sentry.startSpan({ name: 'rootSpan' }, rootSpan => {
+ rootSpan.addLinks([
+ { context: span1.spanContext() },
+ {
+ context: span2.spanContext(),
+ attributes: { 'sentry.link.type': 'previous_trace' },
+ },
+ ]);
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts
new file mode 100644
index 000000000000..0488758a03df
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts
@@ -0,0 +1,30 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+const parentSpan1 = Sentry.startInactiveSpan({ name: 'parent1' });
+parentSpan1.end();
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan(
+ {
+ name: 'parent2',
+ links: [{ context: parentSpan1.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }],
+ },
+ async () => {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ Sentry.startSpan({ name: 'child2.1' }, async childSpan1 => {
+ childSpan1.end();
+ });
+ },
+);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts
new file mode 100644
index 000000000000..a0874274d2bd
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts
@@ -0,0 +1,193 @@
+import { describe, expect, test } from 'vitest';
+import { createRunner } from '../../../utils/runner';
+
+describe('span links', () => {
+ test('should link spans by adding "links" to span options', async () => {
+ let span1_traceId: string, span1_spanId: string;
+
+ await createRunner(__dirname, 'scenario-span-options.ts')
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('parent1');
+
+ const traceContext = event.contexts?.trace;
+ span1_traceId = traceContext?.trace_id as string;
+ span1_spanId = traceContext?.span_id as string;
+ },
+ })
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('parent2');
+
+ const traceContext = event.contexts?.trace;
+ expect(traceContext).toBeDefined();
+ expect(traceContext?.links).toEqual([
+ expect.objectContaining({
+ trace_id: expect.stringMatching(span1_traceId),
+ span_id: expect.stringMatching(span1_spanId),
+ }),
+ ]);
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('should link spans with addLink() in trace context', async () => {
+ let span1_traceId: string, span1_spanId: string;
+
+ await createRunner(__dirname, 'scenario-addLink.ts')
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('span1');
+
+ span1_traceId = event.contexts?.trace?.trace_id as string;
+ span1_spanId = event.contexts?.trace?.span_id as string;
+
+ expect(event.spans).toEqual([]);
+ },
+ })
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('rootSpan');
+
+ expect(event.contexts?.trace?.links).toEqual([
+ expect.objectContaining({
+ trace_id: expect.stringMatching(span1_traceId),
+ span_id: expect.stringMatching(span1_spanId),
+ attributes: expect.objectContaining({
+ 'sentry.link.type': 'previous_trace',
+ }),
+ }),
+ ]);
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('should link spans with addLinks() in trace context', async () => {
+ let span1_traceId: string, span1_spanId: string, span2_traceId: string, span2_spanId: string;
+
+ await createRunner(__dirname, 'scenario-addLinks.ts')
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('span1');
+
+ span1_traceId = event.contexts?.trace?.trace_id as string;
+ span1_spanId = event.contexts?.trace?.span_id as string;
+
+ expect(event.spans).toEqual([]);
+ },
+ })
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('span2');
+
+ span2_traceId = event.contexts?.trace?.trace_id as string;
+ span2_spanId = event.contexts?.trace?.span_id as string;
+
+ expect(event.spans).toEqual([]);
+ },
+ })
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('rootSpan');
+
+ expect(event.contexts?.trace?.links).toEqual([
+ expect.not.objectContaining({ attributes: expect.anything() }) &&
+ expect.objectContaining({
+ trace_id: expect.stringMatching(span1_traceId),
+ span_id: expect.stringMatching(span1_spanId),
+ }),
+ expect.objectContaining({
+ trace_id: expect.stringMatching(span2_traceId),
+ span_id: expect.stringMatching(span2_spanId),
+ attributes: expect.objectContaining({
+ 'sentry.link.type': 'previous_trace',
+ }),
+ }),
+ ]);
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('should link spans with addLink() in nested startSpan() calls', async () => {
+ await createRunner(__dirname, 'scenario-addLink-nested.ts')
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('parent1');
+
+ const parent1_traceId = event.contexts?.trace?.trace_id as string;
+ const parent1_spanId = event.contexts?.trace?.span_id as string;
+
+ const spans = event.spans || [];
+ const child1_1 = spans.find(span => span.description === 'child1.1');
+ const child1_2 = spans.find(span => span.description === 'child1.2');
+
+ expect(child1_1).toBeDefined();
+ expect(child1_1?.links).toEqual([
+ expect.objectContaining({
+ trace_id: expect.stringMatching(parent1_traceId),
+ span_id: expect.stringMatching(parent1_spanId),
+ attributes: expect.objectContaining({
+ 'sentry.link.type': 'previous_trace',
+ }),
+ }),
+ ]);
+
+ expect(child1_2).toBeDefined();
+ expect(child1_2?.links).toEqual([
+ expect.objectContaining({
+ trace_id: expect.stringMatching(parent1_traceId),
+ span_id: expect.stringMatching(parent1_spanId),
+ attributes: expect.objectContaining({
+ 'sentry.link.type': 'previous_trace',
+ }),
+ }),
+ ]);
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('should link spans with addLinks() in nested startSpan() calls', async () => {
+ await createRunner(__dirname, 'scenario-addLinks-nested.ts')
+ .expect({
+ transaction: event => {
+ expect(event.transaction).toBe('parent1');
+
+ const parent1_traceId = event.contexts?.trace?.trace_id as string;
+ const parent1_spanId = event.contexts?.trace?.span_id as string;
+
+ const spans = event.spans || [];
+ const child1_1 = spans.find(span => span.description === 'child1.1');
+ const child2_1 = spans.find(span => span.description === 'child2.1');
+
+ expect(child1_1).toBeDefined();
+
+ expect(child2_1).toBeDefined();
+
+ expect(child2_1?.links).toEqual([
+ expect.not.objectContaining({ attributes: expect.anything() }) &&
+ expect.objectContaining({
+ trace_id: expect.stringMatching(parent1_traceId),
+ span_id: expect.stringMatching(parent1_spanId),
+ }),
+ expect.objectContaining({
+ trace_id: expect.stringMatching(child1_1?.trace_id || 'non-existent-id-fallback'),
+ span_id: expect.stringMatching(child1_1?.span_id || 'non-existent-id-fallback'),
+ attributes: expect.objectContaining({
+ 'sentry.link.type': 'previous_trace',
+ }),
+ }),
+ ]);
+ },
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts
new file mode 100644
index 000000000000..0241785b0535
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+Sentry.startSpan({ name: 'parent' }, () => {
+ for (let i = 0; i < 5000; i++) {
+ Sentry.startInactiveSpan({ name: `child ${i}` }).end();
+ }
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts
new file mode 100644
index 000000000000..31b0af762d9a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts
@@ -0,0 +1,20 @@
+import type { SpanJSON } from '@sentry/core';
+import { expect, test } from 'vitest';
+import { createRunner } from '../../../utils/runner';
+
+test('it limits spans to 1000', async () => {
+ const expectedSpans: SpanJSON[] = [];
+ for (let i = 0; i < 1000; i++) {
+ expectedSpans.push(expect.objectContaining({ description: `child ${i}` }));
+ }
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'parent',
+ spans: expectedSpans,
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js
new file mode 100644
index 000000000000..517326720e58
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js
@@ -0,0 +1,23 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ beforeSend(event) {
+ event.contexts = {
+ ...event.contexts,
+ traceData: {
+ ...Sentry.getTraceData(),
+ metaTags: Sentry.getTraceMetaTags(),
+ },
+ };
+ return event;
+ },
+});
+
+setupOtel(client);
+
+Sentry.captureException(new Error('test error'));
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js
new file mode 100644
index 000000000000..5a030898467e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js
@@ -0,0 +1,31 @@
+const {
+ loggingTransport,
+ startExpressServerAndSendPortToRunner,
+} = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+
+const app = express();
+
+app.get('/test', (_req, res) => {
+ Sentry.withScope(scope => {
+ scope.setContext('traceData', {
+ ...Sentry.getTraceData(),
+ metaTags: Sentry.getTraceMetaTags(),
+ });
+ Sentry.captureException(new Error('test error 2'));
+ });
+ res.status(200).send();
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts
new file mode 100644
index 000000000000..7c6612a0f4f7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts
@@ -0,0 +1,67 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+describe('errors in TwP mode have same trace in trace context and getTraceData()', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ // In a request handler, the spanId is consistent inside of the request
+ test('in incoming request', async () => {
+ const runner = createRunner(__dirname, 'server.js')
+ .expect({
+ event: event => {
+ const { contexts } = event;
+ const { trace_id, span_id } = contexts?.trace || {};
+ expect(trace_id).toMatch(/^[a-f0-9]{32}$/);
+ expect(span_id).toMatch(/^[a-f0-9]{16}$/);
+
+ const traceData = contexts?.traceData || {};
+
+ expect(traceData['sentry-trace']).toEqual(`${trace_id}-${span_id}`);
+
+ expect(traceData.baggage).toContain(`sentry-trace_id=${trace_id}`);
+ expect(traceData.baggage).not.toContain('sentry-sampled=');
+
+ expect(traceData.metaTags).toContain(``);
+ expect(traceData.metaTags).toContain(`sentry-trace_id=${trace_id}`);
+ expect(traceData.metaTags).not.toContain('sentry-sampled=');
+ },
+ })
+ .start();
+ runner.makeRequest('get', '/test');
+ await runner.completed();
+ });
+
+ // Outside of a request handler, the spanId is random
+ test('outside of a request handler', async () => {
+ await createRunner(__dirname, 'no-server.js')
+ .expect({
+ event: event => {
+ const { contexts } = event;
+ const { trace_id, span_id } = contexts?.trace || {};
+ expect(trace_id).toMatch(/^[a-f0-9]{32}$/);
+ expect(span_id).toMatch(/^[a-f0-9]{16}$/);
+
+ const traceData = contexts?.traceData || {};
+
+ expect(traceData['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/);
+ expect(traceData['sentry-trace']).toContain(`${trace_id}-`);
+ // span_id is a random span ID
+ expect(traceData['sentry-trace']).not.toContain(span_id);
+
+ expect(traceData.baggage).toContain(`sentry-trace_id=${trace_id}`);
+ expect(traceData.baggage).not.toContain('sentry-sampled=');
+
+ expect(traceData.metaTags).toMatch(//);
+ expect(traceData.metaTags).toContain(`/);
+ expect(html).toContain('');
+ });
+
+ test('injects tags with new trace if no incoming headers', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+
+ const response = await runner.makeRequest<{ response: string }>('get', '/test');
+
+ const html = response?.response;
+
+ const traceId = html?.match(//)?.[1];
+ expect(traceId).not.toBeUndefined();
+
+ expect(html).toContain(' tags if SDK is disabled", async () => {
+ const traceId = 'cd7ee7a6fe3ebe7ab9c3271559bc203c';
+ const parentSpanId = '100ff0980e7a4ead';
+
+ const runner = createRunner(__dirname, 'server-sdk-disabled.js').start();
+
+ const response = await runner.makeRequest<{ response: string }>('get', '/test', {
+ headers: {
+ 'sentry-trace': `${traceId}-${parentSpanId}-1`,
+ baggage: 'sentry-environment=production',
+ },
+ });
+
+ const html = response?.response;
+
+ expect(html).not.toContain('"sentry-trace"');
+ expect(html).not.toContain('"baggage"');
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs
new file mode 100644
index 000000000000..8834d9742502
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs
@@ -0,0 +1,21 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+ tracesSampleRate: 0.0,
+ // Ensure this gets a correct hint
+ beforeBreadcrumb(breadcrumb, hint) {
+ breadcrumb.data = breadcrumb.data || {};
+ const req = hint?.request;
+ breadcrumb.data.ADDED_PATH = req?.path;
+ return breadcrumb;
+ },
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs
new file mode 100644
index 000000000000..50d1391ee577
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/node-core';
+
+async function run() {
+ Sentry.addBreadcrumb({ message: 'manual breadcrumb' });
+
+ await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
+
+ await Sentry.suppressTracing(() => fetch(`${process.env.SERVER_URL}/api/v4`).then(res => res.text()));
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts
new file mode 100644
index 000000000000..0d1d33bb5fc9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts
@@ -0,0 +1,83 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing fetch', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing fetch requests create breadcrumbs', async () => {
+ const [SERVER_URL, closeTestServer] = await createTestServer().start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ breadcrumbs: [
+ {
+ message: 'manual breadcrumb',
+ timestamp: expect.any(Number),
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v0`,
+ status_code: 404,
+ ADDED_PATH: '/api/v0',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v1`,
+ status_code: 404,
+ ADDED_PATH: '/api/v1',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v2`,
+ status_code: 404,
+ ADDED_PATH: '/api/v2',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v3`,
+ status_code: 404,
+ ADDED_PATH: '/api/v3',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ],
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs
new file mode 100644
index 000000000000..687969d7ec1b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [Sentry.nativeNodeFetchIntegration({ spans: false })],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs
new file mode 100644
index 000000000000..dce36bdb9262
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/node-core';
+
+async function run() {
+ await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts
new file mode 100644
index 000000000000..f61532d9de8b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts
@@ -0,0 +1,50 @@
+import { describe, expect } from 'vitest';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing fetch', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('outgoing fetch requests are correctly instrumented with tracing & spans are disabled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v1', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer;
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs
new file mode 100644
index 000000000000..b2c76f80e13a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs
new file mode 100644
index 000000000000..dce36bdb9262
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/node-core';
+
+async function run() {
+ await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts
new file mode 100644
index 000000000000..b4594c4d9c41
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts
@@ -0,0 +1,50 @@
+import { describe, expect } from 'vitest';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing fetch', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('outgoing fetch requests are correctly instrumented with tracing disabled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v1', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs
new file mode 100644
index 000000000000..fea0bfd36c11
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 1.0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs
new file mode 100644
index 000000000000..dce36bdb9262
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/node-core';
+
+async function run() {
+ await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts
new file mode 100644
index 000000000000..32f24517b3f6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts
@@ -0,0 +1,50 @@
+import { describe, expect } from 'vitest';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing fetch', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('outgoing sampled fetch requests without active span are correctly instrumented', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs
new file mode 100644
index 000000000000..0c77fb8702b7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs
new file mode 100644
index 000000000000..38735e01aaa8
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs
@@ -0,0 +1,15 @@
+import * as Sentry from '@sentry/node-core';
+
+async function run() {
+ // Wrap in span that is not sampled
+ await Sentry.startSpan({ name: 'outer' }, async () => {
+ await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
+ await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
+ });
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts
new file mode 100644
index 000000000000..097236ba4e7f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts
@@ -0,0 +1,50 @@
+import { describe, expect } from 'vitest';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing fetch', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('outgoing fetch requests are correctly instrumented when not sampled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs
new file mode 100644
index 000000000000..1465fc45ca46
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+ // Ensure this gets a correct hint
+ beforeBreadcrumb(breadcrumb, hint) {
+ breadcrumb.data = breadcrumb.data || {};
+ const req = hint?.request;
+ breadcrumb.data.ADDED_PATH = req?.path;
+ return breadcrumb;
+ },
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs
new file mode 100644
index 000000000000..746e6487281a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs
@@ -0,0 +1,45 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+async function run() {
+ Sentry.addBreadcrumb({ message: 'manual breadcrumb' });
+
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpGet(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+
+ await Sentry.suppressTracing(() => makeHttpRequest(`${process.env.SERVER_URL}/api/v4`));
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
+
+function makeHttpGet(url) {
+ return new Promise(resolve => {
+ http.get(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ });
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts
new file mode 100644
index 000000000000..318d4628453b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts
@@ -0,0 +1,79 @@
+import { describe, expect } from 'vitest';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('outgoing http requests create breadcrumbs', async () => {
+ const [SERVER_URL, closeTestServer] = await createTestServer().start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ breadcrumbs: [
+ {
+ message: 'manual breadcrumb',
+ timestamp: expect.any(Number),
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v0`,
+ status_code: 404,
+ ADDED_PATH: '/api/v0',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v1`,
+ status_code: 404,
+ ADDED_PATH: '/api/v1',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v2`,
+ status_code: 404,
+ ADDED_PATH: '/api/v2',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v3`,
+ status_code: 404,
+ ADDED_PATH: '/api/v3',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ],
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs
new file mode 100644
index 000000000000..61706a36eca6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [Sentry.httpIntegration({ spans: false })],
+ transport: loggingTransport,
+ // Ensure this gets a correct hint
+ beforeBreadcrumb(breadcrumb, hint) {
+ breadcrumb.data = breadcrumb.data || {};
+ const req = hint?.request;
+ breadcrumb.data.ADDED_PATH = req?.path;
+ return breadcrumb;
+ },
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs
new file mode 100644
index 000000000000..861d6c29bd2f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs
@@ -0,0 +1,43 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+async function run() {
+ Sentry.addBreadcrumb({ message: 'manual breadcrumb' });
+
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpGet(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
+
+function makeHttpGet(url) {
+ return new Promise(resolve => {
+ http.get(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ });
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts
new file mode 100644
index 000000000000..fe9cba032344
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts
@@ -0,0 +1,202 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http requests with tracing & spans disabled', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing http requests are correctly instrumented with tracing & spans disabled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v1', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ breadcrumbs: [
+ {
+ message: 'manual breadcrumb',
+ timestamp: expect.any(Number),
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v0`,
+ status_code: 200,
+ ADDED_PATH: '/api/v0',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v1`,
+ status_code: 200,
+ ADDED_PATH: '/api/v1',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v2`,
+ status_code: 200,
+ ADDED_PATH: '/api/v2',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v3`,
+ status_code: 200,
+ ADDED_PATH: '/api/v3',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+
+ closeTestServer();
+ });
+ });
+
+ // On older node versions, outgoing requests do not get trace-headers injected, sadly
+ // This is because the necessary diagnostics channel hook is not available yet
+ conditionalTest({ max: 21 })('node <22', () => {
+ test('outgoing http requests generate breadcrumbs correctly with tracing & spans disabled', async () => {
+ expect.assertions(9);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ // This is not instrumented, sadly
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v1', headers => {
+ // This is not instrumented, sadly
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ breadcrumbs: [
+ {
+ message: 'manual breadcrumb',
+ timestamp: expect.any(Number),
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v0`,
+ status_code: 200,
+ ADDED_PATH: '/api/v0',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v1`,
+ status_code: 200,
+ ADDED_PATH: '/api/v1',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v2`,
+ status_code: 200,
+ ADDED_PATH: '/api/v2',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v3`,
+ status_code: 200,
+ ADDED_PATH: '/api/v3',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs
new file mode 100644
index 000000000000..1465fc45ca46
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+ // Ensure this gets a correct hint
+ beforeBreadcrumb(breadcrumb, hint) {
+ breadcrumb.data = breadcrumb.data || {};
+ const req = hint?.request;
+ breadcrumb.data.ADDED_PATH = req?.path;
+ return breadcrumb;
+ },
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs
new file mode 100644
index 000000000000..861d6c29bd2f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs
@@ -0,0 +1,43 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+async function run() {
+ Sentry.addBreadcrumb({ message: 'manual breadcrumb' });
+
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpGet(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
+
+function makeHttpGet(url) {
+ return new Promise(resolve => {
+ http.get(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ });
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts
new file mode 100644
index 000000000000..8727f1cad0de
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts
@@ -0,0 +1,103 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing http requests are correctly instrumented with tracing disabled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v1', headers => {
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ expect(headers['baggage']).toEqual(expect.any(String));
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ breadcrumbs: [
+ {
+ message: 'manual breadcrumb',
+ timestamp: expect.any(Number),
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v0`,
+ status_code: 200,
+ ADDED_PATH: '/api/v0',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v1`,
+ status_code: 200,
+ ADDED_PATH: '/api/v1',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v2`,
+ status_code: 200,
+ ADDED_PATH: '/api/v2',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ {
+ category: 'http',
+ data: {
+ 'http.method': 'GET',
+ url: `${SERVER_URL}/api/v3`,
+ status_code: 200,
+ ADDED_PATH: '/api/v3',
+ },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs
new file mode 100644
index 000000000000..33213733efef
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs
new file mode 100644
index 000000000000..f1603c6dcd8b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs
@@ -0,0 +1,28 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+async function run() {
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts
new file mode 100644
index 000000000000..d89992dd362e
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts
@@ -0,0 +1,53 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing sampled http requests without active span are correctly instrumented', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs
new file mode 100644
index 000000000000..33213733efef
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs
new file mode 100644
index 000000000000..18f508d309a2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+Sentry.startSpan({ name: 'test_span' }, async () => {
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+});
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts
new file mode 100644
index 000000000000..1189afc502e5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts
@@ -0,0 +1,46 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing sampled http requests are correctly instrumented', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ transaction: {
+ // we're not too concerned with the actual transaction here since this is tested elsewhere
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs
new file mode 100644
index 000000000000..0c77fb8702b7
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel.js';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ tracesSampleRate: 0,
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs
new file mode 100644
index 000000000000..e470ae986985
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs
@@ -0,0 +1,31 @@
+import * as Sentry from '@sentry/node-core';
+import * as http from 'http';
+
+async function run() {
+ // Wrap in span that is not sampled
+ await Sentry.startSpan({ name: 'outer' }, async () => {
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+ });
+
+ Sentry.captureException(new Error('foo'));
+}
+
+run();
+
+function makeHttpRequest(url) {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts
new file mode 100644
index 000000000000..60d3345dcb51
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts
@@ -0,0 +1,53 @@
+import { describe, expect } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { createEsmAndCjsTests } from '../../../../utils/runner';
+import { createTestServer } from '../../../../utils/server';
+
+describe('outgoing http', () => {
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ conditionalTest({ min: 22 })('node >=22', () => {
+ test('outgoing http requests are correctly instrumented when not sampled', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner()
+ .withEnv({ SERVER_URL })
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'foo',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+ });
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js
new file mode 100644
index 000000000000..81cc0fc8829c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js
@@ -0,0 +1,41 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ tracesSampleRate: 0.00000001, // It's important that this is not 1, so that we also check logic for NonRecordingSpans, which is usually the edge-case
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const {
+ startExpressServerAndSendPortToRunner,
+ getPortAppIsRunningOn,
+} = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/check', (req, res) => {
+ const appPort = getPortAppIsRunningOn(app);
+ // eslint-disable-next-line no-undef
+ fetch(`http://localhost:${appPort}/bounce`)
+ .then(r => r.json())
+ .then(bounceRes => {
+ res.json({ propagatedData: bounceRes });
+ });
+});
+
+app.get('/bounce', (req, res) => {
+ res.json({
+ baggage: req.headers['baggage'],
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts
new file mode 100644
index 000000000000..40001a9f62f5
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts
@@ -0,0 +1,82 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+describe('sample_rand propagation', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('propagates a sample rand when there is a sentry-trace header and incoming sentry baggage', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-release=foo,sentry-sample_rand=0.424242',
+ },
+ });
+ expect(response).toEqual({
+ propagatedData: {
+ baggage: expect.stringMatching(/sentry-sample_rand=0\.424242/),
+ },
+ });
+ });
+
+ test('does not propagate a sample rand when there is an incoming sentry-trace header but no baggage header', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ },
+ });
+ expect(response).toEqual({
+ propagatedData: {
+ baggage: expect.not.stringMatching(/sentry-sample_rand=0\.[0-9]+/),
+ },
+ });
+ });
+
+ test('propagates a sample_rand that would lead to a positive sampling decision when there is an incoming positive sampling decision but no sample_rand in the baggage header', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-sample_rate=0.25',
+ },
+ });
+
+ const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]);
+
+ expect(sampleRand).toStrictEqual(expect.any(Number));
+ expect(sampleRand).not.toBeNaN();
+ expect(sampleRand).toBeLessThan(0.25);
+ expect(sampleRand).toBeGreaterThanOrEqual(0);
+ });
+
+ test('propagates a sample_rand that would lead to a negative sampling decision when there is an incoming negative sampling decision but no sample_rand in the baggage header', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0',
+ baggage: 'sentry-sample_rate=0.75',
+ },
+ });
+
+ const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]);
+
+ expect(sampleRand).toStrictEqual(expect.any(Number));
+ expect(sampleRand).not.toBeNaN();
+ expect(sampleRand).toBeGreaterThanOrEqual(0.75);
+ expect(sampleRand).toBeLessThan(1);
+ });
+
+ test('no sample_rand when there is no sentry-trace header but a baggage header with sample_rand', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ baggage: 'sentry-sample_rate=0.75,sentry-sample_rand=0.5',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rand=0\.[0-9]+/);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js
new file mode 100644
index 000000000000..42121915d601
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js
@@ -0,0 +1,40 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const {
+ startExpressServerAndSendPortToRunner,
+ getPortAppIsRunningOn,
+} = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/check', (req, res) => {
+ const appPort = getPortAppIsRunningOn(app);
+ // eslint-disable-next-line no-undef
+ fetch(`http://localhost:${appPort}/bounce`)
+ .then(r => r.json())
+ .then(bounceRes => {
+ res.json({ propagatedData: bounceRes });
+ });
+});
+
+app.get('/bounce', (req, res) => {
+ res.json({
+ baggage: req.headers['baggage'],
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts
new file mode 100644
index 000000000000..b3040dc0cfa4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts
@@ -0,0 +1,26 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('parentSampleRate propagation with no tracing enabled', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should propagate an incoming sample rate', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+
+ test('should not propagate a sample rate for root traces', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check');
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate/);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js
new file mode 100644
index 000000000000..6c7042813246
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js
@@ -0,0 +1,41 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ tracesSampleRate: 0,
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const {
+ startExpressServerAndSendPortToRunner,
+ getPortAppIsRunningOn,
+} = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/check', (req, res) => {
+ const appPort = getPortAppIsRunningOn(app);
+ // eslint-disable-next-line no-undef
+ fetch(`http://localhost:${appPort}/bounce`)
+ .then(r => r.json())
+ .then(bounceRes => {
+ res.json({ propagatedData: bounceRes });
+ });
+});
+
+app.get('/bounce', (req, res) => {
+ res.json({
+ baggage: req.headers['baggage'],
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts
new file mode 100644
index 000000000000..219e82dfeb12
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts
@@ -0,0 +1,62 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('parentSampleRate propagation with tracesSampleRate=0', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+
+ test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+
+ test('should not propagate a sample rate when receiving a trace without sampling decision and sample rate', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac',
+ baggage: '',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0/);
+ });
+
+ test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/);
+ });
+
+ test('should not propagate configured sample rate when there is no incoming trace', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check');
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0/);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js
new file mode 100644
index 000000000000..a0ffeb98867a
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js
@@ -0,0 +1,41 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ tracesSampleRate: 0.69,
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const {
+ startExpressServerAndSendPortToRunner,
+ getPortAppIsRunningOn,
+} = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/check', (req, res) => {
+ const appPort = getPortAppIsRunningOn(app);
+ // eslint-disable-next-line no-undef
+ fetch(`http://localhost:${appPort}/bounce`)
+ .then(r => r.json())
+ .then(bounceRes => {
+ res.json({ propagatedData: bounceRes });
+ });
+});
+
+app.get('/bounce', (req, res) => {
+ res.json({
+ baggage: req.headers['baggage'],
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts
new file mode 100644
index 000000000000..147b4c13a1e1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts
@@ -0,0 +1,62 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('parentSampleRate propagation with tracesSampleRate', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+
+ test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+
+ test('should not propagate configured sample rate when receiving a trace without sampling decision and sample rate', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac',
+ baggage: '',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/);
+ });
+
+ test('should not propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/);
+ });
+
+ test('should not propagate configured sample rate when there is no incoming trace', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check');
+ expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js
new file mode 100644
index 000000000000..a4595dd8f3c8
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js
@@ -0,0 +1,55 @@
+const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
+const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
+const Sentry = require('@sentry/node-core');
+const { setupOtel } = require('../../../../utils/setupOtel.js');
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ tracesSampler: ({ inheritOrSampleWith }) => {
+ return inheritOrSampleWith(0.69);
+ },
+ openTelemetryInstrumentations: [new HttpInstrumentation()],
+});
+
+setupOtel(client);
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const {
+ startExpressServerAndSendPortToRunner,
+ getPortAppIsRunningOn,
+} = require('@sentry-internal/node-core-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/check', (req, res) => {
+ Sentry.startSpan({ name: 'check-endpoint' }, async () => {
+ const appPort = getPortAppIsRunningOn(app);
+ try {
+ // eslint-disable-next-line no-undef
+ const response = await fetch(`http://localhost:${appPort}/bounce`);
+ const bounceRes = await response.json();
+ // eslint-disable-next-line no-console
+ console.log('Bounce response:', bounceRes);
+ res.json({ propagatedData: bounceRes });
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('Error fetching bounce:', err);
+ res.status(500).json({ error: err.message });
+ }
+ });
+});
+
+app.get('/bounce', (req, res) => {
+ // eslint-disable-next-line no-console
+ console.log('Bounce headers:', req.headers);
+ res.json({
+ baggage: req.headers['baggage'],
+ });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts
new file mode 100644
index 000000000000..ffab071bbc26
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts
@@ -0,0 +1,62 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('parentSampleRate propagation with tracesSampler and OpenTelemetry HTTP instrumentation', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming trace', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check');
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/);
+ });
+
+ test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (1 -> because there is a positive sampling decision and inheritOrSampleWith was used)', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: '',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=1/);
+ });
+
+ test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (0 -> because there is a negative sampling decision and inheritOrSampleWith was used)', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0',
+ baggage: '',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/);
+ });
+
+ test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (the fallback value -> because there is no sampling decision and inheritOrSampleWith was used)', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac',
+ baggage: '',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/);
+ });
+
+ test('should propagate sample_rate equivalent to incoming sample_rate (because tracesSampler is configured that way)', async () => {
+ const runner = createRunner(__dirname, 'server.js').start();
+ const response = await runner.makeRequest('get', '/check', {
+ headers: {
+ 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1',
+ baggage: 'sentry-sample_rate=0.1337',
+ },
+ });
+
+ expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/);
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts
new file mode 100644
index 000000000000..2676a2f77bef
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
+
+const client = Sentry.init({
+ dsn: 'https://public@o01234987.ingest.sentry.io/1337',
+ release: '1.0',
+ environment: 'prod',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+import cors from 'cors';
+import express from 'express';
+import * as http from 'http';
+
+const app = express();
+
+app.use(cors());
+
+app.get('/test/express', (_req, res) => {
+ const headers = http
+ .get({
+ hostname: 'example.com',
+ })
+ .getHeaders();
+
+ res.send({ test_data: headers });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts
new file mode 100644
index 000000000000..e291ab122ba1
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
+
+const client = Sentry.init({
+ dsn: 'https://public@public.ingest.sentry.io/1337',
+ release: '1.0',
+ environment: 'prod',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+import cors from 'cors';
+import express from 'express';
+import * as http from 'http';
+
+const app = express();
+
+app.use(cors());
+
+app.get('/test/express', (_req, res) => {
+ const headers = http
+ .get({
+ hostname: 'example.com',
+ })
+ .getHeaders();
+
+ res.send({ test_data: headers });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts
new file mode 100644
index 000000000000..5e4c9a7ea3dc
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../../utils/setupOtel';
+
+export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
+
+const client = Sentry.init({
+ dsn: 'https://public@o0000987.ingest.sentry.io/1337',
+ release: '1.0',
+ environment: 'prod',
+ orgId: '01234987',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+import cors from 'cors';
+import express from 'express';
+import * as http from 'http';
+
+const app = express();
+
+app.use(cors());
+
+app.get('/test/express', (_req, res) => {
+ const headers = http
+ .get({
+ hostname: 'example.com',
+ })
+ .getHeaders();
+
+ res.send({ test_data: headers });
+});
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts
new file mode 100644
index 000000000000..1c1fa4e6cf5f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts
@@ -0,0 +1,42 @@
+import { afterAll, expect, test } from 'vitest';
+import { conditionalTest } from '../../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+import type { TestAPIResponse } from './server';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+// This test requires Node.js 22+ because it depends on the 'http.client.request.created'
+// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+
+conditionalTest({ min: 22 })('node >=22', () => {
+ test('should include explicitly set org_id in the baggage header', async () => {
+ const runner = createRunner(__dirname, 'server.ts').start();
+
+ const response = await runner.makeRequest('get', '/test/express');
+ expect(response).toBeDefined();
+
+ const baggage = response?.test_data.baggage;
+ expect(baggage).toContain('sentry-org_id=01234987');
+ });
+
+ test('should extract org_id from DSN host when not explicitly set', async () => {
+ const runner = createRunner(__dirname, 'server-no-explicit-org-id.ts').start();
+
+ const response = await runner.makeRequest('get', '/test/express');
+ expect(response).toBeDefined();
+
+ const baggage = response?.test_data.baggage;
+ expect(baggage).toContain('sentry-org_id=01234987');
+ });
+
+ test('should set undefined org_id when it cannot be extracted', async () => {
+ const runner = createRunner(__dirname, 'server-no-org-id.ts').start();
+
+ const response = await runner.makeRequest('get', '/test/express');
+ expect(response).toBeDefined();
+
+ const baggage = response?.test_data.baggage;
+ expect(baggage).not.toContain('sentry-org_id');
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts
new file mode 100644
index 000000000000..52fc94f0496f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts
@@ -0,0 +1,39 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-core-integration-tests';
+import { setupOtel } from '../../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [],
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+import * as http from 'http';
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+Sentry.startSpan({ name: 'test_span' }, async () => {
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
+ await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
+});
+
+function makeHttpRequest(url: string): Promise {
+ return new Promise(resolve => {
+ http
+ .request(url, httpRes => {
+ httpRes.on('data', () => {
+ // we don't care about data
+ });
+ httpRes.on('end', () => {
+ resolve();
+ });
+ })
+ .end();
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts
new file mode 100644
index 000000000000..8ae06f883b38
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts
@@ -0,0 +1,44 @@
+import { expect, test } from 'vitest';
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
+import { createTestServer } from '../../../utils/server';
+
+// This test requires Node.js 22+ because it depends on the 'http.client.request.created'
+// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+
+conditionalTest({ min: 22 })('node >=22', () => {
+ test('SentryHttpIntegration should instrument correct requests when tracePropagationTargets option is provided', async () => {
+ expect.assertions(11);
+
+ const [SERVER_URL, closeTestServer] = await createTestServer()
+ .get('/api/v0', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1');
+ })
+ .get('/api/v1', headers => {
+ expect(headers['baggage']).toEqual(expect.any(String));
+ expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/));
+ expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1');
+ })
+ .get('/api/v2', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .get('/api/v3', headers => {
+ expect(headers['baggage']).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+ })
+ .start();
+
+ await createRunner(__dirname, 'scenario.ts')
+ .withEnv({ SERVER_URL })
+ .expect({
+ transaction: {
+ // we're not too concerned with the actual transaction here since this is tested elsewhere
+ },
+ })
+ .start()
+ .completed();
+ closeTestServer();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/suites/tsconfig.json b/dev-packages/node-core-integration-tests/suites/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/dev-packages/node-core-integration-tests/suites/winston/subject.ts b/dev-packages/node-core-integration-tests/suites/winston/subject.ts
new file mode 100644
index 000000000000..c8840b855f9b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/winston/subject.ts
@@ -0,0 +1,76 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import winston from 'winston';
+import Transport from 'winston-transport';
+import { setupOtel } from '../../utils/setupOtel';
+
+const client = Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0.0',
+ environment: 'test',
+ _experiments: {
+ enableLogs: true,
+ },
+ transport: loggingTransport,
+});
+
+setupOtel(client);
+
+async function run(): Promise {
+ // Create a custom transport that extends winston-transport
+ const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport);
+
+ // Create logger with default levels
+ const logger = winston.createLogger({
+ transports: [new SentryWinstonTransport()],
+ });
+
+ // Test basic logging
+ logger.info('Test info message');
+ logger.error('Test error message');
+
+ // If custom levels are requested
+ if (process.env.CUSTOM_LEVELS === 'true') {
+ const customLevels = {
+ levels: {
+ error: 0,
+ warn: 1,
+ info: 2,
+ http: 3,
+ verbose: 4,
+ debug: 5,
+ silly: 6,
+ },
+ colors: {
+ error: 'red',
+ warn: 'yellow',
+ info: 'green',
+ http: 'magenta',
+ verbose: 'cyan',
+ debug: 'blue',
+ silly: 'grey',
+ },
+ };
+
+ const customLogger = winston.createLogger({
+ levels: customLevels.levels,
+ transports: [new SentryWinstonTransport()],
+ });
+
+ customLogger.info('Test info message');
+ customLogger.error('Test error message');
+ }
+
+ // If metadata is requested
+ if (process.env.WITH_METADATA === 'true') {
+ logger.info('Test message with metadata', {
+ foo: 'bar',
+ number: 42,
+ });
+ }
+
+ await Sentry.flush();
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+void run();
diff --git a/dev-packages/node-core-integration-tests/suites/winston/test.ts b/dev-packages/node-core-integration-tests/suites/winston/test.ts
new file mode 100644
index 000000000000..034210f8690b
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/winston/test.ts
@@ -0,0 +1,186 @@
+import { afterAll, describe, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+describe('winston integration', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should capture winston logs with default levels', async () => {
+ const runner = createRunner(__dirname, 'subject.ts')
+ .expect({
+ log: {
+ items: [
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'Test info message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'Test error message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ ],
+ },
+ })
+ .start();
+
+ await runner.completed();
+ });
+
+ test('should capture winston logs with custom levels', async () => {
+ const runner = createRunner(__dirname, 'subject.ts')
+ .withEnv({ CUSTOM_LEVELS: 'true' })
+ .expect({
+ log: {
+ items: [
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'Test info message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'Test error message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'Test info message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'Test error message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ ],
+ },
+ })
+ .start();
+
+ await runner.completed();
+ });
+
+ test('should capture winston logs with metadata', async () => {
+ const runner = createRunner(__dirname, 'subject.ts')
+ .withEnv({ WITH_METADATA: 'true' })
+ .expect({
+ log: {
+ items: [
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'Test info message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'Test error message',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ },
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'Test message with metadata',
+ severity_number: expect.any(Number),
+ trace_id: expect.any(String),
+ attributes: {
+ 'sentry.origin': { value: 'auto.logging.winston', type: 'string' },
+ 'sentry.release': { value: '1.0.0', type: 'string' },
+ 'sentry.environment': { value: 'test', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ 'server.address': { value: expect.any(String), type: 'string' },
+ foo: { value: 'bar', type: 'string' },
+ number: { value: 42, type: 'integer' },
+ },
+ },
+ ],
+ },
+ })
+ .start();
+
+ await runner.completed();
+ });
+});
diff --git a/dev-packages/node-core-integration-tests/test.txt b/dev-packages/node-core-integration-tests/test.txt
new file mode 100644
index 000000000000..0a0fa7f94de9
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/test.txt
@@ -0,0 +1,213 @@
+yarn run v1.22.22
+$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak
+ console.log
+ starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line null
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+Done in 4.21s.
diff --git a/dev-packages/node-core-integration-tests/tsconfig.json b/dev-packages/node-core-integration-tests/tsconfig.json
new file mode 100644
index 000000000000..1cd6c0aec734
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["utils/**/*.ts", "src/**/*.ts"],
+
+ "compilerOptions": {
+ // Although this seems wrong to include `DOM` here, it's necessary to make
+ // global fetch available in tests in lower Node versions.
+ "lib": ["DOM", "ES2018"],
+ // package-specific options
+ "esModuleInterop": true,
+ "types": ["node"]
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/tsconfig.test.json b/dev-packages/node-core-integration-tests/tsconfig.test.json
new file mode 100644
index 000000000000..45a6e39b0054
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/tsconfig.test.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["suites/**/*.ts", "vite.config.ts"],
+
+ "compilerOptions": {
+ // Although this seems wrong to include `DOM` here, it's necessary to make
+ // global fetch available in tests in lower Node versions.
+ "lib": ["DOM", "ES2018"],
+ // should include all types from `./tsconfig.json` plus types for all test frameworks used
+ "types": ["node"]
+
+ // other package-specific, test-specific options
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/tsconfig.types.json b/dev-packages/node-core-integration-tests/tsconfig.types.json
new file mode 100644
index 000000000000..65455f66bd75
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/types"
+ }
+}
diff --git a/dev-packages/node-core-integration-tests/utils/assertions.ts b/dev-packages/node-core-integration-tests/utils/assertions.ts
new file mode 100644
index 000000000000..296bdc608bb4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/assertions.ts
@@ -0,0 +1,89 @@
+import type {
+ ClientReport,
+ Envelope,
+ Event,
+ SerializedCheckIn,
+ SerializedLogContainer,
+ SerializedSession,
+ SessionAggregates,
+ TransactionEvent,
+} from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
+import { expect } from 'vitest';
+
+/**
+ * Asserts against a Sentry Event ignoring non-deterministic properties
+ *
+ * @param {Record} actual
+ * @param {Record} expected
+ */
+export const assertSentryEvent = (actual: Event, expected: Record): void => {
+ expect(actual).toMatchObject({
+ event_id: expect.any(String),
+ ...expected,
+ });
+};
+
+/**
+ * Asserts against a Sentry Transaction ignoring non-deterministic properties
+ *
+ * @param {Record} actual
+ * @param {Record} expected
+ */
+export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => {
+ expect(actual).toMatchObject({
+ event_id: expect.any(String),
+ timestamp: expect.anything(),
+ start_timestamp: expect.anything(),
+ spans: expect.any(Array),
+ type: 'transaction',
+ ...expected,
+ });
+};
+
+export function assertSentrySession(actual: SerializedSession, expected: Partial): void {
+ expect(actual).toMatchObject({
+ sid: expect.any(String),
+ ...expected,
+ });
+}
+
+export function assertSentrySessions(actual: SessionAggregates, expected: Partial): void {
+ expect(actual).toMatchObject({
+ ...expected,
+ });
+}
+
+export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void {
+ expect(actual).toMatchObject({
+ check_in_id: expect.any(String),
+ ...expected,
+ });
+}
+
+export function assertSentryClientReport(actual: ClientReport, expected: Partial): void {
+ expect(actual).toMatchObject({
+ ...expected,
+ });
+}
+
+export function assertSentryLogContainer(
+ actual: SerializedLogContainer,
+ expected: Partial,
+): void {
+ expect(actual).toMatchObject({
+ ...expected,
+ });
+}
+
+export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void {
+ expect(actual).toEqual({
+ event_id: expect.any(String),
+ sent_at: expect.any(String),
+ sdk: {
+ name: 'sentry.javascript.node',
+ version: SDK_VERSION,
+ },
+ ...expected,
+ });
+}
diff --git a/dev-packages/node-core-integration-tests/utils/index.ts b/dev-packages/node-core-integration-tests/utils/index.ts
new file mode 100644
index 000000000000..e08d89a92131
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/index.ts
@@ -0,0 +1,57 @@
+import type { EnvelopeItemType } from '@sentry/core';
+import { parseSemver } from '@sentry/core';
+import type * as http from 'http';
+import { describe } from 'vitest';
+
+const NODE_VERSION = parseSemver(process.versions.node).major;
+
+export type TestServerConfig = {
+ url: string;
+ server: http.Server;
+};
+
+export type DataCollectorOptions = {
+ // Optional custom URL
+ url?: string;
+
+ // The expected amount of requests to the envelope endpoint.
+ // If the amount of sent requests is lower than `count`, this function will not resolve.
+ count?: number;
+
+ // The method of the request.
+ method?: 'get' | 'post';
+
+ // Whether to stop the server after the requests have been intercepted
+ endServer?: boolean;
+
+ // Type(s) of the envelopes to capture
+ envelopeType?: EnvelopeItemType | EnvelopeItemType[];
+};
+
+/**
+ * Returns`describe` or `describe.skip` depending on allowed major versions of Node.
+ *
+ * @param {{ min?: number; max?: number }} allowedVersion
+ */
+export function conditionalTest(allowedVersion: {
+ min?: number;
+ max?: number;
+}): typeof describe | typeof describe.skip {
+ if (!NODE_VERSION) {
+ return describe.skip;
+ }
+
+ return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity)
+ ? describe.skip
+ : describe;
+}
+
+/**
+ * Parses response body containing an Envelope
+ *
+ * @param {string} body
+ * @return {*} {Array>}
+ */
+export const parseEnvelope = (body: string): Array> => {
+ return body.split('\n').map(e => JSON.parse(e));
+};
diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts
new file mode 100644
index 000000000000..97b1efa2dbb4
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/runner.ts
@@ -0,0 +1,683 @@
+/* eslint-disable max-lines */
+import type {
+ ClientReport,
+ Envelope,
+ EnvelopeItemType,
+ Event,
+ EventEnvelope,
+ SerializedCheckIn,
+ SerializedLogContainer,
+ SerializedSession,
+ SessionAggregates,
+ TransactionEvent,
+} from '@sentry/core';
+import { normalize } from '@sentry/core';
+import { execSync, spawn, spawnSync } from 'child_process';
+import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
+import { join } from 'path';
+import { afterAll, beforeAll, describe, test } from 'vitest';
+import {
+ assertEnvelopeHeader,
+ assertSentryCheckIn,
+ assertSentryClientReport,
+ assertSentryEvent,
+ assertSentryLogContainer,
+ assertSentrySession,
+ assertSentrySessions,
+ assertSentryTransaction,
+} from './assertions';
+import { createBasicSentryServer } from './server';
+
+const CLEANUP_STEPS = new Set();
+
+export function cleanupChildProcesses(): void {
+ for (const step of CLEANUP_STEPS) {
+ step();
+ }
+ CLEANUP_STEPS.clear();
+}
+
+process.on('exit', cleanupChildProcesses);
+
+/** Promise only resolves when fn returns true */
+async function waitFor(fn: () => boolean, timeout = 10_000, message = 'Timed out waiting'): Promise {
+ let remaining = timeout;
+ while (fn() === false) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ remaining -= 100;
+ if (remaining < 0) {
+ throw new Error(message);
+ }
+ }
+}
+
+type VoidFunction = () => void;
+
+interface DockerOptions {
+ /**
+ * The working directory to run docker compose in
+ */
+ workingDirectory: string[];
+ /**
+ * The strings to look for in the output to know that the docker compose is ready for the test to be run
+ */
+ readyMatches: string[];
+ /**
+ * The command to run after docker compose is up
+ */
+ setupCommand?: string;
+}
+
+/**
+ * Runs docker compose up and waits for the readyMatches to appear in the output
+ *
+ * Returns a function that can be called to docker compose down
+ */
+async function runDockerCompose(options: DockerOptions): Promise {
+ return new Promise((resolve, reject) => {
+ const cwd = join(...options.workingDirectory);
+ const close = (): void => {
+ spawnSync('docker', ['compose', 'down', '--volumes'], {
+ cwd,
+ stdio: process.env.DEBUG ? 'inherit' : undefined,
+ });
+ };
+
+ // ensure we're starting fresh
+ close();
+
+ const child = spawn('docker', ['compose', 'up'], { cwd });
+
+ const timeout = setTimeout(() => {
+ close();
+ reject(new Error('Timed out waiting for docker-compose'));
+ }, 75_000);
+
+ function newData(data: Buffer): void {
+ const text = data.toString('utf8');
+
+ if (process.env.DEBUG) log(text);
+
+ for (const match of options.readyMatches) {
+ if (text.includes(match)) {
+ child.stdout.removeAllListeners();
+ clearTimeout(timeout);
+ if (options.setupCommand) {
+ execSync(options.setupCommand, { cwd, stdio: 'inherit' });
+ }
+ resolve(close);
+ }
+ }
+ }
+
+ child.stdout.on('data', newData);
+ child.stderr.on('data', newData);
+ });
+}
+
+type ExpectedEvent = Partial | ((event: Event) => void);
+type ExpectedTransaction = Partial | ((event: TransactionEvent) => void);
+type ExpectedSession = Partial | ((event: SerializedSession) => void);
+type ExpectedSessions = Partial | ((event: SessionAggregates) => void);
+type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) => void);
+type ExpectedClientReport = Partial | ((event: ClientReport) => void);
+type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void);
+
+type Expected =
+ | {
+ event: ExpectedEvent;
+ }
+ | {
+ transaction: ExpectedTransaction;
+ }
+ | {
+ session: ExpectedSession;
+ }
+ | {
+ sessions: ExpectedSessions;
+ }
+ | {
+ check_in: ExpectedCheckIn;
+ }
+ | {
+ client_report: ExpectedClientReport;
+ }
+ | {
+ log: ExpectedLogContainer;
+ };
+
+type ExpectedEnvelopeHeader =
+ | { event: Partial }
+ | { transaction: Partial }
+ | { session: Partial }
+ | { sessions: Partial }
+ | { log: Partial };
+
+type StartResult = {
+ completed(): Promise;
+ childHasExited(): boolean;
+ getLogs(): string[];
+ makeRequest(
+ method: 'get' | 'post',
+ path: string,
+ options?: { headers?: Record; data?: BodyInit; expectError?: boolean },
+ ): Promise;
+};
+
+export function createEsmAndCjsTests(
+ cwd: string,
+ scenarioPath: string,
+ instrumentPath: string,
+ callback: (
+ createTestRunner: () => ReturnType,
+ testFn: typeof test | typeof test.fails,
+ mode: 'esm' | 'cjs',
+ ) => void,
+ options?: { failsOnCjs?: boolean; failsOnEsm?: boolean },
+): void {
+ const mjsScenarioPath = join(cwd, scenarioPath);
+ const mjsInstrumentPath = join(cwd, instrumentPath);
+
+ if (!mjsScenarioPath.endsWith('.mjs')) {
+ throw new Error(`Scenario path must end with .mjs: ${scenarioPath}`);
+ }
+
+ if (!existsSync(mjsInstrumentPath)) {
+ throw new Error(`Instrument file not found: ${mjsInstrumentPath}`);
+ }
+
+ const cjsScenarioPath = join(cwd, `tmp_${scenarioPath.replace('.mjs', '.cjs')}`);
+ const cjsInstrumentPath = join(cwd, `tmp_${instrumentPath.replace('.mjs', '.cjs')}`);
+
+ describe('esm', () => {
+ const testFn = options?.failsOnEsm ? test.fails : test;
+ callback(() => createRunner(mjsScenarioPath).withFlags('--import', mjsInstrumentPath), testFn, 'esm');
+ });
+
+ describe('cjs', () => {
+ beforeAll(() => {
+ // For the CJS runner, we create some temporary files...
+ convertEsmFileToCjs(mjsScenarioPath, cjsScenarioPath);
+ convertEsmFileToCjs(mjsInstrumentPath, cjsInstrumentPath);
+ });
+
+ afterAll(() => {
+ try {
+ unlinkSync(cjsInstrumentPath);
+ } catch {
+ // Ignore errors here
+ }
+ try {
+ unlinkSync(cjsScenarioPath);
+ } catch {
+ // Ignore errors here
+ }
+ });
+
+ const testFn = options?.failsOnCjs ? test.fails : test;
+ callback(() => createRunner(cjsScenarioPath).withFlags('--require', cjsInstrumentPath), testFn, 'cjs');
+ });
+}
+
+function convertEsmFileToCjs(inputPath: string, outputPath: string): void {
+ const cjsFileContent = readFileSync(inputPath, 'utf8');
+ const cjsFileContentConverted = convertEsmToCjs(cjsFileContent);
+ writeFileSync(outputPath, cjsFileContentConverted);
+}
+
+/** Creates a test runner */
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function createRunner(...paths: string[]) {
+ const testPath = join(...paths);
+
+ if (!existsSync(testPath)) {
+ throw new Error(`Test scenario not found: ${testPath}`);
+ }
+
+ const expectedEnvelopes: Expected[] = [];
+ let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined;
+ const flags: string[] = [];
+ // By default, we ignore session & sessions
+ const ignored: Set = new Set(['session', 'sessions', 'client_report']);
+ let withEnv: Record = {};
+ let withSentryServer = false;
+ let dockerOptions: DockerOptions | undefined;
+ let ensureNoErrorOutput = false;
+ const logs: string[] = [];
+
+ if (testPath.endsWith('.ts')) {
+ flags.push('-r', 'ts-node/register');
+ }
+
+ return {
+ expect: function (expected: Expected) {
+ if (ensureNoErrorOutput) {
+ throw new Error('You should not use `ensureNoErrorOutput` when using `expect`!');
+ }
+ expectedEnvelopes.push(expected);
+ return this;
+ },
+ expectN: function (n: number, expected: Expected) {
+ for (let i = 0; i < n; i++) {
+ expectedEnvelopes.push(expected);
+ }
+ return this;
+ },
+ expectHeader: function (expected: ExpectedEnvelopeHeader) {
+ if (!expectedEnvelopeHeaders) {
+ expectedEnvelopeHeaders = [];
+ }
+
+ expectedEnvelopeHeaders.push(expected);
+ return this;
+ },
+ withEnv: function (env: Record) {
+ withEnv = env;
+ return this;
+ },
+ withFlags: function (...args: string[]) {
+ flags.push(...args);
+ return this;
+ },
+ withInstrument: function (instrumentPath: string) {
+ flags.push('--import', instrumentPath);
+ return this;
+ },
+ withMockSentryServer: function () {
+ withSentryServer = true;
+ return this;
+ },
+ ignore: function (...types: EnvelopeItemType[]) {
+ types.forEach(t => ignored.add(t));
+ return this;
+ },
+ unignore: function (...types: EnvelopeItemType[]) {
+ for (const t of types) {
+ ignored.delete(t);
+ }
+ return this;
+ },
+ withDockerCompose: function (options: DockerOptions) {
+ dockerOptions = options;
+ return this;
+ },
+ ensureNoErrorOutput: function () {
+ if (expectedEnvelopes.length > 0) {
+ throw new Error('You should not use `ensureNoErrorOutput` when using `expect`!');
+ }
+ ensureNoErrorOutput = true;
+ return this;
+ },
+ start: function (): StartResult {
+ let isComplete = false;
+ let completeError: Error | undefined;
+
+ const expectedEnvelopeCount = Math.max(expectedEnvelopes.length, (expectedEnvelopeHeaders || []).length);
+
+ let envelopeCount = 0;
+ let scenarioServerPort: number | undefined;
+ let hasExited = false;
+ let child: ReturnType | undefined;
+
+ function complete(error?: Error): void {
+ if (isComplete) {
+ return;
+ }
+
+ isComplete = true;
+ completeError = error || undefined;
+ child?.kill();
+ }
+
+ /** Called after each expect callback to check if we're complete */
+ function expectCallbackCalled(): void {
+ envelopeCount++;
+ if (envelopeCount === expectedEnvelopeCount) {
+ complete();
+ }
+ }
+
+ function newEnvelope(envelope: Envelope): void {
+ for (const item of envelope[1]) {
+ const envelopeItemType = item[0].type;
+
+ if (ignored.has(envelopeItemType)) {
+ continue;
+ }
+
+ if (expectedEnvelopeHeaders) {
+ const header = envelope[0];
+ const expected = expectedEnvelopeHeaders.shift()?.[envelopeItemType as keyof ExpectedEnvelopeHeader];
+
+ try {
+ if (!expected) {
+ return;
+ }
+
+ assertEnvelopeHeader(header, expected);
+
+ expectCallbackCalled();
+ } catch (e) {
+ complete(e as Error);
+ }
+
+ return;
+ }
+
+ const expected = expectedEnvelopes.shift();
+
+ // Catch any error or failed assertions and pass them to done to end the test quickly
+ try {
+ if (!expected) {
+ return;
+ }
+
+ const expectedType = Object.keys(expected)[0];
+
+ if (expectedType !== envelopeItemType) {
+ throw new Error(
+ `Expected envelope item type '${expectedType}' but got '${envelopeItemType}'. \nItem: ${JSON.stringify(
+ item,
+ )}`,
+ );
+ }
+
+ if ('event' in expected) {
+ expectErrorEvent(item[1] as Event, expected.event);
+ expectCallbackCalled();
+ } else if ('transaction' in expected) {
+ expectTransactionEvent(item[1] as TransactionEvent, expected.transaction);
+ expectCallbackCalled();
+ } else if ('session' in expected) {
+ expectSessionEvent(item[1] as SerializedSession, expected.session);
+ expectCallbackCalled();
+ } else if ('sessions' in expected) {
+ expectSessionsEvent(item[1] as SessionAggregates, expected.sessions);
+ expectCallbackCalled();
+ } else if ('check_in' in expected) {
+ expectCheckInEvent(item[1] as SerializedCheckIn, expected.check_in);
+ expectCallbackCalled();
+ } else if ('client_report' in expected) {
+ expectClientReport(item[1] as ClientReport, expected.client_report);
+ expectCallbackCalled();
+ } else if ('log' in expected) {
+ expectLog(item[1] as SerializedLogContainer, expected.log);
+ expectCallbackCalled();
+ } else {
+ throw new Error(
+ `Unhandled expected envelope item type: ${JSON.stringify(expected)}\nItem: ${JSON.stringify(item)}`,
+ );
+ }
+ } catch (e) {
+ complete(e as Error);
+ }
+ }
+ }
+
+ // We need to properly define & pass these types around for TS 3.8,
+ // which otherwise fails to infer these correctly :(
+ type ServerStartup = [number | undefined, (() => void) | undefined];
+ type DockerStartup = VoidFunction | undefined;
+
+ const serverStartup: Promise = withSentryServer
+ ? createBasicSentryServer(newEnvelope)
+ : Promise.resolve([undefined, undefined]);
+
+ const dockerStartup: Promise = dockerOptions
+ ? runDockerCompose(dockerOptions)
+ : Promise.resolve(undefined);
+
+ const startup = Promise.all([dockerStartup, serverStartup]) as Promise<[DockerStartup, ServerStartup]>;
+
+ startup
+ .then(([dockerChild, [mockServerPort, mockServerClose]]) => {
+ if (mockServerClose) {
+ CLEANUP_STEPS.add(() => {
+ mockServerClose();
+ });
+ }
+
+ if (dockerChild) {
+ CLEANUP_STEPS.add(dockerChild);
+ }
+
+ const env = mockServerPort
+ ? { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }
+ : { ...process.env, ...withEnv };
+
+ if (process.env.DEBUG) log('starting scenario', testPath, flags, env.SENTRY_DSN);
+
+ child = spawn('node', [...flags, testPath], { env });
+
+ CLEANUP_STEPS.add(() => {
+ child?.kill();
+ });
+
+ child.stderr?.on('data', (data: Buffer) => {
+ const output = data.toString();
+ logs.push(output.trim());
+
+ if (process.env.DEBUG) log('stderr line', output);
+
+ if (ensureNoErrorOutput) {
+ complete(new Error(`Expected no error output but got: '${output}'`));
+ }
+ });
+
+ child.on('close', () => {
+ hasExited = true;
+
+ if (ensureNoErrorOutput) {
+ complete();
+ }
+ });
+
+ // Pass error to done to end the test quickly
+ child.on('error', e => {
+ if (process.env.DEBUG) log('scenario error', e);
+ complete(e);
+ });
+
+ function tryParseEnvelopeFromStdoutLine(line: string): void {
+ // Lines can have leading '[something] [{' which we need to remove
+ const cleanedLine = line.replace(/^.*?] \[{"/, '[{"');
+
+ // See if we have a port message
+ if (cleanedLine.startsWith('{"port":')) {
+ const { port } = JSON.parse(cleanedLine) as { port: number };
+ scenarioServerPort = port;
+ return;
+ }
+
+ // Skip any lines that don't start with envelope JSON
+ if (!cleanedLine.startsWith('[{')) {
+ return;
+ }
+
+ try {
+ const envelope = JSON.parse(cleanedLine) as Envelope;
+ newEnvelope(envelope);
+ } catch (_) {
+ //
+ }
+ }
+
+ let buffer = Buffer.alloc(0);
+ child.stdout?.on('data', (data: Buffer) => {
+ // This is horribly memory inefficient but it's only for tests
+ buffer = Buffer.concat([buffer, data]);
+
+ let splitIndex = -1;
+ while ((splitIndex = buffer.indexOf(0xa)) >= 0) {
+ const line = buffer.subarray(0, splitIndex).toString();
+ logs.push(line.trim());
+
+ buffer = Buffer.from(buffer.subarray(splitIndex + 1));
+ if (process.env.DEBUG) log('line', line);
+ tryParseEnvelopeFromStdoutLine(line);
+ }
+ });
+ })
+ .catch(e => complete(e));
+
+ return {
+ completed: async function (): Promise {
+ await waitFor(() => isComplete, 120_000, 'Timed out waiting for test to complete');
+
+ if (completeError) {
+ throw completeError;
+ }
+ },
+ childHasExited: function (): boolean {
+ return hasExited;
+ },
+ getLogs(): string[] {
+ return logs;
+ },
+ makeRequest: async function (
+ method: 'get' | 'post',
+ path: string,
+ options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {},
+ ): Promise {
+ try {
+ await waitFor(() => scenarioServerPort !== undefined, 10_000, 'Timed out waiting for server port');
+ } catch (e) {
+ complete(e as Error);
+ return;
+ }
+
+ const url = `http://localhost:${scenarioServerPort}${path}`;
+ const body = options.data;
+ const headers = options.headers || {};
+ const expectError = options.expectError || false;
+
+ if (process.env.DEBUG) log('making request', method, url, headers, body);
+
+ try {
+ const res = await fetch(url, { headers, method, body });
+
+ if (!res.ok) {
+ if (!expectError) {
+ complete(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`));
+ }
+
+ return;
+ }
+
+ if (expectError) {
+ complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`));
+ return;
+ }
+
+ if (res.headers.get('content-type')?.includes('application/json')) {
+ return await res.json();
+ }
+
+ return (await res.text()) as T;
+ } catch (e) {
+ if (expectError) {
+ return;
+ }
+
+ complete(e as Error);
+ return;
+ }
+ },
+ };
+ },
+ };
+}
+
+function log(...args: unknown[]): void {
+ // eslint-disable-next-line no-console
+ console.log(...args.map(arg => normalize(arg)));
+}
+
+function expectErrorEvent(item: Event, expected: ExpectedEvent): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentryEvent(item, expected);
+ }
+}
+
+function expectTransactionEvent(item: TransactionEvent, expected: ExpectedTransaction): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentryTransaction(item, expected);
+ }
+}
+
+function expectSessionEvent(item: SerializedSession, expected: ExpectedSession): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentrySession(item, expected);
+ }
+}
+
+function expectSessionsEvent(item: SessionAggregates, expected: ExpectedSessions): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentrySessions(item, expected);
+ }
+}
+
+function expectCheckInEvent(item: SerializedCheckIn, expected: ExpectedCheckIn): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentryCheckIn(item, expected);
+ }
+}
+
+function expectClientReport(item: ClientReport, expected: ExpectedClientReport): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentryClientReport(item, expected);
+ }
+}
+
+function expectLog(item: SerializedLogContainer, expected: ExpectedLogContainer): void {
+ if (typeof expected === 'function') {
+ expected(item);
+ } else {
+ assertSentryLogContainer(item, expected);
+ }
+}
+
+/**
+ * Converts ESM import statements to CommonJS require statements
+ * @param content The content of an ESM file
+ * @returns The content with require statements instead of imports
+ */
+function convertEsmToCjs(content: string): string {
+ let newContent = content;
+
+ // Handle default imports: import x from 'y' -> const x = require('y')
+ newContent = newContent.replace(
+ /import\s+([\w*{}\s,]+)\s+from\s+['"]([^'"]+)['"]/g,
+ (_, imports: string, module: string) => {
+ if (imports.includes('* as')) {
+ // Handle namespace imports: import * as x from 'y' -> const x = require('y')
+ return `const ${imports.replace('* as', '').trim()} = require('${module}')`;
+ } else if (imports.includes('{')) {
+ // Handle named imports: import {x, y} from 'z' -> const {x, y} = require('z')
+ return `const ${imports} = require('${module}')`;
+ } else {
+ // Handle default imports: import x from 'y' -> const x = require('y')
+ return `const ${imports} = require('${module}')`;
+ }
+ },
+ );
+
+ // Handle side-effect imports: import 'x' -> require('x')
+ newContent = newContent.replace(/import\s+['"]([^'"]+)['"]/g, (_, module) => {
+ return `require('${module}')`;
+ });
+
+ return newContent;
+}
diff --git a/dev-packages/node-core-integration-tests/utils/server.ts b/dev-packages/node-core-integration-tests/utils/server.ts
new file mode 100644
index 000000000000..92e0477c845c
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/server.ts
@@ -0,0 +1,85 @@
+import type { Envelope } from '@sentry/core';
+import { parseEnvelope } from '@sentry/core';
+import express from 'express';
+import type { AddressInfo } from 'net';
+
+/**
+ * Creates a basic Sentry server that accepts POST to the envelope endpoint
+ *
+ * This does no checks on the envelope, it just calls the callback if it managed to parse an envelope from the raw POST
+ * body data.
+ */
+export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Promise<[number, () => void]> {
+ const app = express();
+
+ app.use(express.raw({ type: () => true, inflate: true, limit: '100mb' }));
+ app.post('/api/:id/envelope/', (req, res) => {
+ try {
+ const env = parseEnvelope(req.body as Buffer);
+ onEnvelope(env);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+
+ res.status(200).send();
+ });
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ const address = server.address() as AddressInfo;
+ resolve([
+ address.port,
+ () => {
+ server.close();
+ },
+ ]);
+ });
+ });
+}
+
+type HeaderAssertCallback = (headers: Record) => void;
+
+/** Creates a test server that can be used to check headers */
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export function createTestServer() {
+ const gets: Array<[string, HeaderAssertCallback, number]> = [];
+ let error: unknown | undefined;
+
+ return {
+ get: function (path: string, callback: HeaderAssertCallback, result = 200) {
+ gets.push([path, callback, result]);
+ return this;
+ },
+ start: async (): Promise<[string, () => void]> => {
+ const app = express();
+
+ for (const [path, callback, result] of gets) {
+ app.get(path, (req, res) => {
+ try {
+ callback(req.headers);
+ } catch (e) {
+ error = e;
+ }
+
+ res.status(result).send();
+ });
+ }
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ const address = server.address() as AddressInfo;
+ resolve([
+ `http://localhost:${address.port}`,
+ () => {
+ server.close();
+ if (error) {
+ throw error;
+ }
+ },
+ ]);
+ });
+ });
+ },
+ };
+}
diff --git a/dev-packages/node-core-integration-tests/utils/setup-tests.ts b/dev-packages/node-core-integration-tests/utils/setup-tests.ts
new file mode 100644
index 000000000000..6f7bb2bec369
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/setup-tests.ts
@@ -0,0 +1,12 @@
+import EventEmitter from 'events';
+
+const setup = async (): Promise => {
+ // Node warns about a potential memory leak
+ // when more than 10 event listeners are assigned inside a single thread.
+ // Initializing Sentry for each test triggers these warnings after 10th test inside Jest thread.
+ // As we know that it's not a memory leak and number of listeners are limited to the number of tests,
+ // removing the limit on listener count here.
+ EventEmitter.defaultMaxListeners = 0;
+};
+
+export default setup;
diff --git a/dev-packages/node-core-integration-tests/utils/setupOtel.js b/dev-packages/node-core-integration-tests/utils/setupOtel.js
new file mode 100644
index 000000000000..bcbf874ac7f0
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/setupOtel.js
@@ -0,0 +1,17 @@
+const { trace, propagation, context } = require('@opentelemetry/api');
+const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base');
+const Sentry = require('@sentry/node-core');
+const { SentryPropagator, SentrySampler, SentrySpanProcessor } = require('@sentry/opentelemetry');
+
+exports.setupOtel = function setupOtel(client) {
+ const provider = new BasicTracerProvider({
+ sampler: client ? new SentrySampler(client) : undefined,
+ spanProcessors: [new SentrySpanProcessor()],
+ });
+
+ trace.setGlobalTracerProvider(provider);
+ propagation.setGlobalPropagator(new SentryPropagator());
+ context.setGlobalContextManager(new Sentry.SentryContextManager());
+
+ Sentry.validateOpenTelemetrySetup();
+};
diff --git a/dev-packages/node-core-integration-tests/utils/setupOtel.ts b/dev-packages/node-core-integration-tests/utils/setupOtel.ts
new file mode 100644
index 000000000000..2c7488b9c64f
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/utils/setupOtel.ts
@@ -0,0 +1,38 @@
+import { context, propagation, trace } from '@opentelemetry/api';
+import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
+import type { Client } from '@sentry/core';
+import * as Sentry from '@sentry/node-core';
+import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry';
+
+export function setupOtel(client: Client | undefined): BasicTracerProvider | undefined {
+ if (!client) {
+ return undefined;
+ }
+
+ const provider = new BasicTracerProvider({
+ sampler: new SentrySampler(client),
+ spanProcessors: [new SentrySpanProcessor()],
+ });
+
+ trace.setGlobalTracerProvider(provider);
+ propagation.setGlobalPropagator(new SentryPropagator());
+ context.setGlobalContextManager(new Sentry.SentryContextManager());
+
+ Sentry.validateOpenTelemetrySetup();
+
+ return provider;
+}
+
+export function cleanupOtel(provider: BasicTracerProvider): void {
+ void provider.forceFlush().catch(() => {
+ // no-op
+ });
+ void provider.shutdown().catch(() => {
+ // no-op
+ });
+
+ // Disable all globally registered APIs
+ trace.disable();
+ context.disable();
+ propagation.disable();
+}
diff --git a/dev-packages/node-core-integration-tests/vite.config.ts b/dev-packages/node-core-integration-tests/vite.config.ts
new file mode 100644
index 000000000000..4b2c3b2a0a74
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/vite.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig } from 'vitest/config';
+import baseConfig from '../../vite/vite.config';
+
+export default defineConfig({
+ ...baseConfig,
+ test: {
+ ...baseConfig.test,
+ isolate: false,
+ coverage: {
+ enabled: false,
+ },
+ include: ['./**/test.ts'],
+ testTimeout: 15000,
+ // Ensure we can see debug output when DEBUG=true
+ ...(process.env.DEBUG
+ ? {
+ disableConsoleIntercept: true,
+ silent: false,
+ }
+ : {}),
+ // By default Vitest uses child processes to run tests but all our tests
+ // already run in their own processes. We use threads instead because the
+ // overhead is significantly less.
+ pool: 'threads',
+ reporters: process.env.DEBUG
+ ? ['default', { summary: false }]
+ : process.env.GITHUB_ACTIONS
+ ? ['dot', 'github-actions']
+ : ['verbose'],
+ },
+});
diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs
index 7cd41344e596..6ef09192eef1 100644
--- a/dev-packages/rollup-utils/npmHelpers.mjs
+++ b/dev-packages/rollup-utils/npmHelpers.mjs
@@ -152,9 +152,14 @@ export function makeOtelLoaders(outputFolder, hookVariant) {
}
const requiredDep = hookVariant === 'otel' ? '@opentelemetry/instrumentation' : '@sentry/node';
- const foundImportInTheMiddleDep = Object.keys(packageDotJSON.dependencies ?? {}).some(key => {
- return key === requiredDep;
- });
+ const foundImportInTheMiddleDep =
+ Object.keys(packageDotJSON.dependencies ?? {}).some(key => {
+ return key === requiredDep;
+ }) ||
+ Object.keys(packageDotJSON.devDependencies ?? {}).some(key => {
+ return key === requiredDep;
+ });
+
if (!foundImportInTheMiddleDep) {
throw new Error(
`You used the makeOtelLoaders() rollup utility but didn't specify the "${requiredDep}" dependency in ${path.resolve(
diff --git a/package.json b/package.json
index d6b8178ecc71..e6488580dc45 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
"dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail",
"dedupe-deps:fix": "yarn-deduplicate yarn.lock",
"postpublish": "lerna run --stream --concurrency 1 postpublish",
- "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test",
- "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test:unit",
+ "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test",
+ "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test:unit",
"test:update-snapshots": "lerna run test:update-snapshots",
"test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\"",
"test:pr:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts --affected",
@@ -68,6 +68,7 @@
"packages/nestjs",
"packages/nextjs",
"packages/node",
+ "packages/node-core",
"packages/node-native",
"packages/nuxt",
"packages/opentelemetry",
@@ -94,6 +95,7 @@
"dev-packages/bundle-analyzer-scenarios",
"dev-packages/e2e-tests",
"dev-packages/node-integration-tests",
+ "dev-packages/node-core-integration-tests",
"dev-packages/test-utils",
"dev-packages/size-limit-gh-action",
"dev-packages/clear-cache-gh-action",
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 90ea06e0ef07..1139f58d092c 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -12,7 +12,9 @@ export {
addEventProcessor,
addIntegration,
amqplibIntegration,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
captureCheckIn,
captureConsoleIntegration,
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index f7a5f77ac0fb..7d2abeaf6a12 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -42,7 +42,9 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
consoleIntegration,
httpIntegration,
diff --git a/packages/browser-utils/src/metrics/lcp.ts b/packages/browser-utils/src/metrics/lcp.ts
index 4d4fbda8f979..abbe4348fa6f 100644
--- a/packages/browser-utils/src/metrics/lcp.ts
+++ b/packages/browser-utils/src/metrics/lcp.ts
@@ -44,7 +44,7 @@ export function trackLcpAsStandaloneSpan(): void {
}
sentSpan = true;
if (pageloadSpanId) {
- sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId);
+ _sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId);
}
cleanupLcpHandler();
}
@@ -88,7 +88,14 @@ export function trackLcpAsStandaloneSpan(): void {
}, 0);
}
-function sendStandaloneLcpSpan(lcpValue: number, entry: LargestContentfulPaint | undefined, pageloadSpanId: string) {
+/**
+ * Exported only for testing!
+ */
+export function _sendStandaloneLcpSpan(
+ lcpValue: number,
+ entry: LargestContentfulPaint | undefined,
+ pageloadSpanId: string,
+) {
DEBUG_BUILD && logger.log(`Sending LCP span (${lcpValue})`);
const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0));
@@ -105,12 +112,21 @@ function sendStandaloneLcpSpan(lcpValue: number, entry: LargestContentfulPaint |
};
if (entry) {
- attributes['lcp.element'] = htmlTreeAsString(entry.element);
- attributes['lcp.id'] = entry.id;
- attributes['lcp.url'] = entry.url;
- attributes['lcp.loadTime'] = entry.loadTime;
- attributes['lcp.renderTime'] = entry.renderTime;
- attributes['lcp.size'] = entry.size;
+ entry.element && (attributes['lcp.element'] = htmlTreeAsString(entry.element));
+ entry.id && (attributes['lcp.id'] = entry.id);
+
+ // Trim URL to the first 200 characters.
+ entry.url && (attributes['lcp.url'] = entry.url.trim().slice(0, 200));
+
+ // loadTime is the time of LCP that's related to receiving the LCP element response..
+ entry.loadTime != null && (attributes['lcp.loadTime'] = entry.loadTime);
+
+ // renderTime is loadTime + rendering time
+ // it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the
+ // `Timing-Allow-Origin` header.
+ entry.renderTime != null && (attributes['lcp.renderTime'] = entry.renderTime);
+
+ entry.size != null && (attributes['lcp.size'] = entry.size);
}
const span = startStandaloneWebVitalSpan({
diff --git a/packages/browser/src/diagnose-sdk.ts b/packages/browser/src/diagnose-sdk.ts
index e2399b8103b0..a8b433856f01 100644
--- a/packages/browser/src/diagnose-sdk.ts
+++ b/packages/browser/src/diagnose-sdk.ts
@@ -25,9 +25,12 @@ export async function diagnoseSdkConnectivity(): Promise<
try {
// If fetch throws, there is likely an ad blocker active or there are other connective issues.
await fetch(
- // We want this to be as close as possible to an actual ingest URL so that ad blockers will actually block the request
- // We are using the "sentry-sdks" org with id 447951 not to pollute any actual organizations.
- 'https://o447951.ingest.sentry.io/api/1337/envelope/?sentry_version=7&sentry_key=1337&sentry_client=sentry.javascript.browser%2F1.33.7',
+ // We are using the
+ // - "sentry-sdks" org with id 447951 not to pollute any actual organizations.
+ // - "diagnose-sdk-connectivity" project with id 4509632503087104
+ // - the public key of said org/project, which is disabled in the project settings
+ // => this DSN: https://c1dfb07d783ad5325c245c1fd3725390@o447951.ingest.us.sentry.io/4509632503087104 (i.e. disabled)
+ 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7',
{
body: '{}',
method: 'POST',
diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts
index cc987fca2966..2918e925afb8 100644
--- a/packages/browser/src/tracing/request.ts
+++ b/packages/browser/src/tracing/request.ts
@@ -2,7 +2,6 @@ import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span, WebFetc
import {
addFetchEndInstrumentationHandler,
addFetchInstrumentationHandler,
- browserPerformanceTimeOrigin,
getActiveSpan,
getClient,
getLocationHref,
@@ -23,10 +22,10 @@ import type { XhrHint } from '@sentry-internal/browser-utils';
import {
addPerformanceInstrumentationHandler,
addXhrInstrumentationHandler,
- extractNetworkProtocol,
SENTRY_XHR_DATA_KEY,
} from '@sentry-internal/browser-utils';
import { WINDOW } from '../helpers';
+import { resourceTimingToSpanAttributes } from './resource-timing';
/** Options for Request Instrumentation */
export interface RequestInstrumentationOptions {
@@ -238,8 +237,8 @@ function addHTTPTimings(span: Span): void {
const cleanup = addPerformanceInstrumentationHandler('resource', ({ entries }) => {
entries.forEach(entry => {
if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) {
- const spanData = resourceTimingEntryToSpanData(entry);
- spanData.forEach(data => span.setAttribute(...data));
+ const spanAttributes = resourceTimingToSpanAttributes(entry);
+ spanAttributes.forEach(attributeArray => span.setAttribute(...attributeArray));
// In the next tick, clean this handler up
// We have to wait here because otherwise this cleans itself up before it is fully done
setTimeout(cleanup);
@@ -248,35 +247,6 @@ function addHTTPTimings(span: Span): void {
});
}
-function getAbsoluteTime(time: number = 0): number {
- return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000;
-}
-
-function resourceTimingEntryToSpanData(resourceTiming: PerformanceResourceTiming): [string, string | number][] {
- const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol);
-
- const timingSpanData: [string, string | number][] = [];
-
- timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]);
-
- if (!browserPerformanceTimeOrigin()) {
- return timingSpanData;
- }
- return [
- ...timingSpanData,
- ['http.request.redirect_start', getAbsoluteTime(resourceTiming.redirectStart)],
- ['http.request.fetch_start', getAbsoluteTime(resourceTiming.fetchStart)],
- ['http.request.domain_lookup_start', getAbsoluteTime(resourceTiming.domainLookupStart)],
- ['http.request.domain_lookup_end', getAbsoluteTime(resourceTiming.domainLookupEnd)],
- ['http.request.connect_start', getAbsoluteTime(resourceTiming.connectStart)],
- ['http.request.secure_connection_start', getAbsoluteTime(resourceTiming.secureConnectionStart)],
- ['http.request.connection_end', getAbsoluteTime(resourceTiming.connectEnd)],
- ['http.request.request_start', getAbsoluteTime(resourceTiming.requestStart)],
- ['http.request.response_start', getAbsoluteTime(resourceTiming.responseStart)],
- ['http.request.response_end', getAbsoluteTime(resourceTiming.responseEnd)],
- ];
-}
-
/**
* A function that determines whether to attach tracing headers to a request.
* We only export this function for testing purposes.
diff --git a/packages/browser/src/tracing/resource-timing.ts b/packages/browser/src/tracing/resource-timing.ts
new file mode 100644
index 000000000000..c741a7e91016
--- /dev/null
+++ b/packages/browser/src/tracing/resource-timing.ts
@@ -0,0 +1,41 @@
+import type { Span } from '@sentry/core';
+import { browserPerformanceTimeOrigin } from '@sentry/core';
+import { extractNetworkProtocol } from '@sentry-internal/browser-utils';
+
+function getAbsoluteTime(time: number = 0): number {
+ return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000;
+}
+
+/**
+ * Converts a PerformanceResourceTiming entry to span data for the resource span.
+ *
+ * @param resourceTiming
+ * @returns An array where the first element is the attribute name and the second element is the attribute value.
+ */
+export function resourceTimingToSpanAttributes(
+ resourceTiming: PerformanceResourceTiming,
+): Array> {
+ const timingSpanData: Array> = [];
+ // Checking for only `undefined` and `null` is intentional because it's
+ // valid for `nextHopProtocol` to be an empty string.
+ if (resourceTiming.nextHopProtocol != undefined) {
+ const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol);
+ timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]);
+ }
+ if (!browserPerformanceTimeOrigin()) {
+ return timingSpanData;
+ }
+ return [
+ ...timingSpanData,
+ ['http.request.redirect_start', getAbsoluteTime(resourceTiming.redirectStart)],
+ ['http.request.fetch_start', getAbsoluteTime(resourceTiming.fetchStart)],
+ ['http.request.domain_lookup_start', getAbsoluteTime(resourceTiming.domainLookupStart)],
+ ['http.request.domain_lookup_end', getAbsoluteTime(resourceTiming.domainLookupEnd)],
+ ['http.request.connect_start', getAbsoluteTime(resourceTiming.connectStart)],
+ ['http.request.secure_connection_start', getAbsoluteTime(resourceTiming.secureConnectionStart)],
+ ['http.request.connection_end', getAbsoluteTime(resourceTiming.connectEnd)],
+ ['http.request.request_start', getAbsoluteTime(resourceTiming.requestStart)],
+ ['http.request.response_start', getAbsoluteTime(resourceTiming.responseStart)],
+ ['http.request.response_end', getAbsoluteTime(resourceTiming.responseEnd)],
+ ];
+}
diff --git a/packages/browser/test/diagnose-sdk.test.ts b/packages/browser/test/diagnose-sdk.test.ts
new file mode 100644
index 000000000000..36584a97f63b
--- /dev/null
+++ b/packages/browser/test/diagnose-sdk.test.ts
@@ -0,0 +1,165 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import type { Client } from '@sentry/core';
+import * as sentryCore from '@sentry/core';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { diagnoseSdkConnectivity } from '../src/diagnose-sdk';
+
+// Mock the @sentry/core module
+vi.mock('@sentry/core', async requireActual => {
+ return {
+ ...((await requireActual()) as any),
+ getClient: vi.fn(),
+ };
+});
+
+// Mock global fetch
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
+
+describe('diagnoseSdkConnectivity', () => {
+ const mockGetClient = sentryCore.getClient as any;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('returns "no-client-active" when no client is active', async () => {
+ mockGetClient.mockReturnValue(undefined);
+
+ const result = await diagnoseSdkConnectivity();
+
+ expect(result).toBe('no-client-active');
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
+
+ it('returns "no-dsn-configured" when client.getDsn() returns undefined', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue(undefined),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+
+ const result = await diagnoseSdkConnectivity();
+
+ expect(result).toBe('no-dsn-configured');
+ expect(mockClient.getDsn).toHaveBeenCalled();
+ expect(mockFetch).not.toHaveBeenCalled();
+ });
+
+ it('returns "sentry-unreachable" when fetch throws an error', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ mockFetch.mockRejectedValue(new Error('Network error'));
+
+ const result = await diagnoseSdkConnectivity();
+
+ expect(result).toBe('sentry-unreachable');
+ expect(mockClient.getDsn).toHaveBeenCalled();
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7',
+ {
+ body: '{}',
+ method: 'POST',
+ mode: 'cors',
+ credentials: 'omit',
+ },
+ );
+ });
+
+ it('returns "sentry-unreachable" when fetch throws a TypeError (common for network issues)', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ mockFetch.mockRejectedValue(new TypeError('Failed to fetch'));
+
+ const result = await diagnoseSdkConnectivity();
+
+ expect(result).toBe('sentry-unreachable');
+ expect(mockClient.getDsn).toHaveBeenCalled();
+ expect(mockFetch).toHaveBeenCalled();
+ });
+
+ it('returns undefined when connectivity check succeeds', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
+
+ const result = await diagnoseSdkConnectivity();
+
+ expect(result).toBeUndefined();
+ expect(mockClient.getDsn).toHaveBeenCalled();
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7',
+ {
+ body: '{}',
+ method: 'POST',
+ mode: 'cors',
+ credentials: 'omit',
+ },
+ );
+ });
+
+ it('returns undefined even when fetch returns an error status (4xx, 5xx)', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ // Mock a 403 response (expected since the DSN is disabled)
+ mockFetch.mockResolvedValue(new Response('Forbidden', { status: 403 }));
+
+ const result = await diagnoseSdkConnectivity();
+
+ // The function only cares about fetch not throwing, not the response status
+ expect(result).toBeUndefined();
+ expect(mockClient.getDsn).toHaveBeenCalled();
+ expect(mockFetch).toHaveBeenCalled();
+ });
+
+ it('uses the correct test endpoint URL', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
+
+ await diagnoseSdkConnectivity();
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://o447951.ingest.sentry.io/api/4509632503087104/envelope/?sentry_version=7&sentry_key=c1dfb07d783ad5325c245c1fd3725390&sentry_client=sentry.javascript.browser%2F1.33.7',
+ expect.objectContaining({
+ body: '{}',
+ method: 'POST',
+ mode: 'cors',
+ credentials: 'omit',
+ }),
+ );
+ });
+
+ it('uses correct fetch options', async () => {
+ const mockClient: Partial = {
+ getDsn: vi.fn().mockReturnValue('https://test@example.com/123'),
+ };
+ mockGetClient.mockReturnValue(mockClient);
+ mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
+
+ await diagnoseSdkConnectivity();
+
+ expect(mockFetch).toHaveBeenCalledWith(expect.any(String), {
+ body: '{}',
+ method: 'POST',
+ mode: 'cors',
+ credentials: 'omit',
+ });
+ });
+});
diff --git a/packages/browser/test/tracing/resource-timing.test.ts b/packages/browser/test/tracing/resource-timing.test.ts
new file mode 100644
index 000000000000..c3ad8b9c0c85
--- /dev/null
+++ b/packages/browser/test/tracing/resource-timing.test.ts
@@ -0,0 +1,537 @@
+import * as utils from '@sentry/core';
+import * as browserUtils from '@sentry-internal/browser-utils';
+import type { MockInstance } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { resourceTimingToSpanAttributes } from '../../src/tracing/resource-timing';
+
+describe('resourceTimingToSpanAttributes', () => {
+ let browserPerformanceTimeOriginSpy: MockInstance;
+ let extractNetworkProtocolSpy: MockInstance;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ browserPerformanceTimeOriginSpy = vi.spyOn(utils, 'browserPerformanceTimeOrigin');
+ extractNetworkProtocolSpy = vi.spyOn(browserUtils, 'extractNetworkProtocol');
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ const createMockResourceTiming = (overrides: Partial = {}): PerformanceResourceTiming => {
+ return {
+ name: 'https://example.com/api',
+ entryType: 'resource',
+ startTime: 100,
+ duration: 200,
+ initiatorType: 'fetch',
+ nextHopProtocol: 'h2',
+ workerStart: 0,
+ redirectStart: 10,
+ redirectEnd: 20,
+ fetchStart: 25,
+ domainLookupStart: 30,
+ domainLookupEnd: 35,
+ connectStart: 40,
+ connectEnd: 50,
+ secureConnectionStart: 45,
+ requestStart: 55,
+ responseStart: 150,
+ responseEnd: 200,
+ transferSize: 1000,
+ encodedBodySize: 800,
+ decodedBodySize: 900,
+ serverTiming: [],
+ workerTiming: [],
+ ...overrides,
+ } as PerformanceResourceTiming;
+ };
+
+ describe('with network protocol information', () => {
+ it('should extract network protocol when nextHopProtocol is available', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: 'h2',
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: 'http',
+ version: '2.0',
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(extractNetworkProtocolSpy).toHaveBeenCalledWith('h2');
+ expect(result).toEqual([
+ ['network.protocol.version', '2.0'],
+ ['network.protocol.name', 'http'],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+
+ it('should handle different network protocols', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: 'http/1.1',
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: 'http',
+ version: '1.1',
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(extractNetworkProtocolSpy).toHaveBeenCalledWith('http/1.1');
+ expect(result).toEqual([
+ ['network.protocol.version', '1.1'],
+ ['network.protocol.name', 'http'],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+
+ it('should extract network protocol even when nextHopProtocol is empty', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(extractNetworkProtocolSpy).toHaveBeenCalledWith('');
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+
+ it('should not extract network protocol when nextHopProtocol is undefined', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: undefined as any,
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(extractNetworkProtocolSpy).not.toHaveBeenCalled();
+ expect(result).toEqual([]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+ });
+
+ describe('without browserPerformanceTimeOrigin', () => {
+ it('should return only network protocol data when browserPerformanceTimeOrigin is not available', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: 'h2',
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: 'http',
+ version: '2.0',
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', '2.0'],
+ ['network.protocol.name', 'http'],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+
+ it('should return network protocol attributes even when empty string and no browserPerformanceTimeOrigin', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ // Mock performance.timeOrigin to be undefined to ensure early return
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+ });
+
+ describe('with browserPerformanceTimeOrigin', () => {
+ beforeEach(() => {
+ browserPerformanceTimeOriginSpy.mockReturnValue(1000000); // 1 second in milliseconds
+ });
+
+ it('should include all timing attributes when browserPerformanceTimeOrigin is available', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: 'h2',
+ redirectStart: 10,
+ fetchStart: 25,
+ domainLookupStart: 30,
+ domainLookupEnd: 35,
+ connectStart: 40,
+ secureConnectionStart: 45,
+ connectEnd: 50,
+ requestStart: 55,
+ responseStart: 150,
+ responseEnd: 200,
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: 'http',
+ version: '2.0',
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', '2.0'],
+ ['network.protocol.name', 'http'],
+ ['http.request.redirect_start', 1000.01], // (1000000 + 10) / 1000
+ ['http.request.fetch_start', 1000.025], // (1000000 + 25) / 1000
+ ['http.request.domain_lookup_start', 1000.03], // (1000000 + 30) / 1000
+ ['http.request.domain_lookup_end', 1000.035], // (1000000 + 35) / 1000
+ ['http.request.connect_start', 1000.04], // (1000000 + 40) / 1000
+ ['http.request.secure_connection_start', 1000.045], // (1000000 + 45) / 1000
+ ['http.request.connection_end', 1000.05], // (1000000 + 50) / 1000
+ ['http.request.request_start', 1000.055], // (1000000 + 55) / 1000
+ ['http.request.response_start', 1000.15], // (1000000 + 150) / 1000
+ ['http.request.response_end', 1000.2], // (1000000 + 200) / 1000
+ ]);
+ });
+
+ it('should handle zero timing values', () => {
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ redirectStart: 0,
+ fetchStart: 0,
+ domainLookupStart: 0,
+ domainLookupEnd: 0,
+ connectStart: 0,
+ secureConnectionStart: 0,
+ connectEnd: 0,
+ requestStart: 0,
+ responseStart: 0,
+ responseEnd: 0,
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ['http.request.redirect_start', 1000], // (1000000 + 0) / 1000
+ ['http.request.fetch_start', 1000],
+ ['http.request.domain_lookup_start', 1000],
+ ['http.request.domain_lookup_end', 1000],
+ ['http.request.connect_start', 1000],
+ ['http.request.secure_connection_start', 1000],
+ ['http.request.connection_end', 1000],
+ ['http.request.request_start', 1000],
+ ['http.request.response_start', 1000],
+ ['http.request.response_end', 1000],
+ ]);
+ });
+
+ it('should combine network protocol and timing attributes', () => {
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: 'http/1.1',
+ redirectStart: 5,
+ fetchStart: 10,
+ domainLookupStart: 15,
+ domainLookupEnd: 20,
+ connectStart: 25,
+ secureConnectionStart: 30,
+ connectEnd: 35,
+ requestStart: 40,
+ responseStart: 80,
+ responseEnd: 100,
+ });
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: 'http',
+ version: '1.1',
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', '1.1'],
+ ['network.protocol.name', 'http'],
+ ['http.request.redirect_start', 1000.005],
+ ['http.request.fetch_start', 1000.01],
+ ['http.request.domain_lookup_start', 1000.015],
+ ['http.request.domain_lookup_end', 1000.02],
+ ['http.request.connect_start', 1000.025],
+ ['http.request.secure_connection_start', 1000.03],
+ ['http.request.connection_end', 1000.035],
+ ['http.request.request_start', 1000.04],
+ ['http.request.response_start', 1000.08],
+ ['http.request.response_end', 1000.1],
+ ]);
+ });
+ });
+
+ describe('fallback to performance.timeOrigin', () => {
+ it('should use performance.timeOrigin when browserPerformanceTimeOrigin returns null', () => {
+ // Mock browserPerformanceTimeOrigin to return null for the main check
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ redirectStart: 20,
+ fetchStart: 40,
+ domainLookupStart: 60,
+ domainLookupEnd: 80,
+ connectStart: 100,
+ secureConnectionStart: 120,
+ connectEnd: 140,
+ requestStart: 160,
+ responseStart: 300,
+ responseEnd: 400,
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ // When browserPerformanceTimeOrigin returns null, function returns early with only network protocol attributes
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ]);
+ });
+
+ it('should use performance.timeOrigin fallback in getAbsoluteTime when available', () => {
+ // Mock browserPerformanceTimeOrigin to return 500000 for the main check
+ browserPerformanceTimeOriginSpy.mockReturnValue(500000);
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ redirectStart: 20,
+ fetchStart: 40,
+ domainLookupStart: 60,
+ domainLookupEnd: 80,
+ connectStart: 100,
+ secureConnectionStart: 120,
+ connectEnd: 140,
+ requestStart: 160,
+ responseStart: 300,
+ responseEnd: 400,
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ['http.request.redirect_start', 500.02], // (500000 + 20) / 1000
+ ['http.request.fetch_start', 500.04], // (500000 + 40) / 1000
+ ['http.request.domain_lookup_start', 500.06], // (500000 + 60) / 1000
+ ['http.request.domain_lookup_end', 500.08], // (500000 + 80) / 1000
+ ['http.request.connect_start', 500.1], // (500000 + 100) / 1000
+ ['http.request.secure_connection_start', 500.12], // (500000 + 120) / 1000
+ ['http.request.connection_end', 500.14], // (500000 + 140) / 1000
+ ['http.request.request_start', 500.16], // (500000 + 160) / 1000
+ ['http.request.response_start', 500.3], // (500000 + 300) / 1000
+ ['http.request.response_end', 500.4], // (500000 + 400) / 1000
+ ]);
+ });
+
+ it('should handle case when neither browserPerformanceTimeOrigin nor performance.timeOrigin is available', () => {
+ browserPerformanceTimeOriginSpy.mockReturnValue(null);
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ // Mock performance.timeOrigin as undefined
+ const originalPerformance = global.performance;
+ global.performance = {
+ ...originalPerformance,
+ timeOrigin: undefined,
+ } as any;
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ // When neither timing source is available, should return network protocol attributes for empty string
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ]);
+
+ // Restore global performance
+ global.performance = originalPerformance;
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle undefined timing values', () => {
+ browserPerformanceTimeOriginSpy.mockReturnValue(1000000);
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ redirectStart: undefined as any,
+ fetchStart: undefined as any,
+ domainLookupStart: undefined as any,
+ domainLookupEnd: undefined as any,
+ connectStart: undefined as any,
+ secureConnectionStart: undefined as any,
+ connectEnd: undefined as any,
+ requestStart: undefined as any,
+ responseStart: undefined as any,
+ responseEnd: undefined as any,
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ['http.request.redirect_start', 1000], // (1000000 + 0) / 1000
+ ['http.request.fetch_start', 1000],
+ ['http.request.domain_lookup_start', 1000],
+ ['http.request.domain_lookup_end', 1000],
+ ['http.request.connect_start', 1000],
+ ['http.request.secure_connection_start', 1000],
+ ['http.request.connection_end', 1000],
+ ['http.request.request_start', 1000],
+ ['http.request.response_start', 1000],
+ ['http.request.response_end', 1000],
+ ]);
+ });
+
+ it('should handle very large timing values', () => {
+ browserPerformanceTimeOriginSpy.mockReturnValue(1000000);
+
+ extractNetworkProtocolSpy.mockReturnValue({
+ name: '',
+ version: 'unknown',
+ });
+
+ const mockResourceTiming = createMockResourceTiming({
+ nextHopProtocol: '',
+ redirectStart: 999999,
+ fetchStart: 999999,
+ domainLookupStart: 999999,
+ domainLookupEnd: 999999,
+ connectStart: 999999,
+ secureConnectionStart: 999999,
+ connectEnd: 999999,
+ requestStart: 999999,
+ responseStart: 999999,
+ responseEnd: 999999,
+ });
+
+ const result = resourceTimingToSpanAttributes(mockResourceTiming);
+
+ expect(result).toEqual([
+ ['network.protocol.version', 'unknown'],
+ ['network.protocol.name', ''],
+ ['http.request.redirect_start', 1999.999], // (1000000 + 999999) / 1000
+ ['http.request.fetch_start', 1999.999],
+ ['http.request.domain_lookup_start', 1999.999],
+ ['http.request.domain_lookup_end', 1999.999],
+ ['http.request.connect_start', 1999.999],
+ ['http.request.secure_connection_start', 1999.999],
+ ['http.request.connection_end', 1999.999],
+ ['http.request.request_start', 1999.999],
+ ['http.request.response_start', 1999.999],
+ ['http.request.response_end', 1999.999],
+ ]);
+ });
+ });
+});
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 38bd74fda8c2..d027539931cc 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -62,7 +62,9 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
consoleIntegration,
httpIntegration,
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index d558603d3377..4656e0cb50fa 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -63,7 +63,7 @@
"devDependencies": {
"@cloudflare/workers-types": "4.20250620.0",
"@types/node": "^18.19.1",
- "wrangler": "^3.67.1"
+ "wrangler": "4.22.0"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts
index 3640d3cf7229..001ab55049ab 100644
--- a/packages/cloudflare/src/handler.ts
+++ b/packages/cloudflare/src/handler.ts
@@ -175,7 +175,10 @@ export function withSentry acc + message.attempts, 0),
+ 'messaging.message.retry.count': batch.messages.reduce(
+ (acc, message) => acc + message.attempts - 1,
+ 0,
+ ),
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'queue.process',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts
index 2e5c0f836e89..ddd4b0010ec0 100644
--- a/packages/cloudflare/test/handler.test.ts
+++ b/packages/cloudflare/test/handler.test.ts
@@ -843,7 +843,7 @@ describe('withSentry', () => {
'messaging.destination.name': batch.queue,
'messaging.system': 'cloudflare',
'messaging.batch.message_count': batch.messages.length,
- 'messaging.message.retry.count': batch.messages.reduce((acc, message) => acc + message.attempts, 0),
+ 'messaging.message.retry.count': batch.messages.reduce((acc, message) => acc + message.attempts - 1, 0),
'sentry.sample_rate': 1,
'sentry.source': 'task',
},
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 14797a9af008..11547ba933a1 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -42,7 +42,9 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
consoleIntegration,
httpIntegration,
diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
index c0b9f246a00c..66e598b5c10f 100644
--- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
+++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
@@ -27,6 +27,31 @@ export function wrapMiddlewareWithSentry(
): (...params: Parameters) => Promise> {
return new Proxy(middleware, {
apply: async (wrappingTarget, thisArg, args: Parameters) => {
+ const tunnelRoute =
+ '_sentryRewritesTunnelPath' in globalThis
+ ? (globalThis as Record)._sentryRewritesTunnelPath
+ : undefined;
+
+ if (tunnelRoute && typeof tunnelRoute === 'string') {
+ const req: unknown = args[0];
+ // Check if the current request matches the tunnel route
+ if (req instanceof Request) {
+ const url = new URL(req.url);
+ const isTunnelRequest = url.pathname.startsWith(tunnelRoute);
+
+ if (isTunnelRequest) {
+ // Create a simple response that mimics NextResponse.next() so we don't need to import internals here
+ // which breaks next 13 apps
+ // https://github.com/vercel/next.js/blob/c12c9c1f78ad384270902f0890dc4cd341408105/packages/next/src/server/web/spec-extension/response.ts#L146
+ return new Response(null, {
+ status: 200,
+ headers: {
+ 'x-middleware-next': '1',
+ },
+ }) as ReturnType;
+ }
+ }
+ }
// TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack.
return withIsolationScope(isolationScope => {
const req: unknown = args[0];
diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts
index e8172efea72e..bf9531a9ddd6 100644
--- a/packages/nextjs/src/config/types.ts
+++ b/packages/nextjs/src/config/types.ts
@@ -462,6 +462,11 @@ export type SentryBuildOptions = {
*/
errorHandler?: (err: Error) => void;
+ /**
+ * Suppress the warning about the `onRouterTransitionStart` hook.
+ */
+ suppressOnRouterTransitionStartWarning?: boolean;
+
/**
* Contains a set of experimental flags that might change in future releases. These flags enable
* features that are still in development and may be modified, renamed, or removed without notice.
diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts
index 88050713ec8c..84bd0a01cb5e 100644
--- a/packages/nextjs/src/config/withSentryConfig.ts
+++ b/packages/nextjs/src/config/withSentryConfig.ts
@@ -107,17 +107,15 @@ function getFinalConfigObject(
showedExportModeTunnelWarning = true;
// eslint-disable-next-line no-console
console.warn(
- '[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses serverside features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option',
+ '[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses server-side features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option',
);
}
} else {
const resolvedTunnelRoute =
- typeof userSentryOptions.tunnelRoute === 'boolean'
- ? generateRandomTunnelRoute()
- : userSentryOptions.tunnelRoute;
+ userSentryOptions.tunnelRoute === true ? generateRandomTunnelRoute() : userSentryOptions.tunnelRoute;
// Update the global options object to use the resolved value everywhere
- userSentryOptions.tunnelRoute = resolvedTunnelRoute;
+ userSentryOptions.tunnelRoute = resolvedTunnelRoute || undefined;
setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute);
}
}
@@ -236,7 +234,8 @@ function getFinalConfigObject(
const instrumentationClientFileContents = getInstrumentationClientFileContents();
if (
instrumentationClientFileContents !== undefined &&
- !instrumentationClientFileContents.includes('onRouterTransitionStart')
+ !instrumentationClientFileContents.includes('onRouterTransitionStart') &&
+ !userSentryOptions.suppressOnRouterTransitionStartWarning
) {
// eslint-disable-next-line no-console
console.warn(
diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts
index 70d032d2e7b2..c96184df51cf 100644
--- a/packages/nextjs/test/config/wrappers.test.ts
+++ b/packages/nextjs/test/config/wrappers.test.ts
@@ -2,7 +2,12 @@ import type { Client } from '@sentry/core';
import * as SentryCore from '@sentry/core';
import type { IncomingMessage, ServerResponse } from 'http';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
-import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common';
+import {
+ wrapGetInitialPropsWithSentry,
+ wrapGetServerSidePropsWithSentry,
+ wrapMiddlewareWithSentry,
+} from '../../src/common';
+import type { EdgeRouteHandler } from '../../src/edge/types';
const startSpanManualSpy = vi.spyOn(SentryCore, 'startSpanManual');
@@ -84,3 +89,102 @@ describe('data-fetching function wrappers should not create manual spans', () =>
expect(mockSetAttribute).not.toHaveBeenCalled();
});
});
+
+describe('wrapMiddlewareWithSentry', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ if ('_sentryRewritesTunnelPath' in globalThis) {
+ delete (globalThis as any)._sentryRewritesTunnelPath;
+ }
+ });
+
+ test('should skip processing and return NextResponse.next() for tunnel route requests', async () => {
+ // Set up tunnel route in global
+ (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';
+
+ const origFunction: EdgeRouteHandler = vi.fn(async () => ({ status: 200 }));
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ // Create a mock Request that matches the tunnel route
+ const mockRequest = new Request('https://example.com/monitoring/tunnel?o=123');
+
+ const result = await wrappedOriginal(mockRequest);
+
+ // Should skip calling the original function
+ expect(origFunction).not.toHaveBeenCalled();
+ expect(result).toBeDefined();
+ });
+
+ test('should process normal request and call original function', async () => {
+ const mockReturnValue = { status: 200 };
+ const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ const mockRequest = new Request('https://example.com/api/users', { method: 'GET' });
+
+ const result = await wrappedOriginal(mockRequest);
+
+ expect(origFunction).toHaveBeenCalledWith(mockRequest);
+ expect(result).toBe(mockReturnValue);
+ });
+
+ test('should handle non-Request arguments', async () => {
+ const mockReturnValue = { status: 200 };
+ const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ const mockArgs = { someProperty: 'value' };
+
+ const result = await wrappedOriginal(mockArgs);
+
+ expect(origFunction).toHaveBeenCalledWith(mockArgs);
+ expect(result).toBe(mockReturnValue);
+ });
+
+ test('should handle errors in middleware function', async () => {
+ const testError = new Error('Test middleware error');
+ const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => {
+ throw testError;
+ });
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ const mockRequest = new Request('https://example.com/api/users');
+
+ await expect(wrappedOriginal(mockRequest)).rejects.toThrow('Test middleware error');
+ expect(origFunction).toHaveBeenCalledWith(mockRequest);
+ });
+
+ test('should not process tunnel route when no tunnel path is set', async () => {
+ if ('_sentryRewritesTunnelPath' in globalThis) {
+ delete (globalThis as any)._sentryRewritesTunnelPath;
+ }
+
+ const mockReturnValue = { status: 200 };
+ const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ const mockRequest = new Request('https://example.com/monitoring/tunnel/sentry?o=123');
+
+ const result = await wrappedOriginal(mockRequest);
+
+ // Should process normally since no tunnel path is configured
+ expect(origFunction).toHaveBeenCalledWith(mockRequest);
+ expect(result).toBe(mockReturnValue);
+ });
+
+ test('should process request when tunnel path is set but request does not match', async () => {
+ (globalThis as any)._sentryRewritesTunnelPath = '/monitoring/tunnel';
+
+ const mockReturnValue = { status: 200 };
+ const origFunction: EdgeRouteHandler = vi.fn(async (..._args) => mockReturnValue);
+ const wrappedOriginal = wrapMiddlewareWithSentry(origFunction);
+
+ const mockRequest = new Request('https://example.com/api/users', { method: 'GET' });
+
+ const result = await wrappedOriginal(mockRequest);
+
+ // Should process normally since request doesn't match tunnel path
+ expect(origFunction).toHaveBeenCalledWith(mockRequest);
+ expect(result).toBe(mockReturnValue);
+ });
+});
diff --git a/packages/node-core/.eslintrc.js b/packages/node-core/.eslintrc.js
new file mode 100644
index 000000000000..6da218bd8641
--- /dev/null
+++ b/packages/node-core/.eslintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+ env: {
+ node: true,
+ },
+ extends: ['../../.eslintrc.js'],
+ rules: {
+ '@sentry-internal/sdk/no-class-field-initializers': 'off',
+ },
+};
diff --git a/packages/node-core/LICENSE b/packages/node-core/LICENSE
new file mode 100644
index 000000000000..0da96cd2f885
--- /dev/null
+++ b/packages/node-core/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Functional Software, Inc. dba Sentry
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/node-core/README.md b/packages/node-core/README.md
new file mode 100644
index 000000000000..570957a394ee
--- /dev/null
+++ b/packages/node-core/README.md
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+# Official Sentry SDK for Node-Core
+
+[](https://www.npmjs.com/package/@sentry/node-core)
+[](https://www.npmjs.com/package/@sentry/node-core)
+[](https://www.npmjs.com/package/@sentry/node-core)
+
+> [!CAUTION]
+> This package is in alpha state and may be subject to breaking changes.
+
+Unlike the `@sentry/node` SDK, this SDK comes with no OpenTelemetry auto-instrumentation out of the box. It requires the following OpenTelemetry dependencies and supports both v1 and v2 of OpenTelemetry:
+
+- `@opentelemetry/api`
+- `@opentelemetry/context-async-hooks`
+- `@opentelemetry/core`
+- `@opentelemetry/instrumentation`
+- `@opentelemetry/resources`
+- `@opentelemetry/sdk-trace-base`
+- `@opentelemetry/semantic-conventions`.
+
+## Installation
+
+```bash
+npm install @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions
+
+# Or yarn
+yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions
+```
+
+## Usage
+
+Sentry should be initialized as early in your app as possible. It is essential that you call `Sentry.init` before you
+require any other modules in your application, otherwise any auto-instrumentation will **not** work.
+
+You also **have to** set up OpenTelemetry, if you prefer not to, consider using the `@sentry/node` SDK instead.
+Without setting up OpenTelemetry, you only get basic error tracking out of the box without proper scope isolation.
+
+You need to create a file named `instrument.js` that imports and initializes Sentry:
+
+```js
+// CJS Syntax
+const { trace, propagation, context } = require('@opentelemetry/api');
+const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
+const Sentry = require('@sentry/node-core');
+const { SentrySpanProcessor, SentryPropagator, SentrySampler } = require('@sentry/opentelemetry');
+// ESM Syntax
+import { context, propagation, trace } from '@opentelemetry/api';
+import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
+import * as Sentry from '@sentry/node-core';
+import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry';
+
+const sentryClient = Sentry.init({
+ dsn: '__DSN__',
+ // ...
+});
+
+if (sentryClient) {
+ // Note: This could be BasicTracerProvider or any other provider depending on how you want to use the
+ // OpenTelemetry SDK
+ const provider = new NodeTracerProvider({
+ // Ensure the correct subset of traces is sent to Sentry
+ // This also ensures trace propagation works as expected
+ sampler: new SentrySampler(sentryClient),
+ spanProcessors: [
+ // Ensure spans are correctly linked & sent to Sentry
+ new SentrySpanProcessor(),
+ // Add additional processors here
+ ],
+ });
+
+ trace.setGlobalTracerProvider(provider);
+ propagation.setGlobalPropagator(new SentryPropagator());
+ context.setGlobalContextManager(new Sentry.SentryContextManager());
+}
+
+// Set up the OpenTelemetry logger to use Sentry's logger
+Sentry.setupOpenTelemetryLogger();
+
+// validate your setup
+Sentry.validateOpenTelemetrySetup();
+```
+
+You need to require or import the `instrument.js` file before importing any other modules in your application. This is
+necessary to ensure that Sentry can automatically instrument all modules in your application:
+
+```js
+// Import this first!
+import './instrument';
+
+// Now import other modules
+import http from 'http';
+
+// Your application code goes here
+```
+
+### ESM Support
+
+When running your application in ESM mode, you should use the Node.js
+[`--import`](https://nodejs.org/api/cli.html#--importmodule) command line option to ensure that Sentry is loaded before
+the application code is evaluated.
+
+Adjust the Node.js call for your application to use the `--import` parameter and point it at `instrument.js`, which
+contains your `Sentry.init`() code:
+
+```bash
+# Note: This is only available for Node v18.19.0 onwards.
+node --import ./instrument.mjs app.mjs
+```
+
+If it is not possible for you to pass the `--import` flag to the Node.js binary, you can alternatively use the
+`NODE_OPTIONS` environment variable as follows:
+
+```bash
+NODE_OPTIONS="--import ./instrument.mjs" npm run start
+```
+
+## Links
+
+- [Official SDK Docs](https://docs.sentry.io/quickstart/)
diff --git a/packages/node-core/package.json b/packages/node-core/package.json
new file mode 100644
index 000000000000..80f951f8ae4b
--- /dev/null
+++ b/packages/node-core/package.json
@@ -0,0 +1,109 @@
+{
+ "name": "@sentry/node-core",
+ "version": "9.35.0",
+ "description": "Sentry Node-Core SDK",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "/build"
+ ],
+ "main": "build/cjs/index.js",
+ "module": "build/esm/index.js",
+ "types": "build/types/index.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "import": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/esm/index.js"
+ },
+ "require": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/cjs/index.js"
+ }
+ },
+ "./import": {
+ "import": {
+ "default": "./build/import-hook.mjs"
+ }
+ },
+ "./loader": {
+ "import": {
+ "default": "./build/loader-hook.mjs"
+ }
+ },
+ "./init": {
+ "import": {
+ "default": "./build/esm/init.js"
+ },
+ "require": {
+ "default": "./build/cjs/init.js"
+ }
+ }
+ },
+ "typesVersions": {
+ "<5.0": {
+ "build/types/index.d.ts": [
+ "build/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0",
+ "@opentelemetry/resources": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.34.0"
+ },
+ "dependencies": {
+ "@sentry/core": "9.35.0",
+ "@sentry/opentelemetry": "9.35.0",
+ "import-in-the-middle": "^1.14.2"
+ },
+ "devDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.2",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
+ "@opentelemetry/semantic-conventions": "^1.34.0",
+ "@types/node": "^18.19.1"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8",
+ "build:watch": "run-p build:transpile:watch build:types:watch",
+ "build:dev:watch": "yarn build:watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "npm pack",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build coverage sentry-node-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module",
+ "test": "yarn test:unit",
+ "test:unit": "vitest run",
+ "test:watch": "vitest --watch",
+ "yalc:publish": "yalc publish --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/node-core/rollup.anr-worker.config.mjs b/packages/node-core/rollup.anr-worker.config.mjs
new file mode 100644
index 000000000000..e12c21f5ae72
--- /dev/null
+++ b/packages/node-core/rollup.anr-worker.config.mjs
@@ -0,0 +1,31 @@
+import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils';
+
+export function createWorkerCodeBuilder(entry, outDir) {
+ let base64Code;
+
+ return [
+ makeBaseBundleConfig({
+ bundleType: 'node-worker',
+ entrypoints: [entry],
+ licenseTitle: '@sentry/node-core',
+ outputFileBase: () => 'worker-script.js',
+ packageSpecificConfig: {
+ output: {
+ dir: outDir,
+ sourcemap: false,
+ },
+ plugins: [
+ {
+ name: 'output-base64-worker-script',
+ renderChunk(code) {
+ base64Code = Buffer.from(code).toString('base64');
+ },
+ },
+ ],
+ },
+ }),
+ () => {
+ return base64Code;
+ },
+ ];
+}
diff --git a/packages/node-core/rollup.npm.config.mjs b/packages/node-core/rollup.npm.config.mjs
new file mode 100644
index 000000000000..8e18333836ef
--- /dev/null
+++ b/packages/node-core/rollup.npm.config.mjs
@@ -0,0 +1,43 @@
+import replace from '@rollup/plugin-replace';
+import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils';
+import { createWorkerCodeBuilder } from './rollup.anr-worker.config.mjs';
+
+const [anrWorkerConfig, getAnrBase64Code] = createWorkerCodeBuilder(
+ 'src/integrations/anr/worker.ts',
+ 'build/esm/integrations/anr',
+);
+
+const [localVariablesWorkerConfig, getLocalVariablesBase64Code] = createWorkerCodeBuilder(
+ 'src/integrations/local-variables/worker.ts',
+ 'build/esm/integrations/local-variables',
+);
+
+export default [
+ ...makeOtelLoaders('./build', 'otel'),
+ // The workers needs to be built first since it's their output is copied in the main bundle.
+ anrWorkerConfig,
+ localVariablesWorkerConfig,
+ ...makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: ['src/index.ts', 'src/init.ts'],
+ packageSpecificConfig: {
+ output: {
+ // set exports to 'named' or 'auto' so that rollup doesn't warn
+ exports: 'named',
+ preserveModules: true,
+ },
+ plugins: [
+ replace({
+ delimiters: ['###', '###'],
+ // removes some rollup warnings
+ preventAssignment: true,
+ values: {
+ AnrWorkerScript: () => getAnrBase64Code(),
+ LocalVariablesWorkerScript: () => getLocalVariablesBase64Code(),
+ },
+ }),
+ ],
+ },
+ }),
+ ),
+];
diff --git a/packages/node/src/cron/common.ts b/packages/node-core/src/cron/common.ts
similarity index 100%
rename from packages/node/src/cron/common.ts
rename to packages/node-core/src/cron/common.ts
diff --git a/packages/node/src/cron/cron.ts b/packages/node-core/src/cron/cron.ts
similarity index 100%
rename from packages/node/src/cron/cron.ts
rename to packages/node-core/src/cron/cron.ts
diff --git a/packages/node/src/cron/index.ts b/packages/node-core/src/cron/index.ts
similarity index 100%
rename from packages/node/src/cron/index.ts
rename to packages/node-core/src/cron/index.ts
diff --git a/packages/node/src/cron/node-cron.ts b/packages/node-core/src/cron/node-cron.ts
similarity index 100%
rename from packages/node/src/cron/node-cron.ts
rename to packages/node-core/src/cron/node-cron.ts
diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node-core/src/cron/node-schedule.ts
similarity index 100%
rename from packages/node/src/cron/node-schedule.ts
rename to packages/node-core/src/cron/node-schedule.ts
diff --git a/packages/node-core/src/debug-build.ts b/packages/node-core/src/debug-build.ts
new file mode 100644
index 000000000000..60aa50940582
--- /dev/null
+++ b/packages/node-core/src/debug-build.ts
@@ -0,0 +1,8 @@
+declare const __DEBUG_BUILD__: boolean;
+
+/**
+ * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
+ *
+ * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
+ */
+export const DEBUG_BUILD = __DEBUG_BUILD__;
diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts
new file mode 100644
index 000000000000..6d478ea912e9
--- /dev/null
+++ b/packages/node-core/src/index.ts
@@ -0,0 +1,153 @@
+import * as logger from './logs/exports';
+
+export { httpIntegration } from './integrations/http';
+export {
+ SentryHttpInstrumentation,
+ type SentryHttpInstrumentationOptions,
+} from './integrations/http/SentryHttpInstrumentation';
+export { nativeNodeFetchIntegration } from './integrations/node-fetch';
+export {
+ SentryNodeFetchInstrumentation,
+ type SentryNodeFetchInstrumentationOptions,
+} from './integrations/node-fetch/SentryNodeFetchInstrumentation';
+
+export { nodeContextIntegration } from './integrations/context';
+export { contextLinesIntegration } from './integrations/contextlines';
+export { localVariablesIntegration } from './integrations/local-variables';
+export { modulesIntegration } from './integrations/modules';
+export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception';
+export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection';
+// eslint-disable-next-line deprecation/deprecation
+export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr';
+
+export { spotlightIntegration } from './integrations/spotlight';
+export { childProcessIntegration } from './integrations/childProcess';
+export { createSentryWinstonTransport } from './integrations/winston';
+
+export { SentryContextManager } from './otel/contextManager';
+export { setupOpenTelemetryLogger } from './otel/logger';
+export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument';
+
+export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk';
+export { setIsolationScope } from './sdk/scope';
+export { getSentryRelease, defaultStackParser } from './sdk/api';
+export { createGetModuleFromFilename } from './utils/module';
+export { addOriginToSpan } from './utils/addOriginToSpan';
+export { getRequestUrl } from './utils/getRequestUrl';
+export { isCjs } from './utils/commonjs';
+export { ensureIsWrapped } from './utils/ensureIsWrapped';
+export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext';
+export { envToBool } from './utils/envToBool';
+export { makeNodeTransport, type NodeTransportOptions } from './transports';
+export type { HTTPModuleRequestIncomingMessage } from './transports/http-module';
+export { NodeClient } from './sdk/client';
+export { cron } from './cron';
+export { NODE_VERSION } from './nodeVersion';
+
+export type { NodeOptions } from './types';
+
+export {
+ // This needs exporting so the NodeClient can be used without calling init
+ setOpenTelemetryContextAsyncContextStrategy as setNodeAsyncContextStrategy,
+} from '@sentry/opentelemetry';
+
+export {
+ addBreadcrumb,
+ isInitialized,
+ isEnabled,
+ getGlobalScope,
+ lastEventId,
+ close,
+ createTransport,
+ flush,
+ SDK_VERSION,
+ getSpanStatusFromHttpCode,
+ setHttpStatus,
+ captureCheckIn,
+ withMonitor,
+ requestDataIntegration,
+ functionToStringIntegration,
+ // eslint-disable-next-line deprecation/deprecation
+ inboundFiltersIntegration,
+ eventFiltersIntegration,
+ linkedErrorsIntegration,
+ addEventProcessor,
+ setContext,
+ setExtra,
+ setExtras,
+ setTag,
+ setTags,
+ setUser,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
+ setCurrentClient,
+ Scope,
+ setMeasurement,
+ getSpanDescendants,
+ parameterize,
+ getClient,
+ getCurrentScope,
+ getIsolationScope,
+ getTraceData,
+ getTraceMetaTags,
+ continueTrace,
+ withScope,
+ withIsolationScope,
+ captureException,
+ captureEvent,
+ captureMessage,
+ captureFeedback,
+ captureConsoleIntegration,
+ dedupeIntegration,
+ extraErrorDataIntegration,
+ rewriteFramesIntegration,
+ startSession,
+ captureSession,
+ endSession,
+ addIntegration,
+ startSpan,
+ startSpanManual,
+ startInactiveSpan,
+ startNewTrace,
+ suppressTracing,
+ getActiveSpan,
+ withActiveSpan,
+ getRootSpan,
+ spanToJSON,
+ spanToTraceHeader,
+ spanToBaggageHeader,
+ trpcMiddleware,
+ updateSpanName,
+ supabaseIntegration,
+ instrumentSupabaseClient,
+ zodErrorsIntegration,
+ profiler,
+ consoleLoggingIntegration,
+ consoleIntegration,
+ wrapMcpServerWithSentry,
+ featureFlagsIntegration,
+} from '@sentry/core';
+
+export type {
+ Breadcrumb,
+ BreadcrumbHint,
+ PolymorphicRequest,
+ RequestEventData,
+ SdkInfo,
+ Event,
+ EventHint,
+ ErrorEvent,
+ Exception,
+ Session,
+ SeverityLevel,
+ StackFrame,
+ Stacktrace,
+ Thread,
+ User,
+ Span,
+ FeatureFlagsIntegration,
+} from '@sentry/core';
+
+export { logger };
diff --git a/packages/node-core/src/init.ts b/packages/node-core/src/init.ts
new file mode 100644
index 000000000000..7dc0cb1546a3
--- /dev/null
+++ b/packages/node-core/src/init.ts
@@ -0,0 +1,9 @@
+import { init } from './sdk';
+
+/**
+ * The @sentry/node-core/init export can be used with the node --import and --require args to initialize the SDK entirely via
+ * environment variables.
+ *
+ * > SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 SENTRY_TRACES_SAMPLE_RATE=1.0 node --import=@sentry/node/init app.mjs
+ */
+init();
diff --git a/packages/node-core/src/integrations/anr/common.ts b/packages/node-core/src/integrations/anr/common.ts
new file mode 100644
index 000000000000..44fb519828fb
--- /dev/null
+++ b/packages/node-core/src/integrations/anr/common.ts
@@ -0,0 +1,84 @@
+import type { Contexts, DsnComponents, Primitive, SdkMetadata } from '@sentry/core';
+
+/**
+ * Configuration options for the ANR (Application Not Responding) integration.
+ *
+ * These options control how the ANR detection system monitors the Node.js event loop
+ * and reports blocking events.
+ *
+ * @deprecated The ANR integration has been deprecated. Use `eventLoopBlockIntegration` from `@sentry/node-native` instead.
+ */
+export interface AnrIntegrationOptions {
+ /**
+ * Interval to send heartbeat messages to the ANR worker thread.
+ *
+ * The main thread sends heartbeat messages to the worker thread at this interval
+ * to indicate that the event loop is still responsive. Lower values provide more
+ * precise detection but may increase overhead.
+ *
+ * @default 50 (milliseconds)
+ */
+ pollInterval: number;
+
+ /**
+ * Threshold in milliseconds to trigger an ANR event.
+ *
+ * When the worker thread doesn't receive a heartbeat message for this duration,
+ * it considers the main thread to be blocked and triggers an ANR event.
+ *
+ * @default 5000 (milliseconds)
+ */
+ anrThreshold: number;
+
+ /**
+ * Whether to capture a stack trace when the ANR event is triggered.
+ *
+ * When enabled, uses the Node.js inspector API to capture the stack trace
+ * of the blocking code. This provides more detailed information about what
+ * caused the ANR but requires the debugger to be enabled.
+ *
+ * **Note:** This opens the inspector API and required ports.
+ *
+ * @default false
+ */
+ captureStackTrace: boolean;
+
+ /**
+ * Maximum number of ANR events to send per application session.
+ *
+ * Once this limit is reached, the ANR worker thread will exit to prevent
+ * sending duplicate events. This helps avoid spamming Sentry with repeated
+ * ANR events from the same blocking issue.
+ *
+ * @default 1
+ */
+ maxAnrEvents: number;
+
+ /**
+ * Static tags to include with all ANR events.
+ *
+ * These tags will be attached to every ANR event sent by this integration,
+ * useful for categorizing or filtering ANR events in Sentry.
+ */
+ staticTags: { [key: string]: Primitive };
+
+ /**
+ * @ignore Internal use only.
+ *
+ * If this is supplied, stack frame filenames will be rewritten to be relative to this path.
+ * This is used internally for better stack trace readability.
+ */
+ appRootPath: string | undefined;
+}
+
+// eslint-disable-next-line deprecation/deprecation
+export interface WorkerStartData extends AnrIntegrationOptions {
+ debug: boolean;
+ sdkMetadata: SdkMetadata;
+ dsn: DsnComponents;
+ tunnel: string | undefined;
+ release: string | undefined;
+ environment: string;
+ dist: string | undefined;
+ contexts: Contexts;
+}
diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node-core/src/integrations/anr/index.ts
similarity index 79%
rename from packages/node/src/integrations/anr/index.ts
rename to packages/node-core/src/integrations/anr/index.ts
index 5d5cc2daa5f6..9615ca241198 100644
--- a/packages/node/src/integrations/anr/index.ts
+++ b/packages/node-core/src/integrations/anr/index.ts
@@ -66,6 +66,7 @@ const INTEGRATION_NAME = 'Anr';
type AnrInternal = { startWorker: () => void; stopWorker: () => void };
+// eslint-disable-next-line deprecation/deprecation
const _anrIntegration = ((options: Partial = {}) => {
if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
throw new Error('ANR detection requires Node 16.17.0 or later');
@@ -114,8 +115,45 @@ const _anrIntegration = ((options: Partial = {}) => {
} as Integration & AnrInternal;
}) satisfies IntegrationFn;
+// eslint-disable-next-line deprecation/deprecation
type AnrReturn = (options?: Partial) => Integration & AnrInternal;
+/**
+ * Application Not Responding (ANR) integration for Node.js applications.
+ *
+ * @deprecated The ANR integration has been deprecated. Use `eventLoopBlockIntegration` from `@sentry/node-native` instead.
+ *
+ * Detects when the Node.js main thread event loop is blocked for more than the configured
+ * threshold (5 seconds by default) and reports these as Sentry events.
+ *
+ * ANR detection uses a worker thread to monitor the event loop in the main app thread.
+ * The main app thread sends a heartbeat message to the ANR worker thread every 50ms by default.
+ * If the ANR worker does not receive a heartbeat message for the configured threshold duration,
+ * it triggers an ANR event.
+ *
+ * - Node.js 16.17.0 or higher
+ * - Only supported in the Node.js runtime (not browsers)
+ * - Not supported for Node.js clusters
+ *
+ * Overhead should be minimal:
+ * - Main thread: Only polling the ANR worker over IPC every 50ms
+ * - Worker thread: Consumes around 10-20 MB of RAM
+ * - When ANR detected: Brief pause in debugger to capture stack trace (negligible compared to the blocking)
+ *
+ * @example
+ * ```javascript
+ * Sentry.init({
+ * dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
+ * integrations: [
+ * Sentry.anrIntegration({
+ * anrThreshold: 5000,
+ * captureStackTrace: true,
+ * pollInterval: 50,
+ * }),
+ * ],
+ * });
+ * ```
+ */
export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
/**
@@ -125,6 +163,7 @@ export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
*/
async function _startWorker(
client: NodeClient,
+ // eslint-disable-next-line deprecation/deprecation
integrationOptions: Partial,
): Promise<() => void> {
const dsn = client.getDsn();
@@ -229,7 +268,13 @@ async function _startWorker(
export function disableAnrDetectionForCallback(callback: () => T): T;
export function disableAnrDetectionForCallback(callback: () => Promise): Promise;
/**
- * Disables ANR detection for the duration of the callback
+ * Temporarily disables ANR detection for the duration of a callback function.
+ *
+ * This utility function allows you to disable ANR detection during operations that
+ * are expected to block the event loop, such as intensive computational tasks or
+ * synchronous I/O operations.
+ *
+ * @deprecated The ANR integration has been deprecated. Use `eventLoopBlockIntegration` from `@sentry/node-native` instead.
*/
export function disableAnrDetectionForCallback(callback: () => T | Promise): T | Promise {
const integration = getClient()?.getIntegrationByName(INTEGRATION_NAME) as AnrInternal | undefined;
diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node-core/src/integrations/anr/worker.ts
similarity index 100%
rename from packages/node/src/integrations/anr/worker.ts
rename to packages/node-core/src/integrations/anr/worker.ts
diff --git a/packages/node/src/integrations/childProcess.ts b/packages/node-core/src/integrations/childProcess.ts
similarity index 100%
rename from packages/node/src/integrations/childProcess.ts
rename to packages/node-core/src/integrations/childProcess.ts
diff --git a/packages/node/src/integrations/context.ts b/packages/node-core/src/integrations/context.ts
similarity index 100%
rename from packages/node/src/integrations/context.ts
rename to packages/node-core/src/integrations/context.ts
diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node-core/src/integrations/contextlines.ts
similarity index 100%
rename from packages/node/src/integrations/contextlines.ts
rename to packages/node-core/src/integrations/contextlines.ts
diff --git a/packages/node/src/integrations/diagnostic_channel.d.ts b/packages/node-core/src/integrations/diagnostic_channel.d.ts
similarity index 100%
rename from packages/node/src/integrations/diagnostic_channel.d.ts
rename to packages/node-core/src/integrations/diagnostic_channel.d.ts
diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
similarity index 99%
rename from packages/node/src/integrations/http/SentryHttpInstrumentation.ts
rename to packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
index b8c339b30185..d1cfc3b1ea0c 100644
--- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts
+++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
@@ -5,7 +5,7 @@ import type * as http from 'node:http';
import type * as https from 'node:https';
import type { EventEmitter } from 'node:stream';
import { context, propagation } from '@opentelemetry/api';
-import { isTracingSuppressed, VERSION } from '@opentelemetry/core';
+import { isTracingSuppressed } from '@opentelemetry/core';
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
@@ -24,6 +24,7 @@ import {
logger,
LRUMap,
parseUrl,
+ SDK_VERSION,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
@@ -136,7 +137,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase;
public constructor(config: SentryHttpInstrumentationOptions = {}) {
- super(INSTRUMENTATION_NAME, VERSION, config);
+ super(INSTRUMENTATION_NAME, SDK_VERSION, config);
this._propagationDecisionMap = new LRUMap(100);
this._ignoreOutgoingRequestsMap = new WeakMap();
diff --git a/packages/node-core/src/integrations/http/index.ts b/packages/node-core/src/integrations/http/index.ts
new file mode 100644
index 000000000000..8bd69c22a8e7
--- /dev/null
+++ b/packages/node-core/src/integrations/http/index.ts
@@ -0,0 +1,141 @@
+import type { IncomingMessage, RequestOptions } from 'node:http';
+import { defineIntegration } from '@sentry/core';
+import { generateInstrumentOnce } from '../../otel/instrument';
+import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation';
+import { SentryHttpInstrumentation } from './SentryHttpInstrumentation';
+
+const INTEGRATION_NAME = 'Http';
+
+interface HttpOptions {
+ /**
+ * Whether breadcrumbs should be recorded for outgoing requests.
+ * Defaults to true
+ */
+ breadcrumbs?: boolean;
+
+ /**
+ * Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry.
+ * Read more about Release Health: https://docs.sentry.io/product/releases/health/
+ *
+ * Defaults to `true`.
+ */
+ trackIncomingRequestsAsSessions?: boolean;
+
+ /**
+ * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate.
+ *
+ * Defaults to `60000` (60s).
+ */
+ sessionFlushingDelayMS?: number;
+
+ /**
+ * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
+ * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
+ *
+ * The `url` param contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request.
+ * For example: `'https://someService.com/users/details?id=123'`
+ *
+ * The `request` param contains the original {@type RequestOptions} object used to make the outgoing request.
+ * You can use it to filter on additional properties like method, headers, etc.
+ */
+ ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
+
+ /**
+ * Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`.
+ * Spans will be non recording if tracing is disabled.
+ *
+ * The `urlPath` param consists of the URL path and query string (if any) of the incoming request.
+ * For example: `'/users/details?id=123'`
+ *
+ * The `request` param contains the original {@type IncomingMessage} object of the incoming request.
+ * You can use it to filter on additional properties like method, headers, etc.
+ */
+ ignoreIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean;
+
+ /**
+ * Do not capture spans for incoming HTTP requests with the given status codes.
+ * By default, spans with 404 status code are ignored.
+ * Expects an array of status codes or a range of status codes, e.g. [[300,399], 404] would ignore 3xx and 404 status codes.
+ *
+ * @default `[404]`
+ */
+ dropSpansForIncomingRequestStatusCodes?: (number | [number, number])[];
+
+ /**
+ * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
+ * This can be useful for long running requests where the body is not needed and we want to avoid capturing it.
+ *
+ * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request.
+ * @param request Contains the {@type RequestOptions} object used to make the incoming request.
+ */
+ ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean;
+
+ /**
+ * Controls the maximum size of incoming HTTP request bodies attached to events.
+ *
+ * Available options:
+ * - 'none': No request bodies will be attached
+ * - 'small': Request bodies up to 1,000 bytes will be attached
+ * - 'medium': Request bodies up to 10,000 bytes will be attached (default)
+ * - 'always': Request bodies will always be attached
+ *
+ * Note that even with 'always' setting, bodies exceeding 1MB will never be attached
+ * for performance and security reasons.
+ *
+ * @default 'medium'
+ */
+ maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
+
+ /**
+ * If true, do not generate spans for incoming requests at all.
+ * This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans.
+ */
+ disableIncomingRequestSpans?: boolean;
+}
+
+const instrumentSentryHttp = generateInstrumentOnce(
+ `${INTEGRATION_NAME}.sentry`,
+ options => {
+ return new SentryHttpInstrumentation(options);
+ },
+);
+
+/**
+ * The http integration instruments Node's internal http and https modules.
+ * It creates breadcrumbs for outgoing HTTP requests which will be attached to the currently active span.
+ */
+export const httpIntegration = defineIntegration((options: HttpOptions = {}) => {
+ const dropSpansForIncomingRequestStatusCodes = options.dropSpansForIncomingRequestStatusCodes ?? [404];
+
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ instrumentSentryHttp({
+ ...options,
+ extractIncomingTraceFromHeader: true,
+ propagateTraceInOutgoingRequests: true,
+ });
+ },
+ processEvent(event) {
+ // Drop transaction if it has a status code that should be ignored
+ if (event.type === 'transaction') {
+ const statusCode = event.contexts?.trace?.data?.['http.response.status_code'];
+ if (
+ typeof statusCode === 'number' &&
+ dropSpansForIncomingRequestStatusCodes.some(code => {
+ if (typeof code === 'number') {
+ return code === statusCode;
+ }
+
+ const [min, max] = code;
+ return statusCode >= min && statusCode <= max;
+ })
+ ) {
+ return null;
+ }
+ }
+
+ return event;
+ },
+ };
+});
diff --git a/packages/node/src/integrations/local-variables/common.ts b/packages/node-core/src/integrations/local-variables/common.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/common.ts
rename to packages/node-core/src/integrations/local-variables/common.ts
diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node-core/src/integrations/local-variables/index.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/index.ts
rename to packages/node-core/src/integrations/local-variables/index.ts
diff --git a/packages/node/src/integrations/local-variables/inspector.d.ts b/packages/node-core/src/integrations/local-variables/inspector.d.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/inspector.d.ts
rename to packages/node-core/src/integrations/local-variables/inspector.d.ts
diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node-core/src/integrations/local-variables/local-variables-async.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/local-variables-async.ts
rename to packages/node-core/src/integrations/local-variables/local-variables-async.ts
diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/local-variables-sync.ts
rename to packages/node-core/src/integrations/local-variables/local-variables-sync.ts
diff --git a/packages/node/src/integrations/local-variables/worker.ts b/packages/node-core/src/integrations/local-variables/worker.ts
similarity index 100%
rename from packages/node/src/integrations/local-variables/worker.ts
rename to packages/node-core/src/integrations/local-variables/worker.ts
diff --git a/packages/node/src/integrations/modules.ts b/packages/node-core/src/integrations/modules.ts
similarity index 100%
rename from packages/node/src/integrations/modules.ts
rename to packages/node-core/src/integrations/modules.ts
diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts
similarity index 98%
rename from packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts
rename to packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts
index 2ee9f55c78a1..3b7b745077be 100644
--- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts
+++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts
@@ -1,5 +1,5 @@
import { context } from '@opentelemetry/api';
-import { isTracingSuppressed, VERSION } from '@opentelemetry/core';
+import { isTracingSuppressed } from '@opentelemetry/core';
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase } from '@opentelemetry/instrumentation';
import type { SanitizedRequestData } from '@sentry/core';
@@ -11,6 +11,7 @@ import {
getTraceData,
LRUMap,
parseUrl,
+ SDK_VERSION,
} from '@sentry/core';
import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry';
import * as diagch from 'diagnostics_channel';
@@ -65,7 +66,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase;
public constructor(config: SentryNodeFetchInstrumentationOptions = {}) {
- super('@sentry/instrumentation-node-fetch', VERSION, config);
+ super('@sentry/instrumentation-node-fetch', SDK_VERSION, config);
this._channelSubs = [];
this._propagationDecisionMap = new LRUMap(100);
this._ignoreOutgoingRequestsMap = new WeakMap();
diff --git a/packages/node-core/src/integrations/node-fetch/index.ts b/packages/node-core/src/integrations/node-fetch/index.ts
new file mode 100644
index 000000000000..4ca41bddfda8
--- /dev/null
+++ b/packages/node-core/src/integrations/node-fetch/index.ts
@@ -0,0 +1,39 @@
+import type { IntegrationFn } from '@sentry/core';
+import { defineIntegration } from '@sentry/core';
+import { generateInstrumentOnce } from '../../otel/instrument';
+import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';
+
+const INTEGRATION_NAME = 'NodeFetch';
+
+interface NodeFetchOptions {
+ /**
+ * Whether breadcrumbs should be recorded for requests.
+ * Defaults to true
+ */
+ breadcrumbs?: boolean;
+
+ /**
+ * Do not capture spans or breadcrumbs for outgoing fetch requests to URLs where the given callback returns `true`.
+ * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
+ */
+ ignoreOutgoingRequests?: (url: string) => boolean;
+}
+
+const instrumentSentryNodeFetch = generateInstrumentOnce(
+ `${INTEGRATION_NAME}.sentry`,
+ SentryNodeFetchInstrumentation,
+ (options: NodeFetchOptions) => {
+ return options;
+ },
+);
+
+const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {
+ return {
+ name: 'NodeFetch',
+ setupOnce() {
+ instrumentSentryNodeFetch(options);
+ },
+ };
+}) satisfies IntegrationFn;
+
+export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration);
diff --git a/packages/node-core/src/integrations/node-fetch/types.ts b/packages/node-core/src/integrations/node-fetch/types.ts
new file mode 100644
index 000000000000..0139dadde413
--- /dev/null
+++ b/packages/node-core/src/integrations/node-fetch/types.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/types.ts
+ */
+
+export interface UndiciRequest {
+ origin: string;
+ method: string;
+ path: string;
+ /**
+ * Serialized string of headers in the form `name: value\r\n` for v5
+ * Array of strings v6
+ */
+ headers: string | string[];
+ /**
+ * Helper method to add headers (from v6)
+ */
+ addHeader: (name: string, value: string) => void;
+ throwOnError: boolean;
+ completed: boolean;
+ aborted: boolean;
+ idempotent: boolean;
+ contentLength: number | null;
+ contentType: string | null;
+ body: unknown;
+}
+
+export interface UndiciResponse {
+ headers: Buffer[];
+ statusCode: number;
+ statusText: string;
+}
diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node-core/src/integrations/onuncaughtexception.ts
similarity index 100%
rename from packages/node/src/integrations/onuncaughtexception.ts
rename to packages/node-core/src/integrations/onuncaughtexception.ts
diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts
similarity index 100%
rename from packages/node/src/integrations/onunhandledrejection.ts
rename to packages/node-core/src/integrations/onunhandledrejection.ts
diff --git a/packages/node/src/integrations/processSession.ts b/packages/node-core/src/integrations/processSession.ts
similarity index 100%
rename from packages/node/src/integrations/processSession.ts
rename to packages/node-core/src/integrations/processSession.ts
diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node-core/src/integrations/spotlight.ts
similarity index 100%
rename from packages/node/src/integrations/spotlight.ts
rename to packages/node-core/src/integrations/spotlight.ts
diff --git a/packages/node/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts
similarity index 100%
rename from packages/node/src/integrations/winston.ts
rename to packages/node-core/src/integrations/winston.ts
diff --git a/packages/node/src/logs/capture.ts b/packages/node-core/src/logs/capture.ts
similarity index 100%
rename from packages/node/src/logs/capture.ts
rename to packages/node-core/src/logs/capture.ts
diff --git a/packages/node/src/logs/exports.ts b/packages/node-core/src/logs/exports.ts
similarity index 100%
rename from packages/node/src/logs/exports.ts
rename to packages/node-core/src/logs/exports.ts
diff --git a/packages/node/src/nodeVersion.ts b/packages/node-core/src/nodeVersion.ts
similarity index 100%
rename from packages/node/src/nodeVersion.ts
rename to packages/node-core/src/nodeVersion.ts
diff --git a/packages/node/src/otel/contextManager.ts b/packages/node-core/src/otel/contextManager.ts
similarity index 100%
rename from packages/node/src/otel/contextManager.ts
rename to packages/node-core/src/otel/contextManager.ts
diff --git a/packages/node/src/otel/instrument.ts b/packages/node-core/src/otel/instrument.ts
similarity index 98%
rename from packages/node/src/otel/instrument.ts
rename to packages/node-core/src/otel/instrument.ts
index c5e94991140a..6f19190afe91 100644
--- a/packages/node/src/otel/instrument.ts
+++ b/packages/node-core/src/otel/instrument.ts
@@ -25,6 +25,7 @@ export function generateInstrumentOnce<
*/
export function generateInstrumentOnce(
name: string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
diff --git a/packages/node-core/src/otel/logger.ts b/packages/node-core/src/otel/logger.ts
new file mode 100644
index 000000000000..53cbdc63c3ee
--- /dev/null
+++ b/packages/node-core/src/otel/logger.ts
@@ -0,0 +1,18 @@
+import { diag, DiagLogLevel } from '@opentelemetry/api';
+import { logger } from '@sentry/core';
+
+/**
+ * Setup the OTEL logger to use our own logger.
+ */
+export function setupOpenTelemetryLogger(): void {
+ const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, {
+ get(target, prop, receiver) {
+ const actualProp = prop === 'verbose' ? 'debug' : prop;
+ return Reflect.get(target, actualProp, receiver);
+ },
+ });
+
+ // Disable diag, to ensure this works even if called multiple times
+ diag.disable();
+ diag.setLogger(otelLogger, DiagLogLevel.DEBUG);
+}
diff --git a/packages/node/src/proxy/base.ts b/packages/node-core/src/proxy/base.ts
similarity index 100%
rename from packages/node/src/proxy/base.ts
rename to packages/node-core/src/proxy/base.ts
diff --git a/packages/node/src/proxy/helpers.ts b/packages/node-core/src/proxy/helpers.ts
similarity index 100%
rename from packages/node/src/proxy/helpers.ts
rename to packages/node-core/src/proxy/helpers.ts
diff --git a/packages/node/src/proxy/index.ts b/packages/node-core/src/proxy/index.ts
similarity index 98%
rename from packages/node/src/proxy/index.ts
rename to packages/node-core/src/proxy/index.ts
index 788b302eeab3..8ff6741623b2 100644
--- a/packages/node/src/proxy/index.ts
+++ b/packages/node-core/src/proxy/index.ts
@@ -28,7 +28,9 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
/* eslint-disable @typescript-eslint/no-unused-vars */
+// eslint-disable-next-line import/no-duplicates
import type * as http from 'node:http';
+// eslint-disable-next-line import/no-duplicates
import type { OutgoingHttpHeaders } from 'node:http';
import * as net from 'node:net';
import * as tls from 'node:tls';
diff --git a/packages/node/src/proxy/parse-proxy-response.ts b/packages/node-core/src/proxy/parse-proxy-response.ts
similarity index 100%
rename from packages/node/src/proxy/parse-proxy-response.ts
rename to packages/node-core/src/proxy/parse-proxy-response.ts
diff --git a/packages/node/src/sdk/api.ts b/packages/node-core/src/sdk/api.ts
similarity index 100%
rename from packages/node/src/sdk/api.ts
rename to packages/node-core/src/sdk/api.ts
diff --git a/packages/node/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts
similarity index 97%
rename from packages/node/src/sdk/client.ts
rename to packages/node-core/src/sdk/client.ts
index 9bfb83bc6d99..022e3cb1ac33 100644
--- a/packages/node/src/sdk/client.ts
+++ b/packages/node-core/src/sdk/client.ts
@@ -83,11 +83,8 @@ export class NodeClient extends ServerRuntimeClient {
// eslint-disable-next-line jsdoc/require-jsdoc
public async flush(timeout?: number): Promise {
const provider = this.traceProvider;
- const spanProcessor = provider?.activeSpanProcessor;
- if (spanProcessor) {
- await spanProcessor.forceFlush();
- }
+ await provider?.forceFlush();
if (this.getOptions().sendClientReports) {
this._flushOutcomes();
diff --git a/packages/node-core/src/sdk/esmLoader.ts b/packages/node-core/src/sdk/esmLoader.ts
new file mode 100644
index 000000000000..487e2ce0c613
--- /dev/null
+++ b/packages/node-core/src/sdk/esmLoader.ts
@@ -0,0 +1,31 @@
+import { consoleSandbox, GLOBAL_OBJ, logger } from '@sentry/core';
+import { createAddHookMessageChannel } from 'import-in-the-middle';
+import moduleModule from 'module';
+
+/** Initialize the ESM loader. */
+export function maybeInitializeEsmLoader(): void {
+ const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split('.').map(Number);
+
+ // Register hook was added in v20.6.0 and v18.19.0
+ if (nodeMajor >= 21 || (nodeMajor === 20 && nodeMinor >= 6) || (nodeMajor === 18 && nodeMinor >= 19)) {
+ if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered) {
+ try {
+ const { addHookMessagePort } = createAddHookMessageChannel();
+ // @ts-expect-error register is available in these versions
+ moduleModule.register('import-in-the-middle/hook.mjs', import.meta.url, {
+ data: { addHookMessagePort, include: [] },
+ transferList: [addHookMessagePort],
+ });
+ } catch (error) {
+ logger.warn('Failed to register ESM hook', error);
+ }
+ }
+ } else {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[Sentry] You are using Node.js v${process.versions.node} in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.`,
+ );
+ });
+ }
+}
diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts
new file mode 100644
index 000000000000..47c4256c5c2f
--- /dev/null
+++ b/packages/node-core/src/sdk/index.ts
@@ -0,0 +1,246 @@
+import type { Integration, Options } from '@sentry/core';
+import {
+ consoleIntegration,
+ consoleSandbox,
+ functionToStringIntegration,
+ getCurrentScope,
+ getIntegrationsToSetup,
+ hasSpansEnabled,
+ inboundFiltersIntegration,
+ linkedErrorsIntegration,
+ logger,
+ propagationContextFromHeaders,
+ requestDataIntegration,
+ stackParserFromStackParserOptions,
+} from '@sentry/core';
+import {
+ enhanceDscWithOpenTelemetryRootSpanName,
+ openTelemetrySetupCheck,
+ setOpenTelemetryContextAsyncContextStrategy,
+ setupEventContextTrace,
+} from '@sentry/opentelemetry';
+import { DEBUG_BUILD } from '../debug-build';
+import { childProcessIntegration } from '../integrations/childProcess';
+import { nodeContextIntegration } from '../integrations/context';
+import { contextLinesIntegration } from '../integrations/contextlines';
+import { httpIntegration } from '../integrations/http';
+import { localVariablesIntegration } from '../integrations/local-variables';
+import { modulesIntegration } from '../integrations/modules';
+import { nativeNodeFetchIntegration } from '../integrations/node-fetch';
+import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception';
+import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
+import { processSessionIntegration } from '../integrations/processSession';
+import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
+import { makeNodeTransport } from '../transports';
+import type { NodeClientOptions, NodeOptions } from '../types';
+import { isCjs } from '../utils/commonjs';
+import { envToBool } from '../utils/envToBool';
+import { defaultStackParser, getSentryRelease } from './api';
+import { NodeClient } from './client';
+import { maybeInitializeEsmLoader } from './esmLoader';
+
+/**
+ * Get default integrations for the Node-Core SDK.
+ */
+export function getDefaultIntegrations(): Integration[] {
+ return [
+ // Common
+ // TODO(v10): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration`
+ // eslint-disable-next-line deprecation/deprecation
+ inboundFiltersIntegration(),
+ functionToStringIntegration(),
+ linkedErrorsIntegration(),
+ requestDataIntegration(),
+ // Native Wrappers
+ consoleIntegration(),
+ httpIntegration(),
+ nativeNodeFetchIntegration(),
+ // Global Handlers
+ onUncaughtExceptionIntegration(),
+ onUnhandledRejectionIntegration(),
+ // Event Info
+ contextLinesIntegration(),
+ localVariablesIntegration(),
+ nodeContextIntegration(),
+ childProcessIntegration(),
+ processSessionIntegration(),
+ modulesIntegration(),
+ ];
+}
+
+/**
+ * Initialize Sentry for Node.
+ */
+export function init(options: NodeOptions | undefined = {}): NodeClient | undefined {
+ return _init(options, getDefaultIntegrations);
+}
+
+/**
+ * Initialize Sentry for Node, without any integrations added by default.
+ */
+export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient {
+ return _init(options, () => []);
+}
+
+/**
+ * Initialize Sentry for Node, without performance instrumentation.
+ */
+function _init(
+ _options: NodeOptions | undefined = {},
+ getDefaultIntegrationsImpl: (options: Options) => Integration[],
+): NodeClient {
+ const options = getClientOptions(_options, getDefaultIntegrationsImpl);
+
+ if (options.debug === true) {
+ if (DEBUG_BUILD) {
+ logger.enable();
+ } else {
+ // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.');
+ });
+ }
+ }
+
+ if (!isCjs() && options.registerEsmLoaderHooks !== false) {
+ maybeInitializeEsmLoader();
+ }
+
+ setOpenTelemetryContextAsyncContextStrategy();
+
+ const scope = getCurrentScope();
+ scope.update(options.initialScope);
+
+ if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) {
+ options.integrations.push(
+ spotlightIntegration({
+ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
+ }),
+ );
+ }
+
+ const client = new NodeClient(options);
+ // The client is on the current scope, from where it generally is inherited
+ getCurrentScope().setClient(client);
+
+ client.init();
+
+ logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`);
+
+ client.startClientReportTracking();
+
+ updateScopeFromEnvVariables();
+
+ enhanceDscWithOpenTelemetryRootSpanName(client);
+ setupEventContextTrace(client);
+
+ return client;
+}
+
+/**
+ * Validate that your OpenTelemetry setup is correct.
+ */
+export function validateOpenTelemetrySetup(): void {
+ if (!DEBUG_BUILD) {
+ return;
+ }
+
+ const setup = openTelemetrySetupCheck();
+
+ const required: ReturnType = ['SentryContextManager', 'SentryPropagator'];
+
+ if (hasSpansEnabled()) {
+ required.push('SentrySpanProcessor');
+ }
+
+ for (const k of required) {
+ if (!setup.includes(k)) {
+ logger.error(
+ `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`,
+ );
+ }
+ }
+
+ if (!setup.includes('SentrySampler')) {
+ logger.warn(
+ 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.',
+ );
+ }
+}
+
+function getClientOptions(
+ options: NodeOptions,
+ getDefaultIntegrationsImpl: (options: Options) => Integration[],
+): NodeClientOptions {
+ const release = getRelease(options.release);
+ const spotlight =
+ options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT;
+ const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate);
+
+ const mergedOptions = {
+ ...options,
+ dsn: options.dsn ?? process.env.SENTRY_DSN,
+ environment: options.environment ?? process.env.SENTRY_ENVIRONMENT,
+ sendClientReports: options.sendClientReports ?? true,
+ transport: options.transport ?? makeNodeTransport,
+ stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
+ release,
+ tracesSampleRate,
+ spotlight,
+ debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG),
+ };
+
+ const integrations = options.integrations;
+ const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions);
+
+ return {
+ ...mergedOptions,
+ integrations: getIntegrationsToSetup({
+ defaultIntegrations,
+ integrations,
+ }),
+ };
+}
+
+function getRelease(release: NodeOptions['release']): string | undefined {
+ if (release !== undefined) {
+ return release;
+ }
+
+ const detectedRelease = getSentryRelease();
+ if (detectedRelease !== undefined) {
+ return detectedRelease;
+ }
+
+ return undefined;
+}
+
+function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']): number | undefined {
+ if (tracesSampleRate !== undefined) {
+ return tracesSampleRate;
+ }
+
+ const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE;
+ if (!sampleRateFromEnv) {
+ return undefined;
+ }
+
+ const parsed = parseFloat(sampleRateFromEnv);
+ return isFinite(parsed) ? parsed : undefined;
+}
+
+/**
+ * Update scope and propagation context based on environmental variables.
+ *
+ * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md
+ * for more details.
+ */
+function updateScopeFromEnvVariables(): void {
+ if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) {
+ const sentryTraceEnv = process.env.SENTRY_TRACE;
+ const baggageEnv = process.env.SENTRY_BAGGAGE;
+ const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv);
+ getCurrentScope().setPropagationContext(propagationContext);
+ }
+}
diff --git a/packages/node/src/sdk/scope.ts b/packages/node-core/src/sdk/scope.ts
similarity index 100%
rename from packages/node/src/sdk/scope.ts
rename to packages/node-core/src/sdk/scope.ts
diff --git a/packages/node/src/transports/http-module.ts b/packages/node-core/src/transports/http-module.ts
similarity index 100%
rename from packages/node/src/transports/http-module.ts
rename to packages/node-core/src/transports/http-module.ts
diff --git a/packages/node/src/transports/http.ts b/packages/node-core/src/transports/http.ts
similarity index 100%
rename from packages/node/src/transports/http.ts
rename to packages/node-core/src/transports/http.ts
diff --git a/packages/node/src/transports/index.ts b/packages/node-core/src/transports/index.ts
similarity index 100%
rename from packages/node/src/transports/index.ts
rename to packages/node-core/src/transports/index.ts
diff --git a/packages/node-core/src/types.ts b/packages/node-core/src/types.ts
new file mode 100644
index 000000000000..a331b876166d
--- /dev/null
+++ b/packages/node-core/src/types.ts
@@ -0,0 +1,178 @@
+import type { Span as WriteableSpan } from '@opentelemetry/api';
+import type { Instrumentation } from '@opentelemetry/instrumentation';
+import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
+import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core';
+import type { NodeTransportOptions } from './transports';
+
+export interface BaseNodeOptions {
+ /**
+ * List of strings/regex controlling to which outgoing requests
+ * the SDK will attach tracing headers.
+ *
+ * By default the SDK will attach those headers to all outgoing
+ * requests. If this option is provided, the SDK will match the
+ * request URL of outgoing requests against the items in this
+ * array, and only attach tracing headers if a match was found.
+ *
+ * @example
+ * ```js
+ * Sentry.init({
+ * tracePropagationTargets: ['api.site.com'],
+ * });
+ * ```
+ */
+ tracePropagationTargets?: TracePropagationTargets;
+
+ /**
+ * Sets profiling sample rate when @sentry/profiling-node is installed
+ *
+ * @deprecated
+ */
+ profilesSampleRate?: number;
+
+ /**
+ * Function to compute profiling sample rate dynamically and filter unwanted profiles.
+ *
+ * Profiling is enabled if either this or `profilesSampleRate` is defined. If both are defined, `profilesSampleRate` is
+ * ignored.
+ *
+ * Will automatically be passed a context object of default and optional custom data.
+ *
+ * @returns A sample rate between 0 and 1 (0 drops the profile, 1 guarantees it will be sent). Returning `true` is
+ * equivalent to returning 1 and returning `false` is equivalent to returning 0.
+ *
+ * @deprecated
+ */
+ profilesSampler?: (samplingContext: SamplingContext) => number | boolean;
+
+ /**
+ * Sets profiling session sample rate - only evaluated once per SDK initialization.
+ * @default 0
+ */
+ profileSessionSampleRate?: number;
+
+ /**
+ * Set the lifecycle of the profiler.
+ *
+ * - `manual`: The profiler will be manually started and stopped.
+ * - `trace`: The profiler will be automatically started when when a span is sampled and stopped when there are no more sampled spans.
+ *
+ * @default 'manual'
+ */
+ profileLifecycle?: 'manual' | 'trace';
+
+ /**
+ * If set to `false`, the SDK will not automatically detect the `serverName`.
+ *
+ * This is useful if you are using the SDK in a CLI app or Electron where the
+ * hostname might be considered PII.
+ *
+ * @default true
+ */
+ includeServerName?: boolean;
+
+ /** Sets an optional server name (device name) */
+ serverName?: string;
+
+ /**
+ * Include local variables with stack traces.
+ *
+ * Requires the `LocalVariables` integration.
+ */
+ includeLocalVariables?: boolean;
+
+ /**
+ * If you use Spotlight by Sentry during development, use
+ * this option to forward captured Sentry events to Spotlight.
+ *
+ * Either set it to true, or provide a specific Spotlight Sidecar URL.
+ *
+ * More details: https://spotlightjs.com/
+ *
+ * IMPORTANT: Only set this option to `true` while developing, not in production!
+ */
+ spotlight?: boolean | string;
+
+ /**
+ * Provide an array of OpenTelemetry Instrumentations that should be registered.
+ *
+ * Use this option if you want to register OpenTelemetry instrumentation that the Sentry SDK does not yet have support for.
+ */
+ openTelemetryInstrumentations?: Instrumentation[];
+
+ /**
+ * Provide an array of additional OpenTelemetry SpanProcessors that should be registered.
+ */
+ openTelemetrySpanProcessors?: SpanProcessor[];
+
+ /**
+ * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span.
+ * The SDK will automatically clean up spans that have no finished parent after this duration.
+ * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing.
+ * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early.
+ * In this case, you can increase this duration to a value that fits your expected data.
+ *
+ * Defaults to 300 seconds (5 minutes).
+ */
+ maxSpanWaitDuration?: number;
+
+ /**
+ * Whether to register ESM loader hooks to automatically instrument libraries.
+ * This is necessary to auto instrument libraries that are loaded via ESM imports, but it can cause issues
+ * with certain libraries. If you run into problems running your app with this enabled,
+ * please raise an issue in https://github.com/getsentry/sentry-javascript.
+ *
+ * Defaults to `true`.
+ */
+ registerEsmLoaderHooks?: boolean;
+
+ /**
+ * Configures in which interval client reports will be flushed. Defaults to `60_000` (milliseconds).
+ */
+ clientReportFlushInterval?: number;
+
+ /**
+ * By default, the SDK will try to identify problems with your instrumentation setup and warn you about it.
+ * If you want to disable these warnings, set this to `true`.
+ */
+ disableInstrumentationWarnings?: boolean;
+
+ /**
+ * Controls how many milliseconds to wait before shutting down. The default is 2 seconds. Setting this too low can cause
+ * problems for sending events from command line applications. Setting it too
+ * high can cause the application to block for users with network connectivity
+ * problems.
+ */
+ shutdownTimeout?: number;
+
+ /** Callback that is executed when a fatal global error occurs. */
+ onFatalError?(this: void, error: Error): void;
+}
+
+/**
+ * Configuration options for the Sentry Node SDK
+ * @see @sentry/core Options for more information.
+ */
+export interface NodeOptions extends Options, BaseNodeOptions {}
+
+/**
+ * Configuration options for the Sentry Node SDK Client class
+ * @see NodeClient for more information.
+ */
+export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {}
+
+export interface CurrentScopes {
+ scope: Scope;
+ isolationScope: Scope;
+}
+
+/**
+ * The base `Span` type is basically a `WriteableSpan`.
+ * There are places where we basically want to allow passing _any_ span,
+ * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`.
+ * You'll have to make sur to check relevant fields before accessing them.
+ *
+ * Note that technically, the `Span` exported from `@opentelemetry/sdk-trace-base` matches this,
+ * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive.
+ */
+export type AbstractSpan = WriteableSpan | ReadableSpan | Span;
diff --git a/packages/node/src/utils/addOriginToSpan.ts b/packages/node-core/src/utils/addOriginToSpan.ts
similarity index 100%
rename from packages/node/src/utils/addOriginToSpan.ts
rename to packages/node-core/src/utils/addOriginToSpan.ts
diff --git a/packages/node/src/utils/baggage.ts b/packages/node-core/src/utils/baggage.ts
similarity index 100%
rename from packages/node/src/utils/baggage.ts
rename to packages/node-core/src/utils/baggage.ts
diff --git a/packages/node/src/utils/commonjs.ts b/packages/node-core/src/utils/commonjs.ts
similarity index 100%
rename from packages/node/src/utils/commonjs.ts
rename to packages/node-core/src/utils/commonjs.ts
diff --git a/packages/node/src/utils/createMissingInstrumentationContext.ts b/packages/node-core/src/utils/createMissingInstrumentationContext.ts
similarity index 100%
rename from packages/node/src/utils/createMissingInstrumentationContext.ts
rename to packages/node-core/src/utils/createMissingInstrumentationContext.ts
diff --git a/packages/node/src/utils/debug.ts b/packages/node-core/src/utils/debug.ts
similarity index 100%
rename from packages/node/src/utils/debug.ts
rename to packages/node-core/src/utils/debug.ts
diff --git a/packages/node/src/utils/ensureIsWrapped.ts b/packages/node-core/src/utils/ensureIsWrapped.ts
similarity index 95%
rename from packages/node/src/utils/ensureIsWrapped.ts
rename to packages/node-core/src/utils/ensureIsWrapped.ts
index 3a6518e7ec14..70253d9debb7 100644
--- a/packages/node/src/utils/ensureIsWrapped.ts
+++ b/packages/node-core/src/utils/ensureIsWrapped.ts
@@ -1,4 +1,4 @@
-import { isWrapped } from '@opentelemetry/core';
+import { isWrapped } from '@opentelemetry/instrumentation';
import { consoleSandbox, getClient, getGlobalScope, hasSpansEnabled, isEnabled } from '@sentry/core';
import type { NodeClient } from '../sdk/client';
import { isCjs } from './commonjs';
diff --git a/packages/node/src/utils/entry-point.ts b/packages/node-core/src/utils/entry-point.ts
similarity index 100%
rename from packages/node/src/utils/entry-point.ts
rename to packages/node-core/src/utils/entry-point.ts
diff --git a/packages/node/src/utils/envToBool.ts b/packages/node-core/src/utils/envToBool.ts
similarity index 100%
rename from packages/node/src/utils/envToBool.ts
rename to packages/node-core/src/utils/envToBool.ts
diff --git a/packages/node/src/utils/errorhandling.ts b/packages/node-core/src/utils/errorhandling.ts
similarity index 100%
rename from packages/node/src/utils/errorhandling.ts
rename to packages/node-core/src/utils/errorhandling.ts
diff --git a/packages/node/src/utils/getRequestUrl.ts b/packages/node-core/src/utils/getRequestUrl.ts
similarity index 100%
rename from packages/node/src/utils/getRequestUrl.ts
rename to packages/node-core/src/utils/getRequestUrl.ts
diff --git a/packages/node/src/utils/module.ts b/packages/node-core/src/utils/module.ts
similarity index 100%
rename from packages/node/src/utils/module.ts
rename to packages/node-core/src/utils/module.ts
diff --git a/packages/node/src/utils/prepareEvent.ts b/packages/node-core/src/utils/prepareEvent.ts
similarity index 100%
rename from packages/node/src/utils/prepareEvent.ts
rename to packages/node-core/src/utils/prepareEvent.ts
diff --git a/packages/node-core/test/cron.test.ts b/packages/node-core/test/cron.test.ts
new file mode 100644
index 000000000000..efa146b90f20
--- /dev/null
+++ b/packages/node-core/test/cron.test.ts
@@ -0,0 +1,224 @@
+import * as SentryCore from '@sentry/core';
+import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import { cron } from '../src';
+import type { CronJob, CronJobParams } from '../src/cron/cron';
+import type { NodeCron, NodeCronOptions } from '../src/cron/node-cron';
+
+describe('cron check-ins', () => {
+ let withMonitorSpy: MockInstance;
+
+ beforeEach(() => {
+ withMonitorSpy = vi.spyOn(SentryCore, 'withMonitor');
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('cron', () => {
+ class CronJobMock {
+ constructor(
+ cronTime: CronJobParams['cronTime'],
+ onTick: CronJobParams['onTick'],
+ _onComplete?: CronJobParams['onComplete'],
+ _start?: CronJobParams['start'],
+ _timeZone?: CronJobParams['timeZone'],
+ _context?: CronJobParams['context'],
+ _runOnInit?: CronJobParams['runOnInit'],
+ _utcOffset?: CronJobParams['utcOffset'],
+ _unrefTimeout?: CronJobParams['unrefTimeout'],
+ ) {
+ expect(cronTime).toBe('* * * Jan,Sep Sun');
+ expect(onTick).toBeInstanceOf(Function);
+ setImmediate(() => onTick(undefined, undefined));
+ }
+
+ static from(params: CronJobParams): CronJob {
+ return new CronJobMock(
+ params.cronTime,
+ params.onTick,
+ params.onComplete,
+ params.start,
+ params.timeZone,
+ params.context,
+ params.runOnInit,
+ params.utcOffset,
+ params.unrefTimeout,
+ );
+ }
+ }
+
+ test('new CronJob()', () =>
+ new Promise(done => {
+ expect.assertions(4);
+
+ const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job');
+
+ new CronJobWithCheckIn(
+ '* * * Jan,Sep Sun',
+ () => {
+ expect(withMonitorSpy).toHaveBeenCalledTimes(1);
+ expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
+ schedule: { type: 'crontab', value: '* * * 1,9 0' },
+ timezone: 'America/Los_Angeles',
+ });
+ done();
+ },
+ undefined,
+ true,
+ 'America/Los_Angeles',
+ );
+ }));
+
+ test('CronJob.from()', () =>
+ new Promise(done => {
+ expect.assertions(4);
+
+ const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job');
+
+ CronJobWithCheckIn.from({
+ cronTime: '* * * Jan,Sep Sun',
+ onTick: () => {
+ expect(withMonitorSpy).toHaveBeenCalledTimes(1);
+ expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
+ schedule: { type: 'crontab', value: '* * * 1,9 0' },
+ });
+ done();
+ },
+ });
+ }));
+
+ test('throws with multiple jobs same name', () => {
+ const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job');
+
+ CronJobWithCheckIn.from({
+ cronTime: '* * * Jan,Sep Sun',
+ onTick: () => {
+ //
+ },
+ });
+
+ expect(() => {
+ CronJobWithCheckIn.from({
+ cronTime: '* * * Jan,Sep Sun',
+ onTick: () => {
+ //
+ },
+ });
+ }).toThrowError("A job named 'my-cron-job' has already been scheduled");
+ });
+ });
+
+ describe('node-cron', () => {
+ test('calls withMonitor', () =>
+ new Promise(done => {
+ expect.assertions(5);
+
+ const nodeCron: NodeCron = {
+ schedule: (expression: string, callback: () => void, options?: NodeCronOptions): unknown => {
+ expect(expression).toBe('* * * Jan,Sep Sun');
+ expect(callback).toBeInstanceOf(Function);
+ expect(options?.name).toBe('my-cron-job');
+ return callback();
+ },
+ };
+
+ const cronWithCheckIn = cron.instrumentNodeCron(nodeCron);
+
+ cronWithCheckIn.schedule(
+ '* * * Jan,Sep Sun',
+ () => {
+ expect(withMonitorSpy).toHaveBeenCalledTimes(1);
+ expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
+ schedule: { type: 'crontab', value: '* * * 1,9 0' },
+ });
+ done();
+ },
+ { name: 'my-cron-job' },
+ );
+ }));
+
+ test('throws without supplied name', () => {
+ const nodeCron: NodeCron = {
+ schedule: (): unknown => {
+ return undefined;
+ },
+ };
+
+ const cronWithCheckIn = cron.instrumentNodeCron(nodeCron);
+
+ expect(() => {
+ // @ts-expect-error Initially missing name
+ cronWithCheckIn.schedule('* * * * *', () => {
+ //
+ });
+ }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
+ });
+ });
+
+ describe('node-schedule', () => {
+ test('calls withMonitor', () =>
+ new Promise(done => {
+ expect.assertions(5);
+
+ class NodeScheduleMock {
+ scheduleJob(
+ nameOrExpression: string | Date | object,
+ expressionOrCallback: string | Date | object | (() => void),
+ callback: () => void,
+ ): unknown {
+ expect(nameOrExpression).toBe('my-cron-job');
+ expect(expressionOrCallback).toBe('* * * Jan,Sep Sun');
+ expect(callback).toBeInstanceOf(Function);
+ return callback();
+ }
+ }
+
+ const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock());
+
+ scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => {
+ expect(withMonitorSpy).toHaveBeenCalledTimes(1);
+ expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), {
+ schedule: { type: 'crontab', value: '* * * 1,9 0' },
+ });
+ done();
+ });
+ }));
+
+ test('throws without crontab string', () => {
+ class NodeScheduleMock {
+ scheduleJob(_: string, __: string | Date, ___: () => void): unknown {
+ return undefined;
+ }
+ }
+
+ const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock());
+
+ expect(() => {
+ scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => {
+ //
+ });
+ }).toThrowError(
+ "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string",
+ );
+ });
+
+ test('throws without job name', () => {
+ class NodeScheduleMock {
+ scheduleJob(_: string, __: () => void): unknown {
+ return undefined;
+ }
+ }
+
+ const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock());
+
+ expect(() => {
+ scheduleWithCheckIn.scheduleJob('* * * * *', () => {
+ //
+ });
+ }).toThrowError(
+ "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string",
+ );
+ });
+ });
+});
diff --git a/packages/node-core/test/helpers/conditional.ts b/packages/node-core/test/helpers/conditional.ts
new file mode 100644
index 000000000000..ceea11315db4
--- /dev/null
+++ b/packages/node-core/test/helpers/conditional.ts
@@ -0,0 +1,19 @@
+import { parseSemver } from '@sentry/core';
+import { it, test } from 'vitest';
+
+const NODE_VERSION = parseSemver(process.versions.node).major;
+
+/**
+ * Returns`describe` or `describe.skip` depending on allowed major versions of Node.
+ *
+ * @param {{ min?: number; max?: number }} allowedVersion
+ */
+export const conditionalTest = (allowedVersion: { min?: number; max?: number }) => {
+ if (!NODE_VERSION) {
+ return it.skip;
+ }
+
+ return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity)
+ ? test.skip
+ : test;
+};
diff --git a/packages/node-core/test/helpers/error.ts b/packages/node-core/test/helpers/error.ts
new file mode 100644
index 000000000000..03d4150c3f11
--- /dev/null
+++ b/packages/node-core/test/helpers/error.ts
@@ -0,0 +1,4 @@
+/* this method is exported from an external file to be able to test contextlines when adding an external file */
+export function getError(): Error {
+ return new Error('mock error');
+}
diff --git a/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts b/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts
new file mode 100644
index 000000000000..8cff09d3c0ee
--- /dev/null
+++ b/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts
@@ -0,0 +1,13 @@
+import { createTransport, resolvedSyncPromise } from '@sentry/core';
+import type { NodeClientOptions } from '../../src/types';
+
+export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions {
+ return {
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ integrations: [],
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
+ stackParser: () => [],
+ ...options,
+ };
+}
diff --git a/packages/node-core/test/helpers/mockSdkInit.ts b/packages/node-core/test/helpers/mockSdkInit.ts
new file mode 100644
index 000000000000..f627e9999946
--- /dev/null
+++ b/packages/node-core/test/helpers/mockSdkInit.ts
@@ -0,0 +1,143 @@
+import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api';
+import { Resource } from '@opentelemetry/resources';
+import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
+import {
+ ATTR_SERVICE_NAME,
+ ATTR_SERVICE_VERSION,
+ SEMRESATTRS_SERVICE_NAMESPACE,
+} from '@opentelemetry/semantic-conventions';
+import {
+ createTransport,
+ getClient,
+ getCurrentScope,
+ getGlobalScope,
+ getIsolationScope,
+ logger,
+ resolvedSyncPromise,
+ SDK_VERSION,
+} from '@sentry/core';
+import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry';
+import type { NodeClient } from '../../src';
+import { SentryContextManager, validateOpenTelemetrySetup } from '../../src';
+import { init } from '../../src/sdk';
+import type { NodeClientOptions } from '../../src/types';
+
+const PUBLIC_DSN = 'https://username@domain/123';
+
+// About 277h - this must fit into new Array(len)!
+const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000;
+
+/** Clamp span processor timeout to reasonable values, mirroring Node SDK behavior. */
+function clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): number | undefined {
+ if (maxSpanWaitDuration == null) {
+ return undefined;
+ }
+
+ // We guard for a max. value here, because we create an array with this length
+ // So if this value is too large, this would fail
+ if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) {
+ logger.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`);
+ return MAX_MAX_SPAN_WAIT_DURATION;
+ } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) {
+ logger.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.');
+ return undefined;
+ }
+
+ return maxSpanWaitDuration;
+}
+
+export function resetGlobals(): void {
+ getCurrentScope().clear();
+ getCurrentScope().setClient(undefined);
+ getIsolationScope().clear();
+ getGlobalScope().clear();
+}
+
+export function setupOtel(client: NodeClient): BasicTracerProvider | undefined {
+ if (!client) {
+ return undefined;
+ }
+
+ const clientOptions = client.getOptions();
+ const spanProcessorTimeout = clampSpanProcessorTimeout(clientOptions.maxSpanWaitDuration);
+
+ // Create and configure TracerProvider with same config as Node SDK
+ const provider = new BasicTracerProvider({
+ sampler: new SentrySampler(client),
+ resource: new Resource({
+ [ATTR_SERVICE_NAME]: 'node',
+ // eslint-disable-next-line deprecation/deprecation
+ [SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry',
+ [ATTR_SERVICE_VERSION]: SDK_VERSION,
+ }),
+ forceFlushTimeoutMillis: 500,
+ spanProcessors: [
+ new SentrySpanProcessor({
+ timeout: spanProcessorTimeout,
+ }),
+ ],
+ });
+
+ // Register as globals
+ trace.setGlobalTracerProvider(provider);
+ propagation.setGlobalPropagator(new SentryPropagator());
+ context.setGlobalContextManager(new SentryContextManager());
+
+ validateOpenTelemetrySetup();
+
+ return provider;
+}
+
+export function mockSdkInit(options?: Partial) {
+ resetGlobals();
+ const client = init({
+ dsn: PUBLIC_DSN,
+ defaultIntegrations: false,
+ // We are disabling client reports because we would be acquiring resources with every init call and that would leak
+ // memory every time we call init in the tests
+ sendClientReports: false,
+ // Use a mock transport to prevent network calls
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
+ ...options,
+ });
+
+ // Always set up OpenTelemetry if we have a client
+ if (client) {
+ const provider = setupOtel(client);
+ // Important: Link the provider to the client so getProvider() can find it
+ client.traceProvider = provider;
+ }
+
+ return client;
+}
+
+export function cleanupOtel(_provider?: BasicTracerProvider): void {
+ const provider = getProvider(_provider);
+
+ if (provider) {
+ void provider.forceFlush();
+ void provider.shutdown();
+ }
+
+ // Disable all globally registered APIs
+ trace.disable();
+ context.disable();
+ propagation.disable();
+
+ // Reset globals to ensure clean state
+ resetGlobals();
+}
+
+export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined {
+ let provider = _provider || getClient()?.traceProvider || trace.getTracerProvider();
+
+ if (provider instanceof ProxyTracerProvider) {
+ provider = provider.getDelegate();
+ }
+
+ if (!(provider instanceof BasicTracerProvider)) {
+ return undefined;
+ }
+
+ return provider;
+}
diff --git a/packages/node-core/test/integration/breadcrumbs.test.ts b/packages/node-core/test/integration/breadcrumbs.test.ts
new file mode 100644
index 000000000000..5e6b4aff3cb4
--- /dev/null
+++ b/packages/node-core/test/integration/breadcrumbs.test.ts
@@ -0,0 +1,358 @@
+import { addBreadcrumb, captureException, withIsolationScope, withScope } from '@sentry/core';
+import { startSpan } from '@sentry/opentelemetry';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { getClient } from '../../src/';
+import type { NodeClient } from '../../src/sdk/client';
+import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit';
+
+describe('Integration | breadcrumbs', () => {
+ const beforeSendTransaction = vi.fn(() => null);
+
+ afterEach(() => {
+ cleanupOtel();
+ });
+
+ describe('without tracing', () => {
+ it('correctly adds & retrieves breadcrumbs', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ beforeSend, beforeBreadcrumb });
+
+ const client = getClient() as NodeClient;
+
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+ addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } });
+ addBreadcrumb({ timestamp: 123455, message: 'test3' });
+
+ const error = new Error('test');
+ captureException(error);
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(3);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test1', timestamp: 123456 },
+ { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 },
+ { message: 'test3', timestamp: 123455 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('handles parallel scopes', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ beforeSend, beforeBreadcrumb });
+
+ const client = getClient();
+
+ const error = new Error('test');
+
+ addBreadcrumb({ timestamp: 123456, message: 'test0' });
+
+ withIsolationScope(() => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+ });
+
+ withIsolationScope(() => {
+ addBreadcrumb({ timestamp: 123456, message: 'test2' });
+ captureException(error);
+ });
+
+ withIsolationScope(() => {
+ addBreadcrumb({ timestamp: 123456, message: 'test3' });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(4);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test0', timestamp: 123456 },
+ { message: 'test2', timestamp: 123456 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+ });
+
+ it('correctly adds & retrieves breadcrumbs', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction });
+
+ const client = getClient() as NodeClient;
+
+ const error = new Error('test');
+
+ startSpan({ name: 'test' }, () => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+
+ startSpan({ name: 'inner1' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } });
+ });
+
+ startSpan({ name: 'inner2' }, () => {
+ addBreadcrumb({ timestamp: 123455, message: 'test3' });
+ });
+
+ captureException(error);
+ });
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(3);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test1', timestamp: 123456 },
+ { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 },
+ { message: 'test3', timestamp: 123455 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('correctly adds & retrieves breadcrumbs for the current isolation span only', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction });
+
+ const client = getClient() as NodeClient;
+
+ const error = new Error('test');
+
+ withIsolationScope(() => {
+ startSpan({ name: 'test1' }, () => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1-a' });
+
+ startSpan({ name: 'inner1' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test1-b' });
+ });
+ });
+ });
+
+ withIsolationScope(() => {
+ startSpan({ name: 'test2' }, () => {
+ addBreadcrumb({ timestamp: 123456, message: 'test2-a' });
+
+ startSpan({ name: 'inner2' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2-b' });
+ });
+
+ captureException(error);
+ });
+ });
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(4);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test2-a', timestamp: 123456 },
+ { message: 'test2-b', timestamp: 123457 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('ignores scopes inside of root span', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction });
+
+ const client = getClient() as NodeClient;
+
+ const error = new Error('test');
+
+ startSpan({ name: 'test1' }, () => {
+ withScope(() => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+ });
+ startSpan({ name: 'inner1' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2' });
+ });
+
+ captureException(error);
+ });
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(2);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test1', timestamp: 123456 },
+ { message: 'test2', timestamp: 123457 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('handles deep nesting of scopes', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction });
+
+ const client = getClient() as NodeClient;
+
+ const error = new Error('test');
+
+ startSpan({ name: 'test1' }, () => {
+ withScope(() => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+ });
+ startSpan({ name: 'inner1' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2' });
+
+ startSpan({ name: 'inner2' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test3' });
+
+ startSpan({ name: 'inner3' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test4' });
+
+ captureException(error);
+
+ startSpan({ name: 'inner4' }, () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test5' });
+ });
+
+ addBreadcrumb({ timestamp: 123457, message: 'test6' });
+ });
+ });
+ });
+
+ addBreadcrumb({ timestamp: 123456, message: 'test99' });
+ });
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test1', timestamp: 123456 },
+ { message: 'test2', timestamp: 123457 },
+ { message: 'test3', timestamp: 123457 },
+ { message: 'test4', timestamp: 123457 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('correctly adds & retrieves breadcrumbs in async spans', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction });
+
+ const client = getClient() as NodeClient;
+
+ const error = new Error('test');
+
+ const promise1 = withIsolationScope(async () => {
+ await startSpan({ name: 'test' }, async () => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1' });
+
+ await startSpan({ name: 'inner1' }, async () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2' });
+ });
+
+ await startSpan({ name: 'inner2' }, async () => {
+ addBreadcrumb({ timestamp: 123455, message: 'test3' });
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ captureException(error);
+ });
+ });
+
+ const promise2 = withIsolationScope(async () => {
+ await startSpan({ name: 'test-b' }, async () => {
+ addBreadcrumb({ timestamp: 123456, message: 'test1-b' });
+
+ await startSpan({ name: 'inner1b' }, async () => {
+ addBreadcrumb({ timestamp: 123457, message: 'test2-b' });
+ });
+
+ await startSpan({ name: 'inner2b' }, async () => {
+ addBreadcrumb({ timestamp: 123455, message: 'test3-b' });
+ });
+ });
+ });
+
+ await Promise.all([promise1, promise2]);
+
+ await client.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeBreadcrumb).toHaveBeenCalledTimes(6);
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test1', timestamp: 123456 },
+ { message: 'test2', timestamp: 123457 },
+ { message: 'test3', timestamp: 123455 },
+ ],
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+});
diff --git a/packages/node-core/test/integration/scope.test.ts b/packages/node-core/test/integration/scope.test.ts
new file mode 100644
index 000000000000..22bb1867ed52
--- /dev/null
+++ b/packages/node-core/test/integration/scope.test.ts
@@ -0,0 +1,684 @@
+import { getCapturedScopesOnSpan, getCurrentScope } from '@sentry/core';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import * as Sentry from '../../src/';
+import { cleanupOtel, mockSdkInit, resetGlobals } from '../helpers/mockSdkInit';
+
+describe('Integration | Scope', () => {
+ afterEach(() => {
+ cleanupOtel();
+ });
+
+ describe.each([
+ ['with tracing', true],
+ ['without tracing', false],
+ ])('%s', (_name, tracingEnabled) => {
+ it('correctly syncs OTEL context & Sentry hub/scope', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeSendTransaction = vi.fn(() => null);
+
+ const client = mockSdkInit({ tracesSampleRate: tracingEnabled ? 1 : 0, beforeSend, beforeSendTransaction });
+
+ const rootScope = getCurrentScope();
+
+ const error = new Error('test error');
+ let spanId: string | undefined;
+ let traceId: string | undefined;
+
+ rootScope.setTag('tag1', 'val1');
+
+ Sentry.withScope(scope1 => {
+ scope1.setTag('tag2', 'val2');
+
+ Sentry.withScope(scope2b => {
+ scope2b.setTag('tag3-b', 'val3-b');
+ });
+
+ Sentry.withScope(scope2 => {
+ scope2.setTag('tag3', 'val3');
+
+ Sentry.startSpan({ name: 'outer' }, span => {
+ expect(getCapturedScopesOnSpan(span).scope).toBe(tracingEnabled ? scope2 : undefined);
+
+ spanId = span.spanContext().spanId;
+ traceId = span.spanContext().traceId;
+
+ Sentry.setTag('tag4', 'val4');
+
+ Sentry.captureException(error);
+ });
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+
+ if (spanId) {
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: {
+ span_id: spanId,
+ trace_id: traceId,
+ },
+ }),
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ }
+
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ tag4: 'val4',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+
+ if (tracingEnabled) {
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
+ // Note: Scope for transaction is taken at `start` time, not `finish` time
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.origin': 'manual',
+ 'sentry.source': 'custom',
+ 'sentry.sample_rate': 1,
+ },
+ span_id: spanId,
+ status: 'ok',
+ trace_id: traceId,
+ origin: 'manual',
+ },
+ }),
+ spans: [],
+ start_timestamp: expect.any(Number),
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ tag4: 'val4',
+ },
+ timestamp: expect.any(Number),
+ transaction_info: { source: 'custom' },
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+ }
+ });
+
+ it('isolates parallel root scopes', async () => {
+ const beforeSend = vi.fn(() => null);
+ const beforeSendTransaction = vi.fn(() => null);
+
+ const client = mockSdkInit({ tracesSampleRate: tracingEnabled ? 1 : 0, beforeSend, beforeSendTransaction });
+
+ const rootScope = getCurrentScope();
+
+ const error1 = new Error('test error 1');
+ const error2 = new Error('test error 2');
+ let spanId1: string | undefined;
+ let spanId2: string | undefined;
+ let traceId1: string | undefined;
+ let traceId2: string | undefined;
+
+ rootScope.setTag('tag1', 'val1');
+
+ Sentry.withScope(scope1 => {
+ scope1.setTag('tag2', 'val2a');
+
+ Sentry.withScope(scope2 => {
+ scope2.setTag('tag3', 'val3a');
+
+ Sentry.startSpan({ name: 'outer' }, span => {
+ spanId1 = span.spanContext().spanId;
+ traceId1 = span.spanContext().traceId;
+
+ Sentry.setTag('tag4', 'val4a');
+
+ Sentry.captureException(error1);
+ });
+ });
+ });
+
+ Sentry.withScope(scope1 => {
+ scope1.setTag('tag2', 'val2b');
+
+ Sentry.withScope(scope2 => {
+ scope2.setTag('tag3', 'val3b');
+
+ Sentry.startSpan({ name: 'outer' }, span => {
+ spanId2 = span.spanContext().spanId;
+ traceId2 = span.spanContext().traceId;
+
+ Sentry.setTag('tag4', 'val4b');
+
+ Sentry.captureException(error2);
+ });
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(2);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: spanId1
+ ? {
+ span_id: spanId1,
+ trace_id: traceId1,
+ }
+ : expect.any(Object),
+ }),
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2a',
+ tag3: 'val3a',
+ tag4: 'val4a',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error1,
+ syntheticException: expect.any(Error),
+ },
+ );
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: spanId2
+ ? {
+ span_id: spanId2,
+ trace_id: traceId2,
+ }
+ : expect.any(Object),
+ }),
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2b',
+ tag3: 'val3b',
+ tag4: 'val4b',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error2,
+ syntheticException: expect.any(Error),
+ },
+ );
+
+ if (tracingEnabled) {
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(2);
+ }
+ });
+ });
+
+ describe('global scope', () => {
+ beforeEach(() => {
+ resetGlobals();
+ });
+
+ it('works before calling init', () => {
+ const globalScope = Sentry.getGlobalScope();
+ expect(globalScope).toBeDefined();
+ // No client attached
+ expect(globalScope.getClient()).toBeUndefined();
+ // Repeatedly returns the same instance
+ expect(Sentry.getGlobalScope()).toBe(globalScope);
+
+ globalScope.setTag('tag1', 'val1');
+ globalScope.setTag('tag2', 'val2');
+
+ expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+
+ // Now when we call init, the global scope remains intact
+ Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false });
+
+ expect(globalScope.getClient()).toBeUndefined();
+ expect(Sentry.getGlobalScope()).toBe(globalScope);
+ expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+ });
+
+ it('is applied to events', async () => {
+ const beforeSend = vi.fn();
+ const client = mockSdkInit({ beforeSend });
+
+ const globalScope = Sentry.getGlobalScope();
+ globalScope.setTag('tag1', 'val1');
+ globalScope.setTag('tag2', 'val2');
+
+ const error = new Error('test error');
+ Sentry.captureException(error);
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+ });
+
+ describe('isolation scope', () => {
+ beforeEach(() => {
+ resetGlobals();
+ });
+
+ it('works before calling init', () => {
+ const isolationScope = Sentry.getIsolationScope();
+ expect(isolationScope).toBeDefined();
+ // No client attached
+ expect(isolationScope.getClient()).toBeUndefined();
+ // Repeatedly returns the same instance
+ expect(Sentry.getIsolationScope()).toBe(isolationScope);
+
+ isolationScope.setTag('tag1', 'val1');
+ isolationScope.setTag('tag2', 'val2');
+
+ expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+
+ // Now when we call init, the isolation scope remains intact
+ Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false });
+
+ // client is only attached to global scope by default
+ expect(isolationScope.getClient()).toBeUndefined();
+ expect(Sentry.getIsolationScope()).toBe(isolationScope);
+ expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+ });
+
+ it('is applied to events', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const isolationScope = Sentry.getIsolationScope();
+ isolationScope.setTag('tag1', 'val1');
+ isolationScope.setTag('tag2', 'val2');
+
+ const error = new Error('test error');
+ Sentry.captureException(error);
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('withIsolationScope works', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const initialIsolationScope = Sentry.getIsolationScope();
+ initialIsolationScope.setTag('tag1', 'val1');
+ initialIsolationScope.setTag('tag2', 'val2');
+
+ const initialCurrentScope = Sentry.getCurrentScope();
+
+ const error = new Error('test error');
+
+ Sentry.withIsolationScope(newIsolationScope => {
+ newIsolationScope.setTag('tag4', 'val4');
+ });
+
+ Sentry.withIsolationScope(newIsolationScope => {
+ expect(Sentry.getCurrentScope()).not.toBe(initialCurrentScope);
+ expect(Sentry.getIsolationScope()).toBe(newIsolationScope);
+ expect(newIsolationScope).not.toBe(initialIsolationScope);
+
+ // Data is forked off original isolation scope
+ expect(newIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+ newIsolationScope.setTag('tag3', 'val3');
+
+ Sentry.captureException(error);
+ });
+
+ expect(initialIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('can be deeply nested', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const initialIsolationScope = Sentry.getIsolationScope();
+ initialIsolationScope.setTag('tag1', 'val1');
+
+ const error = new Error('test error');
+
+ Sentry.withIsolationScope(newIsolationScope => {
+ newIsolationScope.setTag('tag2', 'val2');
+
+ Sentry.withIsolationScope(newIsolationScope => {
+ newIsolationScope.setTag('tag3', 'val3');
+
+ Sentry.withIsolationScope(newIsolationScope => {
+ newIsolationScope.setTag('tag4', 'val4');
+ });
+
+ Sentry.captureException(error);
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+ });
+
+ describe('current scope', () => {
+ beforeEach(() => {
+ resetGlobals();
+ });
+
+ it('works before calling init', () => {
+ const currentScope = Sentry.getCurrentScope();
+ expect(currentScope).toBeDefined();
+ // No client attached
+ expect(currentScope.getClient()).toBeUndefined();
+ // Repeatedly returns the same instance
+ expect(Sentry.getCurrentScope()).toBe(currentScope);
+
+ currentScope.setTag('tag1', 'val1');
+ currentScope.setTag('tag2', 'val2');
+
+ expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+
+ // Now when we call init, the current scope remains intact
+ Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false });
+
+ // client is attached to current scope
+ expect(currentScope.getClient()).toBeDefined();
+
+ expect(Sentry.getCurrentScope()).toBe(currentScope);
+ expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+ });
+
+ it('is applied to events', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const currentScope = Sentry.getCurrentScope();
+ currentScope.setTag('tag1', 'val1');
+ currentScope.setTag('tag2', 'val2');
+
+ const error = new Error('test error');
+ Sentry.captureException(error);
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('withScope works', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const isolationScope = Sentry.getIsolationScope();
+ const initialCurrentScope = Sentry.getCurrentScope();
+ initialCurrentScope.setTag('tag1', 'val1');
+ initialCurrentScope.setTag('tag2', 'val2');
+
+ const error = new Error('test error');
+
+ Sentry.withScope(newCurrentScope => {
+ newCurrentScope.setTag('tag4', 'val4');
+ });
+
+ Sentry.withScope(newCurrentScope => {
+ expect(Sentry.getCurrentScope()).toBe(newCurrentScope);
+ expect(Sentry.getIsolationScope()).toBe(isolationScope);
+ expect(newCurrentScope).not.toBe(initialCurrentScope);
+
+ // Data is forked off original isolation scope
+ expect(newCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+ newCurrentScope.setTag('tag3', 'val3');
+
+ Sentry.captureException(error);
+ });
+
+ expect(initialCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('can be deeply nested', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const initialCurrentScope = Sentry.getCurrentScope();
+ initialCurrentScope.setTag('tag1', 'val1');
+
+ const error = new Error('test error');
+
+ Sentry.withScope(currentScope => {
+ currentScope.setTag('tag2', 'val2');
+ expect(Sentry.getCurrentScope()).toBe(currentScope);
+
+ Sentry.withScope(currentScope => {
+ currentScope.setTag('tag3', 'val3');
+ expect(Sentry.getCurrentScope()).toBe(currentScope);
+
+ Sentry.withScope(currentScope => {
+ currentScope.setTag('tag4', 'val4');
+ expect(Sentry.getCurrentScope()).toBe(currentScope);
+ });
+
+ Sentry.captureException(error);
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+
+ it('automatically forks with OTEL context', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ const initialCurrentScope = Sentry.getCurrentScope();
+ initialCurrentScope.setTag('tag1', 'val1');
+
+ const error = new Error('test error');
+
+ Sentry.startSpan({ name: 'outer' }, () => {
+ Sentry.getCurrentScope().setTag('tag2', 'val2');
+
+ Sentry.startSpan({ name: 'inner 1' }, () => {
+ Sentry.getCurrentScope().setTag('tag3', 'val3');
+
+ Sentry.startSpan({ name: 'inner 2' }, () => {
+ Sentry.getCurrentScope().setTag('tag4', 'val4');
+ });
+
+ Sentry.captureException(error);
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2',
+ tag3: 'val3',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+ });
+
+ describe('scope merging', () => {
+ beforeEach(() => {
+ resetGlobals();
+ });
+
+ it('merges data from global, isolation and current scope', async () => {
+ const beforeSend = vi.fn();
+ mockSdkInit({ beforeSend });
+ const client = Sentry.getClient();
+
+ Sentry.getGlobalScope().setTag('tag1', 'val1');
+
+ const error = new Error('test error');
+
+ Sentry.withIsolationScope(isolationScope => {
+ Sentry.getCurrentScope().setTag('tag2', 'val2a');
+ isolationScope.setTag('tag2', 'val2b');
+ isolationScope.setTag('tag3', 'val3');
+
+ Sentry.withScope(currentScope => {
+ currentScope.setTag('tag4', 'val4');
+
+ Sentry.captureException(error);
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSend).toHaveBeenCalledTimes(1);
+ expect(beforeSend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tags: {
+ tag1: 'val1',
+ tag2: 'val2a',
+ tag3: 'val3',
+ tag4: 'val4',
+ },
+ }),
+ {
+ event_id: expect.any(String),
+ originalException: error,
+ syntheticException: expect.any(Error),
+ },
+ );
+ });
+ });
+});
diff --git a/packages/node-core/test/integration/transactions.test.ts b/packages/node-core/test/integration/transactions.test.ts
new file mode 100644
index 000000000000..db499cd368df
--- /dev/null
+++ b/packages/node-core/test/integration/transactions.test.ts
@@ -0,0 +1,685 @@
+import { context, trace, TraceFlags } from '@opentelemetry/api';
+import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
+import type { TransactionEvent } from '@sentry/core';
+import { logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import { SentrySpanProcessor } from '@sentry/opentelemetry';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import * as Sentry from '../../src';
+import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit';
+
+describe('Integration | Transactions', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ cleanupOtel();
+ });
+
+ it('correctly creates transaction & spans', async () => {
+ const transactions: TransactionEvent[] = [];
+ const beforeSendTransaction = vi.fn(event => {
+ transactions.push(event);
+ return null;
+ });
+
+ mockSdkInit({
+ tracesSampleRate: 1,
+ beforeSendTransaction,
+ release: '8.0.0',
+ });
+
+ const client = Sentry.getClient()!;
+
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 });
+ Sentry.setTag('outer.tag', 'test value');
+
+ Sentry.startSpan(
+ {
+ op: 'test op',
+ name: 'test name',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test',
+ },
+ },
+ span => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 });
+
+ span.setAttributes({
+ 'test.outer': 'test value',
+ });
+
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
+ subSpan.end();
+
+ Sentry.setTag('test.tag', 'test value');
+
+ Sentry.startSpan({ name: 'inner span 2' }, innerSpan => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 });
+
+ innerSpan.setAttributes({
+ 'test.inner': 'test value',
+ });
+ });
+ },
+ );
+
+ await client.flush();
+
+ expect(transactions).toHaveLength(1);
+ const transaction = transactions[0]!;
+
+ expect(transaction.breadcrumbs).toEqual([
+ { message: 'test breadcrumb 1', timestamp: 123456 },
+ { message: 'test breadcrumb 2', timestamp: 123456 },
+ { message: 'test breadcrumb 3', timestamp: 123456 },
+ ]);
+
+ expect(transaction.contexts?.otel).toEqual({
+ resource: {
+ 'service.name': 'node',
+ 'service.namespace': 'sentry',
+ 'service.version': expect.any(String),
+ 'telemetry.sdk.language': 'nodejs',
+ 'telemetry.sdk.name': 'opentelemetry',
+ 'telemetry.sdk.version': expect.any(String),
+ },
+ });
+
+ expect(transaction.contexts?.trace).toEqual({
+ data: {
+ 'sentry.op': 'test op',
+ 'sentry.origin': 'auto.test',
+ 'sentry.source': 'task',
+ 'sentry.sample_rate': 1,
+ 'test.outer': 'test value',
+ },
+ op: 'test op',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.test',
+ });
+
+ expect(transaction.sdkProcessingMetadata?.sampleRate).toEqual(1);
+ expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({
+ environment: 'production',
+ public_key: expect.any(String),
+ sample_rate: '1',
+ sampled: 'true',
+ release: '8.0.0',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ transaction: 'test name',
+ sample_rand: expect.any(String),
+ });
+
+ expect(transaction.environment).toEqual('production');
+ expect(transaction.event_id).toEqual(expect.any(String));
+ expect(transaction.start_timestamp).toEqual(expect.any(Number));
+ expect(transaction.timestamp).toEqual(expect.any(Number));
+ expect(transaction.transaction).toEqual('test name');
+
+ expect(transaction.tags).toEqual({
+ 'outer.tag': 'test value',
+ 'test.tag': 'test value',
+ });
+ expect(transaction.transaction_info).toEqual({ source: 'task' });
+ expect(transaction.type).toEqual('transaction');
+
+ expect(transaction.spans).toHaveLength(2);
+ const spans = transaction.spans || [];
+
+ // note: Currently, spans do not have any context/span added to them
+ // This is the same behavior as for the "regular" SDKs
+ expect(spans).toEqual([
+ {
+ data: {
+ 'sentry.origin': 'manual',
+ },
+ description: 'inner span 1',
+ origin: 'manual',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ {
+ data: {
+ 'test.inner': 'test value',
+ 'sentry.origin': 'manual',
+ },
+ description: 'inner span 2',
+ origin: 'manual',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ ]);
+ });
+
+ it('correctly creates concurrent transaction & spans', async () => {
+ const beforeSendTransaction = vi.fn(() => null);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+
+ const client = Sentry.getClient()!;
+
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 });
+
+ Sentry.withIsolationScope(() => {
+ Sentry.startSpan(
+ {
+ op: 'test op',
+ name: 'test name',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test',
+ },
+ },
+ span => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 });
+
+ span.setAttributes({
+ 'test.outer': 'test value',
+ });
+
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
+ subSpan.end();
+
+ Sentry.setTag('test.tag', 'test value');
+
+ Sentry.startSpan({ name: 'inner span 2' }, innerSpan => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 });
+
+ innerSpan.setAttributes({
+ 'test.inner': 'test value',
+ });
+ });
+ },
+ );
+ });
+
+ Sentry.withIsolationScope(() => {
+ Sentry.startSpan({ op: 'test op b', name: 'test name b' }, span => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 });
+
+ span.setAttributes({
+ 'test.outer': 'test value b',
+ });
+
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' });
+ subSpan.end();
+
+ Sentry.setTag('test.tag', 'test value b');
+
+ Sentry.startSpan({ name: 'inner span 2b' }, innerSpan => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 });
+
+ innerSpan.setAttributes({
+ 'test.inner': 'test value b',
+ });
+ });
+ });
+ });
+
+ await client.flush();
+
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(2);
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test breadcrumb 1', timestamp: 123456 },
+ { message: 'test breadcrumb 2', timestamp: 123456 },
+ { message: 'test breadcrumb 3', timestamp: 123456 },
+ ],
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.op': 'test op',
+ 'sentry.origin': 'auto.test',
+ 'sentry.source': 'task',
+ 'test.outer': 'test value',
+ 'sentry.sample_rate': 1,
+ },
+ op: 'test op',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.test',
+ },
+ }),
+ spans: [expect.any(Object), expect.any(Object)],
+ start_timestamp: expect.any(Number),
+ tags: {
+ 'test.tag': 'test value',
+ },
+ timestamp: expect.any(Number),
+ transaction: 'test name',
+ transaction_info: { source: 'task' },
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test breadcrumb 1', timestamp: 123456 },
+ { message: 'test breadcrumb 2b', timestamp: 123456 },
+ { message: 'test breadcrumb 3b', timestamp: 123456 },
+ ],
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.op': 'test op b',
+ 'sentry.origin': 'manual',
+ 'sentry.source': 'custom',
+ 'test.outer': 'test value b',
+ 'sentry.sample_rate': 1,
+ },
+ op: 'test op b',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'manual',
+ },
+ }),
+ spans: [expect.any(Object), expect.any(Object)],
+ start_timestamp: expect.any(Number),
+ tags: {
+ 'test.tag': 'test value b',
+ },
+ timestamp: expect.any(Number),
+ transaction: 'test name b',
+ transaction_info: { source: 'custom' },
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+ });
+
+ it('correctly creates concurrent transaction & spans when using native OTEL tracer', async () => {
+ const beforeSendTransaction = vi.fn(() => null);
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+
+ const client = Sentry.getClient();
+
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 });
+
+ Sentry.withIsolationScope(() => {
+ client?.tracer.startActiveSpan('test name', span => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 });
+
+ span.setAttributes({
+ 'test.outer': 'test value',
+ });
+
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
+ subSpan.end();
+
+ Sentry.setTag('test.tag', 'test value');
+
+ client.tracer.startActiveSpan('inner span 2', innerSpan => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 });
+
+ innerSpan.setAttributes({
+ 'test.inner': 'test value',
+ });
+
+ innerSpan.end();
+ });
+
+ span.end();
+ });
+ });
+
+ Sentry.withIsolationScope(() => {
+ client?.tracer.startActiveSpan('test name b', span => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 });
+
+ span.setAttributes({
+ 'test.outer': 'test value b',
+ });
+
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' });
+ subSpan.end();
+
+ Sentry.setTag('test.tag', 'test value b');
+
+ client.tracer.startActiveSpan('inner span 2b', innerSpan => {
+ Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 });
+
+ innerSpan.setAttributes({
+ 'test.inner': 'test value b',
+ });
+
+ innerSpan.end();
+ });
+
+ span.end();
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(2);
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test breadcrumb 1', timestamp: 123456 },
+ { message: 'test breadcrumb 2', timestamp: 123456 },
+ { message: 'test breadcrumb 3', timestamp: 123456 },
+ ],
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.origin': 'manual',
+ 'sentry.source': 'custom',
+ 'test.outer': 'test value',
+ 'sentry.sample_rate': 1,
+ },
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'manual',
+ },
+ }),
+ spans: [expect.any(Object), expect.any(Object)],
+ start_timestamp: expect.any(Number),
+ tags: {
+ 'test.tag': 'test value',
+ },
+ timestamp: expect.any(Number),
+ transaction: 'test name',
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ breadcrumbs: [
+ { message: 'test breadcrumb 1', timestamp: 123456 },
+ { message: 'test breadcrumb 2b', timestamp: 123456 },
+ { message: 'test breadcrumb 3b', timestamp: 123456 },
+ ],
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.origin': 'manual',
+ 'sentry.source': 'custom',
+ 'test.outer': 'test value b',
+ 'sentry.sample_rate': 1,
+ },
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'manual',
+ },
+ }),
+ spans: [expect.any(Object), expect.any(Object)],
+ start_timestamp: expect.any(Number),
+ tags: {
+ 'test.tag': 'test value b',
+ },
+ timestamp: expect.any(Number),
+ transaction: 'test name b',
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+ });
+
+ it('correctly creates transaction & spans with a trace header data', async () => {
+ const beforeSendTransaction = vi.fn(() => null);
+
+ const traceId = 'd4cda95b652f4a1592b449d5929fda1b';
+ const parentSpanId = '6e0c63257de34c92';
+
+ const spanContext = {
+ traceId,
+ spanId: parentSpanId,
+ sampled: true,
+ isRemote: true,
+ traceFlags: TraceFlags.SAMPLED,
+ };
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+
+ const client = Sentry.getClient()!;
+
+ // We simulate the correct context we'd normally get from the SentryPropagator
+ context.with(trace.setSpanContext(context.active(), spanContext), () => {
+ Sentry.startSpan(
+ {
+ op: 'test op',
+ name: 'test name',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test',
+ },
+ },
+ () => {
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
+ subSpan.end();
+
+ Sentry.startSpan({ name: 'inner span 2' }, () => {});
+ },
+ );
+ });
+
+ await client.flush();
+
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
+ expect(beforeSendTransaction).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: {
+ data: {
+ 'sentry.op': 'test op',
+ 'sentry.origin': 'auto.test',
+ 'sentry.source': 'task',
+ },
+ op: 'test op',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ parent_span_id: parentSpanId,
+ status: 'ok',
+ trace_id: traceId,
+ origin: 'auto.test',
+ },
+ }),
+ // spans are circular (they have a reference to the transaction), which leads to jest choking on this
+ // instead we compare them in detail below
+ spans: [expect.any(Object), expect.any(Object)],
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: 'test name',
+ transaction_info: { source: 'task' },
+ type: 'transaction',
+ }),
+ {
+ event_id: expect.any(String),
+ },
+ );
+
+ // Checking the spans here, as they are circular to the transaction...
+ const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown];
+ const spans = runArgs[0].spans || [];
+
+ // note: Currently, spans do not have any context/span added to them
+ // This is the same behavior as for the "regular" SDKs
+ expect(spans).toEqual([
+ {
+ data: {
+ 'sentry.origin': 'manual',
+ },
+ description: 'inner span 1',
+ origin: 'manual',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ },
+ {
+ data: {
+ 'sentry.origin': 'manual',
+ },
+ description: 'inner span 2',
+ origin: 'manual',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ },
+ ]);
+ });
+
+ it('cleans up spans that are not flushed for over 5 mins', async () => {
+ const beforeSendTransaction = vi.fn(() => null);
+
+ const now = Date.now();
+ vi.useFakeTimers();
+ vi.setSystemTime(now);
+
+ const logs: unknown[] = [];
+ vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg));
+
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+
+ const provider = getProvider();
+ const multiSpanProcessor = provider?.activeSpanProcessor as
+ | (SpanProcessor & { _spanProcessors?: SpanProcessor[] })
+ | undefined;
+ const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find(
+ spanProcessor => spanProcessor instanceof SentrySpanProcessor,
+ ) as SentrySpanProcessor | undefined;
+
+ const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined;
+
+ if (!exporter) {
+ throw new Error('No exporter found, aborting test...');
+ }
+
+ void Sentry.startSpan({ name: 'test name' }, async () => {
+ Sentry.startInactiveSpan({ name: 'inner span 1' }).end();
+ Sentry.startInactiveSpan({ name: 'inner span 2' }).end();
+
+ // Pretend this is pending for 10 minutes
+ await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000));
+ });
+
+ vi.advanceTimersByTime(1);
+
+ // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent
+ const finishedSpans1 = [];
+ exporter['_finishedSpanBuckets'].forEach((bucket: any) => {
+ if (bucket) {
+ finishedSpans1.push(...bucket.spans);
+ }
+ });
+ expect(finishedSpans1.length).toBe(2);
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(0);
+
+ // Now wait for 5 mins
+ vi.advanceTimersByTime(5 * 60 * 1_000 + 1);
+
+ // Adding another span will trigger the cleanup
+ Sentry.startSpan({ name: 'other span' }, () => {});
+
+ vi.advanceTimersByTime(1);
+
+ // Old spans have been cleared away
+ const finishedSpans2 = [];
+ exporter['_finishedSpanBuckets'].forEach((bucket: any) => {
+ if (bucket) {
+ finishedSpans2.push(...bucket.spans);
+ }
+ });
+ expect(finishedSpans2.length).toBe(0);
+
+ // Called once for the 'other span'
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
+
+ expect(logs).toEqual(
+ expect.arrayContaining([
+ 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.',
+ 'SpanExporter exported 1 spans, 0 spans are waiting for their parent spans to finish',
+ ]),
+ );
+ });
+
+ it('allows to configure `maxSpanWaitDuration` to capture long running spans', async () => {
+ const transactions: TransactionEvent[] = [];
+ const beforeSendTransaction = vi.fn(event => {
+ transactions.push(event);
+ return null;
+ });
+
+ const now = Date.now();
+ vi.useFakeTimers();
+ vi.setSystemTime(now);
+
+ const logs: unknown[] = [];
+ vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg));
+
+ mockSdkInit({
+ tracesSampleRate: 1,
+ beforeSendTransaction,
+ maxSpanWaitDuration: 100 * 60,
+ });
+
+ Sentry.startSpanManual({ name: 'test name' }, rootSpan => {
+ const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' });
+ subSpan.end();
+
+ Sentry.startSpanManual({ name: 'inner span 2' }, innerSpan => {
+ // Child span ends after 10 min
+ setTimeout(
+ () => {
+ innerSpan.end();
+ },
+ 10 * 60 * 1_000,
+ );
+ });
+
+ // root span ends after 99 min
+ setTimeout(
+ () => {
+ rootSpan.end();
+ },
+ 99 * 10 * 1_000,
+ );
+ });
+
+ // Now wait for 100 mins
+ vi.advanceTimersByTime(100 * 60 * 1_000);
+
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(1);
+ expect(transactions).toHaveLength(1);
+ const transaction = transactions[0]!;
+
+ expect(transaction.transaction).toEqual('test name');
+ const spans = transaction.spans || [];
+
+ expect(spans).toHaveLength(2);
+
+ expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 1' }));
+ expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 2' }));
+ });
+});
diff --git a/packages/node/test/integrations/context.test.ts b/packages/node-core/test/integrations/context.test.ts
similarity index 100%
rename from packages/node/test/integrations/context.test.ts
rename to packages/node-core/test/integrations/context.test.ts
diff --git a/packages/node/test/integrations/contextlines.test.ts b/packages/node-core/test/integrations/contextlines.test.ts
similarity index 100%
rename from packages/node/test/integrations/contextlines.test.ts
rename to packages/node-core/test/integrations/contextlines.test.ts
diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node-core/test/integrations/localvariables.test.ts
similarity index 100%
rename from packages/node/test/integrations/localvariables.test.ts
rename to packages/node-core/test/integrations/localvariables.test.ts
diff --git a/packages/node/test/integrations/request-session-tracking.test.ts b/packages/node-core/test/integrations/request-session-tracking.test.ts
similarity index 100%
rename from packages/node/test/integrations/request-session-tracking.test.ts
rename to packages/node-core/test/integrations/request-session-tracking.test.ts
diff --git a/packages/node/test/integrations/spotlight.test.ts b/packages/node-core/test/integrations/spotlight.test.ts
similarity index 100%
rename from packages/node/test/integrations/spotlight.test.ts
rename to packages/node-core/test/integrations/spotlight.test.ts
diff --git a/packages/node/test/logs/exports.test.ts b/packages/node-core/test/logs/exports.test.ts
similarity index 100%
rename from packages/node/test/logs/exports.test.ts
rename to packages/node-core/test/logs/exports.test.ts
diff --git a/packages/node-core/test/sdk/api.test.ts b/packages/node-core/test/sdk/api.test.ts
new file mode 100644
index 000000000000..5c2c32ad13d0
--- /dev/null
+++ b/packages/node-core/test/sdk/api.test.ts
@@ -0,0 +1,104 @@
+import type { Event } from '@sentry/core';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { getActiveSpan, getClient, startInactiveSpan, startSpan, withActiveSpan } from '../../src';
+import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit';
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ cleanupOtel();
+});
+
+describe('withActiveSpan()', () => {
+ it('should set the active span within the callback', () => {
+ mockSdkInit({ tracesSampleRate: 1 });
+
+ const inactiveSpan = startInactiveSpan({ name: 'inactive-span' });
+
+ expect(getActiveSpan()).not.toBe(inactiveSpan);
+
+ withActiveSpan(inactiveSpan, () => {
+ expect(getActiveSpan()).toBe(inactiveSpan);
+ });
+ });
+
+ it('should create child spans when calling startSpan within the callback', async () => {
+ const beforeSendTransaction = vi.fn(() => null);
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+ const client = getClient();
+
+ const inactiveSpan = startInactiveSpan({ name: 'inactive-span' });
+
+ withActiveSpan(inactiveSpan, () => {
+ startSpan({ name: 'child-span' }, () => {});
+ });
+
+ startSpan({ name: 'floating-span' }, () => {});
+
+ inactiveSpan.end();
+
+ await client?.flush();
+
+ // The child span should be a child of the inactive span
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ transaction: 'inactive-span',
+ spans: expect.arrayContaining([expect.any(Object)]),
+ }),
+ expect.anything(),
+ );
+
+ // The floating span should be a separate transaction
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ transaction: 'floating-span',
+ }),
+ expect.anything(),
+ );
+ });
+
+ it('when `null` is passed, no span should be active within the callback', () => {
+ expect.assertions(1);
+ startSpan({ name: 'parent-span' }, () => {
+ withActiveSpan(null, () => {
+ expect(getActiveSpan()).toBeUndefined();
+ });
+ });
+ });
+
+ it('when `null` is passed, should start a new trace for new spans', async () => {
+ const transactions: Event[] = [];
+ const beforeSendTransaction = vi.fn((event: Event) => {
+ transactions.push(event);
+ return null;
+ });
+ mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction });
+ const client = getClient();
+
+ startSpan({ name: 'parent-span' }, () => {
+ withActiveSpan(null, () => {
+ startSpan({ name: 'child-span' }, () => {});
+ });
+ });
+
+ await client?.flush();
+
+ expect(beforeSendTransaction).toHaveBeenCalledTimes(2);
+
+ // The child span should be a child of the inactive span
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ transaction: 'parent-span',
+ spans: expect.not.arrayContaining([expect.objectContaining({ description: 'child-span' })]),
+ }),
+ expect.anything(),
+ );
+
+ // The floating span should be a separate transaction
+ expect(beforeSendTransaction).toHaveBeenCalledWith(
+ expect.objectContaining({
+ transaction: 'child-span',
+ }),
+ expect.anything(),
+ );
+ });
+});
diff --git a/packages/node-core/test/sdk/client.test.ts b/packages/node-core/test/sdk/client.test.ts
new file mode 100644
index 000000000000..f053b1ba7e0e
--- /dev/null
+++ b/packages/node-core/test/sdk/client.test.ts
@@ -0,0 +1,324 @@
+import { ProxyTracer } from '@opentelemetry/api';
+import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation';
+import type { Event, EventHint, Log } from '@sentry/core';
+import { getCurrentScope, getGlobalScope, getIsolationScope, Scope, SDK_VERSION } from '@sentry/core';
+import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry';
+import * as os from 'os';
+import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest';
+import { NodeClient } from '../../src';
+import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions';
+import { cleanupOtel } from '../helpers/mockSdkInit';
+
+describe('NodeClient', () => {
+ beforeEach(() => {
+ getIsolationScope().clear();
+ getGlobalScope().clear();
+ getCurrentScope().clear();
+ getCurrentScope().setClient(undefined);
+ setOpenTelemetryContextAsyncContextStrategy();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ cleanupOtel();
+ });
+
+ it('sets correct metadata', () => {
+ const options = getDefaultNodeClientOptions();
+ const client = new NodeClient(options);
+
+ expect(client.getOptions()).toEqual({
+ dsn: expect.any(String),
+ integrations: [],
+ transport: options.transport,
+ stackParser: options.stackParser,
+ _metadata: {
+ sdk: {
+ name: 'sentry.javascript.node',
+ packages: [
+ {
+ name: 'npm:@sentry/node',
+ version: SDK_VERSION,
+ },
+ ],
+ version: SDK_VERSION,
+ },
+ },
+ platform: 'node',
+ runtime: { name: 'node', version: expect.any(String) },
+ serverName: expect.any(String),
+ tracesSampleRate: 1,
+ });
+ });
+
+ it('exposes a tracer', () => {
+ const client = new NodeClient(getDefaultNodeClientOptions());
+
+ const tracer = client.tracer;
+ expect(tracer).toBeDefined();
+ expect(tracer).toBeInstanceOf(ProxyTracer);
+
+ // Ensure we always get the same tracer instance
+ const tracer2 = client.tracer;
+
+ expect(tracer2).toBe(tracer);
+ });
+
+ describe('_prepareEvent', () => {
+ const currentScope = new Scope();
+ const isolationScope = new Scope();
+
+ test('adds platform to event', () => {
+ const options = getDefaultNodeClientOptions({});
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.platform).toEqual('node');
+ });
+
+ test('adds runtime context to event', () => {
+ const options = getDefaultNodeClientOptions({});
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.contexts?.runtime).toEqual({
+ name: 'node',
+ version: process.version,
+ });
+ });
+
+ test('adds server name to event when value passed in options', () => {
+ const options = getDefaultNodeClientOptions({ serverName: 'foo' });
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.server_name).toEqual('foo');
+ });
+
+ test('adds server name to event when value given in env', () => {
+ const options = getDefaultNodeClientOptions({});
+ process.env.SENTRY_NAME = 'foo';
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.server_name).toEqual('foo');
+
+ delete process.env.SENTRY_NAME;
+ });
+
+ test('adds hostname as event server name when no value given', () => {
+ const options = getDefaultNodeClientOptions({});
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.server_name).toEqual(os.hostname());
+ });
+
+ test('does not add hostname when includeServerName = false', () => {
+ const options = getDefaultNodeClientOptions({});
+ options.includeServerName = false;
+ const client = new NodeClient(options);
+
+ const event: Event = {};
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.server_name).toBeUndefined();
+ });
+
+ test("doesn't clobber existing runtime data", () => {
+ const options = getDefaultNodeClientOptions({ serverName: 'bar' });
+ const client = new NodeClient(options);
+
+ const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } };
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' });
+ expect(event.contexts?.runtime).not.toEqual({ name: 'node', version: process.version });
+ });
+
+ test("doesn't clobber existing server name", () => {
+ const options = getDefaultNodeClientOptions({ serverName: 'bar' });
+ const client = new NodeClient(options);
+
+ const event: Event = { server_name: 'foo' };
+ const hint: EventHint = {};
+ client['_prepareEvent'](event, hint, currentScope, isolationScope);
+
+ expect(event.server_name).toEqual('foo');
+ expect(event.server_name).not.toEqual('bar');
+ });
+ });
+
+ describe('captureCheckIn', () => {
+ it('sends a checkIn envelope', () => {
+ const options = getDefaultNodeClientOptions({
+ serverName: 'bar',
+ release: '1.0.0',
+ environment: 'dev',
+ });
+ const client = new NodeClient(options);
+
+ const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
+
+ const id = client.captureCheckIn(
+ { monitorSlug: 'foo', status: 'in_progress' },
+ {
+ schedule: {
+ type: 'crontab',
+ value: '0 * * * *',
+ },
+ checkinMargin: 2,
+ maxRuntime: 12333,
+ timezone: 'Canada/Eastern',
+ },
+ );
+
+ expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
+ expect(sendEnvelopeSpy).toHaveBeenCalledWith([
+ expect.any(Object),
+ [
+ [
+ expect.any(Object),
+ {
+ check_in_id: id,
+ monitor_slug: 'foo',
+ status: 'in_progress',
+ release: '1.0.0',
+ environment: 'dev',
+ monitor_config: {
+ schedule: {
+ type: 'crontab',
+ value: '0 * * * *',
+ },
+ checkin_margin: 2,
+ max_runtime: 12333,
+ timezone: 'Canada/Eastern',
+ },
+ },
+ ],
+ ],
+ ]);
+
+ client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id });
+
+ expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2);
+ expect(sendEnvelopeSpy).toHaveBeenCalledWith([
+ expect.any(Object),
+ [
+ [
+ expect.any(Object),
+ {
+ check_in_id: id,
+ monitor_slug: 'foo',
+ duration: 1222,
+ status: 'ok',
+ release: '1.0.0',
+ environment: 'dev',
+ },
+ ],
+ ],
+ ]);
+ });
+
+ it('sends a checkIn envelope for heartbeat checkIns', () => {
+ const options = getDefaultNodeClientOptions({
+ serverName: 'server',
+ release: '1.0.0',
+ environment: 'dev',
+ });
+ const client = new NodeClient(options);
+
+ const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
+
+ const id = client.captureCheckIn({ monitorSlug: 'heartbeat-monitor', status: 'ok' });
+
+ expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
+ expect(sendEnvelopeSpy).toHaveBeenCalledWith([
+ expect.any(Object),
+ [
+ [
+ expect.any(Object),
+ {
+ check_in_id: id,
+ monitor_slug: 'heartbeat-monitor',
+ status: 'ok',
+ release: '1.0.0',
+ environment: 'dev',
+ },
+ ],
+ ],
+ ]);
+ });
+
+ it('does not send a checkIn envelope if disabled', () => {
+ const options = getDefaultNodeClientOptions({ serverName: 'bar', enabled: false });
+ const client = new NodeClient(options);
+
+ const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
+
+ client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' });
+
+ expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ it('registers instrumentations provided with `openTelemetryInstrumentations`', () => {
+ const registerInstrumentationsSpy = vi
+ .spyOn(opentelemetryInstrumentationPackage, 'registerInstrumentations')
+ .mockImplementationOnce(() => () => undefined);
+ const instrumentationsArray = ['foobar'] as unknown as opentelemetryInstrumentationPackage.Instrumentation[];
+
+ new NodeClient(getDefaultNodeClientOptions({ openTelemetryInstrumentations: instrumentationsArray }));
+
+ expect(registerInstrumentationsSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ instrumentations: instrumentationsArray,
+ }),
+ );
+ });
+
+ describe('log capture', () => {
+ it('adds server name to log attributes', () => {
+ const options = getDefaultNodeClientOptions({ _experiments: { enableLogs: true } });
+ const client = new NodeClient(options);
+
+ const log: Log = { level: 'info', message: 'test message', attributes: {} };
+ client.emit('beforeCaptureLog', log);
+
+ expect(log.attributes).toEqual({
+ 'server.address': expect.any(String),
+ });
+ });
+
+ it('preserves existing log attributes', () => {
+ const serverName = 'test-server';
+ const options = getDefaultNodeClientOptions({ serverName, _experiments: { enableLogs: true } });
+ const client = new NodeClient(options);
+
+ const log: Log = { level: 'info', message: 'test message', attributes: { 'existing.attr': 'value' } };
+ client.emit('beforeCaptureLog', log);
+
+ expect(log.attributes).toEqual({
+ 'existing.attr': 'value',
+ 'server.address': serverName,
+ });
+ });
+ });
+});
diff --git a/packages/node-core/test/sdk/init.test.ts b/packages/node-core/test/sdk/init.test.ts
new file mode 100644
index 000000000000..dc523a843b92
--- /dev/null
+++ b/packages/node-core/test/sdk/init.test.ts
@@ -0,0 +1,269 @@
+import type { Integration } from '@sentry/core';
+import { logger } from '@sentry/core';
+import * as SentryOpentelemetry from '@sentry/opentelemetry';
+import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { getClient } from '../../src/';
+import { init, validateOpenTelemetrySetup } from '../../src/sdk';
+import { NodeClient } from '../../src/sdk/client';
+import { cleanupOtel } from '../helpers/mockSdkInit';
+
+// eslint-disable-next-line no-var
+declare var global: any;
+
+const PUBLIC_DSN = 'https://username@domain/123';
+
+class MockIntegration implements Integration {
+ public name: string;
+ public setupOnce: Mock = vi.fn();
+ public constructor(name: string) {
+ this.name = name;
+ }
+}
+
+describe('init()', () => {
+ beforeEach(() => {
+ global.__SENTRY__ = {};
+ });
+
+ afterEach(() => {
+ cleanupOtel();
+
+ vi.clearAllMocks();
+ });
+
+ describe('integrations', () => {
+ it("doesn't install default integrations if told not to", () => {
+ init({ dsn: PUBLIC_DSN, defaultIntegrations: false });
+
+ const client = getClient();
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ integrations: [],
+ }),
+ );
+ });
+
+ it('installs merged default integrations, with overrides provided through options', () => {
+ const mockDefaultIntegrations = [
+ new MockIntegration('Some mock integration 2.1'),
+ new MockIntegration('Some mock integration 2.2'),
+ ];
+
+ const mockIntegrations = [
+ new MockIntegration('Some mock integration 2.1'),
+ new MockIntegration('Some mock integration 2.3'),
+ ];
+
+ init({ dsn: PUBLIC_DSN, integrations: mockIntegrations, defaultIntegrations: mockDefaultIntegrations });
+
+ expect(mockDefaultIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(0);
+ expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1);
+ expect(mockIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1);
+ expect(mockIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1);
+ });
+
+ it('installs integrations returned from a callback function', () => {
+ const mockDefaultIntegrations = [
+ new MockIntegration('Some mock integration 3.1'),
+ new MockIntegration('Some mock integration 3.2'),
+ ];
+
+ const newIntegration = new MockIntegration('Some mock integration 3.3');
+
+ init({
+ dsn: PUBLIC_DSN,
+ defaultIntegrations: mockDefaultIntegrations,
+ integrations: integrations => {
+ const newIntegrations = [...integrations];
+ newIntegrations[1] = newIntegration;
+ return newIntegrations;
+ },
+ });
+
+ expect(mockDefaultIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1);
+ expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(0);
+ expect(newIntegration.setupOnce as Mock).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('returns initialized client', () => {
+ const client = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true });
+
+ expect(client).toBeInstanceOf(NodeClient);
+ });
+
+ describe('environment variable options', () => {
+ const originalProcessEnv = { ...process.env };
+
+ afterEach(() => {
+ process.env = originalProcessEnv;
+ global.__SENTRY__ = {};
+ cleanupOtel();
+ vi.clearAllMocks();
+ });
+
+ it('sets debug from `SENTRY_DEBUG` env variable', () => {
+ process.env.SENTRY_DEBUG = '1';
+
+ const client = init({ dsn: PUBLIC_DSN });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ debug: true,
+ }),
+ );
+ });
+
+ it('prefers `debug` option over `SENTRY_DEBUG` env variable', () => {
+ process.env.SENTRY_DEBUG = '1';
+
+ const client = init({ dsn: PUBLIC_DSN, debug: false });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ debug: false,
+ }),
+ );
+ });
+
+ it('sets tracesSampleRate from `SENTRY_TRACES_SAMPLE_RATE` env variable', () => {
+ process.env.SENTRY_TRACES_SAMPLE_RATE = '0.5';
+
+ const client = init({ dsn: PUBLIC_DSN });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ tracesSampleRate: 0.5,
+ }),
+ );
+ });
+
+ it('prefers `tracesSampleRate` option over `SENTRY_TRACES_SAMPLE_RATE` env variable', () => {
+ process.env.SENTRY_TRACES_SAMPLE_RATE = '0.5';
+
+ const client = init({ dsn: PUBLIC_DSN, tracesSampleRate: 0.1 });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ tracesSampleRate: 0.1,
+ }),
+ );
+ });
+
+ it('sets release from `SENTRY_RELEASE` env variable', () => {
+ process.env.SENTRY_RELEASE = '1.0.0';
+
+ const client = init({ dsn: PUBLIC_DSN });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ release: '1.0.0',
+ }),
+ );
+ });
+
+ it('prefers `release` option over `SENTRY_RELEASE` env variable', () => {
+ process.env.SENTRY_RELEASE = '1.0.0';
+
+ const client = init({ dsn: PUBLIC_DSN, release: '2.0.0' });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ release: '2.0.0',
+ }),
+ );
+ });
+
+ it('sets environment from `SENTRY_ENVIRONMENT` env variable', () => {
+ process.env.SENTRY_ENVIRONMENT = 'production';
+
+ const client = init({ dsn: PUBLIC_DSN });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ environment: 'production',
+ }),
+ );
+ });
+
+ it('prefers `environment` option over `SENTRY_ENVIRONMENT` env variable', () => {
+ process.env.SENTRY_ENVIRONMENT = 'production';
+
+ const client = init({ dsn: PUBLIC_DSN, environment: 'staging' });
+
+ expect(client?.getOptions()).toEqual(
+ expect.objectContaining({
+ environment: 'staging',
+ }),
+ );
+ });
+ });
+});
+
+describe('validateOpenTelemetrySetup', () => {
+ afterEach(() => {
+ global.__SENTRY__ = {};
+ cleanupOtel();
+ vi.clearAllMocks();
+ });
+
+ it('works with correct setup', () => {
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
+
+ vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => {
+ return ['SentryContextManager', 'SentryPropagator', 'SentrySampler'];
+ });
+
+ validateOpenTelemetrySetup();
+
+ expect(errorSpy).toHaveBeenCalledTimes(0);
+ expect(warnSpy).toHaveBeenCalledTimes(0);
+ });
+
+ it('works with missing setup, without tracing', () => {
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
+
+ vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => {
+ return [];
+ });
+
+ validateOpenTelemetrySetup();
+
+ // Without tracing, this is expected only twice
+ expect(errorSpy).toHaveBeenCalledTimes(2);
+ expect(warnSpy).toHaveBeenCalledTimes(1);
+
+ expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.'));
+ expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.'));
+ expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.'));
+ });
+
+ it('works with missing setup, with tracing', () => {
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {});
+
+ vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => {
+ return [];
+ });
+
+ init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true, tracesSampleRate: 1 });
+
+ validateOpenTelemetrySetup();
+
+ expect(errorSpy).toHaveBeenCalledTimes(3);
+ expect(warnSpy).toHaveBeenCalledTimes(1);
+
+ expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.'));
+ expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.'));
+ expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySpanProcessor.'));
+ expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.'));
+ });
+
+ // Regression test for https://github.com/getsentry/sentry-javascript/issues/15558
+ it('accepts an undefined transport', () => {
+ init({ dsn: PUBLIC_DSN, transport: undefined });
+ });
+});
diff --git a/packages/node/test/transports/http.test.ts b/packages/node-core/test/transports/http.test.ts
similarity index 100%
rename from packages/node/test/transports/http.test.ts
rename to packages/node-core/test/transports/http.test.ts
diff --git a/packages/node/test/transports/https.test.ts b/packages/node-core/test/transports/https.test.ts
similarity index 100%
rename from packages/node/test/transports/https.test.ts
rename to packages/node-core/test/transports/https.test.ts
diff --git a/packages/node-core/test/transports/test-server-certs.ts b/packages/node-core/test/transports/test-server-certs.ts
new file mode 100644
index 000000000000..a5ce436c4234
--- /dev/null
+++ b/packages/node-core/test/transports/test-server-certs.ts
@@ -0,0 +1,48 @@
+export default {
+ key: `-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A
+bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4
+6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t
+q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH
+M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth
+AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABAoIBADLsjEPB59gJKxVH
+pqvfE7SRi4enVFP1MM6hEGMcM1ls/qg1vkp11q8G/Rz5ui8VsNWY6To5hmDAKQCN
+akMxaksCn9nDzeHHqWvxxCMzXcMuoYkc1vYa613KqJ7twzDtJKdx2oD8tXoR06l9
+vg2CL4idefOkmsCK3xioZjxBpC6jF6ybvlY241MGhaAGRHmP6ik1uFJ+6Y8smh6R
+AQKO0u0oQPy6bka9F6DTP6BMUeZ+OA/oOrrb5FxTHu8AHcyCSk2wHnCkB9EF/Ou2
+xSWrnu0O0/0Px6OO9oEsNSq2/fKNV9iuEU8LeAoDVm4ysyMrPce2c4ZsB4U244bj
+yQpQZ6ECgYEA9KwA7Lmyf+eeZHxEM4MNSqyeXBtSKu4Zyk0RRY1j69ConjHKet3Q
+ylVedXQ0/FJAHHKEm4zFGZtnaaxrzCIcQSKJBCoaA+cN44MM3D1nKmHjgPy8R/yE
+BNgIVwJB1MmVSGa+NYnQgUomcCIEr/guNMIxV7p2iybqoxaEHKLfGFUCgYEAwVn1
+8LARsZihLUdxxbAc9+v/pBeMTrkTw1eN1ki9VWYoRam2MLozehEzabt677cU4h7+
+bjdKCKo1x2liY9zmbIiVHssv9Jf3E9XhcajsXB42m1+kjUYVPh8o9lDXcatV9EKt
+DZK8wfRY9boyDKB2zRyo6bvIEK3qWbas31W3a8cCgYA6w0TFliPkzEAiaiYHKSZ8
+FNFD1dv6K41OJQxM5BRngom81MCImdWXgsFY/DvtjeOP8YEfysNbzxMbMioBsP+Q
+NTcrJOFypn+TcNoZ2zV33GLDi++8ak1azHfUTdp5vKB57xMn0J2fL6vjqoftq3GN
+gkZPh50I9qPL35CDQCrMsQKBgC6tFfc1uf/Cld5FagzMOCINodguKxvyB/hXUZFS
+XAqar8wpbScUPEsSjfPPY50s+GiiDM/0nvW6iWMLaMos0J+Q1VbqvDfy2525O0Ri
+ADU4wfv+Oc41BfnKMexMlcYGE6j006v8KX81Cqi/e0ebETLw4UITp/eG1JU1yUPd
+AHuPAoGBAL25v4/onoH0FBLdEwb2BAENxc+0g4In1T+83jfHbfD0gOF3XTbgH4FF
+MduIG8qBoZC5whiZ3qH7YJK7sydaM1bDwiesqIik+gEUE65T7S2ZF84y5GC5JjTf
+z6v6i+DMCIJXDY5/gjzOED6UllV2Jrn2pDoV++zVyR6KAwXpCmK6
+-----END RSA PRIVATE KEY-----`,
+ cert: `-----BEGIN CERTIFICATE-----
+MIIDETCCAfkCFCMI53aBdS2kWTrw39Kkv93ErG3iMA0GCSqGSIb3DQEBCwUAMEUx
+CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
+cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwMzI4MDgzODQwWhcNNDkwODEyMDgz
+ODQwWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
+CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A
+bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4
+6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t
+q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH
+M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth
+AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQBh4BKiByhyvAc5uHj5bkSqspY2xZWW8xiEGaCaQWDMlyjP9mVVWFHfE3XL
+lzsJdZVnHDZUliuA5L+qTEpLJ5GmgDWqnKp3HdhtkL16mPbPyJLPY0X+m7wvoZRt
+RwLfFCx1E13m0ktYWWgmSCnBl+rI7pyagDhZ2feyxsMrecCazyG/llFBuyWSOnIi
+OHxjdHV7be5c8uOOp1iNB9j++LW1pRVrSCWOKRLcsUBal73FW+UvhM5+1If/F9pF
+GNQrMhVRA8aHD0JAu3tpjYRKRuOpAbbqtiAUSbDPsJBQy/K9no2K83G7+AV+aGai
+HXfQqFFJS6xGKU79azH51wLVEGXq
+-----END CERTIFICATE-----`,
+};
diff --git a/packages/node-core/test/tsconfig.json b/packages/node-core/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/node-core/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/node/test/utils/ensureIsWrapped.test.ts b/packages/node-core/test/utils/ensureIsWrapped.test.ts
similarity index 100%
rename from packages/node/test/utils/ensureIsWrapped.test.ts
rename to packages/node-core/test/utils/ensureIsWrapped.test.ts
diff --git a/packages/node/test/utils/entry-point.test.ts b/packages/node-core/test/utils/entry-point.test.ts
similarity index 100%
rename from packages/node/test/utils/entry-point.test.ts
rename to packages/node-core/test/utils/entry-point.test.ts
diff --git a/packages/node/test/utils/envToBool.test.ts b/packages/node-core/test/utils/envToBool.test.ts
similarity index 100%
rename from packages/node/test/utils/envToBool.test.ts
rename to packages/node-core/test/utils/envToBool.test.ts
diff --git a/packages/node/test/utils/getRequestUrl.test.ts b/packages/node-core/test/utils/getRequestUrl.test.ts
similarity index 100%
rename from packages/node/test/utils/getRequestUrl.test.ts
rename to packages/node-core/test/utils/getRequestUrl.test.ts
diff --git a/packages/node/test/utils/instrument.test.ts b/packages/node-core/test/utils/instrument.test.ts
similarity index 100%
rename from packages/node/test/utils/instrument.test.ts
rename to packages/node-core/test/utils/instrument.test.ts
diff --git a/packages/node-core/test/utils/module.test.ts b/packages/node-core/test/utils/module.test.ts
new file mode 100644
index 000000000000..73404c37673e
--- /dev/null
+++ b/packages/node-core/test/utils/module.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest';
+import { createGetModuleFromFilename } from '../../src';
+
+describe('createGetModuleFromFilename', () => {
+ it.each([
+ ['/path/to/file.js', 'file'],
+ ['/path/to/file.mjs', 'file'],
+ ['/path/to/file.cjs', 'file'],
+ ['file.js', 'file'],
+ ])('returns the module name from a filename %s', (filename, expected) => {
+ const getModule = createGetModuleFromFilename();
+ expect(getModule(filename)).toBe(expected);
+ });
+
+ it('applies the given base path', () => {
+ const getModule = createGetModuleFromFilename('/path/to/base');
+ expect(getModule('/path/to/base/file.js')).toBe('file');
+ });
+
+ it('decodes URI-encoded file names', () => {
+ const getModule = createGetModuleFromFilename();
+ expect(getModule('/path%20with%space/file%20with%20spaces(1).js')).toBe('file with spaces(1)');
+ });
+
+ it('returns undefined if no filename is provided', () => {
+ const getModule = createGetModuleFromFilename();
+ expect(getModule(undefined)).toBeUndefined();
+ });
+
+ it.each([
+ ['/path/to/base/node_modules/@sentry/test/file.js', '@sentry.test:file'],
+ ['/path/to/base/node_modules/somePkg/file.js', 'somePkg:file'],
+ ])('handles node_modules file paths %s', (filename, expected) => {
+ const getModule = createGetModuleFromFilename();
+ expect(getModule(filename)).toBe(expected);
+ });
+
+ it('handles windows paths with passed basePath and node_modules', () => {
+ const getModule = createGetModuleFromFilename('C:\\path\\to\\base', true);
+ expect(getModule('C:\\path\\to\\base\\node_modules\\somePkg\\file.js')).toBe('somePkg:file');
+ });
+
+ it('handles windows paths with default basePath', () => {
+ const getModule = createGetModuleFromFilename(undefined, true);
+ expect(getModule('C:\\path\\to\\base\\somePkg\\file.js')).toBe('file');
+ });
+});
diff --git a/packages/node-core/tsconfig.json b/packages/node-core/tsconfig.json
new file mode 100644
index 000000000000..b9683a850600
--- /dev/null
+++ b/packages/node-core/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ "lib": ["es2018", "es2020.string"],
+ "module": "Node16"
+ }
+}
diff --git a/packages/node-core/tsconfig.test.json b/packages/node-core/tsconfig.test.json
new file mode 100644
index 000000000000..3f2ffb86f0f7
--- /dev/null
+++ b/packages/node-core/tsconfig.test.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*", "./src/integrations/diagnostic_channel.d.ts", "vite.config.ts"],
+
+ "compilerOptions": {
+ // should include all types from `./tsconfig.json` plus types for all test frameworks used
+ "types": ["node"]
+
+ // other package-specific, test-specific options
+ }
+}
diff --git a/packages/node-core/tsconfig.types.json b/packages/node-core/tsconfig.types.json
new file mode 100644
index 000000000000..65455f66bd75
--- /dev/null
+++ b/packages/node-core/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/types"
+ }
+}
diff --git a/packages/node-core/vite.config.ts b/packages/node-core/vite.config.ts
new file mode 100644
index 000000000000..f18ec92095bc
--- /dev/null
+++ b/packages/node-core/vite.config.ts
@@ -0,0 +1,8 @@
+import baseConfig from '../../vite/vite.config';
+
+export default {
+ ...baseConfig,
+ test: {
+ ...baseConfig.test,
+ },
+};
diff --git a/packages/node/package.json b/packages/node/package.json
index d7519d947bf3..dd48c1d3d8e1 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -94,8 +94,9 @@
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-base": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.34.0",
- "@prisma/instrumentation": "6.10.1",
+ "@prisma/instrumentation": "6.11.1",
"@sentry/core": "9.35.0",
+ "@sentry/node-core": "9.35.0",
"@sentry/opentelemetry": "9.35.0",
"import-in-the-middle": "^1.14.2",
"minimatch": "^9.0.0"
diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs
index e0483c673d1c..93fd1d8c16ca 100644
--- a/packages/node/rollup.npm.config.mjs
+++ b/packages/node/rollup.npm.config.mjs
@@ -1,22 +1,7 @@
-import replace from '@rollup/plugin-replace';
import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils';
-import { createWorkerCodeBuilder } from './rollup.anr-worker.config.mjs';
-
-const [anrWorkerConfig, getAnrBase64Code] = createWorkerCodeBuilder(
- 'src/integrations/anr/worker.ts',
- 'build/esm/integrations/anr',
-);
-
-const [localVariablesWorkerConfig, getLocalVariablesBase64Code] = createWorkerCodeBuilder(
- 'src/integrations/local-variables/worker.ts',
- 'build/esm/integrations/local-variables',
-);
export default [
...makeOtelLoaders('./build', 'otel'),
- // The workers needs to be built first since it's their output is copied in the main bundle.
- anrWorkerConfig,
- localVariablesWorkerConfig,
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/init.ts', 'src/preload.ts'],
@@ -26,17 +11,6 @@ export default [
exports: 'named',
preserveModules: true,
},
- plugins: [
- replace({
- delimiters: ['###', '###'],
- // removes some rollup warnings
- preventAssignment: true,
- values: {
- AnrWorkerScript: () => getAnrBase64Code(),
- LocalVariablesWorkerScript: () => getLocalVariablesBase64Code(),
- },
- }),
- ],
},
}),
),
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 71970174721c..afcc42f16e84 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -1,17 +1,6 @@
-import * as logger from './logs/exports';
-
export { httpIntegration } from './integrations/http';
export { nativeNodeFetchIntegration } from './integrations/node-fetch';
export { fsIntegration } from './integrations/fs';
-
-export { nodeContextIntegration } from './integrations/context';
-export { contextLinesIntegration } from './integrations/contextlines';
-export { localVariablesIntegration } from './integrations/local-variables';
-export { modulesIntegration } from './integrations/modules';
-export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception';
-export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection';
-export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr';
-
export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express';
export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify';
export { graphqlIntegration } from './integrations/tracing/graphql';
@@ -28,15 +17,12 @@ export { prismaIntegration } from './integrations/tracing/prisma';
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa';
export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect';
-export { spotlightIntegration } from './integrations/spotlight';
export { knexIntegration } from './integrations/tracing/knex';
export { tediousIntegration } from './integrations/tracing/tedious';
export { genericPoolIntegration } from './integrations/tracing/genericPool';
export { dataloaderIntegration } from './integrations/tracing/dataloader';
export { amqplibIntegration } from './integrations/tracing/amqplib';
export { vercelAIIntegration } from './integrations/tracing/vercelai';
-export { childProcessIntegration } from './integrations/childProcess';
-export { createSentryWinstonTransport } from './integrations/winston';
export {
launchDarklyIntegration,
buildLaunchDarklyFlagUsedHandler,
@@ -46,24 +32,14 @@ export {
unleashIntegration,
} from './integrations/featureFlagShims';
-export { SentryContextManager } from './otel/contextManager';
-export { generateInstrumentOnce } from './otel/instrument';
-
export {
init,
getDefaultIntegrations,
getDefaultIntegrationsWithoutPerformance,
initWithoutDefaultIntegrations,
- validateOpenTelemetrySetup,
} from './sdk';
export { initOpenTelemetry, preloadOpenTelemetry } from './sdk/initOtel';
export { getAutoPerformanceIntegrations } from './integrations/tracing';
-export { getSentryRelease, defaultStackParser } from './sdk/api';
-export { createGetModuleFromFilename } from './utils/module';
-export { makeNodeTransport } from './transports';
-export { NodeClient } from './sdk/client';
-export { cron } from './cron';
-export { NODE_VERSION } from './nodeVersion';
export type { NodeOptions } from './types';
@@ -171,4 +147,29 @@ export type {
FeatureFlagsIntegration,
} from '@sentry/core';
-export { logger };
+export {
+ logger,
+ nodeContextIntegration,
+ contextLinesIntegration,
+ localVariablesIntegration,
+ modulesIntegration,
+ onUncaughtExceptionIntegration,
+ onUnhandledRejectionIntegration,
+ // eslint-disable-next-line deprecation/deprecation
+ anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
+ disableAnrDetectionForCallback,
+ spotlightIntegration,
+ childProcessIntegration,
+ createSentryWinstonTransport,
+ SentryContextManager,
+ generateInstrumentOnce,
+ getSentryRelease,
+ defaultStackParser,
+ createGetModuleFromFilename,
+ makeNodeTransport,
+ NodeClient,
+ cron,
+ NODE_VERSION,
+ validateOpenTelemetrySetup,
+} from '@sentry/node-core';
diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts
deleted file mode 100644
index fc1b23e35b1d..000000000000
--- a/packages/node/src/integrations/anr/common.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Contexts, DsnComponents, Primitive, SdkMetadata } from '@sentry/core';
-
-export interface AnrIntegrationOptions {
- /**
- * Interval to send heartbeat messages to the ANR worker.
- *
- * Defaults to 50ms.
- */
- pollInterval: number;
- /**
- * Threshold in milliseconds to trigger an ANR event.
- *
- * Defaults to 5000ms.
- */
- anrThreshold: number;
- /**
- * Whether to capture a stack trace when the ANR event is triggered.
- *
- * Defaults to `false`.
- *
- * This uses the node debugger which enables the inspector API and opens the required ports.
- */
- captureStackTrace: boolean;
- /**
- * Maximum number of ANR events to send.
- *
- * Defaults to 1.
- */
- maxAnrEvents: number;
- /**
- * Tags to include with ANR events.
- */
- staticTags: { [key: string]: Primitive };
- /**
- * @ignore Internal use only.
- *
- * If this is supplied, stack frame filenames will be rewritten to be relative to this path.
- */
- appRootPath: string | undefined;
-}
-
-export interface WorkerStartData extends AnrIntegrationOptions {
- debug: boolean;
- sdkMetadata: SdkMetadata;
- dsn: DsnComponents;
- tunnel: string | undefined;
- release: string | undefined;
- environment: string;
- dist: string | undefined;
- contexts: Contexts;
-}
diff --git a/packages/node/src/integrations/fs.ts b/packages/node/src/integrations/fs.ts
index 5ceedd571283..2fd05ad0a09d 100644
--- a/packages/node/src/integrations/fs.ts
+++ b/packages/node/src/integrations/fs.ts
@@ -1,6 +1,6 @@
import { FsInstrumentation } from '@opentelemetry/instrumentation-fs';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { generateInstrumentOnce } from '../otel/instrument';
+import { generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'FileSystem';
diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts
index 96e9b84315be..e46e830f9d16 100644
--- a/packages/node/src/integrations/http/index.ts
+++ b/packages/node/src/integrations/http/index.ts
@@ -4,15 +4,16 @@ import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-h
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import type { Span } from '@sentry/core';
import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core';
-import { NODE_VERSION } from '../../nodeVersion';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import type { NodeClient } from '../../sdk/client';
-import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module';
+import type { HTTPModuleRequestIncomingMessage, NodeClient } from '@sentry/node-core';
+import {
+ type SentryHttpInstrumentationOptions,
+ addOriginToSpan,
+ generateInstrumentOnce,
+ getRequestUrl,
+ NODE_VERSION,
+ SentryHttpInstrumentation,
+} from '@sentry/node-core';
import type { NodeClientOptions } from '../../types';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
-import { getRequestUrl } from '../../utils/getRequestUrl';
-import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation';
-import { SentryHttpInstrumentation } from './SentryHttpInstrumentation';
const INTEGRATION_NAME = 'Http';
diff --git a/packages/node/src/integrations/node-fetch/index.ts b/packages/node/src/integrations/node-fetch/index.ts
index e85ce34dfb35..7929d00c87e0 100644
--- a/packages/node/src/integrations/node-fetch/index.ts
+++ b/packages/node/src/integrations/node-fetch/index.ts
@@ -2,10 +2,9 @@ import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, getClient, hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import type { NodeClient } from '../../sdk/client';
+import type { NodeClient } from '@sentry/node-core';
+import { generateInstrumentOnce, SentryNodeFetchInstrumentation } from '@sentry/node-core';
import type { NodeClientOptions } from '../../types';
-import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';
const INTEGRATION_NAME = 'NodeFetch';
diff --git a/packages/node/src/integrations/tracing/amqplib.ts b/packages/node/src/integrations/tracing/amqplib.ts
index 78f5ab627081..bbbdb0b4ae71 100644
--- a/packages/node/src/integrations/tracing/amqplib.ts
+++ b/packages/node/src/integrations/tracing/amqplib.ts
@@ -2,8 +2,7 @@ import type { Span } from '@opentelemetry/api';
import { type AmqplibInstrumentationConfig, AmqplibInstrumentation } from '@opentelemetry/instrumentation-amqplib';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Amqplib';
diff --git a/packages/node/src/integrations/tracing/connect.ts b/packages/node/src/integrations/tracing/connect.ts
index 80b31f8f1930..c2ab3716bd33 100644
--- a/packages/node/src/integrations/tracing/connect.ts
+++ b/packages/node/src/integrations/tracing/connect.ts
@@ -8,8 +8,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { ensureIsWrapped } from '../../utils/ensureIsWrapped';
+import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
type ConnectApp = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/node/src/integrations/tracing/dataloader.ts b/packages/node/src/integrations/tracing/dataloader.ts
index f1420e74b1f7..4c541d0e9dec 100644
--- a/packages/node/src/integrations/tracing/dataloader.ts
+++ b/packages/node/src/integrations/tracing/dataloader.ts
@@ -6,7 +6,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
-import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';
+import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
const INTEGRATION_NAME = 'Dataloader';
diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts
index e13ce6212b79..52dfc373d470 100644
--- a/packages/node/src/integrations/tracing/express.ts
+++ b/packages/node/src/integrations/tracing/express.ts
@@ -13,10 +13,8 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
spanToJSON,
} from '@sentry/core';
+import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../debug-build';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
-import { ensureIsWrapped } from '../../utils/ensureIsWrapped';
import { ExpressInstrumentationV5 } from './express-v5/instrumentation';
const INTEGRATION_NAME = 'Express';
diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts
index b514cb80d32e..b2202a915a47 100644
--- a/packages/node/src/integrations/tracing/fastify/index.ts
+++ b/packages/node/src/integrations/tracing/fastify/index.ts
@@ -11,8 +11,8 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
+import { generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../../debug-build';
-import { generateInstrumentOnce } from '../../../otel/instrument';
import { FastifyOtelInstrumentation } from './fastify-otel/index';
import type { FastifyInstance, FastifyReply, FastifyRequest } from './types';
import { FastifyInstrumentationV3 } from './v3/instrumentation';
diff --git a/packages/node/src/integrations/tracing/genericPool.ts b/packages/node/src/integrations/tracing/genericPool.ts
index c5f31d385247..811913f31587 100644
--- a/packages/node/src/integrations/tracing/genericPool.ts
+++ b/packages/node/src/integrations/tracing/genericPool.ts
@@ -1,7 +1,7 @@
import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
-import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';
+import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
const INTEGRATION_NAME = 'GenericPool';
diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts
index 5301ad5180b0..db5760329e8d 100644
--- a/packages/node/src/integrations/tracing/graphql.ts
+++ b/packages/node/src/integrations/tracing/graphql.ts
@@ -3,9 +3,8 @@ import { SpanStatusCode } from '@opentelemetry/api';
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemetry';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
interface GraphqlOptions {
/**
diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts
index 8748840b83a3..f06bfeb18478 100644
--- a/packages/node/src/integrations/tracing/hapi/index.ts
+++ b/packages/node/src/integrations/tracing/hapi/index.ts
@@ -12,9 +12,8 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
+import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../../debug-build';
-import { generateInstrumentOnce } from '../../../otel/instrument';
-import { ensureIsWrapped } from '../../../utils/ensureIsWrapped';
import type { Request, RequestEvent, Server } from './types';
const INTEGRATION_NAME = 'Hapi';
diff --git a/packages/node/src/integrations/tracing/kafka.ts b/packages/node/src/integrations/tracing/kafka.ts
index 37d976f5d4e1..94787d92b5b1 100644
--- a/packages/node/src/integrations/tracing/kafka.ts
+++ b/packages/node/src/integrations/tracing/kafka.ts
@@ -1,8 +1,7 @@
import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Kafka';
diff --git a/packages/node/src/integrations/tracing/knex.ts b/packages/node/src/integrations/tracing/knex.ts
index ce3b1e1a5a5f..433e10e7ee70 100644
--- a/packages/node/src/integrations/tracing/knex.ts
+++ b/packages/node/src/integrations/tracing/knex.ts
@@ -1,7 +1,7 @@
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
-import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';
+import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
const INTEGRATION_NAME = 'Knex';
diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts
index 43b901afebee..049cc9064f9a 100644
--- a/packages/node/src/integrations/tracing/koa.ts
+++ b/packages/node/src/integrations/tracing/koa.ts
@@ -11,10 +11,8 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
spanToJSON,
} from '@sentry/core';
+import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../debug-build';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
-import { ensureIsWrapped } from '../../utils/ensureIsWrapped';
interface KoaOptions {
/**
diff --git a/packages/node/src/integrations/tracing/lrumemoizer.ts b/packages/node/src/integrations/tracing/lrumemoizer.ts
index 29c67299c101..134a595495e3 100644
--- a/packages/node/src/integrations/tracing/lrumemoizer.ts
+++ b/packages/node/src/integrations/tracing/lrumemoizer.ts
@@ -1,7 +1,7 @@
import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
+import { generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'LruMemoizer';
diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts
index 7a1072dda187..7cc66c28e22f 100644
--- a/packages/node/src/integrations/tracing/mongo.ts
+++ b/packages/node/src/integrations/tracing/mongo.ts
@@ -1,8 +1,7 @@
import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Mongo';
diff --git a/packages/node/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose.ts
index e4ec855a79de..8e81e43b83d7 100644
--- a/packages/node/src/integrations/tracing/mongoose.ts
+++ b/packages/node/src/integrations/tracing/mongoose.ts
@@ -1,8 +1,7 @@
import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Mongoose';
diff --git a/packages/node/src/integrations/tracing/mysql.ts b/packages/node/src/integrations/tracing/mysql.ts
index 0c2354b3f94a..fbceb4b6cbaf 100644
--- a/packages/node/src/integrations/tracing/mysql.ts
+++ b/packages/node/src/integrations/tracing/mysql.ts
@@ -1,7 +1,7 @@
import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
+import { generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Mysql';
diff --git a/packages/node/src/integrations/tracing/mysql2.ts b/packages/node/src/integrations/tracing/mysql2.ts
index ccf534402a54..437c582f56b1 100644
--- a/packages/node/src/integrations/tracing/mysql2.ts
+++ b/packages/node/src/integrations/tracing/mysql2.ts
@@ -1,8 +1,7 @@
import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Mysql2';
diff --git a/packages/node/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts
index 46c1c850a8b3..d3b3c0cc0edf 100644
--- a/packages/node/src/integrations/tracing/postgres.ts
+++ b/packages/node/src/integrations/tracing/postgres.ts
@@ -1,8 +1,7 @@
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'Postgres';
diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts
index c5efb7f6bef7..45d581edc45f 100644
--- a/packages/node/src/integrations/tracing/postgresjs.ts
+++ b/packages/node/src/integrations/tracing/postgresjs.ts
@@ -26,8 +26,7 @@ import {
SPAN_STATUS_ERROR,
startSpanManual,
} from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
-import { addOriginToSpan } from '../../utils/addOriginToSpan';
+import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core';
const INTEGRATION_NAME = 'PostgresJs';
const SUPPORTED_VERSIONS = ['>=3.0.0 <4'];
diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts
index c674e0bfdfbe..bc845362b6bb 100644
--- a/packages/node/src/integrations/tracing/prisma.ts
+++ b/packages/node/src/integrations/tracing/prisma.ts
@@ -1,7 +1,7 @@
import type { Instrumentation } from '@opentelemetry/instrumentation';
import { PrismaInstrumentation } from '@prisma/instrumentation';
import { consoleSandbox, defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
+import { generateInstrumentOnce } from '@sentry/node-core';
import type { PrismaV5TracingHelper } from './prisma/vendor/v5-tracing-helper';
import type { PrismaV6TracingHelper } from './prisma/vendor/v6-tracing-helper';
diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts
index b6ec8a89ec60..66c95705e457 100644
--- a/packages/node/src/integrations/tracing/redis.ts
+++ b/packages/node/src/integrations/tracing/redis.ts
@@ -13,7 +13,7 @@ import {
spanToJSON,
truncate,
} from '@sentry/core';
-import { generateInstrumentOnce } from '../../otel/instrument';
+import { generateInstrumentOnce } from '@sentry/node-core';
import {
calculateCacheItemSize,
GET_COMMANDS,
diff --git a/packages/node/src/integrations/tracing/tedious.ts b/packages/node/src/integrations/tracing/tedious.ts
index 0585ec01a9c5..d9ae7d186410 100644
--- a/packages/node/src/integrations/tracing/tedious.ts
+++ b/packages/node/src/integrations/tracing/tedious.ts
@@ -1,7 +1,7 @@
import { TediousInstrumentation } from '@opentelemetry/instrumentation-tedious';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core';
-import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';
+import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core';
const TEDIUS_INSTRUMENTED_METHODS = new Set([
'callProcedure',
diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts
index 9ee5fb29f11d..5588384e22c4 100644
--- a/packages/node/src/integrations/tracing/vercelai/index.ts
+++ b/packages/node/src/integrations/tracing/vercelai/index.ts
@@ -1,7 +1,6 @@
import type { Client, IntegrationFn } from '@sentry/core';
import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
-import { generateInstrumentOnce } from '../../../otel/instrument';
-import type { modulesIntegration } from '../../modules';
+import { type modulesIntegration, generateInstrumentOnce } from '@sentry/node-core';
import { INTEGRATION_NAME } from './constants';
import { SentryVercelAiInstrumentation } from './instrumentation';
import type { VercelAiOptions } from './types';
diff --git a/packages/node/src/preload.ts b/packages/node/src/preload.ts
index 0e4af146197f..615ab0c1a008 100644
--- a/packages/node/src/preload.ts
+++ b/packages/node/src/preload.ts
@@ -1,5 +1,5 @@
+import { envToBool } from '@sentry/node-core';
import { preloadOpenTelemetry } from './sdk/initOtel';
-import { envToBool } from './utils/envToBool';
const debug = envToBool(process.env.SENTRY_DEBUG);
const integrationsStr = process.env.SENTRY_PRELOAD_INTEGRATIONS;
diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts
index e693d3976fe4..7afa959b2ce8 100644
--- a/packages/node/src/sdk/index.ts
+++ b/packages/node/src/sdk/index.ts
@@ -1,72 +1,27 @@
import type { Integration, Options } from '@sentry/core';
+import { hasSpansEnabled } from '@sentry/core';
+import type { NodeClient } from '@sentry/node-core';
import {
- consoleIntegration,
- consoleSandbox,
- functionToStringIntegration,
- getCurrentScope,
- getIntegrationsToSetup,
- hasSpansEnabled,
- inboundFiltersIntegration,
- linkedErrorsIntegration,
- logger,
- propagationContextFromHeaders,
- requestDataIntegration,
- stackParserFromStackParserOptions,
-} from '@sentry/core';
-import {
- enhanceDscWithOpenTelemetryRootSpanName,
- openTelemetrySetupCheck,
- setOpenTelemetryContextAsyncContextStrategy,
- setupEventContextTrace,
-} from '@sentry/opentelemetry';
-import { DEBUG_BUILD } from '../debug-build';
-import { childProcessIntegration } from '../integrations/childProcess';
-import { nodeContextIntegration } from '../integrations/context';
-import { contextLinesIntegration } from '../integrations/contextlines';
+ getDefaultIntegrations as getNodeCoreDefaultIntegrations,
+ init as initNodeCore,
+ validateOpenTelemetrySetup,
+} from '@sentry/node-core';
import { httpIntegration } from '../integrations/http';
-import { localVariablesIntegration } from '../integrations/local-variables';
-import { modulesIntegration } from '../integrations/modules';
import { nativeNodeFetchIntegration } from '../integrations/node-fetch';
-import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception';
-import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
-import { processSessionIntegration } from '../integrations/processSession';
-import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
import { getAutoPerformanceIntegrations } from '../integrations/tracing';
-import { makeNodeTransport } from '../transports';
-import type { NodeClientOptions, NodeOptions } from '../types';
-import { isCjs } from '../utils/commonjs';
-import { envToBool } from '../utils/envToBool';
-import { defaultStackParser, getSentryRelease } from './api';
-import { NodeClient } from './client';
-import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel';
+import type { NodeOptions } from '../types';
+import { initOpenTelemetry } from './initOtel';
/**
* Get default integrations, excluding performance.
*/
export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
- return [
- // Common
- // TODO(v10): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration`
- // eslint-disable-next-line deprecation/deprecation
- inboundFiltersIntegration(),
- functionToStringIntegration(),
- linkedErrorsIntegration(),
- requestDataIntegration(),
- // Native Wrappers
- consoleIntegration(),
- httpIntegration(),
- nativeNodeFetchIntegration(),
- // Global Handlers
- onUncaughtExceptionIntegration(),
- onUnhandledRejectionIntegration(),
- // Event Info
- contextLinesIntegration(),
- localVariablesIntegration(),
- nodeContextIntegration(),
- childProcessIntegration(),
- processSessionIntegration(),
- modulesIntegration(),
- ];
+ const nodeCoreIntegrations = getNodeCoreDefaultIntegrations();
+
+ // Filter out the node-core HTTP and NodeFetch integrations and replace them with Node SDK's composite versions
+ return nodeCoreIntegrations
+ .filter(integration => integration.name !== 'Http' && integration.name !== 'NodeFetch')
+ .concat(httpIntegration(), nativeNodeFetchIntegration());
}
/** Get the default integrations for the Node SDK. */
@@ -89,180 +44,32 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi
}
/**
- * Initialize Sentry for Node, without any integrations added by default.
- */
-export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient {
- return _init(options, () => []);
-}
-
-/**
- * Initialize Sentry for Node, without performance instrumentation.
+ * Internal initialization function.
*/
function _init(
- _options: NodeOptions | undefined = {},
+ options: NodeOptions | undefined = {},
getDefaultIntegrationsImpl: (options: Options) => Integration[],
-): NodeClient {
- const options = getClientOptions(_options, getDefaultIntegrationsImpl);
-
- if (options.debug === true) {
- if (DEBUG_BUILD) {
- logger.enable();
- } else {
- // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped
- consoleSandbox(() => {
- // eslint-disable-next-line no-console
- console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.');
- });
- }
- }
-
- if (!isCjs() && options.registerEsmLoaderHooks !== false) {
- maybeInitializeEsmLoader();
- }
-
- setOpenTelemetryContextAsyncContextStrategy();
-
- const scope = getCurrentScope();
- scope.update(options.initialScope);
-
- if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) {
- options.integrations.push(
- spotlightIntegration({
- sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined,
- }),
- );
- }
-
- const client = new NodeClient(options);
- // The client is on the current scope, from where it generally is inherited
- getCurrentScope().setClient(client);
-
- client.init();
-
- logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`);
-
- client.startClientReportTracking();
-
- updateScopeFromEnvVariables();
+): NodeClient | undefined {
+ const client = initNodeCore({
+ ...options,
+ // Only use Node SDK defaults if none provided
+ defaultIntegrations: options.defaultIntegrations ?? getDefaultIntegrationsImpl(options),
+ });
- // If users opt-out of this, they _have_ to set up OpenTelemetry themselves
- // There is no way to use this SDK without OpenTelemetry!
- if (!options.skipOpenTelemetrySetup) {
+ // Add Node SDK specific OpenTelemetry setup
+ if (client && !options.skipOpenTelemetrySetup) {
initOpenTelemetry(client, {
spanProcessors: options.openTelemetrySpanProcessors,
});
validateOpenTelemetrySetup();
}
- enhanceDscWithOpenTelemetryRootSpanName(client);
- setupEventContextTrace(client);
-
return client;
}
/**
- * Validate that your OpenTelemetry setup is correct.
- */
-export function validateOpenTelemetrySetup(): void {
- if (!DEBUG_BUILD) {
- return;
- }
-
- const setup = openTelemetrySetupCheck();
-
- const required: ReturnType = ['SentryContextManager', 'SentryPropagator'];
-
- if (hasSpansEnabled()) {
- required.push('SentrySpanProcessor');
- }
-
- for (const k of required) {
- if (!setup.includes(k)) {
- logger.error(
- `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`,
- );
- }
- }
-
- if (!setup.includes('SentrySampler')) {
- logger.warn(
- 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.',
- );
- }
-}
-
-function getClientOptions(
- options: NodeOptions,
- getDefaultIntegrationsImpl: (options: Options) => Integration[],
-): NodeClientOptions {
- const release = getRelease(options.release);
- const spotlight =
- options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT;
- const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate);
-
- const mergedOptions = {
- ...options,
- dsn: options.dsn ?? process.env.SENTRY_DSN,
- environment: options.environment ?? process.env.SENTRY_ENVIRONMENT,
- sendClientReports: options.sendClientReports ?? true,
- transport: options.transport ?? makeNodeTransport,
- stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
- release,
- tracesSampleRate,
- spotlight,
- debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG),
- };
-
- const integrations = options.integrations;
- const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions);
-
- return {
- ...mergedOptions,
- integrations: getIntegrationsToSetup({
- defaultIntegrations,
- integrations,
- }),
- };
-}
-
-function getRelease(release: NodeOptions['release']): string | undefined {
- if (release !== undefined) {
- return release;
- }
-
- const detectedRelease = getSentryRelease();
- if (detectedRelease !== undefined) {
- return detectedRelease;
- }
-
- return undefined;
-}
-
-function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']): number | undefined {
- if (tracesSampleRate !== undefined) {
- return tracesSampleRate;
- }
-
- const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE;
- if (!sampleRateFromEnv) {
- return undefined;
- }
-
- const parsed = parseFloat(sampleRateFromEnv);
- return isFinite(parsed) ? parsed : undefined;
-}
-
-/**
- * Update scope and propagation context based on environmental variables.
- *
- * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md
- * for more details.
+ * Initialize Sentry for Node, without any integrations added by default.
*/
-function updateScopeFromEnvVariables(): void {
- if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) {
- const sentryTraceEnv = process.env.SENTRY_TRACE;
- const baggageEnv = process.env.SENTRY_BAGGAGE;
- const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv);
- getCurrentScope().setPropagationContext(propagationContext);
- }
+export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient | undefined {
+ return _init(options, () => []);
}
diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts
index 26b9cfa71f72..3c35a1fecc3a 100644
--- a/packages/node/src/sdk/initOtel.ts
+++ b/packages/node/src/sdk/initOtel.ts
@@ -1,4 +1,4 @@
-import { context, diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api';
+import { context, propagation, trace } from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
@@ -8,14 +8,12 @@ import {
SEMRESATTRS_SERVICE_NAMESPACE,
} from '@opentelemetry/semantic-conventions';
import { consoleSandbox, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core';
+import { type NodeClient, isCjs, SentryContextManager, setupOpenTelemetryLogger } from '@sentry/node-core';
import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry';
import { createAddHookMessageChannel } from 'import-in-the-middle';
import moduleModule from 'module';
import { DEBUG_BUILD } from '../debug-build';
import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing';
-import { SentryContextManager } from '../otel/contextManager';
-import { isCjs } from '../utils/commonjs';
-import type { NodeClient } from './client';
// About 277h - this must fit into new Array(len)!
const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000;
@@ -80,7 +78,6 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void {
if (debug) {
logger.enable();
- setupOpenTelemetryLogger();
}
if (!isCjs()) {
@@ -154,19 +151,3 @@ export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefin
return maxSpanWaitDuration;
}
-
-/**
- * Setup the OTEL logger to use our own logger.
- */
-function setupOpenTelemetryLogger(): void {
- const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, {
- get(target, prop, receiver) {
- const actualProp = prop === 'verbose' ? 'debug' : prop;
- return Reflect.get(target, actualProp, receiver);
- },
- });
-
- // Disable diag, to ensure this works even if called multiple times
- diag.disable();
- diag.setLogger(otelLogger, DiagLogLevel.DEBUG);
-}
diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts
index 1a2afabdc1c6..3d3463d0b5cf 100644
--- a/packages/node/src/types.ts
+++ b/packages/node/src/types.ts
@@ -2,7 +2,7 @@ import type { Span as WriteableSpan } from '@opentelemetry/api';
import type { Instrumentation } from '@opentelemetry/instrumentation';
import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core';
-import type { NodeTransportOptions } from './transports';
+import type { NodeTransportOptions } from '@sentry/node-core';
export interface BaseNodeOptions {
/**
diff --git a/packages/node/test/integrations/tracing/graphql.test.ts b/packages/node/test/integrations/tracing/graphql.test.ts
index 10b8783c3702..925aed91b4ac 100644
--- a/packages/node/test/integrations/tracing/graphql.test.ts
+++ b/packages/node/test/integrations/tracing/graphql.test.ts
@@ -1,7 +1,7 @@
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
+import { INSTRUMENTED } from '@sentry/node-core';
import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest';
import { graphqlIntegration, instrumentGraphql } from '../../../src/integrations/tracing/graphql';
-import { INSTRUMENTED } from '../../../src/otel/instrument';
vi.mock('@opentelemetry/instrumentation-graphql');
diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts
index 9ca221dfba03..39fe81caef90 100644
--- a/packages/node/test/integrations/tracing/koa.test.ts
+++ b/packages/node/test/integrations/tracing/koa.test.ts
@@ -1,7 +1,7 @@
import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa';
+import { INSTRUMENTED } from '@sentry/node-core';
import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest';
import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa';
-import { INSTRUMENTED } from '../../../src/otel/instrument';
vi.mock('@opentelemetry/instrumentation-koa');
diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts
index 34f3048713ff..7710a4ad8721 100644
--- a/packages/node/test/integrations/tracing/mongo.test.ts
+++ b/packages/node/test/integrations/tracing/mongo.test.ts
@@ -1,11 +1,11 @@
import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb';
+import { INSTRUMENTED } from '@sentry/node-core';
import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest';
import {
_defaultDbStatementSerializer,
instrumentMongo,
mongoIntegration,
} from '../../../src/integrations/tracing/mongo';
-import { INSTRUMENTED } from '../../../src/otel/instrument';
vi.mock('@opentelemetry/instrumentation-mongodb');
diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts
index 71f0f12e9e30..078f2d1ec44d 100644
--- a/packages/node/test/sdk/init.test.ts
+++ b/packages/node/test/sdk/init.test.ts
@@ -2,10 +2,9 @@ import type { Integration } from '@sentry/core';
import { logger } from '@sentry/core';
import * as SentryOpentelemetry from '@sentry/opentelemetry';
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { getClient } from '../../src/';
+import { getClient, NodeClient, validateOpenTelemetrySetup } from '../../src/';
import * as auto from '../../src/integrations/tracing';
-import { init, validateOpenTelemetrySetup } from '../../src/sdk';
-import { NodeClient } from '../../src/sdk/client';
+import { init } from '../../src/sdk';
import { cleanupOtel } from '../helpers/mockSdkInit';
// eslint-disable-next-line no-var
diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts
new file mode 100644
index 000000000000..28854a320bc0
--- /dev/null
+++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts
@@ -0,0 +1,57 @@
+import { getActiveSpan, getCurrentScope, getRootSpan, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import type { H3Event } from 'h3';
+
+/**
+ * Update the root span (transaction) name for routes with parameters based on the matched route.
+ */
+export function updateRouteBeforeResponse(event: H3Event): void {
+ if (!event.context.matchedRoute) {
+ return;
+ }
+
+ const matchedRoutePath = event.context.matchedRoute.path;
+
+ // If the matched route path is defined and differs from the event's path, it indicates a parametrized route
+ // Example: Matched route is "/users/:id" and the event's path is "/users/123",
+ if (matchedRoutePath && matchedRoutePath !== event._path) {
+ if (matchedRoutePath === '/**') {
+ // todo: support parametrized SSR pageload spans
+ // If page is server-side rendered, the whole path gets transformed to `/**` (Example : `/users/123` becomes `/**` instead of `/users/:id`).
+ return; // Skip if the matched route is a catch-all route.
+ }
+
+ const method = event._method || 'GET';
+
+ const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`;
+ getCurrentScope().setTransactionName(parametrizedTransactionName);
+
+ const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined
+ if (!activeSpan) {
+ return;
+ }
+
+ const rootSpan = getRootSpan(activeSpan);
+ if (!rootSpan) {
+ return;
+ }
+
+ rootSpan.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'http.route': matchedRoutePath,
+ });
+
+ const params = event.context?.params;
+
+ if (params && typeof params === 'object') {
+ Object.entries(params).forEach(([key, value]) => {
+ // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey
+ rootSpan.setAttributes({
+ [`url.path.parameter.${key}`]: String(value),
+ [`params.${key}`]: String(value),
+ });
+ });
+ }
+
+ logger.log(`Updated transaction name for parametrized route: ${parametrizedTransactionName}`);
+ }
+}
diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
index 9d10e9bd86d0..96fa59a4c643 100644
--- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
@@ -6,6 +6,7 @@ import type { H3Event } from 'h3';
import type { NitroApp, NitroAppPlugin } from 'nitropack';
import type { NuxtRenderHTMLContext } from 'nuxt/app';
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
+import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
import { addSentryTracingMetaTags } from '../utils';
interface CfEventType {
@@ -139,6 +140,8 @@ export const sentryCloudflareNitroPlugin =
},
});
+ nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);
+
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => {
const storedTraceData = event?.context?.cf ? traceDataMap.get(event.context.cf) : undefined;
diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts
index baf9f2029051..0f13fbec0fd3 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts
@@ -5,11 +5,14 @@ import { type EventHandler } from 'h3';
import { defineNitroPlugin } from 'nitropack/runtime';
import type { NuxtRenderHTMLContext } from 'nuxt/app';
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
+import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
import { addSentryTracingMetaTags, flushIfServerless } from '../utils';
export default defineNitroPlugin(nitroApp => {
nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler);
+ nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);
+
nitroApp.hooks.hook('error', sentryCaptureErrorHook);
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json
index 94506eefdd9b..e9f22208e5f6 100644
--- a/packages/opentelemetry/package.json
+++ b/packages/opentelemetry/package.json
@@ -45,7 +45,7 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
"@opentelemetry/core": "^1.30.1 || ^2.0.0",
- "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0",
+ "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0",
"@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
"@opentelemetry/semantic-conventions": "^1.34.0"
},
diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts
index 297c780cfce0..11677b796ffc 100644
--- a/packages/react/src/tanstackrouter.ts
+++ b/packages/react/src/tanstackrouter.ts
@@ -119,7 +119,9 @@ function routeMatchToParamSpanAttributes(match: VendoredTanstackRouterRouteMatch
const paramAttributes: Record = {};
Object.entries(match.params).forEach(([key, value]) => {
- paramAttributes[`url.path.params.${key}`] = value;
+ paramAttributes[`url.path.params.${key}`] = value; // todo(v10): remove attribute which does not adhere to Sentry's semantic convention
+ paramAttributes[`url.path.parameter.${key}`] = value;
+ paramAttributes[`params.${key}`] = value; // params.[key] is an alias
});
return paramAttributes;
diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts
index 0aa30b3c93cc..9a785f1d144c 100644
--- a/packages/remix/src/server/index.ts
+++ b/packages/remix/src/server/index.ts
@@ -5,6 +5,7 @@ export {
addEventProcessor,
addIntegration,
amqplibIntegration,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
disableAnrDetectionForCallback,
captureCheckIn,
diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json
index 7f4172b0b42f..9aea12728dd0 100644
--- a/packages/remix/test/integration/package.json
+++ b/packages/remix/test/integration/package.json
@@ -27,6 +27,7 @@
"@sentry/browser": "file:../../../browser",
"@sentry/core": "file:../../../core",
"@sentry/node": "file:../../../node",
+ "@sentry/node-core": "file:../../../node-core",
"@sentry/opentelemetry": "file:../../../opentelemetry",
"@sentry/react": "file:../../../react",
"@sentry-internal/browser-utils": "file:../../../browser-utils",
diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json
index 3051e3049190..4b11651f0d7f 100644
--- a/packages/replay-internal/package.json
+++ b/packages/replay-internal/package.json
@@ -7,6 +7,16 @@
"types": "build/npm/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
+ "./worker-bundler": {
+ "import": {
+ "types": "./build/npm/types/worker-bundler.d.ts",
+ "default": "./build/npm/esm/worker-bundler.js"
+ },
+ "require": {
+ "types": "./build/npm/types/worker-bundler.d.ts",
+ "default": "./build/npm/cjs/worker-bundler.js"
+ }
+ },
".": {
"import": {
"types": "./build/npm/types/index.d.ts",
diff --git a/packages/replay-internal/rollup.npm.config.mjs b/packages/replay-internal/rollup.npm.config.mjs
index 3b4431fa6829..3d6e8fcf4dff 100644
--- a/packages/replay-internal/rollup.npm.config.mjs
+++ b/packages/replay-internal/rollup.npm.config.mjs
@@ -1,3 +1,4 @@
+import nodeResolve from '@rollup/plugin-node-resolve';
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
export default makeNPMConfigVariants(
@@ -16,4 +17,15 @@ export default makeNPMConfigVariants(
},
},
}),
+).concat(
+ ['esm', 'cjs'].map(format => ({
+ input: ['./src/worker-bundler.ts'],
+ output: {
+ file: `./build/npm/${format}/worker-bundler.js`,
+ strict: false,
+ format,
+ },
+ treeshake: false,
+ plugins: [nodeResolve()],
+ })),
);
diff --git a/packages/replay-internal/src/eventBuffer/index.ts b/packages/replay-internal/src/eventBuffer/index.ts
index 3fc34330aead..d1e40903057d 100644
--- a/packages/replay-internal/src/eventBuffer/index.ts
+++ b/packages/replay-internal/src/eventBuffer/index.ts
@@ -1,13 +1,13 @@
import { getWorkerURL } from '@sentry-internal/replay-worker';
import { DEBUG_BUILD } from '../debug-build';
-import type { EventBuffer } from '../types';
+import type { EventBuffer, ReplayWorkerURL } from '../types';
import { logger } from '../util/logger';
import { EventBufferArray } from './EventBufferArray';
import { EventBufferProxy } from './EventBufferProxy';
interface CreateEventBufferParams {
useCompression: boolean;
- workerUrl?: string;
+ workerUrl?: ReplayWorkerURL;
}
// Treeshakable guard to remove the code of the included compression worker
@@ -36,7 +36,7 @@ export function createEventBuffer({
return new EventBufferArray();
}
-function _loadWorker(customWorkerUrl?: string): EventBufferProxy | void {
+function _loadWorker(customWorkerUrl?: ReplayWorkerURL): EventBufferProxy | void {
try {
const workerUrl = customWorkerUrl || _getWorkerUrl();
diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts
index 87fa1182aeeb..6fc607e1c2d8 100644
--- a/packages/replay-internal/src/replay.ts
+++ b/packages/replay-internal/src/replay.ts
@@ -53,6 +53,7 @@ import { isExpired } from './util/isExpired';
import { isSessionExpired } from './util/isSessionExpired';
import { logger } from './util/logger';
import { resetReplayIdOnDynamicSamplingContext } from './util/resetReplayIdOnDynamicSamplingContext';
+import { closestElementOfNode } from './util/rrweb';
import { sendReplay } from './util/sendReplay';
import { RateLimitError } from './util/sendReplayRequest';
import type { SKIPPED } from './util/throttle';
@@ -1304,7 +1305,20 @@ export class ReplayContainer implements ReplayContainerInterface {
}
/** Handler for rrweb.record.onMutation */
- private _onMutationHandler(mutations: unknown[]): boolean {
+ private _onMutationHandler(mutations: MutationRecord[]): boolean {
+ const { ignoreMutations } = this._options._experiments;
+ if (ignoreMutations?.length) {
+ if (
+ mutations.some(mutation => {
+ const el = closestElementOfNode(mutation.target);
+ const selector = ignoreMutations.join(',');
+ return el?.matches(selector);
+ })
+ ) {
+ return false;
+ }
+ }
+
const count = mutations.length;
const mutationLimit = this._options.mutationLimit;
@@ -1336,3 +1350,12 @@ export class ReplayContainer implements ReplayContainerInterface {
return true;
}
}
+
+interface MutationRecord {
+ type: string;
+ target: Node;
+ oldValue: string | null;
+ addedNodes: NodeList;
+ removedNodes: NodeList;
+ attributeName: string | null;
+}
diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts
index c5336dbe5d25..68ee0d749067 100644
--- a/packages/replay-internal/src/types/replay.ts
+++ b/packages/replay-internal/src/types/replay.ts
@@ -90,6 +90,8 @@ export interface ReplayNetworkOptions {
networkResponseHeaders: string[];
}
+export type ReplayWorkerURL = string | URL;
+
export interface ReplayPluginOptions extends ReplayNetworkOptions {
/**
* The sample rate for session-long replays. 1.0 will record all sessions and
@@ -130,7 +132,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
* If defined, use this worker URL instead of the default included one for compression.
* This will only be used if `useCompression` is not false.
*/
- workerUrl?: string;
+ workerUrl?: ReplayWorkerURL;
/**
* Block all media (e.g. images, svg, video) in recordings.
@@ -234,6 +236,13 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
*/
recordCrossOriginIframes: boolean;
autoFlushOnFeedback: boolean;
+ /**
+ * Completetly ignore mutations matching the given selectors.
+ * This can be used if a specific type of mutation is causing (e.g. performance) problems.
+ * NOTE: This can be dangerous to use, as mutations are applied as incremental patches.
+ * Make sure to verify that the captured replays still work when using this option.
+ */
+ ignoreMutations: string[];
}>;
}
diff --git a/packages/replay-internal/src/util/rrweb.ts b/packages/replay-internal/src/util/rrweb.ts
new file mode 100644
index 000000000000..838e75000378
--- /dev/null
+++ b/packages/replay-internal/src/util/rrweb.ts
@@ -0,0 +1,18 @@
+/**
+ * Vendored in from @sentry-internal/rrweb.
+ *
+ * This is a copy of the function from rrweb, it is not nicely exported there.
+ */
+export function closestElementOfNode(node: Node | null): HTMLElement | null {
+ if (!node) {
+ return null;
+ }
+
+ // Catch access to node properties to avoid Firefox "permission denied" errors
+ try {
+ const el: HTMLElement | null = node.nodeType === node.ELEMENT_NODE ? (node as HTMLElement) : node.parentElement;
+ return el;
+ } catch (error) {
+ return null;
+ }
+}
diff --git a/packages/replay-internal/src/worker-bundler.ts b/packages/replay-internal/src/worker-bundler.ts
new file mode 100644
index 000000000000..7a10edac4a89
--- /dev/null
+++ b/packages/replay-internal/src/worker-bundler.ts
@@ -0,0 +1 @@
+import '@sentry-internal/replay-worker/worker-bundler';
diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json
index 5215399743be..084ffb1b9d9a 100644
--- a/packages/replay-worker/package.json
+++ b/packages/replay-worker/package.json
@@ -5,6 +5,23 @@
"main": "build/esm/index.js",
"module": "build/esm/index.js",
"types": "build/types/index.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ "./worker-bundler": {
+ "types": "./build/types/worker-bundler.d.ts",
+ "default": "./build/esm/worker-bundler.js"
+ },
+ ".": {
+ "import": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/esm/index.js"
+ },
+ "require": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/cjs/index.js"
+ }
+ }
+ },
"typesVersions": {
"<5.0": {
"build/types/index.d.ts": [
diff --git a/packages/replay-worker/rollup.worker.config.mjs b/packages/replay-worker/rollup.worker.config.mjs
index fd22c54298dc..556994570335 100644
--- a/packages/replay-worker/rollup.worker.config.mjs
+++ b/packages/replay-worker/rollup.worker.config.mjs
@@ -48,6 +48,24 @@ const config = defineConfig([
},
],
},
+ {
+ input: ['./src/_worker.ts'],
+ output: {
+ file: './build/esm/worker-bundler.js',
+ format: 'esm',
+ },
+ treeshake: 'smallest',
+ plugins: [
+ commonjs(),
+ typescript({ tsconfig: './tsconfig.json', inlineSourceMap: false, sourceMap: false, inlineSources: false }),
+ resolve(),
+ terser({
+ mangle: {
+ module: true,
+ },
+ }),
+ ],
+ },
]);
export default config;
diff --git a/packages/replay-worker/src/worker-bundler.ts b/packages/replay-worker/src/worker-bundler.ts
new file mode 100644
index 000000000000..56ce80e6bf5a
--- /dev/null
+++ b/packages/replay-worker/src/worker-bundler.ts
@@ -0,0 +1,3 @@
+// This is replaced at build-time with the content from _worker.ts
+// This is just a placeholder so that types etc. are correct.
+export {};
diff --git a/packages/replay-worker/tsconfig.types.json b/packages/replay-worker/tsconfig.types.json
index 1a83cadfebcd..99004bd20579 100644
--- a/packages/replay-worker/tsconfig.types.json
+++ b/packages/replay-worker/tsconfig.types.json
@@ -1,6 +1,6 @@
{
"extends": "./tsconfig.json",
- "include": ["src/index.ts", "src/worker.ts"],
+ "include": ["src/index.ts", "src/worker.ts", "src/worker-bundler.ts"],
"compilerOptions": {
"declaration": true,
"declarationMap": true,
diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts
index 9574b38f1e47..17e7eaa9ef50 100644
--- a/packages/solidstart/src/server/index.ts
+++ b/packages/solidstart/src/server/index.ts
@@ -8,7 +8,9 @@ export {
addEventProcessor,
addIntegration,
amqplibIntegration,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
captureCheckIn,
captureConsoleIntegration,
diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md
index 11b593087bba..a21adc43b836 100644
--- a/packages/sveltekit/README.md
+++ b/packages/sveltekit/README.md
@@ -10,326 +10,33 @@
[](https://www.npmjs.com/package/@sentry/sveltekit)
[](https://www.npmjs.com/package/@sentry/sveltekit)
-## Links
-
-- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/sveltekit/)
-
## Compatibility
-The minimum supported version of SvelteKit is `1.0.0`. The SDK works best with Vite 4.2 and newer. Older Vite versions
+The minimum supported version of SvelteKit is `2.0.0`. The SDK works best with Vite 4.2 and newer. Older Vite versions
might not generate source maps correctly.
-The SDK supports the following SvelteKit adapters:
-
-- `@sveltejs/adapter-auto` - for Vercel with the Node runtime. Other deployment targets might work but we don't
- guarantee compatibility.
-- `@sveltejs/adapter-vercel` - only for Node (Lambda) runtimes, not yet Vercel's edge runtime
-- `@sveltejs/adapter-node`
-
-If you use the SDK with other adapters, we cannot guarantee that everything works as expected. You might need to
-[manually configure source maps upload](#-configuring-source-maps-upload). The SDK is currently not compatible with
-none-Node server runtimes, such as Vercel's Edge runtime or Cloudflare workers.
+Check our docs for [SvelteKit adapter](https://docs.sentry.io/platforms/javascript/guides/sveltekit/#prerequisites) compatibility.
## General
This package is a wrapper around `@sentry/node` for the server and `@sentry/svelte` for the client side, with added
functionality related to SvelteKit.
-## Automatic Setup
+## Installation
-We recommend installing the SDK by running the
-[Sentry wizard](https://docs.sentry.io/platforms/javascript/guides/sveltekit/#install) in the root directory of your
-project:
+To get started installing the SDK, use the Sentry Next.js Wizard by running the following command in your terminal or
+read the [Getting Started Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/):
```sh
npx @sentry/wizard@latest -i sveltekit
```
-Take a look at the sections below if you want to customize your SDK configuration.
-
-## Manual Setup
-
-If the setup through the wizard doesn't work for you, you can also set up the SDK manually.
-
-### 1. Prerequesits & Installation
-
-1. Install the Sentry SvelteKit SDK:
-
- ```bash
- # Using npm
- npm install @sentry/sveltekit
-
- # Using yarn
- yarn add @sentry/sveltekit
- ```
-
-### 2. Client-side Setup
-
-The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.dev/docs/hooks) to capture error and
-performance data.
-
-1. If you don't already have a client hooks file, create a new one in `src/hooks.client.(js|ts)`.
-
-2. On the top of your `hooks.client.(js|ts)` file, initialize the Sentry SDK:
-
- ```javascript
- // hooks.client.(js|ts)
- import * as Sentry from '@sentry/sveltekit';
-
- Sentry.init({
- dsn: '__DSN__',
- tracesSampleRate: 1.0,
- // For instance, initialize Session Replay:
- replaysSessionSampleRate: 0.1,
- replaysOnErrorSampleRate: 1.0,
- integrations: [Sentry.replayIntegration()],
- });
- ```
-
-3. Add our `handleErrorWithSentry` function to the `handleError` hook:
-
- ```javascript
- // hooks.client.(js|ts)
- import { handleErrorWithSentry } from '@sentry/sveltekit';
-
- const myErrorHandler = ({ error, event }) => {
- console.error('An error occurred on the client side:', error, event);
- };
-
- export const handleError = handleErrorWithSentry(myErrorHandler);
- // or alternatively, if you don't have a custom error handler:
- // export const handleError = handleErrorWithSentry();
- ```
-
-### 3. Server-side Setup
-
-1. If you don't already have a server hooks file, create a new one in `src/hooks.server.(js|ts)`.
-
-2. On the top of your `hooks.server.(js|ts)` file, initialize the Sentry SDK:
-
- ```javascript
- // hooks.server.(js|ts)
- import * as Sentry from '@sentry/sveltekit';
-
- Sentry.init({
- dsn: '__DSN__',
- tracesSampleRate: 1.0,
- });
- ```
-
-3. Add our `handleErrorWithSentry` function to the `handleError` hook:
-
- ```javascript
- // hooks.server.(js|ts)
- import { handleErrorWithSentry } from '@sentry/sveltekit';
-
- const myErrorHandler = ({ error, event }) => {
- console.error('An error occurred on the server side:', error, event);
- };
-
- export const handleError = handleErrorWithSentry(myErrorHandler);
- // or alternatively, if you don't have a custom error handler:
- // export const handleError = handleErrorWithSentry();
- ```
-
-4. Add our request handler to the `handle` hook in `hooks.server.ts`:
-
- ```javascript
- // hooks.server.(js|ts)
- import { sentryHandle } from '@sentry/sveltekit';
-
- export const handle = sentryHandle();
- // or alternatively, if you already have a handler defined, use the `sequence` function
- // see: https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks-sequence
- // export const handle = sequence(sentryHandle(), yourHandler());
- ```
-
-### 4. Vite Setup
-
-Add `sentrySvelteKit` to your Vite plugins in `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time
-features. Make sure that it is added _before_ the `sveltekit` plugin:
-
-```javascript
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [sentrySvelteKit(), sveltekit()],
- // ... rest of your Vite config
-};
-```
-
-This adds the
-[Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to
-your Vite config to automatically upload source maps to Sentry.
-
----
+The wizard will guide you throuhg logging in to Sentry and setting up the SDK. After the wizard setup is completed, the SDK will automatically capture
+unhandled errors, and optionally, traces and replays.
-## Uploading Source Maps
-
-After completing the [Vite Setup](#5-vite-setup), the SDK will automatically upload source maps to Sentry, when you
-build your project. However, you still need to specify your Sentry auth token as well as your org and project slugs. You
-can either set them as env variables (for example in a `.env` file):
-
-- `SENTRY_ORG` your Sentry org slug
-- `SENTRY_PROJECT` your Sentry project slug
-- `SENTRY_AUTH_TOKEN` your Sentry auth token
-
-Or you can pass them in whatever form you prefer to `sentrySvelteKit`:
-
-```js
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [
- sentrySvelteKit({
- sourceMapsUploadOptions: {
- org: 'my-org-slug',
- project: 'my-project-slug',
- authToken: process.env.SENTRY_AUTH_TOKEN,
- },
- }),
- sveltekit(),
- ],
- // ... rest of your Vite config
-};
-```
-
-### Configuring Source maps upload
-
-Under `sourceMapsUploadOptions`, you can also specify all additional options supported by the
-[Sentry Vite Plugin](https://www.npmjs.com/package/@sentry/vite-plugin). This might be useful if you're using adapters
-other than the Node adapter or have a more customized build setup.
-
-```js
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [
- sentrySvelteKit({
- sourceMapsUploadOptions: {
- org: 'my-org-slug',
- project: 'my-project-slug',
- authToken: process.env.SENTRY_AUTH_TOKEN,
- include: ['dist'],
- cleanArtifacts: true,
- setCommits: {
- auto: true,
- },
- },
- }),
- sveltekit(),
- ],
- // ... rest of your Vite config
-};
-```
-
-### Disabling automatic source map upload
-
-If you don't want to upload source maps automatically, you can disable it as follows:
-
-```js
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [
- sentrySvelteKit({
- autoUploadSourceMaps: false,
- }),
- sveltekit(),
- ],
- // ... rest of your Vite config
-};
-```
-
-## Configure Auto-Instrumentation
-
-The SDK mostly relies on [SvelteKit's hooks](https://kit.svelte.dev/docs/hooks) to collect error and performance data.
-However, SvelteKit doesn't yet offer a hook for universal or server-only `load` function calls. Therefore, the SDK uses
-a Vite plugin to auto-instrument `load` functions so that you don't have to add a Sentry wrapper to each function
-manually. Auto-instrumentation is enabled by default, as soon as you add the `sentrySvelteKit()` function call to your
-`vite.config.(js|ts)`. However, you can customize the behavior, or disable it entirely. In this case, you can still
-manually wrap specific `load` functions with the `withSentry` function.
-
-Note: The SDK will only auto-instrument `load` functions in `+page` or `+layout` files that do not yet contain any
-Sentry code. If you already have custom Sentry code in such files, you'll have to
-[manually](#instrument-load-functions-manually) add our wrapper to your `load` functions.
-
-### Customize Auto-instrumentation
-
-By passing the `autoInstrument` option to `sentrySvelteKit` you can disable auto-instrumentation entirely, or customize
-which `load` functions should be instrumented:
-
-```javascript
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [
- sentrySvelteKit({
- autoInstrument: {
- load: true, // universal load functions
- serverLoad: false, // server-only load functions
- },
- }),
- sveltekit(),
- ],
- // ... rest of your Vite config
-};
-```
-
-### Disable Auto-instrumentation
-
-If you set the `autoInstrument` option to `false`, the SDK won't auto-instrument any `load` function. You can still
-[manually instrument](#instrument-load-functions-manually) specific `load` functions.
-
-```javascript
-// vite.config.(js|ts)
-import { sveltekit } from '@sveltejs/kit/vite';
-import { sentrySvelteKit } from '@sentry/sveltekit';
-
-export default {
- plugins: [
- sentrySvelteKit({
- autoInstrument: false;
- }),
- sveltekit(),
- ],
- // ... rest of your Vite config
-};
-```
-
-### Instrument `load` Functions Manually
-
-If you don't want to use auto-instrumentation, you can also manually instrument specific `load` functions with our load
-function wrappers:
-
-To instrument your universal `load` functions in `+(page|layout).(js|ts)`, wrap our `wrapLoadWithSentry` function around
-your load code:
-
-```javascript
-import { wrapLoadWithSentry } from '@sentry/sveltekit';
-
-export const load = wrapLoadWithSentry(event => {
- //... your load code
-});
-```
-
-To instrument server `load` functions in `+(page|layout).server.(js|ts)`, wrap our `wrapServerLoadWithSentry` function
-around your load code:
-
-```javascript
-import { wrapServerLoadWithSentry } from '@sentry/sveltekit';
+## Links
-export const load = wrapServerLoadWithSentry(event => {
- //... your server load code
-});
-```
+- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/sveltekit/)
+- [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_sveltekit)
+- [Sentry Discord Server](https://discord.gg/Ww9hbqr)
+- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry)
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 07d92d03c8ce..043bad823bb3 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -8,7 +8,9 @@ export {
addEventProcessor,
addIntegration,
amqplibIntegration,
+ // eslint-disable-next-line deprecation/deprecation
anrIntegration,
+ // eslint-disable-next-line deprecation/deprecation
disableAnrDetectionForCallback,
captureCheckIn,
captureConsoleIntegration,
diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts
index 0b588c7919d7..2506fd220207 100644
--- a/packages/vue/src/router.ts
+++ b/packages/vue/src/router.ts
@@ -50,35 +50,19 @@ export function instrumentVueRouter(
},
startNavigationSpanFn: (context: StartSpanOptions) => void,
): void {
- let isFirstPageLoad = true;
+ let hasHandledFirstPageLoad = false;
router.onError(error => captureException(error, { mechanism: { handled: false } }));
- router.beforeEach((to, from, next) => {
- // According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2
- // https://router.vuejs.org/api/#router-start-location
- // https://next.router.vuejs.org/api/#start-location
- // Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0).
- // Therefore, a flag was added to track the page-load: isFirstPageLoad
-
- // from.name:
- // - Vue 2: null
- // - Vue 3: undefined
- // - Nuxt: undefined
- // hence only '==' instead of '===', because `undefined == null` evaluates to `true`
- const isPageLoadNavigation =
- (from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad);
-
- if (isFirstPageLoad) {
- isFirstPageLoad = false;
- }
+ router.beforeEach((to, _from, next) => {
+ // We avoid trying to re-fetch the page load span when we know we already handled it the first time
+ const activePageLoadSpan = !hasHandledFirstPageLoad ? getActivePageLoadSpan() : undefined;
- const attributes: SpanAttributes = {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
- };
+ const attributes: SpanAttributes = {};
for (const key of Object.keys(to.params)) {
- attributes[`params.${key}`] = to.params[key];
+ attributes[`url.path.parameter.${key}`] = to.params[key];
+ attributes[`params.${key}`] = to.params[key]; // params.[key] is an alias
}
for (const key of Object.keys(to.query)) {
const value = to.query[key];
@@ -102,30 +86,33 @@ export function instrumentVueRouter(
getCurrentScope().setTransactionName(spanName);
- if (options.instrumentPageLoad && isPageLoadNavigation) {
- const activeRootSpan = getActiveRootSpan();
- if (activeRootSpan) {
- const existingAttributes = spanToJSON(activeRootSpan).data;
- if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
- activeRootSpan.updateName(spanName);
- activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
- }
- // Set router attributes on the existing pageload transaction
- // This will override the origin, and add params & query attributes
- activeRootSpan.setAttributes({
- ...attributes,
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue',
- });
+ // Update the existing page load span with parametrized route information
+ if (options.instrumentPageLoad && activePageLoadSpan) {
+ const existingAttributes = spanToJSON(activePageLoadSpan).data;
+ if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
+ activePageLoadSpan.updateName(spanName);
+ activePageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
}
+
+ // Set router attributes on the existing pageload transaction
+ // This will override the origin, and add params & query attributes
+ activePageLoadSpan.setAttributes({
+ ...attributes,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue',
+ });
+
+ hasHandledFirstPageLoad = true;
}
- if (options.instrumentNavigation && !isPageLoadNavigation) {
- attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
- attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue';
+ if (options.instrumentNavigation && !activePageLoadSpan) {
startNavigationSpanFn({
name: spanName,
op: 'navigation',
- attributes,
+ attributes: {
+ ...attributes,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource,
+ },
});
}
@@ -138,7 +125,7 @@ export function instrumentVueRouter(
});
}
-function getActiveRootSpan(): Span | undefined {
+function getActivePageLoadSpan(): Span | undefined {
const span = getActiveSpan();
const rootSpan = span && getRootSpan(span);
@@ -148,6 +135,5 @@ function getActiveRootSpan(): Span | undefined {
const op = spanToJSON(rootSpan).op;
- // Only use this root span if it is a pageload or navigation span
- return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
+ return op === 'pageload' ? rootSpan : undefined;
}
diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts
index 681fdf7849e6..55dcff7bc25b 100644
--- a/packages/vue/test/router.test.ts
+++ b/packages/vue/test/router.test.ts
@@ -121,8 +121,8 @@ describe('instrumentVueRouter()', () => {
beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload
beforeEachCallback(to, from, mockNext);
- expect(mockStartSpan).toHaveBeenCalledTimes(1);
- expect(mockStartSpan).toHaveBeenCalledWith({
+ expect(mockStartSpan).toHaveBeenCalledTimes(2);
+ expect(mockStartSpan).toHaveBeenLastCalledWith({
name: transactionName,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
@@ -423,6 +423,7 @@ function getAttributesForRoute(route: Route): SpanAttributes {
const attributes: SpanAttributes = {};
for (const key of Object.keys(params)) {
+ attributes[`url.path.parameter.${key}`] = params[key];
attributes[`params.${key}`] = params[key];
}
for (const key of Object.keys(query)) {
diff --git a/yarn.lock b/yarn.lock
index 7575b784e07f..f461fa573525 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2639,37 +2639,49 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==
-"@cloudflare/kv-asset-handler@0.3.4", "@cloudflare/kv-asset-handler@^0.3.4":
+"@cloudflare/kv-asset-handler@0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz#a8588c6a2e89bb3e87fb449295a901c9f6d3e1bf"
+ integrity sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==
+ dependencies:
+ mime "^3.0.0"
+
+"@cloudflare/kv-asset-handler@^0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz#5cc152847c8ae4d280ec5d7f4f6ba8c976b585c3"
integrity sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==
dependencies:
mime "^3.0.0"
-"@cloudflare/workerd-darwin-64@1.20240718.0":
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240718.0.tgz#46f438fb86ccd4772c29db52fe1d076bc9e6ffb4"
- integrity sha512-BsPZcSCgoGnufog2GIgdPuiKicYTNyO/Dp++HbpLRH+yQdX3x4aWx83M+a0suTl1xv76dO4g9aw7SIB6OSgIyQ==
-
-"@cloudflare/workerd-darwin-arm64@1.20240718.0":
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240718.0.tgz#70e1dca5de4869ef3a9b9e296e934848bca6c74f"
- integrity sha512-nlr4gaOO5gcJerILJQph3+2rnas/nx/lYsuaot1ntHu4LAPBoQo1q/Pucj2cSIav4UiMzTbDmoDwPlls4Kteog==
-
-"@cloudflare/workerd-linux-64@1.20240718.0":
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240718.0.tgz#802c04a1a5729f3881c675be3d158ee06c6b1a36"
- integrity sha512-LJ/k3y47pBcjax0ee4K+6ZRrSsqWlfU4lbU8Dn6u5tSC9yzwI4YFNXDrKWInB0vd7RT3w4Yqq1S6ZEbfRrqVUg==
-
-"@cloudflare/workerd-linux-arm64@1.20240718.0":
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240718.0.tgz#cebff9115d48f8d0c2649fdf86ef46b726d1841f"
- integrity sha512-zBEZvy88EcAMGRGfuVtS00Yl7lJdUM9sH7i651OoL+q0Plv9kphlCC0REQPwzxrEYT1qibSYtWcD9IxQGgx2/g==
-
-"@cloudflare/workerd-windows-64@1.20240718.0":
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240718.0.tgz#940893e62df7f5a8ec895572b834c95c1e256fbd"
- integrity sha512-YpCRvvT47XanFum7C3SedOZKK6BfVhqmwdAAVAQFyc4gsCdegZo0JkUkdloC/jwuWlbCACOG2HTADHOqyeolzQ==
+"@cloudflare/unenv-preset@2.3.3":
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz#d586da084aabbca91be04f4592d18655e932bd11"
+ integrity sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==
+
+"@cloudflare/workerd-darwin-64@1.20250617.0":
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz#ad1fe86b69d9dda28cddfc1ebfe4e0029756b93b"
+ integrity sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==
+
+"@cloudflare/workerd-darwin-arm64@1.20250617.0":
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz#e57d632145183238f92ee8b120e4056ee868c137"
+ integrity sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==
+
+"@cloudflare/workerd-linux-64@1.20250617.0":
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz#edfd4ad69b3726bd11e2b03cf14fa60509137bf2"
+ integrity sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==
+
+"@cloudflare/workerd-linux-arm64@1.20250617.0":
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz#f48bf4265f338c3fd25cc19c1d228517d6008896"
+ integrity sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==
+
+"@cloudflare/workerd-windows-64@1.20250617.0":
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz#6b4397fcf01c7b8a547152761cc3bcd63e173a58"
+ integrity sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==
"@cloudflare/workers-types@4.20250620.0":
version "4.20250620.0"
@@ -2931,18 +2943,12 @@
lodash "^4.17.21"
resolve "^1.20.0"
-"@esbuild-plugins/node-globals-polyfill@^0.2.3":
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz#0e4497a2b53c9e9485e149bc92ddb228438d6bcf"
- integrity sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==
-
-"@esbuild-plugins/node-modules-polyfill@^0.2.2":
- version "0.2.2"
- resolved "https://registry.yarnpkg.com/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz#cefa3dc0bd1c16277a8338b52833420c94987327"
- integrity sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==
+"@emnapi/runtime@^1.2.0":
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d"
+ integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==
dependencies:
- escape-string-regexp "^4.0.0"
- rollup-plugin-node-polyfills "^0.2.1"
+ tslib "^2.4.0"
"@esbuild/aix-ppc64@0.20.0":
version "0.20.0"
@@ -2964,16 +2970,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353"
integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==
+"@esbuild/aix-ppc64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162"
+ integrity sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==
+
"@esbuild/aix-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18"
integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==
-"@esbuild/android-arm64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
- integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
-
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
@@ -3004,6 +3010,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018"
integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==
+"@esbuild/android-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz#d11d4fc299224e729e2190cacadbcc00e7a9fd67"
+ integrity sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==
+
"@esbuild/android-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f"
@@ -3014,11 +3025,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==
-"@esbuild/android-arm@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
- integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
-
"@esbuild/android-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
@@ -3049,16 +3055,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee"
integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==
+"@esbuild/android-arm@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz#5660bd25080553dd2a28438f2a401a29959bd9b1"
+ integrity sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==
+
"@esbuild/android-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26"
integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==
-"@esbuild/android-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
- integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
-
"@esbuild/android-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
@@ -3089,16 +3095,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517"
integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==
+"@esbuild/android-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz#18ddde705bf984e8cd9efec54e199ac18bc7bee1"
+ integrity sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==
+
"@esbuild/android-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff"
integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==
-"@esbuild/darwin-arm64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276"
- integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
-
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
@@ -3129,16 +3135,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16"
integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==
+"@esbuild/darwin-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz#b0b7fb55db8fc6f5de5a0207ae986eb9c4766e67"
+ integrity sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==
+
"@esbuild/darwin-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34"
integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==
-"@esbuild/darwin-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
- integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
-
"@esbuild/darwin-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
@@ -3169,16 +3175,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931"
integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==
+"@esbuild/darwin-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz#e6813fdeba0bba356cb350a4b80543fbe66bf26f"
+ integrity sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==
+
"@esbuild/darwin-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418"
integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==
-"@esbuild/freebsd-arm64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
- integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
-
"@esbuild/freebsd-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
@@ -3209,16 +3215,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc"
integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==
+"@esbuild/freebsd-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz#dc11a73d3ccdc308567b908b43c6698e850759be"
+ integrity sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==
+
"@esbuild/freebsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c"
integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==
-"@esbuild/freebsd-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
- integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
-
"@esbuild/freebsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
@@ -3249,16 +3255,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730"
integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==
+"@esbuild/freebsd-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz#91da08db8bd1bff5f31924c57a81dab26e93a143"
+ integrity sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==
+
"@esbuild/freebsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f"
integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==
-"@esbuild/linux-arm64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
- integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
-
"@esbuild/linux-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
@@ -3289,16 +3295,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383"
integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==
+"@esbuild/linux-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz#efc15e45c945a082708f9a9f73bfa8d4db49728a"
+ integrity sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==
+
"@esbuild/linux-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8"
integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==
-"@esbuild/linux-arm@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
- integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
-
"@esbuild/linux-arm@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
@@ -3329,16 +3335,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771"
integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==
+"@esbuild/linux-arm@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz#9b93c3e54ac49a2ede6f906e705d5d906f6db9e8"
+ integrity sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==
+
"@esbuild/linux-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911"
integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==
-"@esbuild/linux-ia32@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
- integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
-
"@esbuild/linux-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
@@ -3369,6 +3375,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333"
integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==
+"@esbuild/linux-ia32@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz#be8ef2c3e1d99fca2d25c416b297d00360623596"
+ integrity sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==
+
"@esbuild/linux-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783"
@@ -3384,11 +3395,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz#91aef76d332cdc7c8942b600fa2307f3387e6f82"
integrity sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==
-"@esbuild/linux-loong64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
- integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
-
"@esbuild/linux-loong64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
@@ -3419,16 +3425,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac"
integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==
+"@esbuild/linux-loong64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz#b0840a2707c3fc02eec288d3f9defa3827cd7a87"
+ integrity sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==
+
"@esbuild/linux-loong64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506"
integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==
-"@esbuild/linux-mips64el@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
- integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
-
"@esbuild/linux-mips64el@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
@@ -3459,16 +3465,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6"
integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==
+"@esbuild/linux-mips64el@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz#2a198e5a458c9f0e75881a4e63d26ba0cf9df39f"
+ integrity sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==
+
"@esbuild/linux-mips64el@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96"
integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==
-"@esbuild/linux-ppc64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
- integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
-
"@esbuild/linux-ppc64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
@@ -3499,16 +3505,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96"
integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==
+"@esbuild/linux-ppc64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz#64f4ae0b923d7dd72fb860b9b22edb42007cf8f5"
+ integrity sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==
+
"@esbuild/linux-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9"
integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==
-"@esbuild/linux-riscv64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
- integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
-
"@esbuild/linux-riscv64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
@@ -3539,16 +3545,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7"
integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==
+"@esbuild/linux-riscv64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz#fb2844b11fdddd39e29d291c7cf80f99b0d5158d"
+ integrity sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==
+
"@esbuild/linux-riscv64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e"
integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==
-"@esbuild/linux-s390x@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
- integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
-
"@esbuild/linux-s390x@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
@@ -3579,16 +3585,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f"
integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==
+"@esbuild/linux-s390x@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz#1466876e0aa3560c7673e63fdebc8278707bc750"
+ integrity sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==
+
"@esbuild/linux-s390x@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d"
integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==
-"@esbuild/linux-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
- integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
-
"@esbuild/linux-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
@@ -3619,21 +3625,26 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24"
integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==
+"@esbuild/linux-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz#c10fde899455db7cba5f11b3bccfa0e41bf4d0cd"
+ integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==
+
"@esbuild/linux-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4"
integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==
+"@esbuild/netbsd-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz#02e483fbcbe3f18f0b02612a941b77be76c111a4"
+ integrity sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==
+
"@esbuild/netbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d"
integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==
-"@esbuild/netbsd-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
- integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
-
"@esbuild/netbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
@@ -3664,6 +3675,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653"
integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==
+"@esbuild/netbsd-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz#ec401fb0b1ed0ac01d978564c5fc8634ed1dc2ed"
+ integrity sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==
+
"@esbuild/netbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79"
@@ -3674,16 +3690,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7"
integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==
+"@esbuild/openbsd-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz#f272c2f41cfea1d91b93d487a51b5c5ca7a8c8c4"
+ integrity sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==
+
"@esbuild/openbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd"
integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==
-"@esbuild/openbsd-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
- integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
-
"@esbuild/openbsd-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
@@ -3714,16 +3730,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273"
integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==
+"@esbuild/openbsd-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz#2e25950bc10fa9db1e5c868e3d50c44f7c150fd7"
+ integrity sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==
+
"@esbuild/openbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0"
integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==
-"@esbuild/sunos-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
- integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
-
"@esbuild/sunos-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
@@ -3754,16 +3770,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403"
integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==
+"@esbuild/sunos-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz#cd596fa65a67b3b7adc5ecd52d9f5733832e1abd"
+ integrity sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==
+
"@esbuild/sunos-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5"
integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==
-"@esbuild/win32-arm64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
- integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
-
"@esbuild/win32-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
@@ -3794,16 +3810,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2"
integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==
+"@esbuild/win32-arm64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz#b4dbcb57b21eeaf8331e424c3999b89d8951dc88"
+ integrity sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==
+
"@esbuild/win32-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e"
integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==
-"@esbuild/win32-ia32@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
- integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
-
"@esbuild/win32-ia32@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
@@ -3834,16 +3850,16 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac"
integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==
+"@esbuild/win32-ia32@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz#410842e5d66d4ece1757634e297a87635eb82f7a"
+ integrity sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==
+
"@esbuild/win32-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d"
integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==
-"@esbuild/win32-x64@0.17.19":
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
- integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
-
"@esbuild/win32-x64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
@@ -3874,6 +3890,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699"
integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==
+"@esbuild/win32-x64@0.25.4":
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc"
+ integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==
+
"@esbuild/win32-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1"
@@ -4407,6 +4428,119 @@
resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340"
integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==
+"@img/sharp-darwin-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
+ integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
+ optionalDependencies:
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+
+"@img/sharp-darwin-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
+ integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
+ optionalDependencies:
+ "@img/sharp-libvips-darwin-x64" "1.0.4"
+
+"@img/sharp-libvips-darwin-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
+ integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
+
+"@img/sharp-libvips-darwin-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
+ integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==
+
+"@img/sharp-libvips-linux-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
+ integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
+
+"@img/sharp-libvips-linux-arm@1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
+ integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==
+
+"@img/sharp-libvips-linux-s390x@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
+ integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
+
+"@img/sharp-libvips-linux-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
+ integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
+
+"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
+ integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
+
+"@img/sharp-libvips-linuxmusl-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
+ integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
+
+"@img/sharp-linux-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
+ integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-arm64" "1.0.4"
+
+"@img/sharp-linux-arm@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
+ integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-arm" "1.0.5"
+
+"@img/sharp-linux-s390x@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
+ integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-s390x" "1.0.4"
+
+"@img/sharp-linux-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
+ integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-x64" "1.0.4"
+
+"@img/sharp-linuxmusl-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
+ integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
+ optionalDependencies:
+ "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+
+"@img/sharp-linuxmusl-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
+ integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
+ optionalDependencies:
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+
+"@img/sharp-wasm32@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
+ integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
+ dependencies:
+ "@emnapi/runtime" "^1.2.0"
+
+"@img/sharp-win32-ia32@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
+ integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
+
+"@img/sharp-win32-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342"
+ integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==
+
"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
@@ -5858,10 +5992,10 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==
-"@prisma/instrumentation@6.10.1":
- version "6.10.1"
- resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.10.1.tgz#805f0dcdc29638009f0da709828396894815c893"
- integrity sha512-JC8qzgEDuFKjuBsqrZvXHINUb12psnE6Qy3q5p2MBhalC1KW1MBBUwuonx6iS5TCfCdtNslHft8uc2r+EdLWWg==
+"@prisma/instrumentation@6.11.1":
+ version "6.11.1"
+ resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.11.1.tgz#db3c40dbf325cf7a816504b8bc009ca3d4734c2f"
+ integrity sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
@@ -9418,7 +9552,12 @@ acorn-typescript@^1.4.3:
resolved "https://registry.yarnpkg.com/acorn-typescript/-/acorn-typescript-1.4.13.tgz#5f851c8bdda0aa716ffdd5f6ac084df8acc6f5ea"
integrity sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==
-acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
+acorn-walk@8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa"
+ integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==
+
+acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1:
version "8.3.3"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
@@ -9435,16 +9574,16 @@ acorn@8.12.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+acorn@8.14.0, acorn@^8.0.4, acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.1, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.6.0, acorn@^8.7.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
+ version "8.14.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
+ integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
+
acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-acorn@^8.0.4, acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.1, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.6.0, acorn@^8.7.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0:
- version "8.14.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
- integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
-
add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
@@ -10737,7 +10876,7 @@ bl@^6.0.11:
inherits "^2.0.4"
readable-stream "^4.2.0"
-blake3-wasm@^2.1.5:
+blake3-wasm@2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz#b22dbb84bc9419ed0159caa76af4b1b132e6ba52"
integrity sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==
@@ -11694,14 +11833,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001646, can
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz#eb200a716c3e796d33d30b9c8890517a72f862c8"
integrity sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw==
-capnp-ts@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/capnp-ts/-/capnp-ts-0.7.0.tgz#16fd8e76b667d002af8fcf4bf92bf15d1a7b54a9"
- integrity sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==
- dependencies:
- debug "^4.3.1"
- tslib "^2.2.0"
-
capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
@@ -12540,16 +12671,16 @@ cookie@^0.6.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
+cookie@^0.7.1, cookie@~0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
+
cookie@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
-cookie@~0.7.2:
- version "0.7.2"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
- integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
-
copy-anything@^2.0.1:
version "2.0.6"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -12979,11 +13110,6 @@ date-fns@^2.29.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
-date-fns@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
- integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
-
dateformat@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
@@ -14777,34 +14903,6 @@ esbuild@0.15.5:
esbuild-windows-64 "0.15.5"
esbuild-windows-arm64 "0.15.5"
-esbuild@0.17.19:
- version "0.17.19"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955"
- integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
- optionalDependencies:
- "@esbuild/android-arm" "0.17.19"
- "@esbuild/android-arm64" "0.17.19"
- "@esbuild/android-x64" "0.17.19"
- "@esbuild/darwin-arm64" "0.17.19"
- "@esbuild/darwin-x64" "0.17.19"
- "@esbuild/freebsd-arm64" "0.17.19"
- "@esbuild/freebsd-x64" "0.17.19"
- "@esbuild/linux-arm" "0.17.19"
- "@esbuild/linux-arm64" "0.17.19"
- "@esbuild/linux-ia32" "0.17.19"
- "@esbuild/linux-loong64" "0.17.19"
- "@esbuild/linux-mips64el" "0.17.19"
- "@esbuild/linux-ppc64" "0.17.19"
- "@esbuild/linux-riscv64" "0.17.19"
- "@esbuild/linux-s390x" "0.17.19"
- "@esbuild/linux-x64" "0.17.19"
- "@esbuild/netbsd-x64" "0.17.19"
- "@esbuild/openbsd-x64" "0.17.19"
- "@esbuild/sunos-x64" "0.17.19"
- "@esbuild/win32-arm64" "0.17.19"
- "@esbuild/win32-ia32" "0.17.19"
- "@esbuild/win32-x64" "0.17.19"
-
esbuild@0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4"
@@ -14834,6 +14932,37 @@ esbuild@0.20.0:
"@esbuild/win32-ia32" "0.20.0"
"@esbuild/win32-x64" "0.20.0"
+esbuild@0.25.4:
+ version "0.25.4"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.4.tgz#bb9a16334d4ef2c33c7301a924b8b863351a0854"
+ integrity sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.25.4"
+ "@esbuild/android-arm" "0.25.4"
+ "@esbuild/android-arm64" "0.25.4"
+ "@esbuild/android-x64" "0.25.4"
+ "@esbuild/darwin-arm64" "0.25.4"
+ "@esbuild/darwin-x64" "0.25.4"
+ "@esbuild/freebsd-arm64" "0.25.4"
+ "@esbuild/freebsd-x64" "0.25.4"
+ "@esbuild/linux-arm" "0.25.4"
+ "@esbuild/linux-arm64" "0.25.4"
+ "@esbuild/linux-ia32" "0.25.4"
+ "@esbuild/linux-loong64" "0.25.4"
+ "@esbuild/linux-mips64el" "0.25.4"
+ "@esbuild/linux-ppc64" "0.25.4"
+ "@esbuild/linux-riscv64" "0.25.4"
+ "@esbuild/linux-s390x" "0.25.4"
+ "@esbuild/linux-x64" "0.25.4"
+ "@esbuild/netbsd-arm64" "0.25.4"
+ "@esbuild/netbsd-x64" "0.25.4"
+ "@esbuild/openbsd-arm64" "0.25.4"
+ "@esbuild/openbsd-x64" "0.25.4"
+ "@esbuild/sunos-x64" "0.25.4"
+ "@esbuild/win32-arm64" "0.25.4"
+ "@esbuild/win32-ia32" "0.25.4"
+ "@esbuild/win32-x64" "0.25.4"
+
esbuild@^0.15.0:
version "0.15.18"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d"
@@ -15529,7 +15658,7 @@ execa@^8.0.1:
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
-exit-hook@2.2.1, exit-hook@^2.2.1:
+exit-hook@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593"
integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==
@@ -15606,6 +15735,11 @@ express@4.21.1, express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18
utils-merge "1.0.1"
vary "~1.1.2"
+exsolve@^1.0.4:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e"
+ integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==
+
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -16679,7 +16813,7 @@ glob-parent@^6.0.1:
dependencies:
is-glob "^4.0.3"
-glob-to-regexp@^0.4.1:
+glob-to-regexp@0.4.1, glob-to-regexp@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
@@ -20048,7 +20182,7 @@ magic-string@0.30.8:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
-magic-string@^0.25.3, magic-string@^0.25.7:
+magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
@@ -20911,23 +21045,23 @@ mini-css-extract-plugin@2.6.1, mini-css-extract-plugin@^2.5.2:
dependencies:
schema-utils "^4.0.0"
-miniflare@3.20240718.1:
- version "3.20240718.1"
- resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.1.tgz#26ccb95be087cd99cd478dbf2e3a3d40f231bf45"
- integrity sha512-mn3MjGnpgYvarCRTfz4TQyVyY8yW0zz7f8LOAPVai78IGC/lcVcyskZcuIr7Zovb2i+IERmmsJAiEPeZHIIKbA==
+miniflare@4.20250617.4:
+ version "4.20250617.4"
+ resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-4.20250617.4.tgz#2baf7fd3e79a75f05c303d8066b61f8faba11663"
+ integrity sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==
dependencies:
"@cspotcode/source-map-support" "0.8.1"
- acorn "^8.8.0"
- acorn-walk "^8.2.0"
- capnp-ts "^0.7.0"
- exit-hook "^2.2.1"
- glob-to-regexp "^0.4.1"
- stoppable "^1.1.0"
- undici "^5.28.4"
- workerd "1.20240718.0"
- ws "^8.17.1"
- youch "^3.2.2"
- zod "^3.22.3"
+ acorn "8.14.0"
+ acorn-walk "8.3.2"
+ exit-hook "2.2.1"
+ glob-to-regexp "0.4.1"
+ sharp "^0.33.5"
+ stoppable "1.1.0"
+ undici "^5.28.5"
+ workerd "1.20250617.0"
+ ws "8.18.0"
+ youch "3.3.4"
+ zod "3.22.3"
minimalistic-assert@^1.0.0:
version "1.0.1"
@@ -21453,7 +21587,7 @@ named-placeholders@^1.1.3:
dependencies:
lru-cache "^7.14.1"
-nanoid@^3.3.11, nanoid@^3.3.3, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.8:
+nanoid@^3.3.11, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
@@ -22485,6 +22619,11 @@ ohash@^1.1.3, ohash@^1.1.4:
resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72"
integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==
+ohash@^2.0.11:
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b"
+ integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==
+
on-exit-leak-free@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
@@ -23193,6 +23332,11 @@ path-to-regexp@3.3.0:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
+path-to-regexp@6.3.0, path-to-regexp@^6.2.0, path-to-regexp@^6.2.1:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
+ integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
+
path-to-regexp@^1.5.3, path-to-regexp@^1.7.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24"
@@ -23200,11 +23344,6 @@ path-to-regexp@^1.5.3, path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
-path-to-regexp@^6.2.0, path-to-regexp@^6.2.1:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
- integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
-
path-to-regexp@^8.1.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4"
@@ -25432,11 +25571,6 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve.exports@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
- integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==
-
resolve@1.22.1:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
@@ -25629,15 +25763,6 @@ rollup-plugin-dts@^6.0.0:
optionalDependencies:
"@babel/code-frame" "^7.24.2"
-rollup-plugin-inject@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4"
- integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==
- dependencies:
- estree-walker "^0.6.1"
- magic-string "^0.25.3"
- rollup-pluginutils "^2.8.1"
-
rollup-plugin-license@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/rollup-plugin-license/-/rollup-plugin-license-3.3.1.tgz#73b68e33477524198d6f3f9befc905f59bf37c53"
@@ -25653,13 +25778,6 @@ rollup-plugin-license@^3.3.1:
spdx-expression-validate "~2.0.0"
spdx-satisfies "~5.0.1"
-rollup-plugin-node-polyfills@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd"
- integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==
- dependencies:
- rollup-plugin-inject "^3.0.0"
-
rollup-plugin-sourcemaps@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed"
@@ -25678,7 +25796,7 @@ rollup-plugin-visualizer@^5.12.0:
source-map "^0.7.4"
yargs "^17.5.1"
-rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
+rollup-pluginutils@^2.8.2:
version "2.8.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
@@ -26206,6 +26324,35 @@ sharp@^0.32.5:
tar-fs "^3.0.4"
tunnel-agent "^0.6.0"
+sharp@^0.33.5:
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e"
+ integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==
+ dependencies:
+ color "^4.2.3"
+ detect-libc "^2.0.3"
+ semver "^7.6.3"
+ optionalDependencies:
+ "@img/sharp-darwin-arm64" "0.33.5"
+ "@img/sharp-darwin-x64" "0.33.5"
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+ "@img/sharp-libvips-darwin-x64" "1.0.4"
+ "@img/sharp-libvips-linux-arm" "1.0.5"
+ "@img/sharp-libvips-linux-arm64" "1.0.4"
+ "@img/sharp-libvips-linux-s390x" "1.0.4"
+ "@img/sharp-libvips-linux-x64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+ "@img/sharp-linux-arm" "0.33.5"
+ "@img/sharp-linux-arm64" "0.33.5"
+ "@img/sharp-linux-s390x" "0.33.5"
+ "@img/sharp-linux-x64" "0.33.5"
+ "@img/sharp-linuxmusl-arm64" "0.33.5"
+ "@img/sharp-linuxmusl-x64" "0.33.5"
+ "@img/sharp-wasm32" "0.33.5"
+ "@img/sharp-win32-ia32" "0.33.5"
+ "@img/sharp-win32-x64" "0.33.5"
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -26964,7 +27111,7 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
-stoppable@^1.1.0:
+stoppable@1.1.0, stoppable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
@@ -27321,6 +27468,7 @@ stylus@0.59.0, stylus@^0.59.0:
sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills:
version "3.36.0"
+ uid fd682f6129e507c00bb4e6319cc5d6b767e36061
resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061"
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
@@ -28330,10 +28478,10 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
-ufo@^1.1.2, ufo@^1.4.0, ufo@^1.5.3, ufo@^1.5.4:
- version "1.5.4"
- resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
- integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
+ufo@^1.1.2, ufo@^1.4.0, ufo@^1.5.3, ufo@^1.5.4, ufo@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
+ integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
uglify-js@^3.1.4:
version "3.13.3"
@@ -28435,10 +28583,10 @@ undici-types@~6.20.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
-undici@^5.25.4, undici@^5.28.4:
- version "5.28.4"
- resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
- integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
+undici@^5.25.4, undici@^5.28.4, undici@^5.28.5:
+ version "5.29.0"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3"
+ integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==
dependencies:
"@fastify/busboy" "^2.0.0"
@@ -28447,6 +28595,17 @@ undici@^6.11.1, undici@^6.19.2:
resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.1.tgz#336025a14162e6837e44ad7b819b35b6c6af0e05"
integrity sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==
+unenv@2.0.0-rc.17:
+ version "2.0.0-rc.17"
+ resolved "https://registry.yarnpkg.com/unenv/-/unenv-2.0.0-rc.17.tgz#fa9b80d30e16f73e2d4a0be568ca97c0fb76bdac"
+ integrity sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==
+ dependencies:
+ defu "^6.1.4"
+ exsolve "^1.0.4"
+ ohash "^2.0.11"
+ pathe "^2.0.3"
+ ufo "^1.6.1"
+
unenv@^1.10.0, unenv@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.10.0.tgz#c3394a6c6e4cfe68d699f87af456fe3f0db39571"
@@ -28458,18 +28617,6 @@ unenv@^1.10.0, unenv@^1.9.0:
node-fetch-native "^1.6.4"
pathe "^1.1.2"
-"unenv@npm:unenv-nightly@1.10.0-1717606461.a117952":
- version "1.10.0-1717606461.a117952"
- resolved "https://registry.yarnpkg.com/unenv-nightly/-/unenv-nightly-1.10.0-1717606461.a117952.tgz#ff0b97e1e159f84be747271e1d55263b4b3eae7e"
- integrity sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==
- dependencies:
- consola "^3.2.3"
- defu "^6.1.4"
- mime "^3.0.0"
- node-fetch-native "^1.6.4"
- pathe "^1.1.2"
- ufo "^1.5.3"
-
unhead@1.11.6, unhead@^1.11.5:
version "1.11.6"
resolved "https://registry.yarnpkg.com/unhead/-/unhead-1.11.6.tgz#2358cfe4e1d2a6f70d992a0ec57bc7ae5f6354dc"
@@ -29920,16 +30067,16 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
-workerd@1.20240718.0:
- version "1.20240718.0"
- resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20240718.0.tgz#7a397d0a159f7362dc3f7b19472190a858d96f7c"
- integrity sha512-w7lOLRy0XecQTg/ujTLWBiJJuoQvzB3CdQ6/8Wgex3QxFhV9Pbnh3UbwIuUfMw3OCCPQc4o7y+1P+mISAgp6yg==
+workerd@1.20250617.0:
+ version "1.20250617.0"
+ resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20250617.0.tgz#8d7d65d29b49e80eeeb5630b582243e1985e6eab"
+ integrity sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==
optionalDependencies:
- "@cloudflare/workerd-darwin-64" "1.20240718.0"
- "@cloudflare/workerd-darwin-arm64" "1.20240718.0"
- "@cloudflare/workerd-linux-64" "1.20240718.0"
- "@cloudflare/workerd-linux-arm64" "1.20240718.0"
- "@cloudflare/workerd-windows-64" "1.20240718.0"
+ "@cloudflare/workerd-darwin-64" "1.20250617.0"
+ "@cloudflare/workerd-darwin-arm64" "1.20250617.0"
+ "@cloudflare/workerd-linux-64" "1.20250617.0"
+ "@cloudflare/workerd-linux-arm64" "1.20250617.0"
+ "@cloudflare/workerd-windows-64" "1.20250617.0"
workerpool@^3.1.1:
version "3.1.2"
@@ -29945,28 +30092,19 @@ workerpool@^6.0.2, workerpool@^6.1.5, workerpool@^6.4.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544"
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
-wrangler@^3.67.1:
- version "3.67.1"
- resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.67.1.tgz#c9bb344b70c8c2106ad33f03beaa063dd5b49526"
- integrity sha512-lLVJxq/OZMfntvZ79WQJNC1OKfxOCs6PLfogqDBuPFEQ3L/Mwqvd9IZ0bB8ahrwUN/K3lSdDPXynk9HfcGZxVw==
- dependencies:
- "@cloudflare/kv-asset-handler" "0.3.4"
- "@esbuild-plugins/node-globals-polyfill" "^0.2.3"
- "@esbuild-plugins/node-modules-polyfill" "^0.2.2"
- blake3-wasm "^2.1.5"
- chokidar "^3.5.3"
- date-fns "^3.6.0"
- esbuild "0.17.19"
- miniflare "3.20240718.1"
- nanoid "^3.3.3"
- path-to-regexp "^6.2.0"
- resolve "^1.22.8"
- resolve.exports "^2.0.2"
- selfsigned "^2.0.1"
- source-map "^0.6.1"
- unenv "npm:unenv-nightly@1.10.0-1717606461.a117952"
- workerd "1.20240718.0"
- xxhash-wasm "^1.0.1"
+wrangler@4.22.0:
+ version "4.22.0"
+ resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-4.22.0.tgz#75967d81db227ad932d7ab797bafde79d4b5a50c"
+ integrity sha512-m8qVO3YxhUTII+4U889G/f5UuLSvMkUkCNatupV2f/SJ+iqaWtP1QbuQII8bs2J/O4rqxsz46Wu2S50u7tKB5Q==
+ dependencies:
+ "@cloudflare/kv-asset-handler" "0.4.0"
+ "@cloudflare/unenv-preset" "2.3.3"
+ blake3-wasm "2.1.5"
+ esbuild "0.25.4"
+ miniflare "4.20250617.4"
+ path-to-regexp "6.3.0"
+ unenv "2.0.0-rc.17"
+ workerd "1.20250617.0"
optionalDependencies:
fsevents "~2.3.2"
@@ -30059,16 +30197,16 @@ write-pkg@4.0.0:
type-fest "^0.4.1"
write-json-file "^3.2.0"
+ws@8.18.0, ws@^8.13.0, ws@^8.18.0, ws@^8.4.2:
+ version "8.18.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
+ integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
+
ws@^7.3.1:
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
-ws@^8.13.0, ws@^8.17.1, ws@^8.18.0, ws@^8.4.2:
- version "8.18.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
- integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
-
ws@~8.17.1:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
@@ -30102,11 +30240,6 @@ xtend@^4.0.0, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-xxhash-wasm@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz#ecc0f813219b727af4d5f3958ca6becee2f2f1ff"
- integrity sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==
-
xxhashjs@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
@@ -30253,12 +30386,12 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
-youch@^3.2.2:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/youch/-/youch-3.3.3.tgz#50cfdf5bc395ce664a5073e31b712ff4a859d928"
- integrity sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==
+youch@3.3.4:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/youch/-/youch-3.3.4.tgz#f13ee0966846c6200e7fb9ece89306d95df5e489"
+ integrity sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==
dependencies:
- cookie "^0.5.0"
+ cookie "^0.7.1"
mustache "^4.2.0"
stacktracey "^2.1.8"
@@ -30281,7 +30414,12 @@ zod-to-json-schema@^3.24.1:
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
-zod@^3.22.3, zod@^3.22.4, zod@^3.24.1:
+zod@3.22.3:
+ version "3.22.3"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060"
+ integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==
+
+zod@^3.22.4, zod@^3.24.1:
version "3.24.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==