diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8ab3e3d77091..44057f39da11 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -910,6 +910,12 @@ jobs:
- name: Set up Bun
if: matrix.test-application == 'node-exports-test-app'
uses: oven-sh/setup-bun@v2
+ - name: Set up AWS SAM
+ if: matrix.test-application == 'aws-serverless'
+ uses: aws-actions/setup-sam@v2
+ with:
+ use-installer: true
+ token: ${{ secrets.GITHUB_TOKEN }}
- name: Restore caches
uses: ./.github/actions/restore-cache
with:
diff --git a/.size-limit.js b/.size-limit.js
index d53eaae56712..dd65a987d506 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -233,7 +233,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '147 KB',
+ limit: '148 KB',
},
{
name: '@sentry/node - without tracing',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f73317a2392..12cdde3b7bea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 10.5.0
+
+- feat(core): better cause data extraction ([#17375](https://github.com/getsentry/sentry-javascript/pull/17375))
+- feat(deps): Bump @sentry/cli from 2.50.2 to 2.51.1 ([#17382](https://github.com/getsentry/sentry-javascript/pull/17382))
+- feat(deps): Bump @sentry/rollup-plugin and @sentry/vite-plugin from 4.0.2 to 4.1.0 ([#17383](https://github.com/getsentry/sentry-javascript/pull/17383))
+- feat(deps): Bump @sentry/webpack-plugin from 4.0.2 to 4.1.0 ([#17381](https://github.com/getsentry/sentry-javascript/pull/17381))
+- feat(node): Capture `SystemError` context and remove paths from message ([#17331](https://github.com/getsentry/sentry-javascript/pull/17331))
+- fix(nextjs): Inject Next.js version for dev symbolication ([#17379](https://github.com/getsentry/sentry-javascript/pull/17379))
+- fix(mcp-server): Add defensive patches for Transport edge cases ([#17291](https://github.com/getsentry/sentry-javascript/pull/17291))
+
+
+ Internal Changes
+
+- chore(repo): Adjust "Publishing a Release" document to include internal changes section in changelog ([#17374](https://github.com/getsentry/sentry-javascript/pull/17374))
+- test(aws): Run E2E tests with AWS SAM ([#17367](https://github.com/getsentry/sentry-javascript/pull/17367))
+- test(node): Add tests for full http.server span attribute coverage ([#17373](https://github.com/getsentry/sentry-javascript/pull/17373))
+
+
+
+Work in this release was contributed by @ha1fstack. Thank you for your contribution!
+
## 10.4.0
### Important Changes
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
deleted file mode 100644
index 25489cf0a35e..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "aws-lambda-layer-cjs",
- "version": "1.0.0",
- "private": true,
- "type": "commonjs",
- "scripts": {
- "start": "node src/run.js",
- "test": "playwright test",
- "clean": "npx rimraf node_modules pnpm-lock.yaml",
- "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",
- "@playwright/test": "~1.53.2"
- },
- "volta": {
- "extends": "../../package.json"
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js
deleted file mode 100644
index 1d6e059e78f3..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const { handle } = require('./lambda-function');
-const event = {};
-const context = {
- invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- functionName: 'my-lambda',
-};
-handle(event, context);
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js
deleted file mode 100644
index 2605f624ca9a..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const child_process = require('child_process');
-
-child_process.execSync('node ./src/run-lambda.js', {
- stdio: 'inherit',
- env: {
- ...process.env,
- // On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
- LAMBDA_TASK_ROOT: process.cwd(),
- _HANDLER: 'src/lambda-function.handle',
-
- NODE_OPTIONS: '--require @sentry/aws-serverless/dist/awslambda-auto',
- SENTRY_DSN: 'http://public@localhost:3031/1337',
- SENTRY_TRACES_SAMPLE_RATE: '1.0',
- SENTRY_DEBUG: 'true',
- },
- cwd: process.cwd(),
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs
deleted file mode 100644
index fc4ac82aa7c6..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'aws-serverless-lambda-layer-cjs',
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
deleted file mode 100644
index b8f7a4b4d51e..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as child_process from 'child_process';
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-
-test('Lambda layer SDK bundle sends events', async ({ request }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-layer-cjs', transactionEvent => {
- return transactionEvent?.transaction === 'my-lambda';
- });
-
- // Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
- // Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc)
- // which are usually enough for us to never have noticed this race condition before.
- // This is a workaround but probably sufficient as long as we only experience it in this test.
- await new Promise(resolve =>
- setTimeout(() => {
- resolve();
- }, 1000),
- );
-
- child_process.execSync('pnpm start', {
- stdio: 'inherit',
- });
-
- const transactionEvent = await transactionEventPromise;
-
- // shows the SDK sent a transaction
- expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
- expect(transactionEvent.contexts?.trace).toEqual({
- data: {
- 'sentry.sample_rate': 1,
- 'sentry.source': 'custom',
- 'sentry.origin': 'auto.otel.aws-lambda',
- 'sentry.op': 'function.aws.lambda',
- 'cloud.account.id': '123453789012',
- 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- 'faas.coldstart': true,
- 'otel.kind': 'SERVER',
- },
- op: 'function.aws.lambda',
- origin: 'auto.otel.aws-lambda',
- span_id: expect.stringMatching(/[a-f0-9]{16}/),
- status: 'ok',
- trace_id: expect.stringMatching(/[a-f0-9]{32}/),
- });
-
- expect(transactionEvent.spans).toHaveLength(2);
-
- // shows that the Otel Http instrumentation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'http.client',
- 'sentry.origin': 'auto.http.otel.http',
- url: 'http://example.com/',
- }),
- description: 'GET http://example.com/',
- op: 'http.client',
- }),
- );
-
- // shows that the manual span creation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'test',
- 'sentry.origin': 'manual',
- }),
- description: 'manual-span',
- op: 'test',
- }),
- );
-
- // shows that the SDK source is correctly detected
- expect(transactionEvent.sdk?.packages).toContainEqual(
- expect.objectContaining({ name: 'aws-lambda-layer:@sentry/aws-serverless' }),
- );
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/.npmrc b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@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/aws-lambda-layer-esm/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/package.json
deleted file mode 100644
index 7a25061dde1c..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "aws-lambda-layer-esm",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "start": "node src/run.mjs",
- "test": "playwright test",
- "clean": "npx rimraf node_modules pnpm-lock.yaml",
- "test:build": "pnpm install",
- "test:assert": "pnpm test"
- },
- "//": "Link from local Lambda layer build",
- "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",
- "@playwright/test": "~1.53.2"
- },
- "volta": {
- "extends": "../../package.json"
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/playwright.config.ts
deleted file mode 100644
index 174593c307df..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/playwright.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-
-export default getPlaywrightConfig();
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run-lambda.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run-lambda.mjs
deleted file mode 100644
index c30903f9883d..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run-lambda.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-import { handle } from './lambda-function.mjs';
-
-const event = {};
-const context = {
- invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- functionName: 'my-lambda',
-};
-await handle(event, context);
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run.mjs
deleted file mode 100644
index 4bcd5886a865..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/run.mjs
+++ /dev/null
@@ -1,17 +0,0 @@
-import child_process from 'node:child_process';
-
-child_process.execSync('node ./src/run-lambda.mjs', {
- stdio: 'inherit',
- env: {
- ...process.env,
- // On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
- LAMBDA_TASK_ROOT: process.cwd(),
- _HANDLER: 'src/lambda-function.handle',
-
- NODE_OPTIONS: '--import @sentry/aws-serverless/awslambda-auto',
- SENTRY_DSN: 'http://public@localhost:3031/1337',
- SENTRY_TRACES_SAMPLE_RATE: '1.0',
- SENTRY_DEBUG: 'true',
- },
- cwd: process.cwd(),
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/start-event-proxy.mjs
deleted file mode 100644
index 03fc10269998..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/start-event-proxy.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'aws-lambda-layer-esm',
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/tests/basic.test.ts
deleted file mode 100644
index 14ae8f9b81b0..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/tests/basic.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as child_process from 'child_process';
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-
-test('Lambda layer SDK bundle sends events', async ({ request }) => {
- const transactionEventPromise = waitForTransaction('aws-lambda-layer-esm', transactionEvent => {
- return transactionEvent?.transaction === 'my-lambda';
- });
-
- // Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
- // Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc)
- // which are usually enough for us to never have noticed this race condition before.
- // This is a workaround but probably sufficient as long as we only experience it in this test.
- await new Promise(resolve =>
- setTimeout(() => {
- resolve();
- }, 1000),
- );
-
- child_process.execSync('pnpm start', {
- stdio: 'inherit',
- });
-
- const transactionEvent = await transactionEventPromise;
-
- // shows the SDK sent a transaction
- expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
- expect(transactionEvent.contexts?.trace).toEqual({
- data: {
- 'sentry.sample_rate': 1,
- 'sentry.source': 'custom',
- 'sentry.origin': 'auto.otel.aws-lambda',
- 'sentry.op': 'function.aws.lambda',
- 'cloud.account.id': '123453789012',
- 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- 'faas.coldstart': true,
- 'otel.kind': 'SERVER',
- },
- op: 'function.aws.lambda',
- origin: 'auto.otel.aws-lambda',
- span_id: expect.stringMatching(/[a-f0-9]{16}/),
- status: 'ok',
- trace_id: expect.stringMatching(/[a-f0-9]{32}/),
- });
-
- expect(transactionEvent.spans).toHaveLength(2);
-
- // shows that the Otel Http instrumentation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'http.client',
- 'sentry.origin': 'auto.http.otel.http',
- url: 'http://example.com/',
- }),
- description: 'GET http://example.com/',
- op: 'http.client',
- }),
- );
-
- // shows that the manual span creation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'test',
- 'sentry.origin': 'manual',
- }),
- description: 'manual-span',
- op: 'test',
- }),
- );
-
- // shows that the SDK source is correctly detected
- expect(transactionEvent.sdk?.packages).toContainEqual(
- expect.objectContaining({ name: 'aws-lambda-layer:@sentry/aws-serverless' }),
- );
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/.npmrc b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@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/aws-serverless-esm/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json
deleted file mode 100644
index c9dc4c959d09..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "node-express-app",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "start": "node src/run.mjs",
- "test": "playwright test",
- "clean": "npx rimraf node_modules pnpm-lock.yaml",
- "test:build": "pnpm install",
- "test:assert": "pnpm test"
- },
- "dependencies": {
- "@sentry/aws-serverless": "* || latest"
- },
- "devDependencies": {
- "@sentry-internal/test-utils": "link:../../../test-utils",
- "@playwright/test": "~1.53.2"
- },
- "volta": {
- "extends": "../../package.json"
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/playwright.config.ts
deleted file mode 100644
index 174593c307df..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/playwright.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-
-export default getPlaywrightConfig();
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/package.json
deleted file mode 100644
index 43afe1b9fe77..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "//": "This is a mock package.json file which is usually created by AWS when deploying the lambda. OTEL instrumentation tries to read this file to get the lambda version",
- "name": "lambda",
- "version": "1.0.0"
-}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run-lambda.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run-lambda.mjs
deleted file mode 100644
index 8356a5ef9bff..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run-lambda.mjs
+++ /dev/null
@@ -1,10 +0,0 @@
-import { handler } from './lambda-function.mjs';
-
-// Simulate minimal event and context objects being passed to the handler by the AWS runtime
-const event = {};
-const context = {
- invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- functionName: 'my-lambda',
-};
-
-await handler(event, context);
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run.mjs
deleted file mode 100644
index 2f67c14a54f7..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/run.mjs
+++ /dev/null
@@ -1,16 +0,0 @@
-import child_process from 'child_process';
-
-child_process.execSync('node ./src/run-lambda.mjs', {
- stdio: 'inherit',
- env: {
- ...process.env,
- // On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
- LAMBDA_TASK_ROOT: process.cwd(),
- _HANDLER: 'src/lambda-function.handler',
-
- NODE_OPTIONS: '--import @sentry/aws-serverless/awslambda-auto',
- SENTRY_DSN: 'http://public@localhost:3031/1337',
- SENTRY_TRACES_SAMPLE_RATE: '1.0',
- },
- cwd: process.cwd(),
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts
deleted file mode 100644
index 38c6e82043cf..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import * as child_process from 'child_process';
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-
-test('AWS Serverless SDK sends events in ESM mode', async ({ request }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-esm', transactionEvent => {
- return transactionEvent?.transaction === 'my-lambda';
- });
-
- // Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
- // Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc)
- // which are usually enough for us to never have noticed this race condition before.
- // This is a workaround but probably sufficient as long as we only experience it in this test.
- await new Promise(resolve =>
- setTimeout(() => {
- resolve();
- }, 1000),
- );
-
- child_process.execSync('pnpm start', {
- stdio: 'inherit',
- });
-
- const transactionEvent = await transactionEventPromise;
-
- // shows the SDK sent a transaction
- expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
- expect(transactionEvent.contexts?.trace).toEqual({
- data: {
- 'sentry.sample_rate': 1,
- 'sentry.source': 'custom',
- 'sentry.origin': 'auto.otel.aws-lambda',
- 'sentry.op': 'function.aws.lambda',
- 'cloud.account.id': '123453789012',
- 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
- 'faas.coldstart': true,
- 'otel.kind': 'SERVER',
- },
- op: 'function.aws.lambda',
- origin: 'auto.otel.aws-lambda',
- span_id: expect.stringMatching(/[a-f0-9]{16}/),
- status: 'ok',
- trace_id: expect.stringMatching(/[a-f0-9]{32}/),
- });
-
- expect(transactionEvent.spans).toHaveLength(2);
-
- // shows that the Otel Http instrumentation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'http.client',
- 'sentry.origin': 'auto.http.otel.http',
- url: 'http://example.com/',
- }),
- description: 'GET http://example.com/',
- op: 'http.client',
- }),
- );
-
- // shows that the manual span creation is working
- expect(transactionEvent.spans).toContainEqual(
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.op': 'manual',
- 'sentry.origin': 'manual',
- }),
- description: 'manual-span',
- op: 'manual',
- }),
- );
-
- // shows that the SDK source is correctly detected
- expect(transactionEvent.sdk?.packages).toContainEqual(
- expect.objectContaining({ name: 'npm:@sentry/aws-serverless' }),
- );
-});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/.npmrc b/dev-packages/e2e-tests/test-applications/aws-serverless/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/.npmrc
rename to dev-packages/e2e-tests/test-applications/aws-serverless/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json
new file mode 100644
index 000000000000..83437b2f9fbf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "aws-lambda-sam",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && npx rimraf node_modules/@sentry/aws-serverless/nodejs",
+ "test:assert": "pnpm test"
+ },
+ "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package",
+ "devDependencies": {
+ "@aws-sdk/client-lambda": "^3.863.0",
+ "@playwright/test": "~1.53.2",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/",
+ "@types/tmp": "^0.2.6",
+ "aws-cdk-lib": "^2.210.0",
+ "constructs": "^10.4.2",
+ "glob": "^11.0.3",
+ "tmp": "^0.2.5"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/playwright.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/playwright.config.ts
rename to dev-packages/e2e-tests/test-applications/aws-serverless/playwright.config.ts
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js
new file mode 100644
index 000000000000..06a9c37a610c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js
@@ -0,0 +1,3 @@
+exports.handler = async (event, context) => {
+ throw new Error('test');
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js
similarity index 87%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js
rename to dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js
index c688ed35a0c4..a614387ddccd 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js
@@ -1,8 +1,7 @@
const Sentry = require('@sentry/aws-serverless');
-
const http = require('http');
-async function handle() {
+exports.handler = async (event, context) => {
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
await new Promise(resolve => {
http.get('http://example.com', res => {
@@ -16,6 +15,4 @@ async function handle() {
});
});
});
-}
-
-module.exports = { handle };
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/lambda-function.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
similarity index 87%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/lambda-function.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
index a9cdd48c1197..b13f30397b62 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-esm/src/lambda-function.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/aws-serverless';
import * as http from 'node:http';
-async function handle() {
+export const handler = Sentry.wrapHandler(async () => {
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
await new Promise(resolve => {
http.get('http://example.com', res => {
@@ -16,6 +16,4 @@ async function handle() {
});
});
});
-}
-
-export { handle };
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js
new file mode 100644
index 000000000000..534909d6764e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js
@@ -0,0 +1,24 @@
+const http = require('http');
+const Sentry = require('@sentry/aws-serverless');
+
+exports.handler = Sentry.wrapHandler(async () => {
+ await new Promise(resolve => {
+ const req = http.request(
+ {
+ host: 'example.com',
+ },
+ res => {
+ res.on('data', d => {
+ process.stdout.write(d);
+ });
+
+ res.on('end', () => {
+ resolve();
+ });
+ },
+ );
+ req.end();
+ });
+
+ Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/lambda-function.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
similarity index 86%
rename from dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/lambda-function.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
index 4d248c4432c7..346613025497 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/src/lambda-function.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
@@ -1,7 +1,7 @@
import * as http from 'node:http';
import * as Sentry from '@sentry/aws-serverless';
-const handler = Sentry.wrapHandler(async () => {
+export const handler = Sentry.wrapHandler(async () => {
await new Promise(resolve => {
const req = http.request(
{
@@ -22,5 +22,3 @@ const handler = Sentry.wrapHandler(async () => {
Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
});
-
-export { handler };
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts
new file mode 100644
index 000000000000..825c9648ee66
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts
@@ -0,0 +1,134 @@
+import { Stack, CfnResource, StackProps } from 'aws-cdk-lib';
+import { Construct } from 'constructs';
+import * as path from 'node:path';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as dns from 'node:dns/promises';
+import { platform } from 'node:process';
+import { globSync } from 'glob';
+import { execFileSync } from 'node:child_process';
+
+const LAMBDA_FUNCTIONS_WITH_LAYER_DIR = './src/lambda-functions-layer';
+const LAMBDA_FUNCTIONS_WITH_NPM_DIR = './src/lambda-functions-npm';
+const LAMBDA_FUNCTION_TIMEOUT = 10;
+const LAYER_DIR = './node_modules/@sentry/aws-serverless/';
+export const SAM_PORT = 3001;
+const NODE_RUNTIME = `nodejs${process.version.split('.').at(0)?.replace('v', '')}.x`;
+
+export class LocalLambdaStack extends Stack {
+ sentryLayer: CfnResource;
+
+ constructor(scope: Construct, id: string, props: StackProps, hostIp: string) {
+ console.log('[LocalLambdaStack] Creating local SAM Lambda Stack');
+ super(scope, id, props);
+
+ this.templateOptions.templateFormatVersion = '2010-09-09';
+ this.templateOptions.transforms = ['AWS::Serverless-2016-10-31'];
+
+ console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack');
+
+ const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR });
+
+ if (!layerZipFile) {
+ throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`);
+ }
+
+ this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', {
+ type: 'AWS::Serverless::LayerVersion',
+ properties: {
+ ContentUri: path.join(LAYER_DIR, layerZipFile),
+ CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'],
+ },
+ });
+
+ const dsn = `http://public@${hostIp}:3031/1337`;
+ console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`);
+
+ this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_LAYER_DIR, dsn, addLayer: true });
+ this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_NPM_DIR, dsn, addLayer: false });
+ }
+
+ private addLambdaFunctions({
+ functionsDir,
+ dsn,
+ addLayer,
+ }: {
+ functionsDir: string;
+ dsn: string;
+ addLayer: boolean;
+ }) {
+ console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${functionsDir} to the SAM stack`);
+
+ const lambdaDirs = fs
+ .readdirSync(functionsDir)
+ .filter(dir => fs.statSync(path.join(functionsDir, dir)).isDirectory());
+
+ for (const lambdaDir of lambdaDirs) {
+ const functionName = `${addLayer ? 'Layer' : 'Npm'}${lambdaDir}`;
+
+ if (!addLayer) {
+ console.log(`[LocalLambdaStack] Install dependencies for ${functionName}`);
+ const packageJson = { dependencies: { '@sentry/aws-serverless': '* || latest' } };
+ fs.writeFileSync(path.join(functionsDir, lambdaDir, 'package.json'), JSON.stringify(packageJson, null, 2));
+ execFileSync('npm', ['install', '--prefix', path.join(functionsDir, lambdaDir)], { stdio: 'inherit' });
+ }
+
+ const isEsm = fs.existsSync(path.join(functionsDir, lambdaDir, 'index.mjs'));
+
+ new CfnResource(this, functionName, {
+ type: 'AWS::Serverless::Function',
+ properties: {
+ CodeUri: path.join(functionsDir, lambdaDir),
+ Handler: 'index.handler',
+ Runtime: NODE_RUNTIME,
+ Timeout: LAMBDA_FUNCTION_TIMEOUT,
+ Layers: addLayer ? [{ Ref: this.sentryLayer.logicalId }] : undefined,
+ Environment: {
+ Variables: {
+ SENTRY_DSN: dsn,
+ SENTRY_TRACES_SAMPLE_RATE: 1.0,
+ SENTRY_DEBUG: true,
+ NODE_OPTIONS: `--${isEsm ? 'import' : 'require'}=@sentry/aws-serverless/awslambda-auto`,
+ },
+ },
+ },
+ });
+
+ console.log(`[LocalLambdaStack] Added Lambda function: ${functionName}`);
+ }
+ }
+
+ static async waitForStack(timeout = 60000, port = SAM_PORT) {
+ const startTime = Date.now();
+ const maxWaitTime = timeout;
+
+ while (Date.now() - startTime < maxWaitTime) {
+ try {
+ const response = await fetch(`http://127.0.0.1:${port}/`);
+
+ if (response.ok || response.status === 404) {
+ console.log(`[LocalLambdaStack] SAM stack is ready`);
+ return;
+ }
+ } catch {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ }
+
+ throw new Error(`[LocalLambdaStack] Failed to start SAM stack after ${timeout}ms`);
+ }
+}
+
+export async function getHostIp() {
+ if (process.env.GITHUB_ACTIONS) {
+ const host = await dns.lookup(os.hostname());
+ return host.address;
+ }
+
+ if (platform === 'darwin' || platform === 'win32') {
+ return 'host.docker.internal';
+ }
+
+ const host = await dns.lookup(os.hostname());
+ return host.address;
+}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
similarity index 70%
rename from dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
index 86605fcb7b9a..196ae2471c69 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'aws-serverless-esm',
+ proxyServerName: 'aws-serverless-lambda-sam',
});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts
new file mode 100644
index 000000000000..707f808218fb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts
@@ -0,0 +1,83 @@
+import { test as base, expect } from '@playwright/test';
+import { App } from 'aws-cdk-lib';
+import * as tmp from 'tmp';
+import { LocalLambdaStack, SAM_PORT, getHostIp } from '../src/stack';
+import { writeFileSync } from 'node:fs';
+import { spawn, execSync } from 'node:child_process';
+import { LambdaClient } from '@aws-sdk/client-lambda';
+
+const DOCKER_NETWORK_NAME = 'lambda-test-network';
+const SAM_TEMPLATE_FILE = 'sam.template.yml';
+
+export { expect };
+
+export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClient: LambdaClient }>({
+ testEnvironment: [
+ async ({}, use) => {
+ console.log('[testEnvironment fixture] Setting up AWS Lambda test infrastructure');
+
+ execSync('docker network prune -f');
+ execSync(`docker network create --driver bridge ${DOCKER_NETWORK_NAME}`);
+
+ const hostIp = await getHostIp();
+ const app = new App();
+
+ const stack = new LocalLambdaStack(app, 'LocalLambdaStack', {}, hostIp);
+ const template = app.synth().getStackByName('LocalLambdaStack').template;
+ writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2));
+
+ const debugLog = tmp.fileSync({ prefix: 'sentry_aws_lambda_tests_sam_debug', postfix: '.log' });
+ console.log(`[test_environment fixture] Writing SAM debug log to: ${debugLog.name}`);
+
+ const process = spawn(
+ 'sam',
+ [
+ 'local',
+ 'start-lambda',
+ '--debug',
+ '--template',
+ SAM_TEMPLATE_FILE,
+ '--warm-containers',
+ 'EAGER',
+ '--docker-network',
+ DOCKER_NETWORK_NAME,
+ ],
+ {
+ stdio: ['ignore', debugLog.fd, debugLog.fd],
+ },
+ );
+
+ try {
+ await LocalLambdaStack.waitForStack();
+
+ await use(stack);
+ } finally {
+ console.log('[testEnvironment fixture] Tearing down AWS Lambda test infrastructure');
+
+ process.kill('SIGTERM');
+ await new Promise(resolve => {
+ process.once('exit', resolve);
+ setTimeout(() => {
+ if (!process.killed) {
+ process.kill('SIGKILL');
+ }
+ resolve(void 0);
+ }, 5000);
+ });
+ }
+ },
+ { scope: 'worker', auto: true },
+ ],
+ lambdaClient: async ({}, use) => {
+ const lambdaClient = new LambdaClient({
+ endpoint: `http://127.0.0.1:${SAM_PORT}`,
+ region: 'us-east-1',
+ credentials: {
+ accessKeyId: 'dummy',
+ secretAccessKey: 'dummy',
+ },
+ });
+
+ await use(lambdaClient);
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
new file mode 100644
index 000000000000..79ad0fa31070
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
@@ -0,0 +1,169 @@
+import { waitForTransaction, waitForError } from '@sentry-internal/test-utils';
+import { InvokeCommand } from '@aws-sdk/client-lambda';
+import { test, expect } from './lambda-fixtures';
+
+test.describe('Lambda layer', () => {
+ test('tracing in CJS works', async ({ lambdaClient }) => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ return transactionEvent?.transaction === 'LayerTracingCjs';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'LayerTracingCjs',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const transactionEvent = await transactionEventPromise;
+
+ // shows the SDK sent a transaction
+ expect(transactionEvent.transaction).toEqual('LayerTracingCjs');
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'sentry.origin': 'auto.otel.aws-lambda',
+ 'sentry.op': 'function.aws.lambda',
+ 'cloud.account.id': '012345678912',
+ 'faas.execution': expect.any(String),
+ 'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:LayerTracingCjs',
+ 'faas.coldstart': true,
+ 'otel.kind': 'SERVER',
+ },
+ op: 'function.aws.lambda',
+ origin: 'auto.otel.aws-lambda',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.spans).toHaveLength(2);
+
+ // shows that the Otel Http instrumentation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ }),
+ description: 'GET http://example.com/',
+ op: 'http.client',
+ }),
+ );
+
+ // shows that the manual span creation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'test',
+ 'sentry.origin': 'manual',
+ }),
+ description: 'manual-span',
+ op: 'test',
+ }),
+ );
+
+ // shows that the SDK source is correctly detected
+ expect(transactionEvent.sdk?.packages).toContainEqual(
+ expect.objectContaining({ name: 'aws-lambda-layer:@sentry/aws-serverless' }),
+ );
+ });
+
+ test('tracing in ESM works', async ({ lambdaClient }) => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ return transactionEvent?.transaction === 'LayerTracingEsm';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'LayerTracingEsm',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const transactionEvent = await transactionEventPromise;
+
+ // shows the SDK sent a transaction
+ expect(transactionEvent.transaction).toEqual('LayerTracingEsm'); // name should be the function name
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'sentry.origin': 'auto.otel.aws-lambda',
+ 'sentry.op': 'function.aws.lambda',
+ 'cloud.account.id': '012345678912',
+ 'faas.execution': expect.any(String),
+ 'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:LayerTracingEsm',
+ 'faas.coldstart': true,
+ 'otel.kind': 'SERVER',
+ },
+ op: 'function.aws.lambda',
+ origin: 'auto.otel.aws-lambda',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.spans).toHaveLength(2);
+
+ // shows that the Otel Http instrumentation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ }),
+ description: 'GET http://example.com/',
+ op: 'http.client',
+ }),
+ );
+
+ // shows that the manual span creation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'test',
+ 'sentry.origin': 'manual',
+ }),
+ description: 'manual-span',
+ op: 'test',
+ }),
+ );
+
+ // shows that the SDK source is correctly detected
+ expect(transactionEvent.sdk?.packages).toContainEqual(
+ expect.objectContaining({ name: 'aws-lambda-layer:@sentry/aws-serverless' }),
+ );
+ });
+
+ test('capturing errors works', async ({ lambdaClient }) => {
+ const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'test';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'LayerError',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const errorEvent = await errorEventPromise;
+
+ // shows the SDK sent an error event
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]).toEqual(
+ expect.objectContaining({
+ type: 'Error',
+ value: 'test',
+ mechanism: {
+ type: 'auto.function.aws-serverless.handler',
+ handled: false,
+ },
+ }),
+ );
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts
new file mode 100644
index 000000000000..9b4183425c95
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts
@@ -0,0 +1,141 @@
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { InvokeCommand } from '@aws-sdk/client-lambda';
+import { test, expect } from './lambda-fixtures';
+
+test.describe('NPM package', () => {
+ test('tracing in CJS works', async ({ lambdaClient }) => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ return transactionEvent?.transaction === 'NpmTracingCjs';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'NpmTracingCjs',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const transactionEvent = await transactionEventPromise;
+
+ // shows the SDK sent a transaction
+ expect(transactionEvent.transaction).toEqual('NpmTracingCjs'); // name should be the function name
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'sentry.origin': 'auto.otel.aws-lambda',
+ 'sentry.op': 'function.aws.lambda',
+ 'cloud.account.id': '012345678912',
+ 'faas.execution': expect.any(String),
+ 'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:NpmTracingCjs',
+ 'faas.coldstart': true,
+ 'otel.kind': 'SERVER',
+ },
+ op: 'function.aws.lambda',
+ origin: 'auto.otel.aws-lambda',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.spans).toHaveLength(2);
+
+ // shows that the Otel Http instrumentation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ }),
+ description: 'GET http://example.com/',
+ op: 'http.client',
+ }),
+ );
+
+ // shows that the manual span creation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'manual',
+ 'sentry.origin': 'manual',
+ }),
+ description: 'manual-span',
+ op: 'manual',
+ }),
+ );
+
+ // shows that the SDK source is correctly detected
+ expect(transactionEvent.sdk?.packages).toContainEqual(
+ expect.objectContaining({ name: 'npm:@sentry/aws-serverless' }),
+ );
+ });
+
+ test('tracing in ESM works', async ({ lambdaClient }) => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ return transactionEvent?.transaction === 'NpmTracingEsm';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'NpmTracingEsm',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const transactionEvent = await transactionEventPromise;
+
+ // shows the SDK sent a transaction
+ expect(transactionEvent.transaction).toEqual('NpmTracingEsm'); // name should be the function name
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'sentry.origin': 'auto.otel.aws-lambda',
+ 'sentry.op': 'function.aws.lambda',
+ 'cloud.account.id': '012345678912',
+ 'faas.execution': expect.any(String),
+ 'faas.id': 'arn:aws:lambda:us-east-1:012345678912:function:NpmTracingEsm',
+ 'faas.coldstart': true,
+ 'otel.kind': 'SERVER',
+ },
+ op: 'function.aws.lambda',
+ origin: 'auto.otel.aws-lambda',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.spans).toHaveLength(2);
+
+ // shows that the Otel Http instrumentation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ }),
+ description: 'GET http://example.com/',
+ op: 'http.client',
+ }),
+ );
+
+ // shows that the manual span creation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'manual',
+ 'sentry.origin': 'manual',
+ }),
+ description: 'manual-span',
+ op: 'manual',
+ }),
+ );
+
+ // shows that the SDK source is correctly detected
+ expect(transactionEvent.sdk?.packages).toContainEqual(
+ expect.objectContaining({ name: 'npm:@sentry/aws-serverless' }),
+ );
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
index f355654bf6a2..596109c0a596 100644
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
@@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [
'NodeClient',
'NODE_VERSION',
'childProcessIntegration',
+ 'systemErrorIntegration',
],
},
{
diff --git a/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs b/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs
new file mode 100644
index 000000000000..1dd8c40c6ccf
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { readFileSync } from 'fs';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+ sendDefaultPii: true,
+});
+
+readFileSync('non-existent-file.txt');
diff --git a/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs b/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs
new file mode 100644
index 000000000000..5321dd062fa2
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import { readFileSync } from 'fs';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: loggingTransport,
+});
+
+readFileSync('non-existent-file.txt');
diff --git a/dev-packages/node-core-integration-tests/suites/system-error/test.ts b/dev-packages/node-core-integration-tests/suites/system-error/test.ts
new file mode 100644
index 000000000000..1725bd11a0f6
--- /dev/null
+++ b/dev-packages/node-core-integration-tests/suites/system-error/test.ts
@@ -0,0 +1,59 @@
+import { afterAll, describe, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+describe('SystemError integration', () => {
+ test('sendDefaultPii: false', async () => {
+ await createRunner(__dirname, 'basic.mjs')
+ .expect({
+ event: {
+ contexts: {
+ node_system_error: {
+ errno: -2,
+ code: 'ENOENT',
+ syscall: 'open',
+ },
+ },
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'ENOENT: no such file or directory, open',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('sendDefaultPii: true', async () => {
+ await createRunner(__dirname, 'basic-pii.mjs')
+ .expect({
+ event: {
+ contexts: {
+ node_system_error: {
+ errno: -2,
+ code: 'ENOENT',
+ syscall: 'open',
+ path: 'non-existent-file.txt',
+ },
+ },
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'ENOENT: no such file or directory, open',
+ },
+ ],
+ },
+ },
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs
new file mode 100644
index 000000000000..8cf2e8a5248f
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument-options.mjs
@@ -0,0 +1,41 @@
+import * as Sentry from '@sentry/node';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+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,
+
+ integrations: [
+ Sentry.httpIntegration({
+ instrumentation: {
+ requestHook: (span, req) => {
+ span.setAttribute('attr1', 'yes');
+ Sentry.setExtra('requestHookCalled', {
+ url: req.url,
+ method: req.method,
+ });
+ },
+ responseHook: (span, res) => {
+ span.setAttribute('attr2', 'yes');
+ Sentry.setExtra('responseHookCalled', {
+ url: res.req.url,
+ method: res.req.method,
+ });
+ },
+ applyCustomAttributesOnSpan: (span, req, res) => {
+ span.setAttribute('attr3', 'yes');
+ Sentry.setExtra('applyCustomAttributesOnSpanCalled', {
+ reqUrl: req.url,
+ reqMethod: req.method,
+ resUrl: res.req.url,
+ resMethod: res.req.method,
+ });
+ },
+ },
+ }),
+ ],
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument.mjs
index 8cf2e8a5248f..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/instrument.mjs
@@ -4,38 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests';
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,
-
- integrations: [
- Sentry.httpIntegration({
- instrumentation: {
- requestHook: (span, req) => {
- span.setAttribute('attr1', 'yes');
- Sentry.setExtra('requestHookCalled', {
- url: req.url,
- method: req.method,
- });
- },
- responseHook: (span, res) => {
- span.setAttribute('attr2', 'yes');
- Sentry.setExtra('responseHookCalled', {
- url: res.req.url,
- method: res.req.method,
- });
- },
- applyCustomAttributesOnSpan: (span, req, res) => {
- span.setAttribute('attr3', 'yes');
- Sentry.setExtra('applyCustomAttributesOnSpanCalled', {
- reqUrl: req.url,
- reqMethod: req.method,
- resUrl: res.req.url,
- resMethod: res.req.method,
- });
- },
- },
- }),
- ],
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs
index 44122f375857..37e629758828 100644
--- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs
@@ -11,6 +11,10 @@ app.get('/test', (_req, res) => {
res.send({ response: 'response 1' });
});
+app.post('/test', (_req, res) => {
+ res.send({ response: 'response 2' });
+});
+
Sentry.setupExpressErrorHandler(app);
startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
index 97043c998814..397b6baa7cc7 100644
--- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
@@ -7,47 +7,134 @@ describe('httpIntegration', () => {
cleanupChildProcesses();
});
- createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
- test('allows to pass instrumentation options to integration', async () => {
- const runner = createRunner()
- .expect({
- transaction: {
- contexts: {
- trace: {
- span_id: expect.stringMatching(/[a-f0-9]{16}/),
- trace_id: expect.stringMatching(/[a-f0-9]{32}/),
- data: {
+ describe('instrumentation options', () => {
+ createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument-options.mjs', (createRunner, test) => {
+ test('allows to pass instrumentation options to integration', async () => {
+ const runner = createRunner()
+ .expect({
+ transaction: {
+ contexts: {
+ trace: {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ url: expect.stringMatching(/\/test$/),
+ 'http.response.status_code': 200,
+ attr1: 'yes',
+ attr2: 'yes',
+ attr3: 'yes',
+ },
+ op: 'http.server',
+ status: 'ok',
+ },
+ },
+ extra: {
+ requestHookCalled: {
url: expect.stringMatching(/\/test$/),
- 'http.response.status_code': 200,
- attr1: 'yes',
- attr2: 'yes',
- attr3: 'yes',
+ method: 'GET',
+ },
+ responseHookCalled: {
+ url: expect.stringMatching(/\/test$/),
+ method: 'GET',
+ },
+ applyCustomAttributesOnSpanCalled: {
+ reqUrl: expect.stringMatching(/\/test$/),
+ reqMethod: 'GET',
+ resUrl: expect.stringMatching(/\/test$/),
+ resMethod: 'GET',
},
- op: 'http.server',
- status: 'ok',
},
},
- extra: {
- requestHookCalled: {
- url: expect.stringMatching(/\/test$/),
- method: 'GET',
- },
- responseHookCalled: {
- url: expect.stringMatching(/\/test$/),
- method: 'GET',
- },
- applyCustomAttributesOnSpanCalled: {
- reqUrl: expect.stringMatching(/\/test$/),
- reqMethod: 'GET',
- resUrl: expect.stringMatching(/\/test$/),
- resMethod: 'GET',
- },
+ })
+ .start();
+ runner.makeRequest('get', '/test');
+ await runner.completed();
+ });
+ });
+ });
+
+ describe('http.server spans', () => {
+ createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('captures correct attributes for GET requests', async () => {
+ const runner = createRunner()
+ .expect({
+ transaction: transaction => {
+ const port = runner.getPort();
+ expect(transaction.transaction).toBe('GET /test');
+ expect(transaction.contexts?.trace?.data).toEqual({
+ 'http.flavor': '1.1',
+ 'http.host': `localhost:${port}`,
+ 'http.method': 'GET',
+ 'http.query': 'a=1&b=2',
+ 'http.response.status_code': 200,
+ 'http.route': '/test',
+ 'http.scheme': 'http',
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ 'http.target': '/test?a=1&b=2',
+ 'http.url': `http://localhost:${port}/test?a=1&b=2`,
+ 'http.user_agent': 'node',
+ 'net.host.ip': '::1',
+ 'net.host.name': 'localhost',
+ 'net.host.port': port,
+ 'net.peer.ip': '::1',
+ 'net.peer.port': expect.any(Number),
+ 'net.transport': 'ip_tcp',
+ 'otel.kind': 'SERVER',
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ url: `http://localhost:${port}/test`,
+ });
},
- },
- })
- .start();
- runner.makeRequest('get', '/test');
- await runner.completed();
+ })
+ .start();
+
+ runner.makeRequest('get', '/test?a=1&b=2#hash');
+ await runner.completed();
+ });
+
+ test('captures correct attributes for POST requests', async () => {
+ const runner = createRunner()
+ .expect({
+ transaction: transaction => {
+ const port = runner.getPort();
+ expect(transaction.transaction).toBe('POST /test');
+ expect(transaction.contexts?.trace?.data).toEqual({
+ 'http.flavor': '1.1',
+ 'http.host': `localhost:${port}`,
+ 'http.method': 'POST',
+ 'http.query': 'a=1&b=2',
+ 'http.request_content_length_uncompressed': 9,
+ 'http.response.status_code': 200,
+ 'http.route': '/test',
+ 'http.scheme': 'http',
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ 'http.target': '/test?a=1&b=2',
+ 'http.url': `http://localhost:${port}/test?a=1&b=2`,
+ 'http.user_agent': 'node',
+ 'net.host.ip': '::1',
+ 'net.host.name': 'localhost',
+ 'net.host.port': port,
+ 'net.peer.ip': '::1',
+ 'net.peer.port': expect.any(Number),
+ 'net.transport': 'ip_tcp',
+ 'otel.kind': 'SERVER',
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ url: `http://localhost:${port}/test`,
+ });
+ },
+ })
+ .start();
+
+ runner.makeRequest('post', '/test?a=1&b=2#hash', { data: 'test body' });
+ await runner.completed();
+ });
});
});
diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts
index f4a176688280..46c031c2c786 100644
--- a/dev-packages/node-integration-tests/utils/runner.ts
+++ b/dev-packages/node-integration-tests/utils/runner.ts
@@ -17,7 +17,7 @@ import { execSync, spawn, spawnSync } from 'child_process';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { basename, join } from 'path';
import { inspect } from 'util';
-import { afterAll, describe, test } from 'vitest';
+import { afterAll, beforeAll, describe, test } from 'vitest';
import {
assertEnvelopeHeader,
assertSentryCheckIn,
@@ -158,6 +158,7 @@ type StartResult = {
completed(): Promise;
childHasExited(): boolean;
getLogs(): string[];
+ getPort(): number | undefined;
makeRequest(
method: 'get' | 'post',
path: string,
@@ -194,74 +195,79 @@ export function createEsmAndCjsTests(
// If additionalDependencies are provided, we also create a nested package.json and install them there.
const uniqueId = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
const tmpDirPath = join(cwd, `tmp_${uniqueId}`);
- mkdirSync(tmpDirPath);
-
- // Copy ESM files as-is into tmp dir
const esmScenarioBasename = basename(scenarioPath);
const esmInstrumentBasename = basename(instrumentPath);
const esmScenarioPathForRun = join(tmpDirPath, esmScenarioBasename);
const esmInstrumentPathForRun = join(tmpDirPath, esmInstrumentBasename);
- writeFileSync(esmScenarioPathForRun, readFileSync(mjsScenarioPath, 'utf8'));
- writeFileSync(esmInstrumentPathForRun, readFileSync(mjsInstrumentPath, 'utf8'));
-
- // Pre-create CJS converted files inside tmp dir
const cjsScenarioPath = join(tmpDirPath, esmScenarioBasename.replace('.mjs', '.cjs'));
const cjsInstrumentPath = join(tmpDirPath, esmInstrumentBasename.replace('.mjs', '.cjs'));
- convertEsmFileToCjs(esmScenarioPathForRun, cjsScenarioPath);
- convertEsmFileToCjs(esmInstrumentPathForRun, cjsInstrumentPath);
-
- // Create a minimal package.json with requested dependencies (if any) and install them
- const additionalDependencies = options?.additionalDependencies ?? {};
- if (Object.keys(additionalDependencies).length > 0) {
- const packageJson = {
- name: 'tmp-integration-test',
- private: true,
- version: '0.0.0',
- dependencies: additionalDependencies,
- } as const;
-
- writeFileSync(join(tmpDirPath, 'package.json'), JSON.stringify(packageJson, null, 2));
-
- try {
- const deps = Object.entries(additionalDependencies).map(([name, range]) => {
- if (!range || typeof range !== 'string') {
- throw new Error(`Invalid version range for "${name}": ${String(range)}`);
- }
- return `${name}@${range}`;
- });
- if (deps.length > 0) {
- // Prefer npm for temp installs to avoid Yarn engine strictness; see https://github.com/vercel/ai/issues/7777
- // We rely on the generated package.json dependencies and run a plain install.
- const result = spawnSync('npm', ['install', '--silent', '--no-audit', '--no-fund'], {
- cwd: tmpDirPath,
- encoding: 'utf8',
+ function createTmpDir(): void {
+ mkdirSync(tmpDirPath);
+
+ // Copy ESM files as-is into tmp dir
+ writeFileSync(esmScenarioPathForRun, readFileSync(mjsScenarioPath, 'utf8'));
+ writeFileSync(esmInstrumentPathForRun, readFileSync(mjsInstrumentPath, 'utf8'));
+
+ // Pre-create CJS converted files inside tmp dir
+ convertEsmFileToCjs(esmScenarioPathForRun, cjsScenarioPath);
+ convertEsmFileToCjs(esmInstrumentPathForRun, cjsInstrumentPath);
+
+ // Create a minimal package.json with requested dependencies (if any) and install them
+ const additionalDependencies = options?.additionalDependencies ?? {};
+ if (Object.keys(additionalDependencies).length > 0) {
+ const packageJson = {
+ name: 'tmp-integration-test',
+ private: true,
+ version: '0.0.0',
+ dependencies: additionalDependencies,
+ } as const;
+
+ writeFileSync(join(tmpDirPath, 'package.json'), JSON.stringify(packageJson, null, 2));
+
+ try {
+ const deps = Object.entries(additionalDependencies).map(([name, range]) => {
+ if (!range || typeof range !== 'string') {
+ throw new Error(`Invalid version range for "${name}": ${String(range)}`);
+ }
+ return `${name}@${range}`;
});
- if (process.env.DEBUG) {
- // eslint-disable-next-line no-console
- console.log('[additionalDependencies via npm]', deps.join(' '));
- // eslint-disable-next-line no-console
- console.log('[npm stdout]', result.stdout);
- // eslint-disable-next-line no-console
- console.log('[npm stderr]', result.stderr);
- }
+ if (deps.length > 0) {
+ // Prefer npm for temp installs to avoid Yarn engine strictness; see https://github.com/vercel/ai/issues/7777
+ // We rely on the generated package.json dependencies and run a plain install.
+ const result = spawnSync('npm', ['install', '--silent', '--no-audit', '--no-fund'], {
+ cwd: tmpDirPath,
+ encoding: 'utf8',
+ });
- if (result.error) {
- throw new Error(`Failed to install additionalDependencies in tmp dir ${tmpDirPath}: ${result.error.message}`);
- }
- if (typeof result.status === 'number' && result.status !== 0) {
- throw new Error(
- `Failed to install additionalDependencies in tmp dir ${tmpDirPath} (exit ${result.status}):\n${
- result.stderr || result.stdout || '(no output)'
- }`,
- );
+ if (process.env.DEBUG) {
+ // eslint-disable-next-line no-console
+ console.log('[additionalDependencies via npm]', deps.join(' '));
+ // eslint-disable-next-line no-console
+ console.log('[npm stdout]', result.stdout);
+ // eslint-disable-next-line no-console
+ console.log('[npm stderr]', result.stderr);
+ }
+
+ if (result.error) {
+ throw new Error(
+ `Failed to install additionalDependencies in tmp dir ${tmpDirPath}: ${result.error.message}`,
+ );
+ }
+ if (typeof result.status === 'number' && result.status !== 0) {
+ throw new Error(
+ `Failed to install additionalDependencies in tmp dir ${tmpDirPath} (exit ${result.status}):\n${
+ result.stderr || result.stdout || '(no output)'
+ }`,
+ );
+ }
}
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Failed to install additionalDependencies:', e);
+ throw e;
}
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error('Failed to install additionalDependencies:', e);
- throw e;
}
}
@@ -280,6 +286,11 @@ export function createEsmAndCjsTests(
callback(() => createRunner(cjsScenarioPath).withFlags('--require', cjsInstrumentPath), cjsTestFn, 'cjs');
});
+ // Create tmp directory
+ beforeAll(() => {
+ createTmpDir();
+ });
+
// Clean up the tmp directory after both esm and cjs suites have run
afterAll(() => {
try {
@@ -617,6 +628,9 @@ export function createRunner(...paths: string[]) {
getLogs(): string[] {
return logs;
},
+ getPort(): number | undefined {
+ return scenarioServerPort;
+ },
makeRequest: async function (
method: 'get' | 'post',
path: string,
diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md
index 3b952672e4e9..810b180c3ba9 100644
--- a/docs/publishing-a-release.md
+++ b/docs/publishing-a-release.md
@@ -44,8 +44,8 @@ These have also been documented via [Cursor Rules](../.cursor/rules/publishing-r
1. Run `yarn changelog` and copy everything.
2. Create a new section in the changelog with the previously determined version number.
3. Paste in the logs you copied earlier.
-4. Delete any which aren't user-facing changes (such as docs or tests).
-5. If there are any important features or fixes, highlight them under the `Important Changes` subheading. If there are no important changes, don't include this section. If the `Important Changes` subheading is used, put all other changes under the `Other Changes` subheading.
+4. If there are any important features or fixes, highlight them under the `Important Changes` subheading. If there are no important changes, don't include this section. If the `Important Changes` subheading is used, put all other user-facing changes under the `Other Changes` subheading.
+5. Any changes that are purely internal (e.g. internal refactors (`ref`) without user-facing changes, tests, chores, etc) should be put under a `` block, where the `` heading is "Internal Changes" (see example).
6. Make sure the changelog entries are ordered alphabetically.
7. If any of the PRs are from external contributors, include underneath the commits
`Work in this release contributed by . Thank you for your contributions!`.
@@ -78,5 +78,13 @@ This feature ships updates to the span names and ops to better match OpenTelemet
- fix(sveltekit): Export `vercelAIIntegration` from `@sentry/node` ([#16496](https://github.com/getsentry/sentry-javascript/pull/16496))
+
+ Internal Changes
+
+- ref(node): Split up incoming & outgoing http handling ([#17358](https://github.com/getsentry/sentry-javascript/pull/17358))
+- test(node): Enable additionalDependencies in integration runner ([#17361](https://github.com/getsentry/sentry-javascript/pull/17361))
+
+
+
Work in this release was contributed by @agrattan0820. Thank you for your contribution!
```
diff --git a/packages/astro/package.json b/packages/astro/package.json
index ec6310d269ba..efa86e9d2417 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -59,7 +59,7 @@
"@sentry/browser": "10.4.0",
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
- "@sentry/vite-plugin": "^4.0.0"
+ "@sentry/vite-plugin": "^4.1.0"
},
"devDependencies": {
"astro": "^3.5.0",
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index a9e81aee7db5..3b2f589f7fc2 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -124,6 +124,7 @@ export {
startSession,
startSpan,
startSpanManual,
+ systemErrorIntegration,
tediousIntegration,
trpcMiddleware,
updateSpanName,
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index b99c481fd1d3..8cbcd31c50a5 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -114,6 +114,7 @@ export {
spanToJSON,
spanToTraceHeader,
spanToBaggageHeader,
+ systemErrorIntegration,
trpcMiddleware,
updateSpanName,
supabaseIntegration,
diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts
index 826ec0a4ae4c..5c40d53174c1 100644
--- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts
+++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts
@@ -205,9 +205,12 @@ interface SourceMapsOptions {
*
* By default (`false`), the plugin automatically uploads source maps during a production build if a Sentry auth token is detected.
*
+ * If set to `"disable-upload"`, the plugin will not upload source maps to Sentry, but will inject debug IDs into the build artifacts.
+ * This is useful if you want to manually upload source maps to Sentry at a later point in time.
+ *
* @default false
*/
- disable?: boolean;
+ disable?: boolean | 'disable-upload';
/**
* A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry.
diff --git a/packages/core/src/integrations/extraerrordata.ts b/packages/core/src/integrations/extraerrordata.ts
index 291648244f6c..ae65739aed5f 100644
--- a/packages/core/src/integrations/extraerrordata.ts
+++ b/packages/core/src/integrations/extraerrordata.ts
@@ -115,7 +115,12 @@ function _extractErrorData(
// Error.cause is a standard property that is non enumerable, we therefore need to access it separately.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
if (captureErrorCause && error.cause !== undefined) {
- extraErrorInfo.cause = isError(error.cause) ? error.cause.toString() : error.cause;
+ if (isError(error.cause)) {
+ const errorName = error.cause.name || error.cause.constructor.name;
+ extraErrorInfo.cause = { [errorName]: _extractErrorData(error.cause as ExtendedError, false, maxValueLength) };
+ } else {
+ extraErrorInfo.cause = error.cause;
+ }
}
// Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that)
diff --git a/packages/core/src/integrations/mcp-server/correlation.ts b/packages/core/src/integrations/mcp-server/correlation.ts
index 7a73f63f64e3..4da7e78e4009 100644
--- a/packages/core/src/integrations/mcp-server/correlation.ts
+++ b/packages/core/src/integrations/mcp-server/correlation.ts
@@ -86,24 +86,17 @@ export function completeSpanWithResults(transport: MCPTransport, requestId: Requ
/**
* Cleans up pending spans for a specific transport (when that transport closes)
* @param transport - MCP transport instance
- * @returns Number of pending spans that were cleaned up
*/
-export function cleanupPendingSpansForTransport(transport: MCPTransport): number {
+export function cleanupPendingSpansForTransport(transport: MCPTransport): void {
const spanMap = transportToSpanMap.get(transport);
- if (!spanMap) {
- return 0;
- }
-
- const pendingCount = spanMap.size;
-
- for (const [, spanData] of spanMap) {
- spanData.span.setStatus({
- code: SPAN_STATUS_ERROR,
- message: 'cancelled',
- });
- spanData.span.end();
+ if (spanMap) {
+ for (const [, spanData] of spanMap) {
+ spanData.span.setStatus({
+ code: SPAN_STATUS_ERROR,
+ message: 'cancelled',
+ });
+ spanData.span.end();
+ }
+ spanMap.clear();
}
-
- spanMap.clear();
- return pendingCount;
}
diff --git a/packages/core/src/integrations/mcp-server/sessionExtraction.ts b/packages/core/src/integrations/mcp-server/sessionExtraction.ts
index 90e235d4e544..62eaa94f9b71 100644
--- a/packages/core/src/integrations/mcp-server/sessionExtraction.ts
+++ b/packages/core/src/integrations/mcp-server/sessionExtraction.ts
@@ -152,21 +152,23 @@ export function extractClientInfo(extra: ExtraHandlerData): {
* @returns Transport type mapping for span attributes
*/
export function getTransportTypes(transport: MCPTransport): { mcpTransport: string; networkTransport: string } {
- const transportName = transport.constructor?.name?.toLowerCase() || '';
-
- if (transportName.includes('stdio')) {
- return { mcpTransport: 'stdio', networkTransport: 'pipe' };
- }
-
- if (transportName.includes('streamablehttp') || transportName.includes('streamable')) {
- return { mcpTransport: 'http', networkTransport: 'tcp' };
+ if (!transport?.constructor) {
+ return { mcpTransport: 'unknown', networkTransport: 'unknown' };
}
-
- if (transportName.includes('sse')) {
- return { mcpTransport: 'sse', networkTransport: 'tcp' };
+ const transportName = typeof transport.constructor?.name === 'string' ? transport.constructor.name : 'unknown';
+ let networkTransport = 'unknown';
+
+ const lowerTransportName = transportName.toLowerCase();
+ if (lowerTransportName.includes('stdio')) {
+ networkTransport = 'pipe';
+ } else if (lowerTransportName.includes('http') || lowerTransportName.includes('sse')) {
+ networkTransport = 'tcp';
}
- return { mcpTransport: 'unknown', networkTransport: 'unknown' };
+ return {
+ mcpTransport: transportName,
+ networkTransport,
+ };
}
/**
@@ -174,12 +176,13 @@ export function getTransportTypes(transport: MCPTransport): { mcpTransport: stri
* @param transport - MCP transport instance
* @param extra - Optional extra handler data
* @returns Transport attributes for span instrumentation
+ * @note sessionId may be undefined during initial setup - session should be established by client during initialize flow
*/
export function buildTransportAttributes(
transport: MCPTransport,
extra?: ExtraHandlerData,
): Record {
- const sessionId = transport.sessionId;
+ const sessionId = transport && 'sessionId' in transport ? transport.sessionId : undefined;
const clientInfo = extra ? extractClientInfo(extra) : {};
const { mcpTransport, networkTransport } = getTransportTypes(transport);
const clientAttributes = getClientAttributes(transport);
diff --git a/packages/core/test/lib/integrations/extraerrordata.test.ts b/packages/core/test/lib/integrations/extraerrordata.test.ts
index a6470c3f6b2c..f935acc689f9 100644
--- a/packages/core/test/lib/integrations/extraerrordata.test.ts
+++ b/packages/core/test/lib/integrations/extraerrordata.test.ts
@@ -55,9 +55,11 @@ describe('ExtraErrorData()', () => {
});
});
- it('doesnt choke on linked errors and stringify names instead', () => {
+ it('should extract error data from the error cause with the same policy', () => {
const error = new TypeError('foo') as ExtendedError;
- error.cause = new SyntaxError('bar');
+ error.cause = new SyntaxError('bar') as ExtendedError;
+ error.cause.baz = 42;
+ error.cause.foo = 'a'.repeat(300);
const enhancedEvent = extraErrorData.processEvent?.(
event,
@@ -69,7 +71,12 @@ describe('ExtraErrorData()', () => {
expect(enhancedEvent.contexts).toEqual({
TypeError: {
- cause: 'SyntaxError: bar',
+ cause: {
+ SyntaxError: {
+ baz: 42,
+ foo: `${'a'.repeat(250)}...`,
+ },
+ },
},
});
});
diff --git a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts
index 5437d4a5a13a..0ad969d5b46e 100644
--- a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts
+++ b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts
@@ -61,7 +61,7 @@ describe('MCP Server Semantic Conventions', () => {
'mcp.session.id': 'test-session-123',
'client.address': '192.168.1.100',
'client.port': 54321,
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'mcp.request.argument.location': '"Seattle, WA"',
@@ -93,7 +93,7 @@ describe('MCP Server Semantic Conventions', () => {
'mcp.resource.uri': 'file:///docs/api.md',
'mcp.request.id': 'req-2',
'mcp.session.id': 'test-session-123',
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'mcp.request.argument.uri': '"file:///docs/api.md"',
@@ -125,7 +125,7 @@ describe('MCP Server Semantic Conventions', () => {
'mcp.prompt.name': 'analyze-code',
'mcp.request.id': 'req-3',
'mcp.session.id': 'test-session-123',
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'mcp.request.argument.name': '"analyze-code"',
@@ -154,7 +154,7 @@ describe('MCP Server Semantic Conventions', () => {
attributes: {
'mcp.method.name': 'notifications/tools/list_changed',
'mcp.session.id': 'test-session-123',
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'sentry.op': 'mcp.notification.client_to_server',
@@ -193,7 +193,7 @@ describe('MCP Server Semantic Conventions', () => {
'mcp.request.id': 'req-4',
'mcp.session.id': 'test-session-123',
// Transport attributes
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
// Sentry-specific
@@ -227,7 +227,7 @@ describe('MCP Server Semantic Conventions', () => {
attributes: {
'mcp.method.name': 'notifications/message',
'mcp.session.id': 'test-session-123',
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'mcp.logging.level': 'info',
diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
index 008942ac4099..996779455574 100644
--- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
+++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
@@ -2,8 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as currentScopes from '../../../../src/currentScopes';
import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server';
import {
+ buildTransportAttributes,
extractSessionDataFromInitializeRequest,
extractSessionDataFromInitializeResponse,
+ getTransportTypes,
} from '../../../../src/integrations/mcp-server/sessionExtraction';
import {
cleanupSessionDataForTransport,
@@ -214,7 +216,7 @@ describe('MCP Server Transport Instrumentation', () => {
'mcp.tool.name': 'process-file',
'mcp.request.id': 'req-stdio-1',
'mcp.session.id': 'stdio-session-456',
- 'mcp.transport': 'stdio', // Should be stdio, not http
+ 'mcp.transport': 'StdioServerTransport',
'network.transport': 'pipe', // Should be pipe, not tcp
'network.protocol.version': '2.0',
'mcp.request.argument.path': '"/tmp/data.txt"',
@@ -245,7 +247,7 @@ describe('MCP Server Transport Instrumentation', () => {
attributes: expect.objectContaining({
'mcp.method.name': 'notifications/message',
'mcp.session.id': 'stdio-session-456',
- 'mcp.transport': 'stdio',
+ 'mcp.transport': 'StdioServerTransport',
'network.transport': 'pipe',
'mcp.logging.level': 'debug',
'mcp.logging.message': 'Processing stdin input',
@@ -286,7 +288,7 @@ describe('MCP Server Transport Instrumentation', () => {
attributes: expect.objectContaining({
'mcp.method.name': 'resources/read',
'mcp.resource.uri': 'https://api.example.com/data',
- 'mcp.transport': 'sse', // Deprecated but supported
+ 'mcp.transport': 'SSEServerTransport',
'network.transport': 'tcp',
'mcp.session.id': 'sse-session-789',
}),
@@ -361,7 +363,7 @@ describe('MCP Server Transport Instrumentation', () => {
'mcp.session.id': 'test-session-direct',
'client.address': '127.0.0.1',
'client.port': 8080,
- 'mcp.transport': 'http',
+ 'mcp.transport': 'StreamableHTTPServerTransport',
'network.transport': 'tcp',
'network.protocol.version': '2.0',
'mcp.request.argument.input': '"test"',
@@ -500,4 +502,86 @@ describe('MCP Server Transport Instrumentation', () => {
expect(getSessionDataForTransport(transportWithoutSession)).toBeUndefined();
});
});
+
+ describe('Transport Type Detection', () => {
+ it('extracts HTTP transport name correctly', () => {
+ const transport = createMockTransport();
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('StreamableHTTPServerTransport');
+ expect(result.networkTransport).toBe('tcp');
+ });
+
+ it('extracts stdio transport and maps to pipe network', () => {
+ const transport = createMockStdioTransport();
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('StdioServerTransport');
+ expect(result.networkTransport).toBe('pipe');
+ });
+
+ it('extracts SSE transport name', () => {
+ const transport = createMockSseTransport();
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('SSEServerTransport');
+ expect(result.networkTransport).toBe('tcp');
+ });
+
+ it('handles transport without constructor', () => {
+ const transport = Object.create(null);
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('unknown');
+ expect(result.networkTransport).toBe('unknown');
+ });
+
+ it('handles transport with null/undefined constructor name', () => {
+ const transport = {
+ constructor: { name: null },
+ onmessage: () => {},
+ send: async () => {},
+ };
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('unknown');
+ expect(result.networkTransport).toBe('unknown');
+ });
+
+ it('returns unknown network transport for unrecognized transport types', () => {
+ const transport = {
+ constructor: { name: 'CustomTransport' },
+ onmessage: () => {},
+ send: async () => {},
+ };
+ const result = getTransportTypes(transport);
+
+ expect(result.mcpTransport).toBe('CustomTransport');
+ expect(result.networkTransport).toBe('unknown');
+ });
+ });
+
+ describe('buildTransportAttributes sessionId handling', () => {
+ it('includes sessionId when present', () => {
+ const transport = createMockTransport();
+ const attributes = buildTransportAttributes(transport);
+
+ expect(attributes['mcp.session.id']).toBe('test-session-123');
+ });
+
+ it('excludes sessionId when undefined', () => {
+ const transport = createMockTransport();
+ transport.sessionId = undefined;
+ const attributes = buildTransportAttributes(transport);
+
+ expect(attributes['mcp.session.id']).toBeUndefined();
+ });
+
+ it('excludes sessionId when not present in transport', () => {
+ const transport = { onmessage: () => {}, send: async () => {} };
+ const attributes = buildTransportAttributes(transport);
+
+ expect(attributes['mcp.session.id']).toBeUndefined();
+ });
+ });
});
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 3d28dfe86d34..9aeb3f42e903 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -47,7 +47,7 @@
"dependencies": {
"@sentry/core": "10.4.0",
"@sentry/react": "10.4.0",
- "@sentry/webpack-plugin": "^4.0.2"
+ "@sentry/webpack-plugin": "^4.1.0"
},
"peerDependencies": {
"gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 8339e95c77a3..26ed56f031d8 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -115,6 +115,7 @@ export {
trpcMiddleware,
updateSpanName,
supabaseIntegration,
+ systemErrorIntegration,
instrumentSupabaseClient,
zodErrorsIntegration,
profiler,
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 9bc9926d4634..9ff2a4ad537a 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -85,7 +85,7 @@
"@sentry/opentelemetry": "10.4.0",
"@sentry/react": "10.4.0",
"@sentry/vercel-edge": "10.4.0",
- "@sentry/webpack-plugin": "^4.0.2",
+ "@sentry/webpack-plugin": "^4.1.0",
"chalk": "3.0.0",
"resolve": "1.22.8",
"rollup": "^4.35.0",
diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts
index ea9f0aeb63ea..9ae0a5ee0bb2 100644
--- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts
+++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts
@@ -12,9 +12,7 @@ type OriginalStackFrameResponse = {
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryBasePath?: string;
- next?: {
- version?: string;
- };
+ _sentryNextJsVersion: string | undefined;
};
/**
@@ -39,9 +37,15 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
try {
if (hint.originalException && hint.originalException instanceof Error && hint.originalException.stack) {
const frames = stackTraceParser.parse(hint.originalException.stack);
+ const nextJsVersion = globalWithInjectedValues._sentryNextJsVersion;
+
+ // If we for whatever reason don't have a Next.js version,
+ // we don't want to symbolicate as this previously lead to infinite loops
+ if (!nextJsVersion) {
+ return event;
+ }
- const nextjsVersion = globalWithInjectedValues.next?.version || '0.0.0';
- const parsedNextjsVersion = nextjsVersion ? parseSemver(nextjsVersion) : {};
+ const parsedNextjsVersion = parseSemver(nextJsVersion);
let resolvedFrames: ({
originalCodeFrame: string | null;
@@ -83,7 +87,9 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
context_line: contextLine,
post_context: postContextLines,
function: resolvedFrame.originalStackFrame.methodName,
- filename: resolvedFrame.originalStackFrame.file || undefined,
+ filename: resolvedFrame.originalStackFrame.file
+ ? stripWebpackInternalPrefix(resolvedFrame.originalStackFrame.file)
+ : undefined,
lineno:
resolvedFrame.originalStackFrame.lineNumber || resolvedFrame.originalStackFrame.line1 || undefined,
colno: resolvedFrame.originalStackFrame.column || resolvedFrame.originalStackFrame.column1 || undefined,
@@ -281,3 +287,21 @@ function parseOriginalCodeFrame(codeFrame: string): {
postContextLines,
};
}
+
+/**
+ * Strips webpack-internal prefixes from filenames to clean up stack traces.
+ *
+ * Examples:
+ * - "webpack-internal:///./components/file.tsx" -> "./components/file.tsx"
+ * - "webpack-internal:///(app-pages-browser)/./components/file.tsx" -> "./components/file.tsx"
+ */
+function stripWebpackInternalPrefix(filename: string): string | undefined {
+ if (!filename) {
+ return filename;
+ }
+
+ const webpackInternalRegex = /^webpack-internal:(?:\/+)?(?:\([^)]*\)\/)?(.+)$/;
+ const match = filename.match(webpackInternalRegex);
+
+ return match ? match[1] : filename;
+}
diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
index 50dd1a14588a..76d98fda25e8 100644
--- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
+++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
@@ -1,8 +1,8 @@
import { debug } from '@sentry/core';
import * as chalk from 'chalk';
-import * as path from 'path';
import type { RouteManifest } from '../manifest/types';
-import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShortcut } from '../types';
+import type { NextConfigObject, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
+import { generateValueInjectionRules } from './generateValueInjectionRules';
/**
* Construct a Turbopack config object from a Next.js config object and a Turbopack options object.
@@ -14,30 +14,23 @@ import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShort
export function constructTurbopackConfig({
userNextConfig,
routeManifest,
+ nextJsVersion,
}: {
userNextConfig: NextConfigObject;
routeManifest?: RouteManifest;
+ nextJsVersion?: string;
}): TurbopackOptions {
const newConfig: TurbopackOptions = {
...userNextConfig.turbopack,
};
- if (routeManifest) {
- newConfig.rules = safelyAddTurbopackRule(newConfig.rules, {
- matcher: '**/instrumentation-client.*',
- rule: {
- loaders: [
- {
- loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
- options: {
- values: {
- _sentryRouteManifest: JSON.stringify(routeManifest),
- },
- },
- },
- ],
- },
- });
+ const valueInjectionRules = generateValueInjectionRules({
+ routeManifest,
+ nextJsVersion,
+ });
+
+ for (const { matcher, rule } of valueInjectionRules) {
+ newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule });
}
return newConfig;
@@ -53,7 +46,7 @@ export function constructTurbopackConfig({
*/
export function safelyAddTurbopackRule(
existingRules: TurbopackOptions['rules'],
- { matcher, rule }: { matcher: string; rule: TurbopackRuleConfigItemOrShortcut },
+ { matcher, rule }: TurbopackMatcherWithRule,
): TurbopackOptions['rules'] {
if (!existingRules) {
return {
diff --git a/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts
new file mode 100644
index 000000000000..58cf7cdd0a15
--- /dev/null
+++ b/packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts
@@ -0,0 +1,69 @@
+import * as path from 'path';
+import type { RouteManifest } from '../manifest/types';
+import type { JSONValue, TurbopackMatcherWithRule } from '../types';
+
+/**
+ * Generate the value injection rules for client and server in turbopack config.
+ */
+export function generateValueInjectionRules({
+ routeManifest,
+ nextJsVersion,
+}: {
+ routeManifest?: RouteManifest;
+ nextJsVersion?: string;
+}): TurbopackMatcherWithRule[] {
+ const rules: TurbopackMatcherWithRule[] = [];
+ const isomorphicValues: Record = {};
+ let clientValues: Record = {};
+ let serverValues: Record = {};
+
+ if (nextJsVersion) {
+ // This is used to determine version-based dev-symbolication behavior
+ isomorphicValues._sentryNextJsVersion = nextJsVersion;
+ }
+
+ if (routeManifest) {
+ clientValues._sentryRouteManifest = JSON.stringify(routeManifest);
+ }
+
+ if (Object.keys(isomorphicValues).length > 0) {
+ clientValues = { ...clientValues, ...isomorphicValues };
+ serverValues = { ...serverValues, ...isomorphicValues };
+ }
+
+ // Client value injection
+ if (Object.keys(clientValues).length > 0) {
+ rules.push({
+ matcher: '**/instrumentation-client.*',
+ rule: {
+ loaders: [
+ {
+ loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
+ options: {
+ values: clientValues,
+ },
+ },
+ ],
+ },
+ });
+ }
+
+ // Server value injection
+ if (Object.keys(serverValues).length > 0) {
+ rules.push({
+ matcher: '**/instrumentation.*',
+ rule: {
+ loaders: [
+ {
+ loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
+ options: {
+ values: serverValues,
+ },
+ },
+ ],
+ },
+ });
+ }
+
+ return rules;
+}
diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts
index b29fbb6881af..18cdc2d38cfc 100644
--- a/packages/nextjs/src/config/types.ts
+++ b/packages/nextjs/src/config/types.ts
@@ -621,7 +621,7 @@ export type EnhancedGlobal = typeof GLOBAL_OBJ & {
SENTRY_RELEASES?: { [key: string]: { id: string } };
};
-type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
+export type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
type TurbopackLoaderItem =
| string
@@ -637,6 +637,11 @@ type TurbopackRuleCondition = {
export type TurbopackRuleConfigItemOrShortcut = TurbopackLoaderItem[] | TurbopackRuleConfigItem;
+export type TurbopackMatcherWithRule = {
+ matcher: string;
+ rule: TurbopackRuleConfigItemOrShortcut;
+};
+
type TurbopackRuleConfigItemOptions = {
loaders: TurbopackLoaderItem[];
as?: string;
diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts
index 9c9c479cd724..b336e1e2ee9b 100644
--- a/packages/nextjs/src/config/webpack.ts
+++ b/packages/nextjs/src/config/webpack.ts
@@ -45,6 +45,7 @@ export function constructWebpackConfigFunction(
userSentryOptions: SentryBuildOptions = {},
releaseName: string | undefined,
routeManifest: RouteManifest | undefined,
+ nextJsVersion: string | undefined,
): WebpackConfigFunction {
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
@@ -90,7 +91,15 @@ export function constructWebpackConfigFunction(
const newConfig = setUpModuleRules(rawNewConfig);
// Add a loader which will inject code that sets global values
- addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, releaseName, routeManifest);
+ addValueInjectionLoader({
+ newConfig,
+ userNextConfig,
+ userSentryOptions,
+ buildContext,
+ releaseName,
+ routeManifest,
+ nextJsVersion,
+ });
addOtelWarningIgnoreRule(newConfig);
@@ -682,14 +691,23 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
*/
// TODO: Remove this loader and replace it with a nextConfig.env (https://web.archive.org/web/20240917153554/https://nextjs.org/docs/app/api-reference/next-config-js/env) or define based (https://github.com/vercel/next.js/discussions/71476) approach.
// In order to remove this loader though we need to make sure the minimum supported Next.js version includes this PR (https://github.com/vercel/next.js/pull/61194), otherwise the nextConfig.env based approach will not work, as our SDK code is not processed by Next.js.
-function addValueInjectionLoader(
- newConfig: WebpackConfigObjectWithModuleRules,
- userNextConfig: NextConfigObject,
- userSentryOptions: SentryBuildOptions,
- buildContext: BuildContext,
- releaseName: string | undefined,
- routeManifest: RouteManifest | undefined,
-): void {
+function addValueInjectionLoader({
+ newConfig,
+ userNextConfig,
+ userSentryOptions,
+ buildContext,
+ releaseName,
+ routeManifest,
+ nextJsVersion,
+}: {
+ newConfig: WebpackConfigObjectWithModuleRules;
+ userNextConfig: NextConfigObject;
+ userSentryOptions: SentryBuildOptions;
+ buildContext: BuildContext;
+ releaseName: string | undefined;
+ routeManifest: RouteManifest | undefined;
+ nextJsVersion: string | undefined;
+}): void {
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
// Check if release creation is disabled to prevent injection that breaks build determinism
@@ -710,6 +728,8 @@ function addValueInjectionLoader(
// Only inject if release creation is not explicitly disabled (to maintain build determinism)
SENTRY_RELEASE: releaseToInject && !buildContext.dev ? { id: releaseToInject } : undefined,
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
+ // This is used to determine version-based dev-symbolication behavior
+ _sentryNextJsVersion: nextJsVersion,
};
const serverValues = {
diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts
index 57fff867f64a..4404fded7e36 100644
--- a/packages/nextjs/src/config/withSentryConfig.ts
+++ b/packages/nextjs/src/config/withSentryConfig.ts
@@ -314,12 +314,19 @@ function getFinalConfigObject(
webpack:
isTurbopack || userSentryOptions.disableSentryWebpackConfig
? incomingUserNextConfigObject.webpack // just return the original webpack config
- : constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName, routeManifest),
+ : constructWebpackConfigFunction(
+ incomingUserNextConfigObject,
+ userSentryOptions,
+ releaseName,
+ routeManifest,
+ nextJsVersion,
+ ),
...(isTurbopackSupported && isTurbopack
? {
turbopack: constructTurbopackConfig({
userNextConfig: incomingUserNextConfigObject,
routeManifest,
+ nextJsVersion,
}),
}
: {}),
diff --git a/packages/nextjs/test/common/devErrorSymbolicationEventProcessor.test.ts b/packages/nextjs/test/common/devErrorSymbolicationEventProcessor.test.ts
new file mode 100644
index 000000000000..4305aad537a8
--- /dev/null
+++ b/packages/nextjs/test/common/devErrorSymbolicationEventProcessor.test.ts
@@ -0,0 +1,261 @@
+import type { Event, EventHint, SpanJSON } from '@sentry/core';
+import { GLOBAL_OBJ } from '@sentry/core';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { devErrorSymbolicationEventProcessor } from '../../src/common/devErrorSymbolicationEventProcessor';
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+ return {
+ ...actual,
+ debug: {
+ error: vi.fn(),
+ },
+ suppressTracing: vi.fn(fn => fn()),
+ };
+});
+
+vi.mock('stacktrace-parser', () => ({
+ parse: vi.fn(),
+}));
+
+global.fetch = vi.fn();
+
+describe('devErrorSymbolicationEventProcessor', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ delete (GLOBAL_OBJ as any)._sentryNextJsVersion;
+ delete (GLOBAL_OBJ as any)._sentryBasePath;
+ });
+
+ describe('Next.js version handling', () => {
+ it('should return event early when _sentryNextJsVersion is undefined', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const mockHint: EventHint = {
+ originalException: new Error('test error'),
+ };
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = undefined;
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it('should return event early when _sentryNextJsVersion is null', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const mockHint: EventHint = {
+ originalException: new Error('test error'),
+ };
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = null;
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it('should return event early when _sentryNextJsVersion is empty string', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const mockHint: EventHint = {
+ originalException: new Error('test error'),
+ };
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = '';
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('error handling', () => {
+ it('should return original event when no originalException in hint', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const mockHint: EventHint = {};
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = '14.1.0';
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it('should return original event when originalException is not an Error', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const mockHint: EventHint = {
+ originalException: 'string error',
+ };
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = '14.1.0';
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it('should return original event when Error has no stack', async () => {
+ const mockEvent: Event = {
+ exception: {
+ values: [
+ {
+ stacktrace: {
+ frames: [{ filename: 'test.js', lineno: 1 }],
+ },
+ },
+ ],
+ },
+ };
+
+ const errorWithoutStack = new Error('test error');
+ delete errorWithoutStack.stack;
+
+ const mockHint: EventHint = {
+ originalException: errorWithoutStack,
+ };
+
+ (GLOBAL_OBJ as any)._sentryNextJsVersion = '14.1.0';
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result).toBe(mockEvent);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('transaction span filtering', () => {
+ it('should filter out spans with __nextjs_original-stack-frame URLs', async () => {
+ const mockEvent: Event = {
+ type: 'transaction',
+ spans: [
+ {
+ data: {
+ 'http.url': 'http://localhost:3000/__nextjs_original-stack-frame?file=test.js',
+ },
+ },
+ {
+ data: {
+ 'http.url': 'http://localhost:3000/__nextjs_original-stack-frames',
+ },
+ },
+ {
+ data: {
+ 'http.url': 'http://localhost:3000/api/users',
+ },
+ },
+ {
+ data: {
+ 'other.attribute': 'value',
+ },
+ },
+ ] as unknown as SpanJSON[], // :^)
+ };
+
+ const mockHint: EventHint = {};
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result?.spans).toHaveLength(2);
+ expect(result?.spans?.[0]?.data?.['http.url']).toBe('http://localhost:3000/api/users');
+ expect(result?.spans?.[1]?.data?.['other.attribute']).toBe('value');
+ });
+
+ it('should preserve spans without http.url attribute', async () => {
+ const mockEvent: Event = {
+ type: 'transaction',
+ spans: [
+ {
+ data: {
+ 'other.attribute': 'value',
+ },
+ },
+ ] as unknown as SpanJSON[],
+ };
+
+ const mockHint: EventHint = {};
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result?.spans).toHaveLength(1);
+ expect(result?.spans?.[0]?.data?.['other.attribute']).toBe('value');
+ });
+
+ it('should handle spans with non-string http.url attribute', async () => {
+ const mockEvent: Event = {
+ type: 'transaction',
+ spans: [
+ {
+ data: {
+ 'http.url': 123, // non-string
+ },
+ },
+ ] as unknown as SpanJSON[],
+ };
+
+ const mockHint: EventHint = {};
+
+ const result = await devErrorSymbolicationEventProcessor(mockEvent, mockHint);
+
+ expect(result?.spans).toHaveLength(1);
+ });
+ });
+});
diff --git a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
index 813d3c0f8894..9750e4245894 100644
--- a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
+++ b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts
@@ -105,7 +105,6 @@ describe('constructTurbopackConfig', () => {
expect(loader.loader).toBe(windowsLoaderPath);
expect(pathResolveSpy).toHaveBeenCalledWith(expect.any(String), '..', 'loaders', 'valueInjectionLoader.js');
- // Restore the original mock behavior
pathResolveSpy.mockReturnValue('/mocked/path/to/valueInjectionLoader.js');
});
});
@@ -189,7 +188,7 @@ describe('constructTurbopackConfig', () => {
const userNextConfig: NextConfigObject = {
turbopack: {
rules: {
- '**/instrumentation-client.*': existingRule,
+ '**/instrumentation.*': existingRule,
},
},
};
@@ -201,7 +200,19 @@ describe('constructTurbopackConfig', () => {
expect(result).toEqual({
rules: {
- '**/instrumentation-client.*': existingRule,
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': existingRule,
},
});
});
@@ -268,6 +279,458 @@ describe('constructTurbopackConfig', () => {
});
});
});
+
+ describe('additional edge cases', () => {
+ it('should handle undefined turbopack property', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: undefined,
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should handle null turbopack property', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: null as any,
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion: '15.0.0',
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '15.0.0',
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '15.0.0',
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should preserve other turbopack properties when adding rules', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ resolveAlias: {
+ '@': './src',
+ '@components': './src/components',
+ },
+ rules: {
+ '*.css': ['css-loader'],
+ },
+ },
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ nextJsVersion: '14.0.0',
+ });
+
+ expect(result).toEqual({
+ resolveAlias: {
+ '@': './src',
+ '@components': './src/components',
+ },
+ rules: {
+ '*.css': ['css-loader'],
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should handle empty rules object in existing turbopack config', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ rules: {},
+ },
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should handle multiple colliding instrumentation rules', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ rules: {
+ '**/instrumentation.*': ['existing-loader'],
+ '**/instrumentation-client.*': { loaders: ['client-loader'] },
+ },
+ },
+ };
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ nextJsVersion: '14.0.0',
+ });
+
+ // Should preserve existing rules and not add new ones
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation.*': ['existing-loader'],
+ '**/instrumentation-client.*': { loaders: ['client-loader'] },
+ },
+ });
+ });
+ });
+
+ describe('Next.js version injection', () => {
+ it('should create turbopack config with Next.js version rule when nextJsVersion is provided', () => {
+ const userNextConfig: NextConfigObject = {};
+ const nextJsVersion = '15.1.0';
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion,
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should create turbopack config with both manifest and Next.js version rules', () => {
+ const userNextConfig: NextConfigObject = {};
+ const nextJsVersion = '14.2.5';
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ nextJsVersion,
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should merge Next.js version rule with existing turbopack config', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ resolveAlias: {
+ '@': './src',
+ },
+ rules: {
+ '*.test.js': ['jest-loader'],
+ },
+ },
+ };
+ const nextJsVersion = '15.0.0';
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion,
+ });
+
+ expect(result).toEqual({
+ resolveAlias: {
+ '@': './src',
+ },
+ rules: {
+ '*.test.js': ['jest-loader'],
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+
+ it('should handle different Next.js version formats', () => {
+ const userNextConfig: NextConfigObject = {};
+ const testVersions = ['13.0.0', '14.1.2-canary.1', '15.0.0-rc.1', '16.0.0'];
+
+ testVersions.forEach(version => {
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion: version,
+ });
+
+ expect(result.rules).toBeDefined();
+ expect(result.rules!['**/instrumentation.*']).toBeDefined();
+
+ const rule = result.rules!['**/instrumentation.*'];
+ const ruleWithLoaders = rule as { loaders: Array<{ loader: string; options: any }> };
+ expect(ruleWithLoaders.loaders[0]!.options.values._sentryNextJsVersion).toBe(version);
+ });
+ });
+
+ it('should not create Next.js version rule when nextJsVersion is undefined', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion: undefined,
+ });
+
+ expect(result).toEqual({});
+ });
+
+ it('should not create Next.js version rule when nextJsVersion is empty string', () => {
+ const userNextConfig: NextConfigObject = {};
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion: '',
+ });
+
+ expect(result).toEqual({});
+ });
+
+ it('should not override existing instrumentation rule when nextJsVersion is provided', () => {
+ const existingRule = {
+ loaders: [
+ {
+ loader: '/existing/loader.js',
+ options: { custom: 'value' },
+ },
+ ],
+ };
+
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ rules: {
+ '**/instrumentation.*': existingRule,
+ },
+ },
+ };
+ const nextJsVersion = '15.1.0';
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ nextJsVersion,
+ });
+
+ expect(result).toEqual({
+ rules: {
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': existingRule,
+ },
+ });
+ });
+
+ it('should handle all parameters together with existing config', () => {
+ const userNextConfig: NextConfigObject = {
+ turbopack: {
+ resolveAlias: {
+ '@components': './src/components',
+ },
+ rules: {
+ '*.scss': ['sass-loader'],
+ },
+ },
+ };
+ const nextJsVersion = '14.0.0';
+
+ const result = constructTurbopackConfig({
+ userNextConfig,
+ routeManifest: mockRouteManifest,
+ nextJsVersion,
+ });
+
+ expect(result).toEqual({
+ resolveAlias: {
+ '@components': './src/components',
+ },
+ rules: {
+ '*.scss': ['sass-loader'],
+ '**/instrumentation-client.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ },
+ '**/instrumentation.*': {
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: nextJsVersion,
+ },
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ });
});
describe('safelyAddTurbopackRule', () => {
@@ -440,4 +903,101 @@ describe('safelyAddTurbopackRule', () => {
});
});
});
+
+ describe('additional edge cases for safelyAddTurbopackRule', () => {
+ it('should handle falsy values in rules', () => {
+ const existingRules = {
+ '*.css': ['css-loader'],
+ '*.disabled': false as any,
+ '*.null': null as any,
+ } as any;
+
+ const result = safelyAddTurbopackRule(existingRules, {
+ matcher: '*.test.js',
+ rule: mockRule,
+ });
+
+ expect(result).toEqual({
+ '*.css': ['css-loader'],
+ '*.disabled': false,
+ '*.null': null,
+ '*.test.js': mockRule,
+ } as any);
+ });
+
+ it('should handle undefined rule value', () => {
+ const existingRules = {
+ '*.css': ['css-loader'],
+ };
+
+ const result = safelyAddTurbopackRule(existingRules, {
+ matcher: '*.test.js',
+ rule: undefined as any,
+ });
+
+ expect(result).toEqual({
+ '*.css': ['css-loader'],
+ '*.test.js': undefined,
+ });
+ });
+
+ it('should handle complex matchers with special characters', () => {
+ const existingRules = {};
+ const complexMatcher = '**/node_modules/**/*.{js,ts}';
+
+ const result = safelyAddTurbopackRule(existingRules, {
+ matcher: complexMatcher,
+ rule: mockRule,
+ });
+
+ expect(result).toEqual({
+ [complexMatcher]: mockRule,
+ });
+ });
+
+ it('should preserve nested rule objects', () => {
+ const complexRule = {
+ loaders: [
+ {
+ loader: '/test/loader.js',
+ options: {
+ nested: {
+ deep: 'value',
+ array: [1, 2, 3],
+ },
+ },
+ },
+ ],
+ as: 'javascript/auto',
+ condition: 'test-condition',
+ };
+
+ const result = safelyAddTurbopackRule(undefined, {
+ matcher: '*.complex.js',
+ rule: complexRule,
+ });
+
+ expect(result).toEqual({
+ '*.complex.js': complexRule,
+ });
+ });
+
+ it('should handle matcher that matches an object property key pattern', () => {
+ const existingRules = {
+ '*.test': ['test-loader'],
+ 'test.*': ['pattern-loader'],
+ };
+
+ const result = safelyAddTurbopackRule(existingRules, {
+ matcher: '*.test',
+ rule: mockRule,
+ });
+
+ // Should not override the existing rule
+ expect(result).toEqual({
+ '*.test': ['test-loader'],
+ 'test.*': ['pattern-loader'],
+ });
+ });
+ });
});
diff --git a/packages/nextjs/test/config/turbopack/generateValueInjectionRules.test.ts b/packages/nextjs/test/config/turbopack/generateValueInjectionRules.test.ts
new file mode 100644
index 000000000000..74e3d24b2cc4
--- /dev/null
+++ b/packages/nextjs/test/config/turbopack/generateValueInjectionRules.test.ts
@@ -0,0 +1,338 @@
+import * as path from 'path';
+import { describe, expect, it, vi } from 'vitest';
+import type { RouteManifest } from '../../../src/config/manifest/types';
+import { generateValueInjectionRules } from '../../../src/config/turbopack/generateValueInjectionRules';
+
+// Mock path.resolve to return a predictable loader path
+vi.mock('path', async () => {
+ const actual = await vi.importActual('path');
+ return {
+ ...actual,
+ resolve: vi.fn().mockReturnValue('/mocked/path/to/valueInjectionLoader.js'),
+ };
+});
+
+describe('generateValueInjectionRules', () => {
+ const mockRouteManifest: RouteManifest = {
+ dynamicRoutes: [{ path: '/users/[id]', regex: '/users/([^/]+)', paramNames: ['id'] }],
+ staticRoutes: [
+ { path: '/users', regex: '/users' },
+ { path: '/api/health', regex: '/api/health' },
+ ],
+ };
+
+ describe('with no inputs', () => {
+ it('should return empty array when no inputs are provided', () => {
+ const result = generateValueInjectionRules({});
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array when inputs are undefined', () => {
+ const result = generateValueInjectionRules({
+ routeManifest: undefined,
+ nextJsVersion: undefined,
+ });
+
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('with nextJsVersion only', () => {
+ it('should generate client and server rules when nextJsVersion is provided', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ });
+
+ expect(result).toHaveLength(2);
+
+ // Client rule
+ const clientRule = result.find(rule => rule.matcher === '**/instrumentation-client.*');
+ expect(clientRule).toBeDefined();
+ expect(clientRule?.rule).toEqual({
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ },
+ },
+ },
+ ],
+ });
+
+ // Server rule
+ const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
+ expect(serverRule).toBeDefined();
+ expect(serverRule?.rule).toEqual({
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ },
+ },
+ },
+ ],
+ });
+ });
+ });
+
+ describe('with routeManifest only', () => {
+ it('should generate only client rule when routeManifest is provided', () => {
+ const result = generateValueInjectionRules({
+ routeManifest: mockRouteManifest,
+ });
+
+ expect(result).toHaveLength(1);
+
+ // Only client rule should exist
+ const clientRule = result.find(rule => rule.matcher === '**/instrumentation-client.*');
+ expect(clientRule).toBeDefined();
+ expect(clientRule?.rule).toEqual({
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ });
+
+ // Server rule should not exist
+ const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
+ expect(serverRule).toBeUndefined();
+ });
+
+ it('should handle empty route manifest', () => {
+ const emptyManifest: RouteManifest = {
+ dynamicRoutes: [],
+ staticRoutes: [],
+ };
+
+ const result = generateValueInjectionRules({
+ routeManifest: emptyManifest,
+ });
+
+ expect(result).toHaveLength(1);
+
+ const clientRule = result.find(rule => rule.matcher === '**/instrumentation-client.*');
+ expect(clientRule?.rule).toMatchObject({
+ loaders: [
+ {
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(emptyManifest),
+ },
+ },
+ },
+ ],
+ });
+ });
+
+ it('should handle complex route manifest', () => {
+ const complexManifest: RouteManifest = {
+ dynamicRoutes: [
+ { path: '/users/[id]', regex: '/users/([^/]+)', paramNames: ['id'] },
+ { path: '/posts/[...slug]', regex: '/posts/(.*)', paramNames: ['slug'] },
+ { path: '/category/[category]/[id]', regex: '/category/([^/]+)/([^/]+)', paramNames: ['category', 'id'] },
+ ],
+ staticRoutes: [
+ { path: '/', regex: '/' },
+ { path: '/about', regex: '/about' },
+ { path: '/api/health', regex: '/api/health' },
+ { path: '/api/users', regex: '/api/users' },
+ ],
+ };
+
+ const result = generateValueInjectionRules({
+ routeManifest: complexManifest,
+ });
+
+ expect(result).toHaveLength(1);
+
+ const clientRule = result.find(rule => rule.matcher === '**/instrumentation-client.*');
+ expect(clientRule?.rule).toMatchObject({
+ loaders: [
+ {
+ options: {
+ values: {
+ _sentryRouteManifest: JSON.stringify(complexManifest),
+ },
+ },
+ },
+ ],
+ });
+ });
+ });
+
+ describe('with both nextJsVersion and routeManifest', () => {
+ it('should generate both client and server rules with combined values', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ routeManifest: mockRouteManifest,
+ });
+
+ expect(result).toHaveLength(2);
+
+ // Client rule should have both values
+ const clientRule = result.find(rule => rule.matcher === '**/instrumentation-client.*');
+ expect(clientRule).toBeDefined();
+ expect(clientRule?.rule).toEqual({
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ _sentryRouteManifest: JSON.stringify(mockRouteManifest),
+ },
+ },
+ },
+ ],
+ });
+
+ // Server rule should have only nextJsVersion
+ const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
+ expect(serverRule).toBeDefined();
+ expect(serverRule?.rule).toEqual({
+ loaders: [
+ {
+ loader: '/mocked/path/to/valueInjectionLoader.js',
+ options: {
+ values: {
+ _sentryNextJsVersion: '14.0.0',
+ },
+ },
+ },
+ ],
+ });
+ });
+
+ it('should handle all combinations of truthy and falsy values', () => {
+ const testCases = [
+ { nextJsVersion: '14.0.0', routeManifest: mockRouteManifest, expectedRules: 2 },
+ { nextJsVersion: '', routeManifest: mockRouteManifest, expectedRules: 1 },
+ { nextJsVersion: '14.0.0', routeManifest: undefined, expectedRules: 2 },
+ { nextJsVersion: '', routeManifest: undefined, expectedRules: 0 },
+ ];
+
+ testCases.forEach(({ nextJsVersion, routeManifest, expectedRules }) => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: nextJsVersion || undefined,
+ routeManifest,
+ });
+
+ expect(result).toHaveLength(expectedRules);
+ });
+ });
+ });
+
+ describe('path resolution', () => {
+ it('should call path.resolve with correct arguments', () => {
+ const pathResolveSpy = vi.spyOn(path, 'resolve');
+
+ generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ });
+
+ expect(pathResolveSpy).toHaveBeenCalledWith(expect.any(String), '..', 'loaders', 'valueInjectionLoader.js');
+ });
+
+ it('should use the resolved path in loader configuration', () => {
+ const customLoaderPath = '/custom/path/to/loader.js';
+ const pathResolveSpy = vi.spyOn(path, 'resolve');
+ pathResolveSpy.mockReturnValue(customLoaderPath);
+
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ });
+
+ expect(result).toHaveLength(2);
+
+ result.forEach(rule => {
+ const ruleWithLoaders = rule.rule as unknown as { loaders: Array<{ loader: string }> };
+ expect(ruleWithLoaders.loaders[0]?.loader).toBe(customLoaderPath);
+ });
+ });
+ });
+
+ describe('rule structure validation', () => {
+ it('should generate rules with correct structure', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ routeManifest: mockRouteManifest,
+ });
+
+ result.forEach(rule => {
+ // Validate top-level structure
+ expect(rule).toHaveProperty('matcher');
+ expect(rule).toHaveProperty('rule');
+ expect(typeof rule.matcher).toBe('string');
+
+ // Validate rule structure
+ const ruleObj = rule.rule as unknown as { loaders: Array };
+ expect(ruleObj).toHaveProperty('loaders');
+ expect(Array.isArray(ruleObj.loaders)).toBe(true);
+ expect(ruleObj.loaders).toHaveLength(1);
+
+ // Validate loader structure
+ const loader = ruleObj.loaders[0];
+ expect(loader).toHaveProperty('loader');
+ expect(loader).toHaveProperty('options');
+ expect(typeof loader.loader).toBe('string');
+ expect(loader.options).toHaveProperty('values');
+ expect(typeof loader.options.values).toBe('object');
+ });
+ });
+
+ it('should generate different matchers for client and server rules', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ });
+
+ const matchers = result.map(rule => rule.matcher);
+ expect(matchers).toContain('**/instrumentation-client.*');
+ expect(matchers).toContain('**/instrumentation.*');
+ expect(matchers).toHaveLength(2);
+ });
+
+ it('should ensure client rules come before server rules', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '14.0.0',
+ });
+
+ expect(result).toHaveLength(2);
+ expect(result[0]?.matcher).toBe('**/instrumentation-client.*');
+ expect(result[1]?.matcher).toBe('**/instrumentation.*');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle zero-length nextJsVersion', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: '',
+ });
+
+ expect(result).toEqual([]);
+ });
+
+ it('should handle whitespace-only nextJsVersion', () => {
+ const result = generateValueInjectionRules({
+ nextJsVersion: ' ',
+ });
+
+ expect(result).toHaveLength(2);
+
+ result.forEach(rule => {
+ const ruleObj = rule.rule as unknown as { loaders: Array<{ options: { values: any } }> };
+ expect(ruleObj.loaders[0]?.options.values._sentryNextJsVersion).toBe(' ');
+ });
+ });
+ });
+});
diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts
index 6d478ea912e9..cf581bd63b66 100644
--- a/packages/node-core/src/index.ts
+++ b/packages/node-core/src/index.ts
@@ -21,6 +21,7 @@ export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejec
export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr';
export { spotlightIntegration } from './integrations/spotlight';
+export { systemErrorIntegration } from './integrations/systemError';
export { childProcessIntegration } from './integrations/childProcess';
export { createSentryWinstonTransport } from './integrations/winston';
diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts
new file mode 100644
index 000000000000..f1fd3f4db0dc
--- /dev/null
+++ b/packages/node-core/src/integrations/systemError.ts
@@ -0,0 +1,76 @@
+import * as util from 'node:util';
+import { defineIntegration } from '@sentry/core';
+
+const INTEGRATION_NAME = 'NodeSystemError';
+
+type SystemErrorContext = {
+ dest?: string; // If present, the file path destination when reporting a file system error
+ errno: number; // The system-provided error number
+ path?: string; // If present, the file path when reporting a file system error
+};
+
+type SystemError = Error & SystemErrorContext;
+
+function isSystemError(error: unknown): error is SystemError {
+ if (!(error instanceof Error)) {
+ return false;
+ }
+
+ if (!('errno' in error) || typeof error.errno !== 'number') {
+ return false;
+ }
+
+ // Appears this is the recommended way to check for Node.js SystemError
+ // https://github.com/nodejs/node/issues/46869
+ return util.getSystemErrorMap().has(error.errno);
+}
+
+type Options = {
+ /**
+ * If true, includes the `path` and `dest` properties in the error context.
+ */
+ includePaths?: boolean;
+};
+
+/**
+ * Captures context for Node.js SystemError errors.
+ */
+export const systemErrorIntegration = defineIntegration((options: Options = {}) => {
+ return {
+ name: INTEGRATION_NAME,
+ processEvent: (event, hint, client) => {
+ if (!isSystemError(hint.originalException)) {
+ return event;
+ }
+
+ const error = hint.originalException;
+
+ const errorContext: SystemErrorContext = {
+ ...error,
+ };
+
+ if (!client.getOptions().sendDefaultPii && options.includePaths !== true) {
+ delete errorContext.path;
+ delete errorContext.dest;
+ }
+
+ event.contexts = {
+ ...event.contexts,
+ node_system_error: errorContext,
+ };
+
+ for (const exception of event.exception?.values || []) {
+ if (exception.value) {
+ if (error.path && exception.value.includes(error.path)) {
+ exception.value = exception.value.replace(`'${error.path}'`, '').trim();
+ }
+ if (error.dest && exception.value.includes(error.dest)) {
+ exception.value = exception.value.replace(`'${error.dest}'`, '').trim();
+ }
+ }
+ }
+
+ return event;
+ },
+ };
+});
diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts
index eb2807193b9b..e5b12166d962 100644
--- a/packages/node-core/src/sdk/index.ts
+++ b/packages/node-core/src/sdk/index.ts
@@ -32,6 +32,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
import { processSessionIntegration } from '../integrations/processSession';
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
+import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
import { isCjs } from '../utils/commonjs';
@@ -52,6 +53,7 @@ export function getDefaultIntegrations(): Integration[] {
functionToStringIntegration(),
linkedErrorsIntegration(),
requestDataIntegration(),
+ systemErrorIntegration(),
// Native Wrappers
consoleIntegration(),
httpIntegration(),
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index bba0f98bc75e..da97071bdd32 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -165,6 +165,7 @@ export {
childProcessIntegration,
createSentryWinstonTransport,
SentryContextManager,
+ systemErrorIntegration,
generateInstrumentOnce,
getSentryRelease,
defaultStackParser,
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index 063375fe78a1..447ea338ef55 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -51,8 +51,8 @@
"@sentry/cloudflare": "10.4.0",
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
- "@sentry/rollup-plugin": "^4.0.2",
- "@sentry/vite-plugin": "^4.0.0",
+ "@sentry/rollup-plugin": "^4.1.0",
+ "@sentry/vite-plugin": "^4.1.0",
"@sentry/vue": "10.4.0"
},
"devDependencies": {
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 9ffa02c164d7..35ba4419be63 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -50,11 +50,11 @@
"@opentelemetry/instrumentation": "^0.203.0",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@sentry/browser": "10.4.0",
- "@sentry/cli": "^2.50.2",
+ "@sentry/cli": "^2.51.1",
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
"@sentry/react": "10.4.0",
- "@sentry/vite-plugin": "^4.0.0",
+ "@sentry/vite-plugin": "^4.1.0",
"glob": "11.0.1"
},
"devDependencies": {
diff --git a/packages/remix/package.json b/packages/remix/package.json
index d4ea69813cb3..d4f1d874d04a 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -68,7 +68,7 @@
"@opentelemetry/instrumentation": "^0.203.0",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@remix-run/router": "1.x",
- "@sentry/cli": "^2.50.2",
+ "@sentry/cli": "^2.51.1",
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
"@sentry/react": "10.4.0",
diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json
index 7f8a8109ed6f..58a0debebc04 100644
--- a/packages/solidstart/package.json
+++ b/packages/solidstart/package.json
@@ -69,7 +69,7 @@
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
"@sentry/solid": "10.4.0",
- "@sentry/vite-plugin": "^4.0.0"
+ "@sentry/vite-plugin": "^4.1.0"
},
"devDependencies": {
"@solidjs/router": "^0.13.4",
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index b51f06355d8f..2b432720ca7b 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -52,7 +52,7 @@
"@sentry/core": "10.4.0",
"@sentry/node": "10.4.0",
"@sentry/svelte": "10.4.0",
- "@sentry/vite-plugin": "^4.0.0",
+ "@sentry/vite-plugin": "^4.1.0",
"magic-string": "0.30.7",
"recast": "0.23.11",
"sorcery": "1.0.0"
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 043bad823bb3..56400dcc5423 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -113,6 +113,7 @@ export {
startSession,
startSpan,
startSpanManual,
+ systemErrorIntegration,
tediousIntegration,
trpcMiddleware,
updateSpanName,
diff --git a/yarn.lock b/yarn.lock
index a79397ae7beb..4175e91b712c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6924,132 +6924,73 @@
mitt "^3.0.0"
"@sentry-internal/test-utils@link:dev-packages/test-utils":
- version "10.2.0"
+ version "10.4.0"
dependencies:
express "^4.21.1"
-"@sentry/babel-plugin-component-annotate@4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.0.0.tgz#2f4dfeabba28a76b5a1b32a1058386e52f32634f"
- integrity sha512-1sozj4esnQBhJ2QO4imiLMl1858StkLjUxFF1KxgX/X1uEL/QlW2MYL8CKzbLeACy1SkR9h4V8GXSZvCnci5Dw==
-
-"@sentry/babel-plugin-component-annotate@4.0.2":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.0.2.tgz#7c8eb80a38b5e6b4c4cea4c391d07581020c91e4"
- integrity sha512-Nr/VamvpQs6w642EI5t+qaCUGnVEro0qqk+S8XO1gc8qSdpc8kkZJFnUk7ozAr+ljYWGfVgWXrxI9lLiriLsRA==
-
-"@sentry/bundler-plugin-core@4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.0.0.tgz#564463cf53f869496ab5d4986e97f86618a67677"
- integrity sha512-dTdbcctT5MJUwdbttZm2zomO+ui1F062ZIkogHeHqlA938Fwd1+9JIJ328+XL4XdcUG2yiFAZBWUPW3bYwoN9A==
- dependencies:
- "@babel/core" "^7.18.5"
- "@sentry/babel-plugin-component-annotate" "4.0.0"
- "@sentry/cli" "^2.49.0"
- dotenv "^16.3.1"
- find-up "^5.0.0"
- glob "^9.3.2"
- magic-string "0.30.8"
- unplugin "1.0.1"
+"@sentry/babel-plugin-component-annotate@4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.0.tgz#6e7168f5fa59f53ac4b68e3f79c5fd54adc13f2e"
+ integrity sha512-UkcnqC7Bp9ODyoBN7BKcRotd1jz/I2vyruE/qjNfRC7UnP+jIRItUWYaXxQPON1fTw+N+egKdByk0M1y2OPv/Q==
-"@sentry/bundler-plugin-core@4.0.2":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.0.2.tgz#2e106ac564d2e4e83e8dbc84f1e84f4eed1d6dde"
- integrity sha512-LeARs8qHhEw19tk+KZd9DDV+Rh/UeapIH0+C09fTmff9p8Y82Cj89pEQ2a1rdUiF/oYIjQX45vnZscB7ra42yw==
+"@sentry/bundler-plugin-core@4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.0.tgz#c1b2f7a890a44e5ac5decc984a133aacf6147dd4"
+ integrity sha512-/5XBtCF6M+9frEXrrvfSWOdOC2q6I1L7oY7qbUVegNkp3kYVGihNZZnJIXGzo9rmwnA0IV7jI3o0pF/HDRqPeA==
dependencies:
"@babel/core" "^7.18.5"
- "@sentry/babel-plugin-component-annotate" "4.0.2"
- "@sentry/cli" "^2.49.0"
+ "@sentry/babel-plugin-component-annotate" "4.1.0"
+ "@sentry/cli" "^2.51.0"
dotenv "^16.3.1"
find-up "^5.0.0"
glob "^9.3.2"
magic-string "0.30.8"
unplugin "1.0.1"
-"@sentry/cli-darwin@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.49.0.tgz#290657e5840b360cb8ca25c8a78f8c0f15c66b03"
- integrity sha512-bgowyDeFuXbjkGq1ZKqcWhmzgfBe7oKIXYWJOOps4+32QfG+YsrdNnottHS01td3bzrJq0QnHj8H12fA81DqrA==
-
-"@sentry/cli-darwin@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.50.2.tgz#fcf924fcc02cfa54748ff07a380334e533635c74"
- integrity sha512-0Pjpl0vQqKhwuZm19z6AlEF+ds3fJg1KWabv8WzGaSc/fwxMEwjFwOZj+IxWBJPV578cXXNvB39vYjjpCH8j7A==
-
-"@sentry/cli-linux-arm64@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.49.0.tgz#a732004d7131f7e7b44f6a64abdccc36efb35d52"
- integrity sha512-dqxsDUd76aDm03fUwUOs5BR7RHLpSb2EH/B1hlWm0mFvo9uY907XxW9wDFx/qDpCdmpC0aF+lF/lOBOrG9B5Fg==
-
-"@sentry/cli-linux-arm64@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.50.2.tgz#ac9e6dba42095832bac8084abab4b86fdd2956f3"
- integrity sha512-03Cj215M3IdoHAwevCxm5oOm9WICFpuLR05DQnODFCeIUsGvE1pZsc+Gm0Ky/ZArq2PlShBJTpbHvXbCUka+0w==
-
-"@sentry/cli-linux-arm@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.49.0.tgz#73719561510df3369e05e9a4898b4e43b8753e4c"
- integrity sha512-RBDIjIGmNsFw+a6vAt6m3D7ROKsMEB9i3u+UuIRxk0/DyHTcfVWxnK/ScPXGILM6PxQ2XOBfOKad0mmiDHBzZA==
-
-"@sentry/cli-linux-arm@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.50.2.tgz#835acd53ca83f6be9fc0d3d85a3cd4c694051bce"
- integrity sha512-jzFwg9AeeuFAFtoCcyaDEPG05TU02uOy1nAX09c1g7FtsyQlPcbhI94JQGmnPzdRjjDmORtwIUiVZQrVTkDM7w==
-
-"@sentry/cli-linux-i686@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.49.0.tgz#8d1bb1378251a3aa995cc4b56bd352fa12a84b66"
- integrity sha512-gDAd5/vJbEhd4Waud0Cd8ZRqLEagDlOvWwNH3KB694EiHJUwzRSiTA1YUVMYGI8Z9UyEA1sKxARwm2Trv99BxA==
-
-"@sentry/cli-linux-i686@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.50.2.tgz#72f0e4bc1c515754aa11225efce711a24fb53524"
- integrity sha512-J+POvB34uVyHbIYF++Bc/OCLw+gqKW0H/y/mY7rRZCiocgpk266M4NtsOBl6bEaurMx1D+BCIEjr4nc01I/rqA==
-
-"@sentry/cli-linux-x64@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.49.0.tgz#7bf58fb7005c89fdde4e1262d5ed35e23065aceb"
- integrity sha512-mbohGvPNhHjUciYNXzkt9TYUebTmxeAp9v9JfLSb/Soz6fubKwEHhpRJuz1zASxVWIR4PuqkePchqN5zhcLC0A==
-
-"@sentry/cli-linux-x64@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.50.2.tgz#d06f8ffd65871b1373a0d2228ab254d9456a615c"
- integrity sha512-81yQVRLj8rnuHoYcrM7QbOw8ubA3weiMdPtTxTim1s6WExmPgnPTKxLCr9xzxGJxFdYo3xIOhtf5JFpUX/3j4A==
-
-"@sentry/cli-win32-arm64@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.49.0.tgz#2bf6dd911acbe3ddb02eec0afb4301bb8fb25b53"
- integrity sha512-3zwvsp61EPpSuGpGdXY4JelVJmNEjoj4vn5m6EFoOtk7OUI5/VFqqR4wchjy9Hjm3Eh6MB5K+KTKXs4W2p18ng==
-
-"@sentry/cli-win32-arm64@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.50.2.tgz#4bd7a140367c17f77d621903cfe0914232108657"
- integrity sha512-QjentLGvpibgiZlmlV9ifZyxV73lnGH6pFZWU5wLeRiaYKxWtNrrHpVs+HiWlRhkwQ0mG1/S40PGNgJ20DJ3gA==
-
-"@sentry/cli-win32-i686@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.49.0.tgz#32e31472ae6c5f69e538a4061d651937fcb8f14a"
- integrity sha512-2oWaNl6z0BaOCAjM1Jxequfgjod3XO6wothxow4kA8e9+43JLhgarSdpwJPgQjcVyxjygwQ3/jKPdUFh0qNOmg==
-
-"@sentry/cli-win32-i686@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.50.2.tgz#1eb997cf780c396446cdd8e63c6d4309894465e8"
- integrity sha512-UkBIIzkQkQ1UkjQX8kHm/+e7IxnEhK6CdgSjFyNlxkwALjDWHJjMztevqAPz3kv4LdM6q1MxpQ/mOqXICNhEGg==
-
-"@sentry/cli-win32-x64@2.49.0":
- version "2.49.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.49.0.tgz#86aab38cb41f885914d7c99ceaab7b6ce52c72c6"
- integrity sha512-dR4ulyrA6ZT7x7cg4Rwm0tcHf4TZz5QO6t1W1jX6uJ9n/U0bOSqSFZHNf/RryiUzQE1g8LBthOYyKGMkET6T8w==
-
-"@sentry/cli-win32-x64@2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.50.2.tgz#1d0c106125b6dc87f3a598ac02519c699f17a6c0"
- integrity sha512-tE27pu1sRRub1Jpmemykv3QHddBcyUk39Fsvv+n4NDpQyMgsyVPcboxBZyby44F0jkpI/q3bUH2tfCB1TYDNLg==
-
-"@sentry/cli@^2.49.0", "@sentry/cli@^2.50.2":
- version "2.50.2"
- resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.50.2.tgz#9fb90f2ae6648fc0104f0ca7f528d2e19ee0ecae"
- integrity sha512-m1L9shxutF3WHSyNld6Y1vMPoXfEyQhoRh1V3SYSdl+4AB40U+zr2sRzFa2OPm7XP4zYNaWuuuHLkY/iHITs8Q==
+"@sentry/cli-darwin@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.51.1.tgz#3a1db065651893f72dad3a502b2d7c2f5e6a7dd8"
+ integrity sha512-R1u8IQdn/7Rr8sf6bVVr0vJT4OqwCFdYsS44Y3OoWGVJW2aAQTWRJOTlV4ueclVLAyUQzmgBjfR8AtiUhd/M5w==
+
+"@sentry/cli-linux-arm64@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.51.1.tgz#b4c957a06bafc13623c48971eadb0cff7d3662a3"
+ integrity sha512-nvA/hdhsw4bKLhslgbBqqvETjXwN1FVmwHLOrRvRcejDO6zeIKUElDiL5UOjGG0NC+62AxyNw5ri8Wzp/7rg9Q==
+
+"@sentry/cli-linux-arm@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.51.1.tgz#f761d0c58d27be503471cee4ffc41875a7d9430b"
+ integrity sha512-Klro17OmSSKOOSaxVKBBNPXet2+HrIDZUTSp8NRl4LQsIubdc1S/aQ79cH/g52Muwzpl3aFwPxyXw+46isfEgA==
+
+"@sentry/cli-linux-i686@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.51.1.tgz#62baaf83c5995e478186289a45315d0acd5bd3bf"
+ integrity sha512-jp4TmR8VXBdT9dLo6mHniQHN0xKnmJoPGVz9h9VDvO2Vp/8o96rBc555D4Am5wJOXmfuPlyjGcmwHlB3+kQRWw==
+
+"@sentry/cli-linux-x64@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.51.1.tgz#0010fe24ad8ef492a917c12feb351ba768e72603"
+ integrity sha512-JuLt0MXM2KHNFmjqXjv23sly56mJmUQzGBWktkpY3r+jE08f5NLKPd5wQ6W/SoLXGIOKnwLz0WoUg7aBVyQdeQ==
+
+"@sentry/cli-win32-arm64@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.51.1.tgz#0894f9a91e6ecb3021ca09fe644f995ff4ff826d"
+ integrity sha512-PiwjTdIFDazTQCTyDCutiSkt4omggYSKnO3HE1+LDjElsFrWY9pJs4fU3D40WAyE2oKu0MarjNH/WxYGdqEAlg==
+
+"@sentry/cli-win32-i686@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.51.1.tgz#6a6c6402cdce4fd038716b2c1e0bfa788b54f3e9"
+ integrity sha512-TMvZZpeiI2HmrDFNVQ0uOiTuYKvjEGOZdmUxe3WlhZW82A/2Oka7sQ24ljcOovbmBOj5+fjCHRUMYvLMCWiysA==
+
+"@sentry/cli-win32-x64@2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.51.1.tgz#d361e37146c9269d40c37459271a6c2cfa1fa8a6"
+ integrity sha512-v2hreYUPPTNK1/N7+DeX7XBN/zb7p539k+2Osf0HFyVBaoUC3Y3+KBwSf4ASsnmgTAK7HCGR+X0NH1vP+icw4w==
+
+"@sentry/cli@^2.51.0", "@sentry/cli@^2.51.1":
+ version "2.51.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.51.1.tgz#c6bdc6025e8f600e44fc76f8274c369aeb5d4df4"
+ integrity sha512-FU+54kNcKJABU0+ekvtnoXHM9zVrDe1zXVFbQT7mS0On0m1P0zFRGdzbnWe2XzpzuEAJXtK6aog/W+esRU9AIA==
dependencies:
https-proxy-agent "^5.0.0"
node-fetch "^2.6.7"
@@ -7057,37 +6998,37 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
optionalDependencies:
- "@sentry/cli-darwin" "2.50.2"
- "@sentry/cli-linux-arm" "2.50.2"
- "@sentry/cli-linux-arm64" "2.50.2"
- "@sentry/cli-linux-i686" "2.50.2"
- "@sentry/cli-linux-x64" "2.50.2"
- "@sentry/cli-win32-arm64" "2.50.2"
- "@sentry/cli-win32-i686" "2.50.2"
- "@sentry/cli-win32-x64" "2.50.2"
-
-"@sentry/rollup-plugin@^4.0.2":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-4.0.2.tgz#4fda1f2456cdbef7f7527921da93a92c35e67612"
- integrity sha512-p8cbo34m8qcJx5LDo1hPiMlR8aSKMyVewBGBV+nGrh6cgeBUOXu8DpIVD0piOna2igon3jyUD6ydrxUtcM5/Qw==
+ "@sentry/cli-darwin" "2.51.1"
+ "@sentry/cli-linux-arm" "2.51.1"
+ "@sentry/cli-linux-arm64" "2.51.1"
+ "@sentry/cli-linux-i686" "2.51.1"
+ "@sentry/cli-linux-x64" "2.51.1"
+ "@sentry/cli-win32-arm64" "2.51.1"
+ "@sentry/cli-win32-i686" "2.51.1"
+ "@sentry/cli-win32-x64" "2.51.1"
+
+"@sentry/rollup-plugin@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-4.1.0.tgz#3948d067bd7cf8a61904b4042190dc9f6888bceb"
+ integrity sha512-HDwWgQRH7JhG15N1Y4XmPik/Qk03TGbiupDkZ8WL+8257BuyQE+s6feJJGCEUoWwROED+jvsFNvWvT2tqnILrw==
dependencies:
- "@sentry/bundler-plugin-core" "4.0.2"
+ "@sentry/bundler-plugin-core" "4.1.0"
unplugin "1.0.1"
-"@sentry/vite-plugin@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-4.0.0.tgz#ac2780e1d2d88371b0a4a6dc834eadc8bc92d8ea"
- integrity sha512-JX5irzvyoOSKto0U0eXDqigsTXdXnPRQaAms/kcU6A6Bf+WaPfCTE5NrJWg6ZeLvi7GiPWch11OO+TB6ZN8RKA==
+"@sentry/vite-plugin@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-4.1.0.tgz#a94eaf2a294b9f16dec99b088cb05d37b364dcf5"
+ integrity sha512-uLZxOAW79sOQH77yWiQct8f3i+LUi36wn2fK62cejZfrGaHu5P+9R4f0Es1L70I3MrsPXOvJ0A6r5PkVS9562g==
dependencies:
- "@sentry/bundler-plugin-core" "4.0.0"
+ "@sentry/bundler-plugin-core" "4.1.0"
unplugin "1.0.1"
-"@sentry/webpack-plugin@^4.0.2":
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.0.2.tgz#be90c73145d8001bc72c73e4eab83d2b1a28d330"
- integrity sha512-UklVtG7Iiw+AvcL0PfiiyW/u3XT+joDAMDvWbx90rFhVSU10ENW5AV5y4pC41qChqEu3P1eBFdaSxg+kdLeqvw==
+"@sentry/webpack-plugin@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-4.1.0.tgz#e95e2dcd10e71dc8c3a16ba5cad9153f5e78c3bc"
+ integrity sha512-YqfDfyGAuT/9YW1kgAPfD7kGUKQCh1E5co+qMdToxi/Mz4xsWJY02rFS5GrJixYktYJfSMze8NiRr89yJMxYHw==
dependencies:
- "@sentry/bundler-plugin-core" "4.0.2"
+ "@sentry/bundler-plugin-core" "4.1.0"
unplugin "1.0.1"
uuid "^9.0.0"
@@ -28607,6 +28548,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"