diff --git a/.craft.yml b/.craft.yml
index 44d245311312..ffc85c13c90b 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -142,26 +142,11 @@ targets:
id: '@sentry-internal/eslint-config-sdk'
includeNames: /^sentry-internal-eslint-config-sdk-\d.*\.tgz$/
- # TODO(v9): Remove this target
# NOTE: We publish the v8 layer under its own name so people on v8 can still get patches
# whenever we release a new v8 version—otherwise we would overwrite the current major lambda layer.
- name: aws-lambda-layer
includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/
- layerName: SentryNodeServerlessSDKv8
- compatibleRuntimes:
- - name: node
- versions:
- - nodejs10.x
- - nodejs12.x
- - nodejs14.x
- - nodejs16.x
- - nodejs18.x
- - nodejs20.x
- license: MIT
-
- # AWS Lambda Layer target
- - name: aws-lambda-layer
- includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/
+ # TODO(v9): change to `SentryNodeServerlessSDKv8` once v9 is released
layerName: SentryNodeServerlessSDK
compatibleRuntimes:
- name: node
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bf9ba21376bb..dcaf1b4cf902 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,6 +4,8 @@ on:
branches:
- develop
- master
+ - v9
+ - v8
- release/**
pull_request:
merge_group:
@@ -105,7 +107,7 @@ jobs:
outputs:
commit_label: '${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}'
# Note: These next three have to be checked as strings ('true'/'false')!
- is_develop: ${{ github.ref == 'refs/heads/develop' }}
+ is_base_branch: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v9' || github.ref == 'refs/heads/v8'}}
is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }}
changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }}
changed_ci: ${{ steps.changed.outputs.workflow == 'true' }}
@@ -126,7 +128,7 @@ jobs:
timeout-minutes: 15
if: |
needs.job_get_metadata.outputs.changed_any_code == 'true' ||
- needs.job_get_metadata.outputs.is_develop == 'true' ||
+ needs.job_get_metadata.outputs.is_base_branch == 'true' ||
needs.job_get_metadata.outputs.is_release == 'true' ||
(needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false')
steps:
@@ -171,7 +173,7 @@ jobs:
key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT || github.sha }}
# On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it
restore-keys:
- ${{needs.job_get_metadata.outputs.is_develop == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}}
+ ${{needs.job_get_metadata.outputs.is_base_branch == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}}
- name: Build packages
# Set the CODECOV_TOKEN for Bundle Analysis
@@ -219,7 +221,7 @@ jobs:
timeout-minutes: 15
runs-on: ubuntu-20.04
if:
- github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_develop == 'true' ||
+ github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_base_branch == 'true' ||
needs.job_get_metadata.outputs.is_release == 'true'
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
@@ -713,7 +715,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- node: [14, 16, 18, 20, 22]
+ node: [14, 16, '18.20.5', 20, 22]
typescript:
- false
include:
@@ -736,8 +738,8 @@ jobs:
node_version: ${{ matrix.node == 14 && '14' || '' }}
- name: Overwrite typescript version
- if: matrix.typescript
- run: node ./scripts/use-ts-version.js ${{ matrix.typescript }}
+ if: matrix.typescript == '3.8'
+ run: node ./scripts/use-ts-3_8.js
working-directory: dev-packages/node-integration-tests
- name: Run integration tests
@@ -1207,7 +1209,8 @@ jobs:
- name: Run E2E test
working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
timeout-minutes: 10
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert
+ run: |
+ xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert
job_required_jobs_passed:
name: All required jobs passed or were skipped
diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml
index 8f63b6ca063b..776f8135178d 100644
--- a/.github/workflows/enforce-license-compliance.yml
+++ b/.github/workflows/enforce-license-compliance.yml
@@ -2,9 +2,18 @@ name: "CI: Enforce License Compliance"
on:
push:
- branches: [master, develop, release/*]
+ branches:
+ - develop
+ - master
+ - v9
+ - v8
+ - release/**
pull_request:
- branches: [master, develop]
+ branches:
+ - develop
+ - master
+ - v9
+ - v8
jobs:
enforce-license-compliance:
diff --git a/.size-limit.js b/.size-limit.js
index 6e73c9234c09..1da6d6b602be 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -47,14 +47,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '75 KB',
+ limit: '76 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '68 KB',
+ limit: '68.5 KB',
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd6332f4c125..7e248963972f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,192 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 8.54.0
+
+- feat(v8/deps): Upgrade all OpenTelemetry dependencies ([#15098](https://github.com/getsentry/sentry-javascript/pull/15098))
+- fix(node/v8): Add compatibility layer for Prisma v5 ([#15210](https://github.com/getsentry/sentry-javascript/pull/15210))
+
+Work in this release was contributed by @nwalters512. Thank you for your contribution!
+
+## 8.53.0
+
+- feat(v8/nuxt): Add `url` to `SourcemapsUploadOptions` (#15202)
+- fix(v8/react): `fromLocation` can be undefined in Tanstack Router Instrumentation (#15237)
+
+Work in this release was contributed by @tannerlinsley. Thank you for your contribution!
+
+## 8.52.1
+
+- fix(v8/nextjs): Fix nextjs build warning (#15226)
+- ref(v8/browser): Add protocol attributes to resource spans #15224
+- ref(v8/core): Don't set `this.name` to `new.target.prototype.constructor.name` (#15222)
+
+Work in this release was contributed by @Zen-cronic. Thank you for your contribution!
+
+## 8.52.0
+
+### Important Changes
+
+- **feat(solidstart): Add `withSentry` wrapper for SolidStart config ([#15135](https://github.com/getsentry/sentry-javascript/pull/15135))**
+
+To enable the SolidStart SDK, wrap your SolidStart Config with `withSentry`. The `sentrySolidStartVite` plugin is now automatically
+added by `withSentry` and you can pass the Sentry build-time options like this:
+
+```js
+import { defineConfig } from '@solidjs/start/config';
+import { withSentry } from '@sentry/solidstart';
+
+export default defineConfig(
+ withSentry(
+ {
+ /* Your SolidStart config options... */
+ },
+ {
+ // Options for setting up source maps
+ org: process.env.SENTRY_ORG,
+ project: process.env.SENTRY_PROJECT,
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ },
+ ),
+);
+```
+
+With the `withSentry` wrapper, the Sentry server config should not be added to the `public` directory anymore.
+Add the Sentry server config in `src/instrument.server.ts`. Then, the server config will be placed inside the server build output as `instrument.server.mjs`.
+
+Now, there are two options to set up the SDK:
+
+1. **(recommended)** Provide an `--import` CLI flag to the start command like this (path depends on your server setup):
+ `node --import ./.output/server/instrument.server.mjs .output/server/index.mjs`
+2. Add `autoInjectServerSentry: 'top-level-import'` and the Sentry config will be imported at the top of the server entry (comes with tracing limitations)
+ ```js
+ withSentry(
+ {
+ /* Your SolidStart config options... */
+ },
+ {
+ // Optional: Install Sentry with a top-level import
+ autoInjectServerSentry: 'top-level-import',
+ },
+ );
+ ```
+
+### Other Changes
+
+- feat(v8/core): Add client outcomes for breadcrumbs buffer ([#15149](https://github.com/getsentry/sentry-javascript/pull/15149))
+- feat(v8/core): Improve error formatting in ZodErrors integration ([#15155](https://github.com/getsentry/sentry-javascript/pull/15155))
+- fix(v8/bun): Ensure instrumentation of `Bun.serve` survives a server reload ([#15157](https://github.com/getsentry/sentry-javascript/pull/15157))
+- fix(v8/core): Pass `module` into `loadModule` ([#15139](https://github.com/getsentry/sentry-javascript/pull/15139)) (#15166)
+
+Work in this release was contributed by @jahands, @jrandolf, and @nathankleyn. Thank you for your contributions!
+
+## 8.51.0
+
+### Important Changes
+
+- **feat(v8/node): Add `prismaInstrumentation` option to Prisma integration as escape hatch for all Prisma versions ([#15128](https://github.com/getsentry/sentry-javascript/pull/15128))**
+
+ This release adds a compatibility API to add support for Prisma version 6.
+ To capture performance data for Prisma version 6:
+
+ 1. Install the `@prisma/instrumentation` package on version 6.
+ 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option:
+
+ ```js
+ import { PrismaInstrumentation } from '@prisma/instrumentation';
+
+ Sentry.init({
+ integrations: [
+ prismaIntegration({
+ // Override the default instrumentation that Sentry uses
+ prismaInstrumentation: new PrismaInstrumentation(),
+ }),
+ ],
+ });
+ ```
+
+ The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions.
+
+ 1. Remove the `previewFeatures = ["tracing"]` option from the client generator block of your Prisma schema.
+
+### Other Changes
+
+- feat(v8/browser): Add `multiplexedtransport.js` CDN bundle ([#15046](https://github.com/getsentry/sentry-javascript/pull/15046))
+- feat(v8/browser): Add Unleash integration ([#14948](https://github.com/getsentry/sentry-javascript/pull/14948))
+- feat(v8/deno): Deprecate Deno SDK as published on deno.land ([#15121](https://github.com/getsentry/sentry-javascript/pull/15121))
+- feat(v8/sveltekit): Deprecate `fetchProxyScriptNonce` option ([#15011](https://github.com/getsentry/sentry-javascript/pull/15011))
+- fix(v8/aws-lambda): Avoid overwriting root span name ([#15054](https://github.com/getsentry/sentry-javascript/pull/15054))
+- fix(v8/core): `fatal` events should set session as crashed ([#15073](https://github.com/getsentry/sentry-javascript/pull/15073))
+- fix(v8/node/nestjs): Use method on current fastify request ([#15104](https://github.com/getsentry/sentry-javascript/pull/15104))
+
+Work in this release was contributed by @tjhiggins, and @nwalters512. Thank you for your contributions!
+
+## 8.50.0
+
+- feat(v8/react): Add support for React Router `createMemoryRouter` ([#14985](https://github.com/getsentry/sentry-javascript/pull/14985))
+
+## 8.49.0
+
+- feat(v8/browser): Flush offline queue on flush and browser online event ([#14969](https://github.com/getsentry/sentry-javascript/pull/14969))
+- feat(v8/react): Add a `handled` prop to ErrorBoundary ([#14978](https://github.com/getsentry/sentry-javascript/pull/14978))
+- fix(profiling/v8): Don't put `require`, `__filename` and `__dirname` on global object ([#14952](https://github.com/getsentry/sentry-javascript/pull/14952))
+- fix(v8/node): Enforce that ContextLines integration does not leave open file handles ([#14997](https://github.com/getsentry/sentry-javascript/pull/14997))
+- fix(v8/replay): Disable mousemove sampling in rrweb for iOS browsers ([#14944](https://github.com/getsentry/sentry-javascript/pull/14944))
+- fix(v8/sveltekit): Ensure source maps deletion is called after source ma… ([#14963](https://github.com/getsentry/sentry-javascript/pull/14963))
+- fix(v8/vue): Re-throw error when no errorHandler exists ([#14943](https://github.com/getsentry/sentry-javascript/pull/14943))
+
+Work in this release was contributed by @HHK1 and @mstrokin. Thank you for your contributions!
+
+## 8.48.0
+
+### Deprecations
+
+- **feat(v8/core): Deprecate `getDomElement` method ([#14799](https://github.com/getsentry/sentry-javascript/pull/14799))**
+
+ Deprecates `getDomElement`. There is no replacement.
+
+### Other changes
+
+- fix(nestjs/v8): Use correct main/module path in package.json ([#14791](https://github.com/getsentry/sentry-javascript/pull/14791))
+- fix(v8/core): Use consistent `continueTrace` implementation in core ([#14819](https://github.com/getsentry/sentry-javascript/pull/14819))
+- fix(v8/node): Correctly resolve debug IDs for ANR events with custom appRoot ([#14823](https://github.com/getsentry/sentry-javascript/pull/14823))
+- fix(v8/node): Ensure `NODE_OPTIONS` is not passed to worker threads ([#14825](https://github.com/getsentry/sentry-javascript/pull/14825))
+- fix(v8/angular): Fall back to element `tagName` when name is not provided to `TraceDirective` ([#14828](https://github.com/getsentry/sentry-javascript/pull/14828))
+- fix(aws-lambda): Remove version suffix from lambda layer ([#14843](https://github.com/getsentry/sentry-javascript/pull/14843))
+- fix(v8/node): Ensure express requests are properly handled ([#14851](https://github.com/getsentry/sentry-javascript/pull/14851))
+- feat(v8/node): Add `openTelemetrySpanProcessors` option ([#14853](https://github.com/getsentry/sentry-javascript/pull/14853))
+- fix(v8/react): Use `Set` as the `allRoutes` container. ([#14878](https://github.com/getsentry/sentry-javascript/pull/14878)) (#14884)
+- fix(v8/react): Improve handling of routes nested under path="/" ([#14897](https://github.com/getsentry/sentry-javascript/pull/14897))
+- feat(v8/core): Add `normalizedRequest` to `samplingContext` ([#14903](https://github.com/getsentry/sentry-javascript/pull/14903))
+- fix(v8/feedback): Avoid lazy loading code for `syncFeedbackIntegration` ([#14918](https://github.com/getsentry/sentry-javascript/pull/14918))
+
+Work in this release was contributed by @arturovt. Thank you for your contribution!
+
+## 8.47.0
+
+- feat(v8/core): Add `updateSpanName` helper function (#14736)
+- feat(v8/node): Do not overwrite prisma `db.system` in newer Prisma versions (#14772)
+- feat(v8/node/deps): Bump @prisma/instrumentation from 5.19.1 to 5.22.0 (#14755)
+- feat(v8/replay): Mask srcdoc iframe contents per default (#14779)
+- ref(v8/nextjs): Fix typo in source maps deletion warning (#14776)
+
+Work in this release was contributed by @aloisklink and @benjick. Thank you for your contributions!
+
+## 8.46.0
+
+- feat: Allow capture of more than 1 ANR event [v8] ([#14713](https://github.com/getsentry/sentry-javascript/pull/14713))
+- feat(node): Detect Railway release name [v8] ([#14714](https://github.com/getsentry/sentry-javascript/pull/14714))
+- fix: Normalise ANR debug image file paths if appRoot was supplied [v8] ([#14709](https://github.com/getsentry/sentry-javascript/pull/14709))
+- fix(nuxt): Remove build config from tsconfig ([#14737](https://github.com/getsentry/sentry-javascript/pull/14737))
+
+Work in this release was contributed by @conor-ob. Thank you for your contribution!
+
+## 8.45.1
+
+- fix(feedback): Return when the `sendFeedback` promise resolves ([#14683](https://github.com/getsentry/sentry-javascript/pull/14683))
+
+Work in this release was contributed by @antonis. Thank you for your contribution!
+
## 8.45.0
- feat(core): Add `handled` option to `captureConsoleIntegration` ([#14664](https://github.com/getsentry/sentry-javascript/pull/14664))
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index 31a7ce76727e..5dadcfff7c6f 100644
--- a/dev-packages/browser-integration-tests/package.json
+++ b/dev-packages/browser-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/browser-integration-tests",
- "version": "8.45.0",
+ "version": "8.54.0",
"main": "index.js",
"license": "MIT",
"engines": {
@@ -43,7 +43,7 @@
"@babel/preset-typescript": "^7.16.7",
"@playwright/test": "^1.44.1",
"@sentry-internal/rrweb": "2.31.0",
- "@sentry/browser": "8.45.0",
+ "@sentry/browser": "8.54.0",
"axios": "1.7.7",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^5.5.0",
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js
new file mode 100644
index 000000000000..1e0303b9c356
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.UnleashClient = class {
+ isEnabled(x) {
+ return x;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryUnleashIntegration],
+ debug: true, // Required to test logging.
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts
new file mode 100644
index 000000000000..9b95d4d51c81
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts
@@ -0,0 +1,59 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers';
+
+sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+ const bundleKey = process.env.PW_BUNDLE || '';
+ const hasDebug = !bundleKey.includes('_min');
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const errorLogs: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() == 'error') {
+ errorLogs.push(msg.text());
+ }
+ });
+
+ const results = await page.evaluate(() => {
+ const unleash = new (window as any).UnleashClient();
+ const res1 = unleash.isEnabled('my-feature');
+ const res2 = unleash.isEnabled(999);
+ const res3 = unleash.isEnabled({});
+ return [res1, res2, res3];
+ });
+
+ // Test that the expected results are still returned. Note isEnabled is identity function for this test.
+ expect(results).toEqual(['my-feature', 999, {}]);
+
+ // Expected error logs.
+ if (hasDebug) {
+ expect(errorLogs).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)',
+ ),
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)',
+ ),
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)',
+ ),
+ ]),
+ );
+ }
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/init.js
new file mode 100644
index 000000000000..dc92fbc296a4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.UnleashClient = class {
+ isEnabled(x) {
+ return x;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryUnleashIntegration],
+ debug: true, // Required to test logging.
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/test.ts
new file mode 100644
index 000000000000..9b95d4d51c81
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/test.ts
@@ -0,0 +1,59 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers';
+
+sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+ const bundleKey = process.env.PW_BUNDLE || '';
+ const hasDebug = !bundleKey.includes('_min');
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const errorLogs: string[] = [];
+ page.on('console', msg => {
+ if (msg.type() == 'error') {
+ errorLogs.push(msg.text());
+ }
+ });
+
+ const results = await page.evaluate(() => {
+ const unleash = new (window as any).UnleashClient();
+ const res1 = unleash.isEnabled('my-feature');
+ const res2 = unleash.isEnabled(999);
+ const res3 = unleash.isEnabled({});
+ return [res1, res2, res3];
+ });
+
+ // Test that the expected results are still returned. Note isEnabled is identity function for this test.
+ expect(results).toEqual(['my-feature', 999, {}]);
+
+ // Expected error logs.
+ if (hasDebug) {
+ expect(errorLogs).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)',
+ ),
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)',
+ ),
+ expect.stringContaining(
+ '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)',
+ ),
+ ]),
+ );
+ }
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts
new file mode 100644
index 000000000000..5bb72caddd24
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts
@@ -0,0 +1,58 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
+
+const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils.
+
+sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ await page.evaluate(bufferSize => {
+ const client = new (window as any).UnleashClient();
+
+ client.isEnabled('feat1');
+ client.isEnabled('strFeat');
+ client.isEnabled('noPayloadFeat');
+ client.isEnabled('jsonFeat');
+ client.isEnabled('noVariantFeat');
+ client.isEnabled('disabledFeat');
+
+ for (let i = 7; i <= bufferSize; i++) {
+ client.isEnabled(`feat${i}`);
+ }
+ client.isEnabled(`feat${bufferSize + 1}`); // eviction
+ client.isEnabled('noPayloadFeat'); // update (move to tail)
+ }, FLAG_BUFFER_SIZE);
+
+ const reqPromise = waitForErrorRequest(page);
+ await page.locator('#error').click();
+ const req = await reqPromise;
+ const event = envelopeRequestParser(req);
+
+ const expectedFlags = [{ flag: 'strFeat', result: true }];
+ expectedFlags.push({ flag: 'jsonFeat', result: true });
+ expectedFlags.push({ flag: 'noVariantFeat', result: true });
+ expectedFlags.push({ flag: 'disabledFeat', result: false });
+ for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false });
+ expectedFlags.push({ flag: 'noPayloadFeat', result: true });
+
+ expect(event.contexts?.flags?.values).toEqual(expectedFlags);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/init.js
new file mode 100644
index 000000000000..9f1f28730cf7
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/init.js
@@ -0,0 +1,50 @@
+import * as Sentry from '@sentry/browser';
+
+window.UnleashClient = class {
+ constructor() {
+ this._featureToVariant = {
+ strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } },
+ noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true },
+ jsonFeat: {
+ name: 'paid-orgs',
+ enabled: true,
+ feature_enabled: true,
+ payload: {
+ type: 'json',
+ value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}',
+ },
+ },
+
+ // Enabled feature with no configured variants.
+ noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true },
+
+ // Disabled feature.
+ disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false },
+ };
+
+ // Variant returned for features that don't exist.
+ // `feature_enabled` may be defined in prod, but we want to test the undefined case.
+ this._fallbackVariant = {
+ name: 'disabled',
+ enabled: false,
+ };
+ }
+
+ isEnabled(toggleName) {
+ const variant = this._featureToVariant[toggleName] || this._fallbackVariant;
+ return variant.feature_enabled || false;
+ }
+
+ getVariant(toggleName) {
+ return this._featureToVariant[toggleName] || this._fallbackVariant;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryUnleashIntegration],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/test.ts
new file mode 100644
index 000000000000..5bb72caddd24
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/test.ts
@@ -0,0 +1,58 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
+
+const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils.
+
+sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ await page.evaluate(bufferSize => {
+ const client = new (window as any).UnleashClient();
+
+ client.isEnabled('feat1');
+ client.isEnabled('strFeat');
+ client.isEnabled('noPayloadFeat');
+ client.isEnabled('jsonFeat');
+ client.isEnabled('noVariantFeat');
+ client.isEnabled('disabledFeat');
+
+ for (let i = 7; i <= bufferSize; i++) {
+ client.isEnabled(`feat${i}`);
+ }
+ client.isEnabled(`feat${bufferSize + 1}`); // eviction
+ client.isEnabled('noPayloadFeat'); // update (move to tail)
+ }, FLAG_BUFFER_SIZE);
+
+ const reqPromise = waitForErrorRequest(page);
+ await page.locator('#error').click();
+ const req = await reqPromise;
+ const event = envelopeRequestParser(req);
+
+ const expectedFlags = [{ flag: 'strFeat', result: true }];
+ expectedFlags.push({ flag: 'jsonFeat', result: true });
+ expectedFlags.push({ flag: 'noVariantFeat', result: true });
+ expectedFlags.push({ flag: 'disabledFeat', result: false });
+ for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false });
+ expectedFlags.push({ flag: 'noPayloadFeat', result: true });
+
+ expect(event.contexts?.flags?.values).toEqual(expectedFlags);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js
new file mode 100644
index 000000000000..ddc74b6427b4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js
@@ -0,0 +1,50 @@
+import * as Sentry from '@sentry/browser';
+
+window.UnleashClient = class {
+ constructor() {
+ this._featureToVariant = {
+ strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } },
+ noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true },
+ jsonFeat: {
+ name: 'paid-orgs',
+ enabled: true,
+ feature_enabled: true,
+ payload: {
+ type: 'json',
+ value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}',
+ },
+ },
+
+ // Enabled feature with no configured variants.
+ noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true },
+
+ // Disabled feature.
+ disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false },
+ };
+
+ // Variant returned for features that don't exist.
+ // `feature_enabled` may be defined in prod, but we want to test the undefined case.
+ this._fallbackVariant = {
+ name: 'disabled',
+ enabled: false,
+ };
+ }
+
+ isEnabled(toggleName) {
+ const variant = this._featureToVariant[toggleName] || this._fallbackVariant;
+ return variant.feature_enabled || false;
+ }
+
+ getVariant(toggleName) {
+ return this._featureToVariant[toggleName] || this._fallbackVariant;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryUnleashIntegration],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js
new file mode 100644
index 000000000000..e6697408128c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js
@@ -0,0 +1,3 @@
+document.getElementById('error').addEventListener('click', () => {
+ throw new Error('Button triggered error');
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html
new file mode 100644
index 000000000000..9330c6c679f4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts
new file mode 100644
index 000000000000..2d821bf6c81d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts
@@ -0,0 +1,65 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
+
+import type { Scope } from '@sentry/browser';
+
+sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true);
+ const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false);
+
+ await page.evaluate(() => {
+ const Sentry = (window as any).Sentry;
+ const errorButton = document.querySelector('#error') as HTMLButtonElement;
+ const unleash = new (window as any).UnleashClient();
+
+ unleash.isEnabled('strFeat');
+
+ Sentry.withScope((scope: Scope) => {
+ unleash.isEnabled('disabledFeat');
+ unleash.isEnabled('strFeat');
+ scope.setTag('isForked', true);
+ if (errorButton) {
+ errorButton.click();
+ }
+ });
+
+ unleash.isEnabled('noPayloadFeat');
+ Sentry.getCurrentScope().setTag('isForked', false);
+ errorButton.click();
+ return true;
+ });
+
+ const forkedReq = await forkedReqPromise;
+ const forkedEvent = envelopeRequestParser(forkedReq);
+
+ const mainReq = await mainReqPromise;
+ const mainEvent = envelopeRequestParser(mainReq);
+
+ expect(forkedEvent.contexts?.flags?.values).toEqual([
+ { flag: 'disabledFeat', result: false },
+ { flag: 'strFeat', result: true },
+ ]);
+
+ expect(mainEvent.contexts?.flags?.values).toEqual([
+ { flag: 'strFeat', result: true },
+ { flag: 'noPayloadFeat', result: true },
+ ]);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/init.js
new file mode 100644
index 000000000000..9f1f28730cf7
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/init.js
@@ -0,0 +1,50 @@
+import * as Sentry from '@sentry/browser';
+
+window.UnleashClient = class {
+ constructor() {
+ this._featureToVariant = {
+ strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } },
+ noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true },
+ jsonFeat: {
+ name: 'paid-orgs',
+ enabled: true,
+ feature_enabled: true,
+ payload: {
+ type: 'json',
+ value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}',
+ },
+ },
+
+ // Enabled feature with no configured variants.
+ noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true },
+
+ // Disabled feature.
+ disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false },
+ };
+
+ // Variant returned for features that don't exist.
+ // `feature_enabled` may be defined in prod, but we want to test the undefined case.
+ this._fallbackVariant = {
+ name: 'disabled',
+ enabled: false,
+ };
+ }
+
+ isEnabled(toggleName) {
+ const variant = this._featureToVariant[toggleName] || this._fallbackVariant;
+ return variant.feature_enabled || false;
+ }
+
+ getVariant(toggleName) {
+ return this._featureToVariant[toggleName] || this._fallbackVariant;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryUnleashIntegration],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/test.ts
new file mode 100644
index 000000000000..2d821bf6c81d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/test.ts
@@ -0,0 +1,65 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
+
+import type { Scope } from '@sentry/browser';
+
+sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true);
+ const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false);
+
+ await page.evaluate(() => {
+ const Sentry = (window as any).Sentry;
+ const errorButton = document.querySelector('#error') as HTMLButtonElement;
+ const unleash = new (window as any).UnleashClient();
+
+ unleash.isEnabled('strFeat');
+
+ Sentry.withScope((scope: Scope) => {
+ unleash.isEnabled('disabledFeat');
+ unleash.isEnabled('strFeat');
+ scope.setTag('isForked', true);
+ if (errorButton) {
+ errorButton.click();
+ }
+ });
+
+ unleash.isEnabled('noPayloadFeat');
+ Sentry.getCurrentScope().setTag('isForked', false);
+ errorButton.click();
+ return true;
+ });
+
+ const forkedReq = await forkedReqPromise;
+ const forkedEvent = envelopeRequestParser(forkedReq);
+
+ const mainReq = await mainReqPromise;
+ const mainEvent = envelopeRequestParser(mainReq);
+
+ expect(forkedEvent.contexts?.flags?.values).toEqual([
+ { flag: 'disabledFeat', result: false },
+ { flag: 'strFeat', result: true },
+ ]);
+
+ expect(mainEvent.contexts?.flags?.values).toEqual([
+ { flag: 'strFeat', result: true },
+ { flag: 'noPayloadFeat', result: true },
+ ]);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts
index 6226ff75dbb9..7d2d949898c2 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import type { Event } from '@sentry/core';
+import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -10,27 +10,34 @@ import {
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
-sentryTest('sets the source to custom when updating the transaction name', async ({ getLocalTestUrl, page }) => {
- if (shouldSkipTracingTest()) {
- sentryTest.skip();
- }
+sentryTest(
+ 'sets the source to custom when updating the transaction name with `span.updateName`',
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
- const url = await getLocalTestUrl({ testDir: __dirname });
+ const url = await getLocalTestUrl({ testDir: __dirname });
- const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
- const traceContextData = eventData.contexts?.trace?.data;
+ const traceContextData = eventData.contexts?.trace?.data;
- expect(traceContextData).toMatchObject({
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
- [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
- });
+ expect(traceContextData).toBeDefined();
- expect(eventData.transaction).toBe('new name');
+ expect(eventData.transaction).toBe('new name');
- expect(eventData.contexts?.trace?.op).toBe('pageload');
- expect(eventData.spans?.length).toBeGreaterThan(0);
- expect(eventData.transaction_info?.source).toEqual('custom');
-});
+ expect(traceContextData).toMatchObject({
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ });
+
+ expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined();
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('custom');
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js
new file mode 100644
index 000000000000..1f0b64911a75
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window._testBaseTimestamp = performance.timeOrigin / 1000;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js
new file mode 100644
index 000000000000..7f0ad0fea340
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js
@@ -0,0 +1,4 @@
+const activeSpan = Sentry.getActiveSpan();
+const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan);
+
+Sentry.updateSpanName(rootSpan, 'new name');
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts
new file mode 100644
index 000000000000..69094b38e4dd
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts
@@ -0,0 +1,43 @@
+import { expect } from '@playwright/test';
+import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core';
+
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/browser';
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest(
+ 'sets the source to custom when updating the transaction name with Sentry.updateSpanName',
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ const traceContextData = eventData.contexts?.trace?.data;
+
+ expect(traceContextData).toBeDefined();
+
+ expect(traceContextData).toMatchObject({
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ });
+
+ expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined();
+
+ expect(eventData.transaction).toBe('new name');
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('custom');
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts
index 7ce5f7195a5b..34e15d1be573 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts
@@ -181,5 +181,9 @@ async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise {
+sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}
+ const isWebkitRun = browserName === 'webkit';
+
// Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox).
await page.route('https://example.com/path/to/image.svg', (route: Route) =>
- route.fulfill({ path: `${__dirname}/assets/image.svg` }),
+ route.fulfill({
+ path: `${__dirname}/assets/image.svg`,
+ headers: {
+ 'Timing-Allow-Origin': '*',
+ 'Content-Type': 'image/svg+xml',
+ },
+ }),
);
await page.route('https://example.com/path/to/script.js', (route: Route) =>
- route.fulfill({ path: `${__dirname}/assets/script.js` }),
+ route.fulfill({
+ path: `${__dirname}/assets/script.js`,
+ headers: {
+ 'Timing-Allow-Origin': '*',
+ 'Content-Type': 'application/javascript',
+ },
+ }),
);
await page.route('https://example.com/path/to/style.css', (route: Route) =>
- route.fulfill({ path: `${__dirname}/assets/style.css` }),
+ route.fulfill({
+ path: `${__dirname}/assets/style.css`,
+ headers: {
+ 'Timing-Allow-Origin': '*',
+ 'Content-Type': 'text/css',
+ },
+ }),
);
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -27,11 +47,14 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca
const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource'));
const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script');
- const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link');
- const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img');
+ const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0];
+ const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0];
+
+ const spanId = eventData.contexts?.trace?.span_id;
+ const traceId = eventData.contexts?.trace?.trace_id;
- expect(imgSpans).toHaveLength(1);
- expect(linkSpans).toHaveLength(1);
+ expect(spanId).toBeDefined();
+ expect(traceId).toBeDefined();
const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle');
@@ -41,11 +64,90 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca
}
expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts);
+ expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
- const spanId = eventData.contexts?.trace?.span_id;
+ const customScriptSpan = scriptSpans?.find(
+ ({ description }) => description === 'https://example.com/path/to/script.js',
+ );
- expect(spanId).toBeDefined();
- expect(imgSpans?.[0].parent_span_id).toBe(spanId);
- expect(linkSpans?.[0].parent_span_id).toBe(spanId);
- expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
+ expect(imgSpan).toEqual({
+ data: {
+ 'http.decoded_response_content_length': expect.any(Number),
+ 'http.response_content_length': expect.any(Number),
+ 'http.response_transfer_size': expect.any(Number),
+ 'network.protocol.name': '',
+ 'network.protocol.version': 'unknown',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
+ 'server.address': 'example.com',
+ 'url.same_origin': false,
+ 'url.scheme': 'https',
+ ...(!isWebkitRun && {
+ 'resource.render_blocking_status': 'non-blocking',
+ 'http.response_delivery_type': '',
+ }),
+ },
+ description: 'https://example.com/path/to/image.svg',
+ op: 'resource.img',
+ origin: 'auto.resource.browser.metrics',
+ parent_span_id: spanId,
+ span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+
+ expect(linkSpan).toEqual({
+ data: {
+ 'http.decoded_response_content_length': expect.any(Number),
+ 'http.response_content_length': expect.any(Number),
+ 'http.response_transfer_size': expect.any(Number),
+ 'network.protocol.name': '',
+ 'network.protocol.version': 'unknown',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
+ 'server.address': 'example.com',
+ 'url.same_origin': false,
+ 'url.scheme': 'https',
+ ...(!isWebkitRun && {
+ 'resource.render_blocking_status': 'non-blocking',
+ 'http.response_delivery_type': '',
+ }),
+ },
+ description: 'https://example.com/path/to/style.css',
+ op: 'resource.link',
+ origin: 'auto.resource.browser.metrics',
+ parent_span_id: spanId,
+ span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+
+ expect(customScriptSpan).toEqual({
+ data: {
+ 'http.decoded_response_content_length': expect.any(Number),
+ 'http.response_content_length': expect.any(Number),
+ 'http.response_transfer_size': expect.any(Number),
+ 'network.protocol.name': '',
+ 'network.protocol.version': 'unknown',
+ 'sentry.op': 'resource.script',
+ 'sentry.origin': 'auto.resource.browser.metrics',
+ 'server.address': 'example.com',
+ 'url.same_origin': false,
+ 'url.scheme': 'https',
+ ...(!isWebkitRun && {
+ 'resource.render_blocking_status': 'non-blocking',
+ 'http.response_delivery_type': '',
+ }),
+ },
+ description: 'https://example.com/path/to/script.js',
+ op: 'resource.script',
+ origin: 'auto.resource.browser.metrics',
+ parent_span_id: spanId,
+ span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
index bcbfa1890cdd..e1aca233ecf2 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
@@ -1,8 +1,10 @@
import { expect } from '@playwright/test';
-import type { Event } from '@sentry/core';
-
import { sentryTest } from '../../../../utils/fixtures';
-import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';
+import {
+ envelopeRequestParser,
+ shouldSkipTracingTest,
+ waitForTransactionRequestOnUrl,
+} from '../../../../utils/helpers';
sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
@@ -11,14 +13,8 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, p
const url = await getLocalTestUrl({ testDir: __dirname });
- // Because we fetch from http://example.com, fetch will throw a CORS error in firefox and webkit.
- // Chromium does not throw for cors errors.
- // This means that we will intercept a dynamic amount of envelopes here.
-
- // We will wait 500ms for all envelopes to be sent. Generally, in all browsers, the last sent
- // envelope contains tracing data.
- const envelopes = await getMultipleSentryEnvelopeRequests(page, 4, { url, timeout: 10000 });
- const tracingEvent = envelopes.find(event => event.type === 'transaction')!; // last envelope contains tracing data on all browsers
+ const req = await waitForTransactionRequestOnUrl(page, url);
+ const tracingEvent = envelopeRequestParser(req);
const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/init.js
index 7cd076a052e5..092c43f75eac 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/init.js
@@ -7,4 +7,5 @@ Sentry.init({
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://example.com'],
tracesSampleRate: 1,
+ autoSessionTracking: false,
});
diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js
new file mode 100644
index 000000000000..9247e1d8bcc2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/browser';
+
+import { makeMultiplexedTransport } from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ transport: makeMultiplexedTransport(Sentry.makeFetchTransport, ({ getEvent }) => {
+ const event = getEvent('event');
+
+ if (event.tags.to === 'a') {
+ return ['https://public@dsn.ingest.sentry.io/1337'];
+ } else if (event.tags.to === 'b') {
+ return ['https://public@dsn.ingest.sentry.io/1337'];
+ } else {
+ throw new Error('Unknown destination');
+ }
+ }),
+});
diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js
new file mode 100644
index 000000000000..89bb4b22eca1
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js
@@ -0,0 +1,10 @@
+setTimeout(() => {
+ Sentry.withScope(scope => {
+ scope.setTag('to', 'a');
+ Sentry.captureException(new Error('Error a'));
+ });
+ Sentry.withScope(scope => {
+ scope.setTag('to', 'b');
+ Sentry.captureException(new Error('Error b'));
+ });
+}, 0);
diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts
new file mode 100644
index 000000000000..0bf274291df4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts
@@ -0,0 +1,20 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/core';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers';
+
+sentryTest('sends event to DSNs specified in makeMultiplexedTransport', async ({ getLocalTestUrl, page }) => {
+ const url = await getLocalTestUrl({ testDir: __dirname });
+ const errorEvents = await getMultipleSentryEnvelopeRequests(page, 2, { envelopeType: 'event', url });
+
+ expect(errorEvents).toHaveLength(2);
+
+ const [evt1, evt2] = errorEvents;
+
+ const errorA = evt1?.tags?.to === 'a' ? evt1 : evt2;
+ const errorB = evt1?.tags?.to === 'b' ? evt1 : evt2;
+
+ expect(errorA.tags?.to).toBe('a');
+ expect(errorB.tags?.to).toBe('b');
+});
diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts
index b9b4dcb4c1f3..7ca1250296b9 100644
--- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts
+++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts
@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
-import type { Package } from '@sentry/core';
+import { type Package } from '@sentry/core';
import HtmlWebpackPlugin, { createHtmlTagObject } from 'html-webpack-plugin';
import type { Compiler } from 'webpack';
@@ -38,6 +38,8 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = {
sessionTimingIntegration: 'sessiontiming',
feedbackIntegration: 'feedback',
moduleMetadataIntegration: 'modulemetadata',
+ // technically, this is not an integration, but let's add it anyway for simplicity
+ makeMultiplexedTransport: 'multiplexedtransport',
};
const BUNDLE_PATHS: Record> = {
diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json
index c5146697c4d4..48c1948947b3 100644
--- a/dev-packages/bundle-analyzer-scenarios/package.json
+++ b/dev-packages/bundle-analyzer-scenarios/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/bundle-analyzer-scenarios",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Scenarios to test bundle analysis with",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios",
diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json
index acf92547dca5..1b55f71e82b3 100644
--- a/dev-packages/clear-cache-gh-action/package.json
+++ b/dev-packages/clear-cache-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/clear-cache-gh-action",
"description": "An internal Github Action to clear GitHub caches.",
- "version": "8.45.0",
+ "version": "8.54.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json
index 6452d7752eba..e33e4ae03cd9 100644
--- a/dev-packages/e2e-tests/package.json
+++ b/dev-packages/e2e-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/e2e-tests",
- "version": "8.45.0",
+ "version": "8.54.0",
"license": "MIT",
"private": true,
"scripts": {
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts
index d437a1d43fdd..1e43d5c6c096 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts
@@ -6,7 +6,10 @@ import { SampleComponent } from '../sample-component/sample-component.components
selector: 'app-cancel',
standalone: true,
imports: [TraceModule, SampleComponent],
- template: ``,
+ template: `
+
+
+ `,
})
@TraceClass({ name: 'ComponentTrackingComponent' })
export class ComponentTrackingComponent implements OnInit, AfterViewInit {
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
index 29c88a6108e2..03a715ce646c 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts
@@ -191,7 +191,7 @@ test.describe('finish routing span', () => {
});
test.describe('TraceDirective', () => {
- test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => {
+ test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => {
const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -201,23 +201,36 @@ test.describe('TraceDirective', () => {
// immediately navigate to a different route
const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
- const traceDirectiveSpan = navigationTxn.spans?.find(
+ const traceDirectiveSpans = navigationTxn.spans?.filter(
span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive',
);
- expect(traceDirectiveSpan).toBeDefined();
- expect(traceDirectiveSpan).toEqual(
- expect.objectContaining({
- data: {
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
- },
- description: '',
- op: 'ui.angular.init',
- origin: 'auto.ui.angular.trace_directive',
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- }),
+ expect(traceDirectiveSpans).toHaveLength(2);
+ expect(traceDirectiveSpans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
+ },
+ description: '', // custom component name passed to trace directive
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_directive',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
+ },
+ description: '', // fallback selector name
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_directive',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ ]),
);
});
});
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/.npmrc b/dev-packages/e2e-tests/test-applications/angular-19/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts
index d437a1d43fdd..a82e5b1acce6 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts
@@ -3,10 +3,13 @@ import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular';
import { SampleComponent } from '../sample-component/sample-component.components';
@Component({
- selector: 'app-cancel',
+ selector: 'app-component-tracking',
standalone: true,
imports: [TraceModule, SampleComponent],
- template: ``,
+ template: `
+
+
+ `,
})
@TraceClass({ name: 'ComponentTrackingComponent' })
export class ComponentTrackingComponent implements OnInit, AfterViewInit {
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
index af85b8ffc405..c2cb2eca34b6 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
@@ -191,7 +191,7 @@ test.describe('finish routing span', () => {
});
test.describe('TraceDirective', () => {
- test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => {
+ test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => {
const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -201,23 +201,36 @@ test.describe('TraceDirective', () => {
// immediately navigate to a different route
const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
- const traceDirectiveSpan = navigationTxn.spans?.find(
+ const traceDirectiveSpans = navigationTxn.spans?.filter(
span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive',
);
- expect(traceDirectiveSpan).toBeDefined();
- expect(traceDirectiveSpan).toEqual(
- expect.objectContaining({
- data: {
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
- },
- description: '',
- op: 'ui.angular.init',
- origin: 'auto.ui.angular.trace_directive',
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- }),
+ expect(traceDirectiveSpans).toHaveLength(2);
+ expect(traceDirectiveSpans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
+ },
+ description: '', // custom component name passed to trace directive
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_directive',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
+ },
+ description: '', // fallback selector name
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_directive',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ ]),
);
});
});
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
index aeee72f96477..b37c7a8c0705 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
@@ -44,7 +44,7 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"typescript": "^5.1.6",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"vite-tsconfig-paths": "^4.2.1"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts
index 33a6b1957d99..ca29bb9fb9ae 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
+import { All, Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
import { flush } from '@sentry/nestjs';
import { AppService } from './app.service';
import { AsyncInterceptor } from './async-example.interceptor';
@@ -121,4 +121,9 @@ export class AppController {
testFunctionName() {
return this.appService.getFunctionName();
}
+
+ @All('test-all')
+ testAll() {
+ return {};
+ }
}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
index 609e01709650..96b60e5d976f 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
@@ -808,3 +808,8 @@ test('Calling canActivate method on service with Injectable decorator returns 20
const response = await fetch(`${baseURL}/test-service-canActivate`);
expect(response.status).toBe(200);
});
+
+test('Calling @All method on service with Injectable decorator returns 200', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-all`);
+ expect(response.status).toBe(200);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx
index c11efda8adc9..b06521f2cb10 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx
@@ -3,9 +3,9 @@ import http from 'http';
export const dynamic = 'force-dynamic';
export default async function Page() {
- await fetch('http://example.com/', { cache: 'no-cache' }).then(res => res.text());
+ await fetch('http://github.com/', { cache: 'no-cache' }).then(res => res.text());
await new Promise(resolve => {
- http.get('http://example.com/', res => {
+ http.get('http://github.com/', res => {
res.on('data', () => {
// Noop consuming some data so that request can close :)
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts
index 65b9c4312d91..fc2feb101760 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts
@@ -19,7 +19,7 @@ test('Should send a transaction with a fetch span', async ({ page }) => {
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.otel.node_fetch',
}),
- description: 'GET http://example.com/',
+ description: 'GET http://github.com/',
}),
);
@@ -30,7 +30,7 @@ test('Should send a transaction with a fetch span', async ({ page }) => {
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.otel.http',
}),
- description: 'GET http://example.com/',
+ description: 'GET http://github.com/',
}),
);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts
index 955988101724..70564e0c12bb 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts
@@ -10,7 +10,7 @@ const buildStderr = fs.readFileSync('.tmp_build_stderr', 'utf-8');
// Assert that there was no funky build time warning when we are on a stable (pinned) version
if (nextjsVersion !== 'latest' && !nextjsVersion.includes('-canary') && !nextjsVersion.includes('-rc')) {
- assert.doesNotMatch(buildStderr, /Import trace for requested module/); // This is Next.js/Webpack speech for "something is off"
+ assert.doesNotMatch(buildStderr, /Import trace for requested module/, `Build warning in output:\n${buildStderr}`); // This is Next.js/Webpack speech for "something is off"
}
// Assert that all static components stay static and all dynamic components stay dynamic
diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json
index bc0b9b4dead7..b42a17fd5438 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express/package.json
@@ -15,9 +15,9 @@
"@sentry/node": "latest || *",
"@trpc/server": "10.45.2",
"@trpc/client": "10.45.2",
- "@types/express": "4.17.17",
- "@types/node": "18.15.1",
- "express": "4.20.0",
+ "@types/express": "^4.17.21",
+ "@types/node": "^18.19.1",
+ "express": "^4.21.2",
"typescript": "4.9.5",
"zod": "~3.22.4"
},
@@ -25,6 +25,9 @@
"@playwright/test": "^1.44.1",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
+ "resolutions": {
+ "@types/qs": "6.9.17"
+ },
"volta": {
"extends": "../../package.json"
}
diff --git a/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json
index 8cb64e989ed9..ce4fafb745ad 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"types": ["node"],
"esModuleInterop": true,
- "lib": ["es2018"],
+ "lib": ["es2020"],
"strict": true,
"outDir": "dist"
},
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel/.gitignore
new file mode 100644
index 000000000000..1521c8b7652b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json
new file mode 100644
index 000000000000..70d97d1fa502
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "node-otel",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@opentelemetry/sdk-node": "0.52.1",
+ "@opentelemetry/exporter-trace-otlp-http": "0.52.1",
+ "@sentry/core": "latest || *",
+ "@sentry/node": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@types/express": "4.17.17",
+ "@types/node": "^18.19.1",
+ "express": "4.19.2",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs
new file mode 100644
index 000000000000..888e61cfb2dc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs
@@ -0,0 +1,34 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig(
+ {
+ startCommand: `pnpm start`,
+ },
+ {
+ webServer: [
+ {
+ command: `node ./start-event-proxy.mjs`,
+ port: 3031,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+ {
+ command: `node ./start-otel-proxy.mjs`,
+ port: 3032,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+ {
+ command: 'pnpm start',
+ port: 3030,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ env: {
+ PORT: 3030,
+ },
+ },
+ ],
+ },
+);
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts
new file mode 100644
index 000000000000..26779990f6d1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts
@@ -0,0 +1,53 @@
+import './instrument';
+
+// Other imports below
+import * as Sentry from '@sentry/node';
+import express from 'express';
+
+const app = express();
+const port = 3030;
+
+app.get('/test-success', function (req, res) {
+ res.send({ version: 'v1' });
+});
+
+app.get('/test-param/:param', function (req, res) {
+ res.send({ paramWas: req.params.param });
+});
+
+app.get('/test-transaction', function (req, res) {
+ Sentry.withActiveSpan(null, async () => {
+ Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => {
+ Sentry.startSpan({ name: 'test-span' }, () => undefined);
+ });
+
+ await Sentry.flush();
+
+ res.send({});
+ });
+});
+
+app.get('/test-error', async function (req, res) {
+ const exceptionId = Sentry.captureException(new Error('This is an error'));
+
+ await Sentry.flush(2000);
+
+ res.send({ exceptionId });
+});
+
+app.get('/test-exception/:id', function (req, _res) {
+ throw new Error(`This is an exception with id ${req.params.id}`);
+});
+
+Sentry.setupExpressErrorHandler(app);
+
+app.use(function onError(err: unknown, req: any, res: any, next: any) {
+ // The error id is attached to `res.sentry` to be returned
+ // and optionally displayed to the user for support.
+ res.statusCode = 500;
+ res.end(res.sentry + '\n');
+});
+
+app.listen(port, () => {
+ console.log(`Example app listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts
new file mode 100644
index 000000000000..bbc5ddf9c30f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts
@@ -0,0 +1,22 @@
+const opentelemetry = require('@opentelemetry/sdk-node');
+const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
+const Sentry = require('@sentry/node');
+
+const sentryClient = Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn:
+ process.env.E2E_TEST_DSN ||
+ 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576',
+ debug: !!process.env.DEBUG,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+
+ // Additional OTEL options
+ openTelemetrySpanProcessors: [
+ new opentelemetry.node.BatchSpanProcessor(
+ new OTLPTraceExporter({
+ url: 'http://localhost:3032/',
+ }),
+ ),
+ ],
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs
new file mode 100644
index 000000000000..e82b876a4979
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'node-otel',
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs
new file mode 100644
index 000000000000..df546bf5ff77
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs
@@ -0,0 +1,6 @@
+import { startProxyServer } from '@sentry-internal/test-utils';
+
+startProxyServer({
+ port: 3032,
+ proxyServerName: 'node-otel-otel',
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts
new file mode 100644
index 000000000000..e5b2d5ff6836
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts
@@ -0,0 +1,29 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Sends correct error event', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-otel', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ await fetch(`${baseURL}/test-exception/123`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
+
+ expect(errorEvent.request).toEqual({
+ method: 'GET',
+ cookies: {},
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/test-exception/123',
+ });
+
+ expect(errorEvent.transaction).toEqual('GET /test-exception/:id');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts
new file mode 100644
index 000000000000..de68adf681b7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts
@@ -0,0 +1,207 @@
+import { expect, test } from '@playwright/test';
+import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends an API route transaction', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('node-otel', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-transaction'
+ );
+ });
+
+ // Ensure we also send data to the OTLP endpoint
+ const otelPromise = waitForPlainRequest('node-otel-otel', data => {
+ const json = JSON.parse(data) as any;
+
+ return json.resourceSpans.length > 0;
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ const otelData = await otelPromise;
+
+ // For now we do not test the actual shape of this, but only existence
+ expect(otelData).toBeDefined();
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.op': 'http.server',
+ 'sentry.sample_rate': 1,
+ url: 'http://localhost:3030/test-transaction',
+ 'otel.kind': 'SERVER',
+ 'http.response.status_code': 200,
+ 'http.url': 'http://localhost:3030/test-transaction',
+ 'http.host': 'localhost:3030',
+ 'net.host.name': 'localhost',
+ 'http.method': 'GET',
+ 'http.scheme': 'http',
+ 'http.target': '/test-transaction',
+ 'http.user_agent': 'node',
+ 'http.flavor': '1.1',
+ 'net.transport': 'ip_tcp',
+ 'net.host.ip': expect.any(String),
+ 'net.host.port': expect.any(Number),
+ 'net.peer.ip': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ 'http.route': '/test-transaction',
+ },
+ op: 'http.server',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.http.otel.http',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: 'GET /test-transaction',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'middleware.express',
+ 'http.route': '/',
+ 'express.name': 'query',
+ 'express.type': 'middleware',
+ },
+ description: 'query',
+ op: 'middleware.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'middleware.express',
+ 'http.route': '/',
+ 'express.name': 'expressInit',
+ 'express.type': 'middleware',
+ },
+ description: 'expressInit',
+ op: 'middleware.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'request_handler.express',
+ 'http.route': '/test-transaction',
+ 'express.name': '/test-transaction',
+ 'express.type': 'request_handler',
+ },
+ description: '/test-transaction',
+ op: 'request_handler.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+});
+
+test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('node-otel', transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' &&
+ transactionEvent.transaction === 'GET /test-exception/:id' &&
+ transactionEvent.request?.url === 'http://localhost:3030/test-exception/777'
+ );
+ });
+
+ await fetch(`${baseURL}/test-exception/777`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
+ expect(transactionEvent.transaction).toEqual('GET /test-exception/:id');
+ expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error');
+ expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500);
+
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'middleware.express',
+ 'http.route': '/',
+ 'express.name': 'query',
+ 'express.type': 'middleware',
+ },
+ description: 'query',
+ op: 'middleware.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'middleware.express',
+ 'http.route': '/',
+ 'express.name': 'expressInit',
+ 'express.type': 'middleware',
+ },
+ description: 'expressInit',
+ op: 'middleware.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(spans).toContainEqual({
+ data: {
+ 'sentry.origin': 'auto.http.otel.express',
+ 'sentry.op': 'request_handler.express',
+ 'http.route': '/test-exception/:id',
+ 'express.name': '/test-exception/:id',
+ 'express.type': 'request_handler',
+ },
+ description: '/test-exception/:id',
+ op: 'request_handler.express',
+ origin: 'auto.http.otel.express',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json
new file mode 100644
index 000000000000..8cb64e989ed9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "esModuleInterop": true,
+ "lib": ["es2018"],
+ "strict": true,
+ "outDir": "dist"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs
similarity index 90%
rename from dev-packages/e2e-tests/test-applications/node-profiling/build.mjs
rename to dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs
index 55ec0b5fae52..4a9aa83d0eec 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs
+++ b/dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs
@@ -11,9 +11,10 @@ console.log('Running build using esbuild version', esbuild.version);
esbuild.buildSync({
platform: 'node',
entryPoints: ['./index.ts'],
- outdir: './dist',
+ outfile: './dist/cjs/index.js',
target: 'esnext',
format: 'cjs',
bundle: true,
loader: { '.node': 'copy' },
+ external: ['@sentry/node', '@sentry/profiling-node'],
});
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs
similarity index 68%
rename from dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs
rename to dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs
index c45e30539bc0..294e53d50635 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs
+++ b/dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs
@@ -11,19 +11,10 @@ console.log('Running build using esbuild version', esbuild.version);
esbuild.buildSync({
platform: 'node',
entryPoints: ['./index.ts'],
- outfile: './dist/index.shimmed.mjs',
+ outfile: './dist/esm/index.mjs',
target: 'esnext',
format: 'esm',
bundle: true,
loader: { '.node': 'copy' },
- banner: {
- js: `
- import { dirname } from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { createRequire } from 'node:module';
- const require = createRequire(import.meta.url);
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- `,
- },
+ external: ['@sentry/node', '@sentry/profiling-node'],
});
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.ts b/dev-packages/e2e-tests/test-applications/node-profiling/index.ts
index d49add80955c..e956a1d9de33 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling/index.ts
+++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.ts
@@ -1,5 +1,5 @@
-const Sentry = require('@sentry/node');
-const { nodeProfilingIntegration } = require('@sentry/profiling-node');
+import * as Sentry from '@sentry/node';
+import { nodeProfilingIntegration } from '@sentry/profiling-node';
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json
index 8aede827a1f3..d78ca10fa25d 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json
@@ -4,8 +4,8 @@
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
- "build": "node build.mjs && node build.shimmed.mjs",
- "test": "node dist/index.js && node --experimental-require-module dist/index.js && node dist/index.shimmed.mjs",
+ "build": "node build-cjs.mjs && node build-esm.mjs",
+ "test": "node dist/cjs/index.js && node --experimental-require-module dist/cjs/index.js && node dist/esm/index.mjs",
"clean": "npx rimraf node_modules dist",
"test:electron": "$(pnpm bin)/electron-rebuild && playwright test",
"test:build": "pnpm run typecheck && pnpm run build",
@@ -17,9 +17,9 @@
"@sentry/electron": "latest || *",
"@sentry/node": "latest || *",
"@sentry/profiling-node": "latest || *",
- "electron": "^33.2.0"
+ "electron": "^33.2.0",
+ "esbuild": "0.20.0"
},
- "devDependencies": {},
"volta": {
"extends": "../../package.json"
},
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore
new file mode 100644
index 000000000000..84634c973eeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore
@@ -0,0 +1,29 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
new file mode 100644
index 000000000000..a4e7dae6d1e2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "react-create-browser-router-test",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@sentry/react": "latest || *",
+ "@types/node": "^18.19.1",
+ "@types/react": "18.0.0",
+ "@types/react-dom": "18.0.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "typescript": "~5.0.0"
+ },
+ "scripts": {
+ "build": "react-scripts build",
+ "start": "serve -s build",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "eslintConfig": {
+ "extends": ["react-app", "react-app/jest"]
+ },
+ "browserslist": {
+ "production": [">0.2%", "not dead", "not op_mini all"],
+ "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "serve": "14.0.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html
new file mode 100644
index 000000000000..39da76522bea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts
new file mode 100644
index 000000000000..ffa61ca49acc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts
@@ -0,0 +1,5 @@
+interface Window {
+ recordedTransactions?: string[];
+ capturedExceptionId?: string;
+ sentryReplayId?: string;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx
new file mode 100644
index 000000000000..88f8cfa502ec
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx
@@ -0,0 +1,66 @@
+import * as Sentry from '@sentry/react';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ RouterProvider,
+ createBrowserRouter,
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+} from 'react-router-dom';
+import Index from './pages/Index';
+import User from './pages/User';
+
+const replay = Sentry.replayIntegration();
+
+Sentry.init({
+ // environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.REACT_APP_E2E_TEST_DSN,
+ integrations: [
+ Sentry.reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ replay,
+ ],
+ // We recommend adjusting this value in production, or using tracesSampler
+ // for finer control
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+
+ tunnel: 'http://localhost:3031',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ debug: !!process.env.DEBUG,
+});
+
+const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
+
+const router = sentryCreateBrowserRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/user/:id',
+ element: ,
+ },
+ ],
+ {
+ // We're testing whether this option is avoided in the integration
+ // We expect this to be ignored
+ initialEntries: ['/user/1'],
+ },
+);
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+
+root.render();
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx
new file mode 100644
index 000000000000..d6b71a1d1279
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx
@@ -0,0 +1,23 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+
+const Index = () => {
+ return (
+ <>
+ {
+ throw new Error('I am an error!');
+ }}
+ />
+
+ navigate
+
+ >
+ );
+};
+
+export default Index;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx
new file mode 100644
index 000000000000..62f0c2d17533
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx
@@ -0,0 +1,8 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+
+const User = () => {
+ return I am a blank page :)
;
+};
+
+export default User;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts
new file mode 100644
index 000000000000..6431bc5fc6b2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs
new file mode 100644
index 000000000000..be93e129284f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-create-browser-router',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts
new file mode 100644
index 000000000000..4a11f07410ab
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Captures exception correctly', async ({ page }) => {
+ const errorEventPromise = waitForError('react-create-browser-router', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.request).toEqual({
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/',
+ });
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts
new file mode 100644
index 000000000000..5ecd098daf94
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts
@@ -0,0 +1,78 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Captures a pageload transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
+ return event.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace).toEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ deviceMemory: expect.any(String),
+ effectiveConnectionType: expect.any(String),
+ hardwareConcurrency: expect.any(String),
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'pageload',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.pageload.react.reactrouter_v6',
+ }),
+ );
+});
+
+test('Captures a navigation transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
+ return event.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto('/');
+ const linkElement = page.locator('id=navigation');
+ await linkElement.click();
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'navigation',
+ 'sentry.origin': 'auto.navigation.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'navigation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.navigation.react.reactrouter_v6',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.spans).toEqual([]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json
new file mode 100644
index 000000000000..4cc95dc2689a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": ["src", "tests"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore
new file mode 100644
index 000000000000..84634c973eeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore
@@ -0,0 +1,29 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
new file mode 100644
index 000000000000..dc6c9b4340f0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "react-create-memory-router-test",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@sentry/react": "latest || *",
+ "@types/node": "^18.19.1",
+ "@types/react": "18.0.0",
+ "@types/react-dom": "18.0.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "typescript": "~5.0.0"
+ },
+ "scripts": {
+ "build": "react-scripts build",
+ "start": "serve -s build",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "eslintConfig": {
+ "extends": ["react-app", "react-app/jest"]
+ },
+ "browserslist": {
+ "production": [">0.2%", "not dead", "not op_mini all"],
+ "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "serve": "14.0.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html
new file mode 100644
index 000000000000..39da76522bea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts
new file mode 100644
index 000000000000..ffa61ca49acc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts
@@ -0,0 +1,5 @@
+interface Window {
+ recordedTransactions?: string[];
+ capturedExceptionId?: string;
+ sentryReplayId?: string;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx
new file mode 100644
index 000000000000..f71572f9dc1f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx
@@ -0,0 +1,65 @@
+import * as Sentry from '@sentry/react';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ RouterProvider,
+ createMemoryRouter,
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+} from 'react-router-dom';
+import Index from './pages/Index';
+import User from './pages/User';
+
+const replay = Sentry.replayIntegration();
+
+Sentry.init({
+ // environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.REACT_APP_E2E_TEST_DSN,
+ integrations: [
+ Sentry.reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ replay,
+ ],
+ // We recommend adjusting this value in production, or using tracesSampler
+ // for finer control
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+
+ tunnel: 'http://localhost:3031',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ debug: !!process.env.DEBUG,
+});
+
+const sentryCreateMemoryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter);
+
+const router = sentryCreateMemoryRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/user/:id',
+ element: ,
+ },
+ ],
+ {
+ initialEntries: ['/', '/user/1', '/user/2'],
+ initialIndex: 2,
+ },
+);
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+
+root.render();
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx
new file mode 100644
index 000000000000..b025f721e100
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx
@@ -0,0 +1,19 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+
+const Index = () => {
+ return (
+ <>
+ {
+ throw new Error('I am an error!');
+ }}
+ />
+ >
+ );
+};
+
+export default Index;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx
new file mode 100644
index 000000000000..e54d6c604e2d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx
@@ -0,0 +1,19 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+
+const User = () => {
+ return (
+
+
+ Home
+
+
+ navigate
+
+
I am a blank page :)
;
+
+ );
+};
+
+export default User;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts
new file mode 100644
index 000000000000..6431bc5fc6b2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs
new file mode 100644
index 000000000000..9c451610f4c7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-create-memory-router',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts
new file mode 100644
index 000000000000..9406ca63e30c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts
@@ -0,0 +1,34 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Captures exception correctly', async ({ page }) => {
+ const errorEventPromise = waitForError('react-create-memory-router', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ // We're on the user page, navigate back to the home page
+ const homeButton = page.locator('id=home-button');
+ await homeButton.click();
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.request).toEqual({
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/',
+ });
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts
new file mode 100644
index 000000000000..7c75c395c3af
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts
@@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Captures a pageload transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-memory-router', event => {
+ return event.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace).toEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'pageload',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.pageload.react.reactrouter_v6',
+ }),
+ );
+});
+
+test('Captures a navigation transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-memory-router', event => {
+ return event.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto('/');
+ const linkElement = page.locator('id=navigation-button');
+ await linkElement.click();
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'navigation',
+ 'sentry.origin': 'auto.navigation.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'navigation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.navigation.react.reactrouter_v6',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.spans).toEqual([]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json
new file mode 100644
index 000000000000..4cc95dc2689a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": ["src", "tests"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx
index f6694a954915..581014169a78 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import {
BrowserRouter,
+ Outlet,
Route,
Routes,
createRoutesFromChildren,
@@ -48,17 +49,28 @@ const DetailsRoutes = () => (
);
+const DetailsRoutesAlternative = () => (
+
+ Details} />
+
+);
+
const ViewsRoutes = () => (
Views} />
} />
+ } />
);
const ProjectsRoutes = () => (
- }>
- No Match Page} />
+ }>
+ Project Page Root} />
+ }>
+ } />
+
+
);
@@ -67,7 +79,7 @@ root.render(
} />
- }>
+ } />
,
);
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx
index aa99b61f89ea..d2362c149f84 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx
@@ -8,6 +8,9 @@ const Index = () => {
navigate
+
+ navigate old
+
>
);
};
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts
index 23bc0aaabe95..2f13b7cc1eac 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts
@@ -10,6 +10,7 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
const rootSpan = await transactionPromise;
+ expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
expect(rootSpan).toMatchObject({
contexts: {
trace: {
@@ -24,6 +25,30 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
});
});
+test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => {
+ const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/projects/234/old-views/234/567`);
+
+ const rootSpan = await transactionPromise;
+
+ expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
+ expect(rootSpan).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.react.reactrouter_v6',
+ },
+ },
+ transaction: '/projects/:projectId/old-views/:viewId/:detailId',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
@@ -52,6 +77,8 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
const linkElement = page.locator('id=navigation');
const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
+
+ expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
expect(navigationTxn).toMatchObject({
contexts: {
trace: {
@@ -65,3 +92,47 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
},
});
});
+
+test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => {
+ const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+ const pageloadTxn = await pageloadTxnPromise;
+
+ expect(pageloadTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.react.reactrouter_v6',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+
+ const linkElement = page.locator('id=old-navigation');
+
+ const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
+
+ expect((await page.innerHTML('#root')).includes('Details')).toBe(true);
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.react.reactrouter_v6',
+ },
+ },
+ transaction: '/projects/:projectId/old-views/:viewId/:detailId',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts
index d9c3e09f2ad2..dc33d271bc18 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts
@@ -190,7 +190,7 @@ test('Sends a Replay recording to Sentry', async ({ browser }) => {
if (response.ok) {
const data = await response.json();
- return data[0];
+ return { data: data[0], length: data[0].length };
}
return response.status;
@@ -199,5 +199,6 @@ test('Sends a Replay recording to Sentry', async ({ browser }) => {
timeout: EVENT_POLLING_TIMEOUT,
},
)
- .toEqual(ReplayRecordingData);
+ // Check that that all expected data is present but relax the order to avoid flakes
+ .toEqual({ data: expect.arrayContaining(ReplayRecordingData), length: ReplayRecordingData.length });
});
diff --git a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
index cbb7afd9d09c..0c727d46de50 100644
--- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
@@ -21,7 +21,7 @@
"postcss": "^8.4.33",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.1",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"vite-plugin-solid": "^2.8.2"
},
"dependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json
index bb37aa10f263..d61ac0a0a322 100644
--- a/dev-packages/e2e-tests/test-applications/solid/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid/package.json
@@ -21,7 +21,7 @@
"postcss": "^8.4.33",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.1",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"vite-plugin-solid": "^2.8.2"
},
"dependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore
new file mode 100644
index 000000000000..a51ed3c20c8d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore
@@ -0,0 +1,46 @@
+
+dist
+.solid
+.output
+.vercel
+.netlify
+.vinxi
+
+# Environment
+.env
+.env*.local
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+*.launch
+.settings/
+
+# Temp
+gitignore
+
+# testing
+/coverage
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md
new file mode 100644
index 000000000000..9a141e9c2f0d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md
@@ -0,0 +1,45 @@
+# SolidStart
+
+Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
+
+## Creating a project
+
+```bash
+# create a new project in the current directory
+npm init solid@latest
+
+# create a new project in my-app
+npm init solid@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a
+development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+Solid apps are built with _presets_, which optimise your project for deployment to different environments.
+
+By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add
+it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
+
+## Testing
+
+Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some
+helpful custom matchers.
+
+To run them, simply start:
+
+```sh
+npm test
+```
+
+## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts
new file mode 100644
index 000000000000..f41b1cb186ef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts
@@ -0,0 +1,11 @@
+import { withSentry } from '@sentry/solidstart';
+import { defineConfig } from '@solidjs/start/config';
+
+export default defineConfig(
+ withSentry(
+ {},
+ {
+ autoInjectServerSentry: 'experimental_dynamic-import',
+ },
+ ),
+);
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
new file mode 100644
index 000000000000..62393e038dce
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "solidstart-dynamic-import-e2e-testapp",
+ "version": "0.0.0",
+ "scripts": {
+ "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
+ "dev": "vinxi dev",
+ "build": "vinxi build && sh ./post_build.sh",
+ "preview": "HOST=localhost PORT=3030 vinxi start",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "type": "module",
+ "dependencies": {
+ "@sentry/solidstart": "latest || *"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@solidjs/meta": "^0.29.4",
+ "@solidjs/router": "^0.13.4",
+ "@solidjs/start": "^1.0.2",
+ "@solidjs/testing-library": "^0.8.7",
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitest/ui": "^1.5.0",
+ "jsdom": "^24.0.0",
+ "solid-js": "1.8.17",
+ "typescript": "^5.4.5",
+ "vinxi": "^0.4.0",
+ "vite": "^5.4.10",
+ "vite-plugin-solid": "^2.10.2",
+ "vitest": "^1.5.0"
+ },
+ "overrides": {
+ "@vercel/nft": "0.27.4"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs
new file mode 100644
index 000000000000..395acfc282f9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: 'pnpm preview',
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh
new file mode 100644
index 000000000000..6ed67c9afb8a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh
@@ -0,0 +1,8 @@
+# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
+
+# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
+# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
+# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
+# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
+cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
+cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico
new file mode 100644
index 000000000000..fb282da0719e
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx
new file mode 100644
index 000000000000..3eb85218b575
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx
@@ -0,0 +1,22 @@
+import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter';
+import { MetaProvider, Title } from '@solidjs/meta';
+import { Router } from '@solidjs/router';
+import { FileRoutes } from '@solidjs/start/router';
+import { Suspense } from 'solid-js';
+
+const SentryRouter = withSentryRouterRouting(Router);
+
+export default function App() {
+ return (
+ (
+
+ SolidStart - with Vitest
+ {props.children}
+
+ )}
+ >
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx
new file mode 100644
index 000000000000..11087fbb5918
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx
@@ -0,0 +1,18 @@
+// @refresh reload
+import * as Sentry from '@sentry/solidstart';
+import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
+import { StartClient, mount } from '@solidjs/start/client';
+
+Sentry.init({
+ // We can't use env variables here, seems like they are stripped
+ // out in production builds.
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ integrations: [solidRouterBrowserTracingIntegration()],
+ tunnel: 'http://localhost:3031/', // proxy server
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ debug: !!import.meta.env.DEBUG,
+});
+
+mount(() => , document.getElementById('app')!);
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx
new file mode 100644
index 000000000000..276935366318
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx
@@ -0,0 +1,21 @@
+// @refresh reload
+import { StartServer, createHandler } from '@solidjs/start/server';
+
+export default createHandler(() => (
+ (
+
+
+
+
+
+ {assets}
+
+
+ {children}
+ {scripts}
+
+
+ )}
+ />
+));
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts
new file mode 100644
index 000000000000..3dd5d8933b7b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/solidstart';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ tunnel: 'http://localhost:3031/', // proxy server
+ debug: !!process.env.DEBUG,
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx
new file mode 100644
index 000000000000..ddd970944bf3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx
@@ -0,0 +1,9 @@
+import { A } from '@solidjs/router';
+
+export default function BackNavigation() {
+ return (
+
+ User 6
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx
new file mode 100644
index 000000000000..5e405e8c4e40
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx
@@ -0,0 +1,15 @@
+export default function ClientErrorPage() {
+ return (
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx
new file mode 100644
index 000000000000..b22607667e7e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx
@@ -0,0 +1,64 @@
+import * as Sentry from '@sentry/solidstart';
+import type { ParentProps } from 'solid-js';
+import { ErrorBoundary, createSignal, onMount } from 'solid-js';
+
+const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
+
+const [count, setCount] = createSignal(1);
+const [caughtError, setCaughtError] = createSignal(false);
+
+export default function ErrorBoundaryTestPage() {
+ return (
+
+ {caughtError() && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+function Throw(props: { error: string }) {
+ onMount(() => {
+ throw new Error(props.error);
+ });
+ return null;
+}
+
+function SampleErrorBoundary(props: ParentProps) {
+ return (
+ (
+
+ Error Boundary Fallback
+
+ {error.message}
+
+
+
+ )}
+ >
+ {props.children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx
new file mode 100644
index 000000000000..9a0b22cc38c6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx
@@ -0,0 +1,31 @@
+import { A } from '@solidjs/router';
+
+export default function Home() {
+ return (
+ <>
+ Welcome to Solid Start
+
+ Visit docs.solidjs.com/solid-start to read the documentation
+
+
+ >
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx
new file mode 100644
index 000000000000..05dce5e10a56
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx
@@ -0,0 +1,17 @@
+import { withServerActionInstrumentation } from '@sentry/solidstart';
+import { createAsync } from '@solidjs/router';
+
+const getPrefecture = async () => {
+ 'use server';
+ return await withServerActionInstrumentation('getPrefecture', () => {
+ throw new Error('Error thrown from Solid Start E2E test app server route');
+
+ return { prefecture: 'Kanagawa' };
+ });
+};
+
+export default function ServerErrorPage() {
+ const data = createAsync(() => getPrefecture());
+
+ return Prefecture: {data()?.prefecture}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx
new file mode 100644
index 000000000000..22abd3ba8803
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx
@@ -0,0 +1,21 @@
+import { withServerActionInstrumentation } from '@sentry/solidstart';
+import { createAsync, useParams } from '@solidjs/router';
+
+const getPrefecture = async () => {
+ 'use server';
+ return await withServerActionInstrumentation('getPrefecture', () => {
+ return { prefecture: 'Ehime' };
+ });
+};
+export default function User() {
+ const params = useParams();
+ const userData = createAsync(() => getPrefecture());
+
+ return (
+
+ User ID: {params.id}
+
+ Prefecture: {userData()?.prefecture}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs
new file mode 100644
index 000000000000..343e434e030b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'solidstart-dynamic-import',
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts
new file mode 100644
index 000000000000..599b5c121455
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts
@@ -0,0 +1,92 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('captures an exception', async ({ page }) => {
+ const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.goto('/error-boundary');
+ // The first page load causes a hydration error on the dev server sometimes - a reload works around this
+ await page.reload();
+ await page.locator('#caughtErrorBtn').click();
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+});
+
+test('captures a second exception after resetting the boundary', async ({ page }) => {
+ const firstErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.goto('/error-boundary');
+ await page.locator('#caughtErrorBtn').click();
+ const firstErrorEvent = await firstErrorEventPromise;
+
+ expect(firstErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+
+ const secondErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.locator('#errorBoundaryResetBtn').click();
+ await page.locator('#caughtErrorBtn').click();
+ const secondErrorEvent = await secondErrorEventPromise;
+
+ expect(secondErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts
new file mode 100644
index 000000000000..3a1b3ad4b812
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('client-side errors', () => {
+ test('captures error thrown on click', async ({ page }) => {
+ const errorPromise = waitForError('solidstart-dynamic-import', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app';
+ });
+
+ await page.goto(`/client-error`);
+ await page.locator('#errorBtn').click();
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Uncaught error thrown from Solid Start E2E test app',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/client-error',
+ });
+ expect(error.transaction).toEqual('/client-error');
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts
new file mode 100644
index 000000000000..7ef5cd0e07de
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('server-side errors', () => {
+ test('captures server action error', async ({ page }) => {
+ const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route';
+ });
+
+ await page.goto(`/server-error`);
+
+ const error = await errorEventPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from Solid Start E2E test app server route',
+ mechanism: {
+ type: 'solidstart',
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: 'GET /server-error',
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts
new file mode 100644
index 000000000000..63f97d519cf8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts
@@ -0,0 +1,95 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('sends a pageload transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+ const pageloadTransaction = await transactionPromise;
+
+ expect(pageloadTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
+
+test('sends a navigation transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+ await page.locator('#navLink').click();
+ const navigationTransaction = await transactionPromise;
+
+ expect(navigationTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/users/5',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
+
+test('updates the transaction when using the back button', async ({ page }) => {
+ // Solid Router sends a `-1` navigation when using the back button.
+ // The sentry solidRouterBrowserTracingIntegration tries to update such
+ // transactions with the proper name once the `useLocation` hook triggers.
+ const navigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/back-navigation`);
+ await page.locator('#navLink').click();
+ const navigationTxn = await navigationTxnPromise;
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/users/6',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+
+ const backNavigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation'
+ );
+ });
+
+ await page.goBack();
+ const backNavigationTxn = await backNavigationTxnPromise;
+
+ expect(backNavigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/back-navigation',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts
new file mode 100644
index 000000000000..c300014bf012
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts
@@ -0,0 +1,55 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/core';
+
+test('sends a server action transaction on pageload', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /users/6';
+ });
+
+ await page.goto('/users/6');
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'getPrefecture',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ },
+ }),
+ ]),
+ );
+});
+
+test('sends a server action transaction on client navigation', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => {
+ return transactionEvent?.transaction === 'POST getPrefecture';
+ });
+
+ await page.goto('/');
+ await page.locator('#navLink').click();
+ await page.waitForURL('/users/5');
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'getPrefecture',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ },
+ }),
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json
new file mode 100644
index 000000000000..6f11292cc5d8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "allowJs": true,
+ "strict": true,
+ "noEmit": true,
+ "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"],
+ "isolatedModules": true,
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts
new file mode 100644
index 000000000000..6c2b639dc300
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts
@@ -0,0 +1,10 @@
+import solid from 'vite-plugin-solid';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [solid()],
+ resolve: {
+ conditions: ['development', 'browser'],
+ },
+ envPrefix: 'PUBLIC_',
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
index f4ff0802e159..9f8d07823804 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
@@ -27,7 +27,7 @@
"solid-js": "1.8.17",
"typescript": "^5.4.5",
"vinxi": "^0.4.0",
- "vite": "^5.2.8",
+ "vite": "^5.4.11",
"vite-plugin-solid": "^2.10.2",
"vitest": "^1.5.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore
new file mode 100644
index 000000000000..a51ed3c20c8d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore
@@ -0,0 +1,46 @@
+
+dist
+.solid
+.output
+.vercel
+.netlify
+.vinxi
+
+# Environment
+.env
+.env*.local
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+*.launch
+.settings/
+
+# Temp
+gitignore
+
+# testing
+/coverage
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md
new file mode 100644
index 000000000000..9a141e9c2f0d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md
@@ -0,0 +1,45 @@
+# SolidStart
+
+Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
+
+## Creating a project
+
+```bash
+# create a new project in the current directory
+npm init solid@latest
+
+# create a new project in my-app
+npm init solid@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a
+development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+Solid apps are built with _presets_, which optimise your project for deployment to different environments.
+
+By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add
+it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
+
+## Testing
+
+Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some
+helpful custom matchers.
+
+To run them, simply start:
+
+```sh
+npm test
+```
+
+## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts
new file mode 100644
index 000000000000..e4e73e9fc570
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts
@@ -0,0 +1,11 @@
+import { withSentry } from '@sentry/solidstart';
+import { defineConfig } from '@solidjs/start/config';
+
+export default defineConfig(
+ withSentry(
+ {},
+ {
+ autoInjectServerSentry: 'top-level-import',
+ },
+ ),
+);
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
new file mode 100644
index 000000000000..3df1995d6354
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "solidstart-top-level-import-e2e-testapp",
+ "version": "0.0.0",
+ "scripts": {
+ "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output",
+ "dev": "vinxi dev",
+ "build": "vinxi build && sh ./post_build.sh",
+ "preview": "HOST=localhost PORT=3030 vinxi start",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "type": "module",
+ "dependencies": {
+ "@sentry/solidstart": "latest || *"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@solidjs/meta": "^0.29.4",
+ "@solidjs/router": "^0.13.4",
+ "@solidjs/start": "^1.0.2",
+ "@solidjs/testing-library": "^0.8.7",
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/user-event": "^14.5.2",
+ "@vitest/ui": "^1.5.0",
+ "jsdom": "^24.0.0",
+ "solid-js": "1.8.17",
+ "typescript": "^5.4.5",
+ "vinxi": "^0.4.0",
+ "vite": "^5.4.10",
+ "vite-plugin-solid": "^2.10.2",
+ "vitest": "^1.5.0"
+ },
+ "overrides": {
+ "@vercel/nft": "0.27.4"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs
new file mode 100644
index 000000000000..395acfc282f9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: 'pnpm preview',
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh
new file mode 100644
index 000000000000..6ed67c9afb8a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh
@@ -0,0 +1,8 @@
+# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved.
+
+# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules`
+# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm
+# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is
+# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher).
+cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules
+cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico
new file mode 100644
index 000000000000..fb282da0719e
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx
new file mode 100644
index 000000000000..3eb85218b575
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx
@@ -0,0 +1,22 @@
+import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter';
+import { MetaProvider, Title } from '@solidjs/meta';
+import { Router } from '@solidjs/router';
+import { FileRoutes } from '@solidjs/start/router';
+import { Suspense } from 'solid-js';
+
+const SentryRouter = withSentryRouterRouting(Router);
+
+export default function App() {
+ return (
+ (
+
+ SolidStart - with Vitest
+ {props.children}
+
+ )}
+ >
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx
new file mode 100644
index 000000000000..11087fbb5918
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx
@@ -0,0 +1,18 @@
+// @refresh reload
+import * as Sentry from '@sentry/solidstart';
+import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
+import { StartClient, mount } from '@solidjs/start/client';
+
+Sentry.init({
+ // We can't use env variables here, seems like they are stripped
+ // out in production builds.
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ integrations: [solidRouterBrowserTracingIntegration()],
+ tunnel: 'http://localhost:3031/', // proxy server
+ // Performance Monitoring
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ debug: !!import.meta.env.DEBUG,
+});
+
+mount(() => , document.getElementById('app')!);
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx
new file mode 100644
index 000000000000..276935366318
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx
@@ -0,0 +1,21 @@
+// @refresh reload
+import { StartServer, createHandler } from '@solidjs/start/server';
+
+export default createHandler(() => (
+ (
+
+
+
+
+
+ {assets}
+
+
+ {children}
+ {scripts}
+
+
+ )}
+ />
+));
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts
new file mode 100644
index 000000000000..3dd5d8933b7b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/solidstart';
+
+Sentry.init({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ tunnel: 'http://localhost:3031/', // proxy server
+ debug: !!process.env.DEBUG,
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx
new file mode 100644
index 000000000000..ddd970944bf3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx
@@ -0,0 +1,9 @@
+import { A } from '@solidjs/router';
+
+export default function BackNavigation() {
+ return (
+
+ User 6
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx
new file mode 100644
index 000000000000..5e405e8c4e40
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx
@@ -0,0 +1,15 @@
+export default function ClientErrorPage() {
+ return (
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx
new file mode 100644
index 000000000000..b22607667e7e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx
@@ -0,0 +1,64 @@
+import * as Sentry from '@sentry/solidstart';
+import type { ParentProps } from 'solid-js';
+import { ErrorBoundary, createSignal, onMount } from 'solid-js';
+
+const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
+
+const [count, setCount] = createSignal(1);
+const [caughtError, setCaughtError] = createSignal(false);
+
+export default function ErrorBoundaryTestPage() {
+ return (
+
+ {caughtError() && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+function Throw(props: { error: string }) {
+ onMount(() => {
+ throw new Error(props.error);
+ });
+ return null;
+}
+
+function SampleErrorBoundary(props: ParentProps) {
+ return (
+ (
+
+ Error Boundary Fallback
+
+ {error.message}
+
+
+
+ )}
+ >
+ {props.children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx
new file mode 100644
index 000000000000..9a0b22cc38c6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx
@@ -0,0 +1,31 @@
+import { A } from '@solidjs/router';
+
+export default function Home() {
+ return (
+ <>
+ Welcome to Solid Start
+
+ Visit docs.solidjs.com/solid-start to read the documentation
+
+
+ >
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx
new file mode 100644
index 000000000000..05dce5e10a56
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx
@@ -0,0 +1,17 @@
+import { withServerActionInstrumentation } from '@sentry/solidstart';
+import { createAsync } from '@solidjs/router';
+
+const getPrefecture = async () => {
+ 'use server';
+ return await withServerActionInstrumentation('getPrefecture', () => {
+ throw new Error('Error thrown from Solid Start E2E test app server route');
+
+ return { prefecture: 'Kanagawa' };
+ });
+};
+
+export default function ServerErrorPage() {
+ const data = createAsync(() => getPrefecture());
+
+ return Prefecture: {data()?.prefecture}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx
new file mode 100644
index 000000000000..22abd3ba8803
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx
@@ -0,0 +1,21 @@
+import { withServerActionInstrumentation } from '@sentry/solidstart';
+import { createAsync, useParams } from '@solidjs/router';
+
+const getPrefecture = async () => {
+ 'use server';
+ return await withServerActionInstrumentation('getPrefecture', () => {
+ return { prefecture: 'Ehime' };
+ });
+};
+export default function User() {
+ const params = useParams();
+ const userData = createAsync(() => getPrefecture());
+
+ return (
+
+ User ID: {params.id}
+
+ Prefecture: {userData()?.prefecture}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs
new file mode 100644
index 000000000000..46cc8824da18
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'solidstart-top-level-import',
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts
new file mode 100644
index 000000000000..49f50f882b50
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts
@@ -0,0 +1,90 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('captures an exception', async ({ page }) => {
+ const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.goto('/error-boundary');
+ await page.locator('#caughtErrorBtn').click();
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+});
+
+test('captures a second exception after resetting the boundary', async ({ page }) => {
+ const firstErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.goto('/error-boundary');
+ await page.locator('#caughtErrorBtn').click();
+ const firstErrorEvent = await firstErrorEventPromise;
+
+ expect(firstErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+
+ const secondErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => {
+ return (
+ !errorEvent.type &&
+ errorEvent.exception?.values?.[0]?.value ===
+ 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app'
+ );
+ });
+
+ await page.locator('#errorBoundaryResetBtn').click();
+ await page.locator('#caughtErrorBtn').click();
+ const secondErrorEvent = await secondErrorEventPromise;
+
+ expect(secondErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts
new file mode 100644
index 000000000000..9e4a0269eee4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('client-side errors', () => {
+ test('captures error thrown on click', async ({ page }) => {
+ const errorPromise = waitForError('solidstart-top-level-import', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app';
+ });
+
+ await page.goto(`/client-error`);
+ await page.locator('#errorBtn').click();
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Uncaught error thrown from Solid Start E2E test app',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/client-error',
+ });
+ expect(error.transaction).toEqual('/client-error');
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts
new file mode 100644
index 000000000000..682dd34e10f9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('server-side errors', () => {
+ test('captures server action error', async ({ page }) => {
+ const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route';
+ });
+
+ await page.goto(`/server-error`);
+
+ const error = await errorEventPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from Solid Start E2E test app server route',
+ mechanism: {
+ type: 'solidstart',
+ handled: false,
+ },
+ },
+ ],
+ },
+ // transaction: 'GET /server-error', --> only possible with `--import` CLI flag
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts
new file mode 100644
index 000000000000..bd5dece39b33
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts
@@ -0,0 +1,95 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('sends a pageload transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+ const pageloadTransaction = await transactionPromise;
+
+ expect(pageloadTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
+
+test('sends a navigation transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+ await page.locator('#navLink').click();
+ const navigationTransaction = await transactionPromise;
+
+ expect(navigationTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/users/5',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
+
+test('updates the transaction when using the back button', async ({ page }) => {
+ // Solid Router sends a `-1` navigation when using the back button.
+ // The sentry solidRouterBrowserTracingIntegration tries to update such
+ // transactions with the proper name once the `useLocation` hook triggers.
+ const navigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => {
+ return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/back-navigation`);
+ await page.locator('#navLink').click();
+ const navigationTxn = await navigationTxnPromise;
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/users/6',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+
+ const backNavigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation'
+ );
+ });
+
+ await page.goBack();
+ const backNavigationTxn = await backNavigationTxnPromise;
+
+ expect(backNavigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.solidstart.solidrouter',
+ },
+ },
+ transaction: '/back-navigation',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts
new file mode 100644
index 000000000000..8072a7e75181
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts
@@ -0,0 +1,55 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/core';
+
+test('sends a server action transaction on pageload', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /users/6';
+ });
+
+ await page.goto('/users/6');
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'getPrefecture',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ },
+ }),
+ ]),
+ );
+});
+
+test('sends a server action transaction on client navigation', async ({ page }) => {
+ const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => {
+ return transactionEvent?.transaction === 'POST getPrefecture';
+ });
+
+ await page.goto('/');
+ await page.locator('#navLink').click();
+ await page.waitForURL('/users/5');
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'getPrefecture',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ },
+ }),
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json
new file mode 100644
index 000000000000..6f11292cc5d8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "allowJs": true,
+ "strict": true,
+ "noEmit": true,
+ "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"],
+ "isolatedModules": true,
+ "paths": {
+ "~/*": ["./src/*"]
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts
new file mode 100644
index 000000000000..6c2b639dc300
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts
@@ -0,0 +1,10 @@
+import solid from 'vite-plugin-solid';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [solid()],
+ resolve: {
+ conditions: ['development', 'browser'],
+ },
+ envPrefix: 'PUBLIC_',
+});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json
index 032a4af9058a..7002a99f69f9 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json
@@ -27,7 +27,7 @@
"solid-js": "1.8.17",
"typescript": "^5.4.5",
"vinxi": "^0.4.0",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"vite-plugin-solid": "^2.10.2",
"vitest": "^1.5.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
index 1022247cc6ea..ed6cf3ada0d7 100644
--- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
@@ -22,7 +22,7 @@
"svelte-check": "^3.6.7",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"dependencies": {
"@sentry/svelte": "latest || *"
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
index 1ce9273bba52..88d9a37ab98c 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
@@ -29,7 +29,7 @@
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"type": "module"
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json
index 0c531cd72357..5a2d9ce7b4d5 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json
@@ -28,7 +28,7 @@
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"type": "module"
}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
index 39f47c873a5f..3f2f87500e25 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
@@ -28,7 +28,7 @@
"svelte": "^4.2.8",
"svelte-check": "^3.6.0",
"typescript": "^5.0.0",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"type": "module"
}
diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
index 54387ae46cde..a2715d739999 100644
--- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
@@ -24,7 +24,7 @@
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.2.2",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"@playwright/test": "^1.44.1",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json
index f34bdf6d6c0e..0cfe8f31a0eb 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json
@@ -32,7 +32,7 @@
"http-server": "^14.1.1",
"npm-run-all2": "^6.2.0",
"typescript": "~5.3.0",
- "vite": "^5.4.10",
+ "vite": "^5.4.11",
"vue-tsc": "^1.8.27"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
index 262cda11b366..b86e56eb4b83 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
@@ -19,7 +19,7 @@ test('sends an error', async ({ page }) => {
type: 'Error',
value: 'This is a Vue test error',
mechanism: {
- type: 'generic',
+ type: 'vue',
handled: false,
},
},
@@ -47,7 +47,7 @@ test('sends an error with a parameterized transaction name', async ({ page }) =>
type: 'Error',
value: 'This is a Vue test error',
mechanism: {
- type: 'generic',
+ type: 'vue',
handled: false,
},
},
diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json
index a4857bd41082..9c11e947160e 100644
--- a/dev-packages/external-contributor-gh-action/package.json
+++ b/dev-packages/external-contributor-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/external-contributor-gh-action",
"description": "An internal Github Action to add external contributors to the CHANGELOG.md file.",
- "version": "8.45.0",
+ "version": "8.54.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index 24be97583c1c..7545243c8652 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/node-integration-tests",
- "version": "8.45.0",
+ "version": "8.54.0",
"license": "MIT",
"engines": {
"node": ">=14.18"
@@ -16,11 +16,12 @@
"build:types": "tsc -p tsconfig.types.json",
"clean": "rimraf -g **/node_modules && run-p clean:script",
"clean:script": "node scripts/clean.js",
- "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)",
+ "prisma-v5:init": "cd suites/tracing/prisma-orm-v5 && ts-node ./setup.ts",
+ "prisma-v6:init": "cd suites/tracing/prisma-orm-v6 && ts-node ./setup.ts",
"lint": "eslint . --format stylish",
"fix": "eslint . --format stylish --fix",
"type-check": "tsc",
- "pretest": "run-s --silent prisma:init",
+ "pretest": "run-s --silent prisma-v5:init prisma-v6:init",
"test": "jest --config ./jest.config.js",
"test:watch": "yarn test --watch"
},
@@ -30,10 +31,9 @@
"@nestjs/common": "10.4.6",
"@nestjs/core": "10.4.6",
"@nestjs/platform-express": "10.4.6",
- "@prisma/client": "5.9.1",
- "@sentry/aws-serverless": "8.45.0",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/aws-serverless": "8.54.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
"@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21",
"@types/pg": "^8.6.5",
diff --git a/dev-packages/node-integration-tests/scripts/use-ts-3_8.js b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js
new file mode 100644
index 000000000000..d759179f8e06
--- /dev/null
+++ b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js
@@ -0,0 +1,39 @@
+/* eslint-disable no-console */
+const { execSync } = require('child_process');
+const { join } = require('path');
+const { readFileSync, writeFileSync } = require('fs');
+
+const cwd = join(__dirname, '../../..');
+
+// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8.
+// We'll pin to the last version of those types that are compatible.
+console.log('Pinning Express types to old versions...');
+
+const packageJsonPath = join(cwd, 'package.json');
+const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
+
+if (!packageJson.resolutions) packageJson.resolutions = {};
+packageJson.resolutions['@types/express'] = '4.17.13';
+packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30';
+
+writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
+
+const tsVersion = '3.8';
+
+console.log(`Installing typescript@${tsVersion}, and @types/node@14...`);
+
+execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, {
+ stdio: 'inherit',
+ cwd,
+});
+
+console.log('Removing unsupported tsconfig options...');
+
+const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json');
+
+const tsConfig = require(baseTscConfigPath);
+
+// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :(
+delete tsConfig.compilerOptions.noUncheckedIndexedAccess;
+
+writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2));
diff --git a/dev-packages/node-integration-tests/scripts/use-ts-version.js b/dev-packages/node-integration-tests/scripts/use-ts-version.js
deleted file mode 100644
index 0b64d735436c..000000000000
--- a/dev-packages/node-integration-tests/scripts/use-ts-version.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-console */
-const { execSync } = require('child_process');
-const { join } = require('path');
-const { writeFileSync } = require('fs');
-
-const cwd = join(__dirname, '../../..');
-
-const tsVersion = process.argv[2] || '3.8';
-
-console.log(`Installing typescript@${tsVersion}...`);
-
-execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion}`, { stdio: 'inherit', cwd });
-
-console.log('Removing unsupported tsconfig options...');
-
-const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json');
-
-const tsConfig = require(baseTscConfigPath);
-
-// TS 3.8 fails build when it encounteres a config option it does not understand, so we remove it :(
-delete tsConfig.compilerOptions.noUncheckedIndexedAccess;
-
-writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2));
diff --git a/dev-packages/node-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-integration-tests/suites/anr/app-path.mjs
new file mode 100644
index 000000000000..b7d32e1aa9b2
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/anr/app-path.mjs
@@ -0,0 +1,36 @@
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+import * as path from 'path';
+import * as url from 'url';
+
+import * as Sentry from '@sentry/node';
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ autoSessionTracking: false,
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })],
+});
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
diff --git a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs
new file mode 100644
index 000000000000..f58eb87f8237
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs
@@ -0,0 +1,36 @@
+import * as assert from 'assert';
+import * as crypto from 'crypto';
+
+import * as Sentry from '@sentry/node';
+
+global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
+
+setTimeout(() => {
+ process.exit();
+}, 10000);
+
+Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ autoSessionTracking: false,
+ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })],
+});
+
+Sentry.setUser({ email: 'person@home.com' });
+Sentry.addBreadcrumb({ message: 'important message!' });
+
+function longWork() {
+ for (let i = 0; i < 20; i++) {
+ const salt = crypto.randomBytes(128).toString('base64');
+ const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512');
+ assert.ok(hash);
+ }
+}
+
+setTimeout(() => {
+ longWork();
+}, 1000);
+
+setTimeout(() => {
+ longWork();
+}, 4000);
diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs
index 18777e5ecdbd..454a35605925 100644
--- a/dev-packages/node-integration-tests/suites/anr/basic.mjs
+++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs
@@ -30,3 +30,8 @@ function longWork() {
setTimeout(() => {
longWork();
}, 1000);
+
+// Ensure we only send one event even with multiple blocking events
+setTimeout(() => {
+ longWork();
+}, 4000);
diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts
index b1750b308d28..cd7df2a86314 100644
--- a/dev-packages/node-integration-tests/suites/anr/test.ts
+++ b/dev-packages/node-integration-tests/suites/anr/test.ts
@@ -31,20 +31,20 @@ const ANR_EVENT = {
mechanism: { type: 'ANR' },
stacktrace: {
frames: expect.arrayContaining([
- {
+ expect.objectContaining({
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: '?',
in_app: true,
- },
- {
+ }),
+ expect.objectContaining({
colno: expect.any(Number),
lineno: expect.any(Number),
filename: expect.any(String),
function: 'longWork',
in_app: true,
- },
+ }),
]),
},
},
@@ -101,7 +101,7 @@ const ANR_EVENT_WITH_DEBUG_META: Event = {
{
type: 'sourcemap',
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
- code_file: expect.stringContaining('basic.'),
+ code_file: expect.stringContaining('basic'),
},
],
},
@@ -123,6 +123,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () =>
.start(done);
});
+ test('Custom appRootPath', done => {
+ const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = {
+ ...ANR_EVENT_WITH_SCOPE,
+ debug_meta: {
+ images: [
+ {
+ type: 'sourcemap',
+ debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
+ code_file: 'app:///app-path.mjs',
+ },
+ ],
+ },
+ };
+
+ createRunner(__dirname, 'app-path.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META })
+ .start(done);
+ });
+
+ test('multiple events via maxAnrEvents', done => {
+ createRunner(__dirname, 'basic-multiple.mjs')
+ .withMockSentryServer()
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .expect({ event: ANR_EVENT_WITH_DEBUG_META })
+ .start(done);
+ });
+
test('blocked indefinitely', done => {
createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done);
});
diff --git a/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs
similarity index 100%
rename from dev-packages/node-integration-tests/suites/contextLines/instrument.mjs
rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs
diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs
similarity index 100%
rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs
rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs
diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs
similarity index 100%
rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs
rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs
diff --git a/dev-packages/node-integration-tests/suites/contextLines/test.ts b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts
similarity index 96%
rename from dev-packages/node-integration-tests/suites/contextLines/test.ts
rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts
index 1912f0b57f04..87f3c1f6fda0 100644
--- a/dev-packages/node-integration-tests/suites/contextLines/test.ts
+++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts
@@ -1,6 +1,6 @@
import { join } from 'path';
-import { conditionalTest } from '../../utils';
-import { createRunner } from '../../utils/runner';
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
conditionalTest({ min: 18 })('ContextLines integration in ESM', () => {
test('reads encoded context lines from filenames with spaces', done => {
diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts
new file mode 100644
index 000000000000..47aec48484b7
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts
@@ -0,0 +1,5 @@
+import * as Sentry from '@sentry/node';
+
+export function captureException(i: number): void {
+ Sentry.captureException(new Error(`error in loop ${i}`));
+}
diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts
new file mode 100644
index 000000000000..c48fae3e2e2e
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts
@@ -0,0 +1,7 @@
+import { captureException } from './nested-file';
+
+export function runSentry(): void {
+ for (let i = 0; i < 10; i++) {
+ captureException(i);
+ }
+}
diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts
new file mode 100644
index 000000000000..0ca16a75fae2
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts
@@ -0,0 +1,30 @@
+import { execSync } from 'node:child_process';
+import * as path from 'node:path';
+
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+});
+
+import { runSentry } from './other-file';
+
+runSentry();
+
+const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' });
+const lsofTable = lsofOutput.split('\n');
+const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, '');
+const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath));
+
+// There should only be a single entry with the main path, otherwise we are leaking file handles from the
+// context lines integration.
+if (numberOfLsofEntriesWithMainPath.length > 1) {
+ // eslint-disable-next-line no-console
+ console.error('Leaked file handles detected');
+ // eslint-disable-next-line no-console
+ console.error(lsofTable);
+ process.exit(1);
+}
diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts
new file mode 100644
index 000000000000..a8437163de07
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts
@@ -0,0 +1,17 @@
+import { conditionalTest } from '../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+conditionalTest({ min: 18 })('ContextLines integration in CJS', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ // Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892
+ test('does not leak open file handles', done => {
+ createRunner(__dirname, 'scenario.ts')
+ .expectN(10, {
+ event: {},
+ })
+ .start(done);
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/server.js b/dev-packages/node-integration-tests/suites/express/requestUser/server.js
new file mode 100644
index 000000000000..d93d22905506
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/requestUser/server.js
@@ -0,0 +1,49 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ debug: true,
+});
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.use((req, _res, next) => {
+ // We simulate this, which would in other cases be done by some middleware
+ req.user = {
+ id: '1',
+ email: 'test@sentry.io',
+ };
+
+ next();
+});
+
+app.get('/test1', (_req, _res) => {
+ throw new Error('error_1');
+});
+
+app.use((_req, _res, next) => {
+ Sentry.setUser({
+ id: '2',
+ email: 'test2@sentry.io',
+ });
+
+ next();
+});
+
+app.get('/test2', (_req, _res) => {
+ throw new Error('error_2');
+});
+
+Sentry.setupExpressErrorHandler(app);
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/test.ts b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts
new file mode 100644
index 000000000000..ff32e2b96c89
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts
@@ -0,0 +1,49 @@
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+describe('express user handling', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('picks user from request', done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ event: {
+ user: {
+ id: '1',
+ email: 'test@sentry.io',
+ },
+ exception: {
+ values: [
+ {
+ value: 'error_1',
+ },
+ ],
+ },
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test1', { expectError: true });
+ });
+
+ test('setUser overwrites user from request', done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ event: {
+ user: {
+ id: '2',
+ email: 'test2@sentry.io',
+ },
+ exception: {
+ values: [
+ {
+ value: 'error_2',
+ },
+ ],
+ },
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test2', { expectError: true });
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js
new file mode 100644
index 000000000000..da31780f2c5f
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js
@@ -0,0 +1,34 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ transport: loggingTransport,
+ tracesSampler: samplingContext => {
+ // The sampling decision is based on whether the data in `normalizedRequest` is available --> this is what we want to test for
+ return (
+ samplingContext.normalizedRequest.url.includes('/test-normalized-request?query=123') &&
+ samplingContext.normalizedRequest.method &&
+ samplingContext.normalizedRequest.query_string === 'query=123' &&
+ !!samplingContext.normalizedRequest.headers
+ );
+ },
+});
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
+
+const app = express();
+
+app.use(cors());
+
+app.get('/test-normalized-request', (_req, res) => {
+ res.send('Success');
+});
+
+Sentry.setupExpressErrorHandler(app);
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js
index c096871cb755..b60ea07b636f 100644
--- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js
+++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js
@@ -15,7 +15,6 @@ Sentry.init({
samplingContext.attributes['http.method'] === 'GET'
);
},
- debug: true,
});
// express must be required after Sentry is initialized
diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts
index a19299787f91..07cc8d094d8f 100644
--- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts
+++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts
@@ -22,3 +22,23 @@ describe('express tracesSampler', () => {
});
});
});
+
+describe('express tracesSampler includes normalizedRequest data', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ describe('CJS', () => {
+ test('correctly samples & passes data to tracesSampler', done => {
+ const runner = createRunner(__dirname, 'scenario-normalizedRequest.js')
+ .expect({
+ transaction: {
+ transaction: 'GET /test-normalized-request',
+ },
+ })
+ .start(done);
+
+ runner.makeRequest('get', '/test-normalized-request?query=123');
+ });
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js
new file mode 100644
index 000000000000..c98e17276d92
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js
@@ -0,0 +1,58 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node');
+
+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,
+});
+
+// express must be required after Sentry is initialized
+const express = require('express');
+const cors = require('cors');
+const bodyParser = require('body-parser');
+const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
+
+const app = express();
+
+app.use(cors());
+app.use(bodyParser.json());
+app.use(bodyParser.text());
+app.use(bodyParser.raw());
+
+app.get('/test/:id/span-updateName', (_req, res) => {
+ const span = Sentry.getActiveSpan();
+ const rootSpan = Sentry.getRootSpan(span);
+ rootSpan.updateName('new-name');
+ res.send({ response: 'response 1' });
+});
+
+app.get('/test/:id/span-updateName-source', (_req, res) => {
+ const span = Sentry.getActiveSpan();
+ const rootSpan = Sentry.getRootSpan(span);
+ rootSpan.updateName('new-name');
+ rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
+ res.send({ response: 'response 2' });
+});
+
+app.get('/test/:id/updateSpanName', (_req, res) => {
+ const span = Sentry.getActiveSpan();
+ const rootSpan = Sentry.getRootSpan(span);
+ Sentry.updateSpanName(rootSpan, 'new-name');
+ res.send({ response: 'response 3' });
+});
+
+app.get('/test/:id/updateSpanNameAndSource', (_req, res) => {
+ const span = Sentry.getActiveSpan();
+ const rootSpan = Sentry.getRootSpan(span);
+ Sentry.updateSpanName(rootSpan, 'new-name');
+ rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component');
+ res.send({ response: 'response 4' });
+});
+
+Sentry.setupExpressErrorHandler(app);
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts
new file mode 100644
index 000000000000..c6345713fd7e
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts
@@ -0,0 +1,94 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+describe('express tracing', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ describe('CJS', () => {
+ // This test documents the unfortunate behaviour of using `span.updateName` on the server-side.
+ // For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation
+ // calls `span.updateName` and overwrites whatever the name was set to before (by us or by users).
+ test("calling just `span.updateName` doesn't update the final name in express (missing source)", done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ transaction: {
+ transaction: 'GET /test/:id/span-updateName',
+ transaction_info: {
+ source: 'route',
+ },
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test/123/span-updateName');
+ });
+
+ // Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal.
+ // Therefore, only the source is updated but the name is still overwritten by Otel.
+ test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ transaction: {
+ transaction: 'GET /test/:id/span-updateName-source',
+ transaction_info: {
+ source: 'custom',
+ },
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test/123/span-updateName-source');
+ });
+
+ // This test documents the correct way to update the span name (and implicitly the source) in Node:
+ test('calling `Sentry.updateSpanName` updates the final name and source in express', done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ transaction: txnEvent => {
+ expect(txnEvent).toMatchObject({
+ transaction: 'new-name',
+ transaction_info: {
+ source: 'custom',
+ },
+ contexts: {
+ trace: {
+ op: 'http.server',
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ });
+ // ensure we delete the internal attribute once we're done with it
+ expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined();
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test/123/updateSpanName');
+ });
+ });
+
+ // This test documents the correct way to update the span name (and implicitly the source) in Node:
+ test('calling `Sentry.updateSpanName` and setting source subsequently updates the final name and sets correct source', done => {
+ createRunner(__dirname, 'server.js')
+ .expect({
+ transaction: txnEvent => {
+ expect(txnEvent).toMatchObject({
+ transaction: 'new-name',
+ transaction_info: {
+ source: 'component',
+ },
+ contexts: {
+ trace: {
+ op: 'http.server',
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' },
+ },
+ },
+ });
+ // ensure we delete the internal attribute once we're done with it
+ expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined();
+ },
+ })
+ .start(done)
+ .makeRequest('get', '/test/123/updateSpanNameAndSource');
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts
index 86b3bf6d9d22..0370b123cab2 100644
--- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts
+++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts
@@ -1,11 +1,42 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
afterAll(() => {
cleanupChildProcesses();
});
-test('should send a manually started root span', done => {
+test('sends a manually started root span with source custom', done => {
createRunner(__dirname, 'scenario.ts')
- .expect({ transaction: { transaction: 'test_span' } })
+ .expect({
+ transaction: {
+ transaction: 'test_span',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
+ .start(done);
+});
+
+test("doesn't change the name for manually started spans even if attributes triggering inference are set", done => {
+ createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'test_span',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
.start(done);
});
diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts
new file mode 100644
index 000000000000..ea30608c1c5c
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts
@@ -0,0 +1,16 @@
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+Sentry.startSpan(
+ { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
+ (span: Sentry.Span) => {
+ span.updateName('new name');
+ },
+);
diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts
new file mode 100644
index 000000000000..676071926b91
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts
@@ -0,0 +1,24 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('updates the span name when calling `span.updateName`', done => {
+ createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'new name',
+ transaction_info: { source: 'url' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' },
+ },
+ },
+ },
+ })
+ .start(done);
+});
diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts
new file mode 100644
index 000000000000..ecf7670fa23d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts
@@ -0,0 +1,16 @@
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+Sentry.startSpan(
+ { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } },
+ (span: Sentry.Span) => {
+ Sentry.updateSpanName(span, 'new name');
+ },
+);
diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts
new file mode 100644
index 000000000000..c5b325fc0ea2
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts
@@ -0,0 +1,24 @@
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('updates the span name and source when calling `updateSpanName`', done => {
+ createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ transaction: 'new name',
+ transaction_info: { source: 'custom' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
+ },
+ },
+ },
+ })
+ .start(done);
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
similarity index 81%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
index 45caa4bb3179..37d45547b537 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
@@ -4,7 +4,7 @@ services:
db:
image: postgres:13
restart: always
- container_name: integration-tests-prisma
+ container_name: integration-tests-prisma-v5
ports:
- '5433:5432'
environment:
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json
similarity index 80%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json
index b40c92b4356e..3ccf81ee2f71 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json
@@ -3,9 +3,6 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
- "engines": {
- "node": ">=16"
- },
"scripts": {
"db-up": "docker compose up -d",
"generate": "prisma generate",
@@ -16,7 +13,7 @@
"author": "",
"license": "ISC",
"dependencies": {
- "@prisma/client": "5.9.1",
- "prisma": "^5.9.1"
+ "@prisma/client": "5.22.0",
+ "prisma": "5.22.0"
}
}
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/migration_lock.toml
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/migration_lock.toml
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/sentry_test/migration.sql
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/sentry_test/migration.sql
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/schema.prisma
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/schema.prisma
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js
new file mode 100644
index 000000000000..767a6f27bdaa
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js
@@ -0,0 +1,52 @@
+const Sentry = require('@sentry/node');
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.prismaIntegration()],
+});
+
+const { randomBytes } = require('crypto');
+const { PrismaClient } = require('@prisma/client');
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+async function run() {
+ const client = new PrismaClient();
+
+ await Sentry.startSpanManual(
+ {
+ name: 'Test Transaction',
+ op: 'transaction',
+ },
+ async span => {
+ await client.user.create({
+ data: {
+ name: 'Tilda',
+ email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`,
+ },
+ });
+
+ await client.user.findMany();
+
+ await client.user.deleteMany({
+ where: {
+ email: {
+ contains: 'sentry.io',
+ },
+ },
+ });
+
+ setTimeout(async () => {
+ span.end();
+ await client.$disconnect();
+ }, 500);
+ },
+ );
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/setup.ts
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/setup.ts
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts
new file mode 100644
index 000000000000..4cc1757c0d19
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts
@@ -0,0 +1,139 @@
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
+
+conditionalTest({ min: 16 })('Prisma ORM Tests', () => {
+ test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => {
+ createRunner(__dirname, 'scenario.js')
+ .expect({
+ transaction: transaction => {
+ expect(transaction.transaction).toBe('Test Transaction');
+
+ const spans = transaction.spans || [];
+ expect(spans.length).toBeGreaterThanOrEqual(5);
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ method: 'create',
+ model: 'User',
+ name: 'User.create',
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:client:operation',
+ status: 'ok',
+ }),
+ );
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:client:serialize',
+ status: 'ok',
+ }),
+ );
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:client:connect',
+ status: 'ok',
+ }),
+ );
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:engine',
+ status: 'ok',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ 'sentry.op': 'db',
+ 'db.system': 'postgresql',
+ },
+ description: 'prisma:engine:connection',
+ status: 'ok',
+ op: 'db',
+ }),
+ );
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'db.statement': expect.stringContaining(
+ 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent',
+ ),
+ 'sentry.origin': 'auto.db.otel.prisma',
+ 'sentry.op': 'db',
+ 'db.system': 'postgresql',
+ 'otel.kind': 'CLIENT',
+ },
+ description: expect.stringContaining(
+ 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent',
+ ),
+ status: 'ok',
+ op: 'db',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:engine:serialize',
+ status: 'ok',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:engine:response_json_serialization',
+ status: 'ok',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ method: 'findMany',
+ model: 'User',
+ name: 'User.findMany',
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:client:operation',
+ status: 'ok',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:client:serialize',
+ status: 'ok',
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.prisma',
+ },
+ description: 'prisma:engine',
+ status: 'ok',
+ }),
+ );
+ },
+ })
+ .start(done);
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/yarn.lock
new file mode 100644
index 000000000000..860aa032d6cc
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/yarn.lock
@@ -0,0 +1,58 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@prisma/client@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802"
+ integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==
+
+"@prisma/debug@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412"
+ integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==
+
+"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2":
+ version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4"
+ integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==
+
+"@prisma/engines@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84"
+ integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+ "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ "@prisma/fetch-engine" "5.22.0"
+ "@prisma/get-platform" "5.22.0"
+
+"@prisma/fetch-engine@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e"
+ integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+ "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ "@prisma/get-platform" "5.22.0"
+
+"@prisma/get-platform@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd"
+ integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+
+fsevents@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+prisma@5.22.0:
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197"
+ integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==
+ dependencies:
+ "@prisma/engines" "5.22.0"
+ optionalDependencies:
+ fsevents "2.3.3"
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml
new file mode 100644
index 000000000000..ddab7cb9c563
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.9'
+
+services:
+ db:
+ image: postgres:13
+ restart: always
+ container_name: integration-tests-prisma-v6
+ ports:
+ - '5434:5432'
+ environment:
+ POSTGRES_USER: prisma
+ POSTGRES_PASSWORD: prisma
+ POSTGRES_DB: tests
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json
new file mode 100644
index 000000000000..3ccf81ee2f71
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "sentry-prisma-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "db-up": "docker compose up -d",
+ "generate": "prisma generate",
+ "migrate": "prisma migrate dev -n sentry-test",
+ "setup": "run-s --silent db-up generate migrate"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@prisma/client": "5.22.0",
+ "prisma": "5.22.0"
+ }
+}
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml
new file mode 100644
index 000000000000..fbffa92c2bb7
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql
new file mode 100644
index 000000000000..8619aaceb2b0
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "email" TEXT NOT NULL,
+ "name" TEXT,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma
new file mode 100644
index 000000000000..71a4923afb8c
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma
@@ -0,0 +1,15 @@
+datasource db {
+ url = "postgresql://prisma:prisma@localhost:5434/tests"
+ provider = "postgresql"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ email String @unique
+ name String?
+}
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.js
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js
rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.js
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts
new file mode 100755
index 000000000000..a0052511b380
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts
@@ -0,0 +1,18 @@
+import { execSync } from 'child_process';
+import { parseSemver } from '@sentry/core';
+
+const NODE_VERSION = parseSemver(process.versions.node);
+
+// Prisma v5 requires Node.js v16+
+// https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-5#nodejs-minimum-version-change
+if (NODE_VERSION.major && NODE_VERSION.major < 16) {
+ // eslint-disable-next-line no-console
+ console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`);
+ process.exit(0);
+}
+
+try {
+ execSync('yarn && yarn setup');
+} catch (_) {
+ process.exit(1);
+}
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts
new file mode 100644
index 000000000000..52633f0e176b
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts
@@ -0,0 +1,17 @@
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
+
+conditionalTest({ min: 16 })('Prisma ORM Tests', () => {
+ test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => {
+ createRunner(__dirname, 'scenario.js')
+ .expect({
+ transaction: transaction => {
+ expect(transaction.transaction).toBe('Test Transaction');
+
+ const spans = transaction.spans || [];
+ expect(spans).toHaveLength(0);
+ },
+ })
+ .start(done);
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock
new file mode 100644
index 000000000000..860aa032d6cc
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock
@@ -0,0 +1,58 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@prisma/client@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802"
+ integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==
+
+"@prisma/debug@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412"
+ integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==
+
+"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2":
+ version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4"
+ integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==
+
+"@prisma/engines@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84"
+ integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+ "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ "@prisma/fetch-engine" "5.22.0"
+ "@prisma/get-platform" "5.22.0"
+
+"@prisma/fetch-engine@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e"
+ integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+ "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
+ "@prisma/get-platform" "5.22.0"
+
+"@prisma/get-platform@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd"
+ integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==
+ dependencies:
+ "@prisma/debug" "5.22.0"
+
+fsevents@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+prisma@5.22.0:
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197"
+ integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==
+ dependencies:
+ "@prisma/engines" "5.22.0"
+ optionalDependencies:
+ fsevents "2.3.3"
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts
deleted file mode 100644
index dd92de5d0292..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { conditionalTest } from '../../../utils';
-import { createRunner } from '../../../utils/runner';
-
-conditionalTest({ min: 16 })('Prisma ORM Tests', () => {
- test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: {
- method: 'create',
- model: 'User',
- name: 'User.create',
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:client:operation',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:client:serialize',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:client:connect',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:engine',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'db.type': 'postgres',
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:engine:connection',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'db.statement': expect.stringContaining(
- 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent',
- ),
- 'sentry.origin': 'auto.db.otel.prisma',
- 'db.system': 'prisma',
- 'sentry.op': 'db',
- },
- description: expect.stringContaining(
- 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent',
- ),
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:engine:serialize',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:engine:response_json_serialization',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- method: 'findMany',
- model: 'User',
- name: 'User.findMany',
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:client:operation',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:client:serialize',
- status: 'ok',
- }),
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.db.otel.prisma',
- },
- description: 'prisma:engine',
- status: 'ok',
- }),
- ]),
- };
-
- createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
- });
-});
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock
deleted file mode 100644
index 9c0fc47be4be..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock
+++ /dev/null
@@ -1,51 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@prisma/client@5.9.1":
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64"
- integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==
-
-"@prisma/debug@5.9.1":
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3"
- integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==
-
-"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64":
- version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
- resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660"
- integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ==
-
-"@prisma/engines@5.9.1":
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073"
- integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==
- dependencies:
- "@prisma/debug" "5.9.1"
- "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
- "@prisma/fetch-engine" "5.9.1"
- "@prisma/get-platform" "5.9.1"
-
-"@prisma/fetch-engine@5.9.1":
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818"
- integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==
- dependencies:
- "@prisma/debug" "5.9.1"
- "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64"
- "@prisma/get-platform" "5.9.1"
-
-"@prisma/get-platform@5.9.1":
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e"
- integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==
- dependencies:
- "@prisma/debug" "5.9.1"
-
-prisma@^5.9.1:
- version "5.9.1"
- resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa"
- integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==
- dependencies:
- "@prisma/engines" "5.9.1"
diff --git a/dev-packages/node-integration-tests/test.txt b/dev-packages/node-integration-tests/test.txt
new file mode 100644
index 000000000000..64dae8790895
--- /dev/null
+++ b/dev-packages/node-integration-tests/test.txt
@@ -0,0 +1,213 @@
+yarn run v1.22.22
+$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak
+ console.log
+ starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line null
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+ console.log
+ line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ProcessAndThreadBreadcrumbs","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]]
+
+ at log (utils/runner.ts:462:11)
+
+Done in 4.21s.
diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts
index bc4fb901e2db..a3fe726767b4 100644
--- a/dev-packages/node-integration-tests/utils/runner.ts
+++ b/dev-packages/node-integration-tests/utils/runner.ts
@@ -168,6 +168,12 @@ export function createRunner(...paths: string[]) {
expectedEnvelopes.push(expected);
return this;
},
+ expectN: function (n: number, expected: Expected) {
+ for (let i = 0; i < n; i++) {
+ expectedEnvelopes.push(expected);
+ }
+ return this;
+ },
expectHeader: function (expected: ExpectedEnvelopeHeader) {
if (!expectedEnvelopeHeaders) {
expectedEnvelopeHeaders = [];
diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json
index 763f043da327..0158d94974c4 100644
--- a/dev-packages/rollup-utils/package.json
+++ b/dev-packages/rollup-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/rollup-utils",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils",
diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json
index c40c18645d6a..5c6107f725c4 100644
--- a/dev-packages/size-limit-gh-action/package.json
+++ b/dev-packages/size-limit-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/size-limit-gh-action",
"description": "An internal Github Action to compare the current size of a PR against the one on develop.",
- "version": "8.45.0",
+ "version": "8.54.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json
index 09ad4cf5a55d..a1bf82222bd6 100644
--- a/dev-packages/test-utils/package.json
+++ b/dev-packages/test-utils/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "8.45.0",
+ "version": "8.54.0",
"name": "@sentry-internal/test-utils",
"author": "Sentry",
"license": "MIT",
@@ -45,7 +45,7 @@
},
"devDependencies": {
"@playwright/test": "^1.44.1",
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/docs/assets/run-release-workflow.png b/docs/assets/run-release-workflow.png
new file mode 100644
index 000000000000..50af8d111fe8
Binary files /dev/null and b/docs/assets/run-release-workflow.png differ
diff --git a/docs/changelog/v7.md b/docs/changelog/v7.md
index e784702015e0..cef925871efa 100644
--- a/docs/changelog/v7.md
+++ b/docs/changelog/v7.md
@@ -3,13 +3,49 @@
Support for Sentry SDK v7 will be dropped soon. We recommend migrating to the latest version of the SDK. You can migrate
from `v7` of the SDK to `v8` by following the [migration guide](../../MIGRATION.md#upgrading-from-7x-to-8x).
+## 7.120.3
+
+- fix(v7/publish): Ensure discontinued packages are published with `latest` tag (#14926)
+
+## 7.120.2
+
+- fix(tracing-internal): Fix case when lrp keys offset is 0 (#14615)
+
+Work in this release contributed by @LubomirIgonda1. Thank you for your contribution!
+
+## 7.120.1
+
+- fix(v7/cdn): Ensure `_sentryModuleMetadata` is not mangled (#14357)
+
+Work in this release contributed by @gilisho. Thank you for your contribution!
+
+## 7.120.0
+
+- feat(v7/browser): Add moduleMetadataIntegration lazy loading support (#13822)
+
+Work in this release contributed by @gilisho. Thank you for your contribution!
+
+## 7.119.2
+
+- chore(nextjs/v7): Bump rollup to 2.79.2
+
+## 7.119.1
+
+- fix(browser/v7): Ensure wrap() only returns functions (#13838 backport)
+
+Work in this release contributed by @legobeat. Thank you for your contribution!
+
+## 7.119.0
+
+- backport(tracing): Report dropped spans for transactions (#13343)
+
## 7.118.0
- fix(v7/bundle): Ensure CDN bundles do not overwrite `window.Sentry` (#12579)
## 7.117.0
-- feat(browser/v7): Publish browserprofling CDN bundle (#12224)
+- feat(browser/v7): Publish browser profiling CDN bundle (#12224)
- fix(v7/publish): Add `v7` tag to `@sentry/replay` (#12304)
## 7.116.0
diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md
index 7b88d6bd41b8..a3fd5b64f0ea 100644
--- a/docs/publishing-a-release.md
+++ b/docs/publishing-a-release.md
@@ -20,6 +20,21 @@ _These steps are only relevant to Sentry employees when preparing and publishing
[@getsentry/releases-approvers](https://github.com/orgs/getsentry/teams/release-approvers) to approve the release. a.
Once the release is completed, a sync from `master` ->` develop` will be automatically triggered
+## Publishing a release for previous majors
+
+1. Run `yarn changelog` on the major branch (e.g. `v8`) and determine what version will be released (we use
+ [semver](https://semver.org))
+2. Create a branch, e.g. `changelog-8.45.1`, off the major branch (e.g. `v8`)
+3. Update `CHANGELOG.md` to add an entry for the next release number and a list of changes since the
+ last release. (See details below.)
+4. Open a PR with the title `meta(changelog): Update changelog for VERSION` against the major branch.
+5. Once the PR is merged, open the [Prepare Release workflow](https://github.com/getsentry/sentry-javascript/actions/workflows/release.yml) and
+ fill in 
+ 1. The major branch you want to release for, e.g. `v8`
+ 2. The version you want to release, e.g. `8.45.1`
+ 3. The major branch to merge into, e.g. `v8`
+6. Run the release workflow
+
## Updating the Changelog
1. Run `yarn changelog` and copy everything.
diff --git a/lerna.json b/lerna.json
index febf4090d555..e2fe0e986c20 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "8.45.0",
+ "version": "8.54.0",
"npmClient": "yarn"
}
diff --git a/package.json b/package.json
index e948ae773c72..b8a10c1cb6c7 100644
--- a/package.json
+++ b/package.json
@@ -113,7 +113,7 @@
"@types/jest": "^27.4.1",
"@types/jsdom": "^21.1.6",
"@types/node": "^14.18.0",
- "@vitest/coverage-v8": "^1.6.0",
+ "@vitest/coverage-v8": "^2.1.8",
"deepmerge": "^4.2.2",
"downlevel-dts": "~0.11.0",
"eslint": "7.32.0",
@@ -134,7 +134,7 @@
"ts-jest": "^27.1.4",
"ts-node": "10.9.1",
"typescript": "4.9.5",
- "vitest": "^1.6.0",
+ "vitest": "^2.1.8",
"yalc": "^1.0.0-pre.53"
},
"//_resolutions_comment": [
diff --git a/packages/angular/package.json b/packages/angular/package.json
index 06bb0492c2f7..0715eae655e7 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/angular",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Angular",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular",
@@ -21,8 +21,8 @@
"rxjs": "^6.5.5 || ^7.x"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
"tslib": "^2.4.1"
},
"devDependencies": {
diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts
index c347a5e19b2e..a5b9391e6ee4 100644
--- a/packages/angular/src/tracing.ts
+++ b/packages/angular/src/tracing.ts
@@ -1,3 +1,5 @@
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+import { ElementRef } from '@angular/core';
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
import { Directive, Injectable, Input, NgModule } from '@angular/core';
import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router';
@@ -235,10 +237,17 @@ export class TraceService implements OnDestroy {
}
}
-const UNKNOWN_COMPONENT = 'unknown';
-
/**
- * A directive that can be used to capture initialization lifecycle of the whole component.
+ * Captures the initialization lifecycle of the component this directive is applied to.
+ * Specifically, this directive measures the time between `ngOnInit` and `ngAfterViewInit`
+ * of the component.
+ *
+ * Falls back to the component's selector if no name is provided.
+ *
+ * @example
+ * ```html
+ *
+ * ```
*/
@Directive({ selector: '[trace]' })
export class TraceDirective implements OnInit, AfterViewInit {
@@ -246,13 +255,19 @@ export class TraceDirective implements OnInit, AfterViewInit {
private _tracingSpan?: Span;
+ public constructor(private readonly _host: ElementRef) {}
+
/**
* Implementation of OnInit lifecycle method
* @inheritdoc
*/
public ngOnInit(): void {
if (!this.componentName) {
- this.componentName = UNKNOWN_COMPONENT;
+ // Technically, the `trace` binding should always be provided.
+ // However, if it is incorrectly declared on the element without a
+ // value (e.g., ``), we fall back to using `tagName`
+ // (which is e.g. `APP-COMPONENT`).
+ this.componentName = this._host.nativeElement.tagName.toLowerCase();
}
if (getActiveSpan()) {
diff --git a/packages/angular/vitest.config.ts b/packages/angular/vitest.config.ts
index 9f09af3b153e..82015893133b 100644
--- a/packages/angular/vitest.config.ts
+++ b/packages/angular/vitest.config.ts
@@ -1,10 +1,9 @@
-import type { UserConfig } from 'vitest';
import { defineConfig } from 'vitest/config';
import baseConfig from '../../vite/vite.config';
export default defineConfig({
test: {
- ...(baseConfig as UserConfig & { test: any }).test,
+ ...baseConfig.test,
coverage: {},
globals: true,
setupFiles: ['./setup-test.ts'],
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 43c374a766cc..b8461f46233c 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/astro",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Astro",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro",
@@ -56,14 +56,14 @@
"astro": ">=3.x || >=4.0.0-beta || >=5.x"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
"@sentry/vite-plugin": "^2.22.6"
},
"devDependencies": {
"astro": "^3.5.0",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 7eca9de9a41a..ad7481816b4d 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -138,6 +138,7 @@ export {
startSpanManual,
tediousIntegration,
trpcMiddleware,
+ updateSpanName,
withActiveSpan,
withIsolationScope,
withMonitor,
diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts
index ce87a51c3af7..cfcce38d624d 100644
--- a/packages/astro/src/index.types.ts
+++ b/packages/astro/src/index.types.ts
@@ -27,8 +27,6 @@ export declare function flush(timeout?: number | undefined): PromiseLike {
- event.transaction = context.functionName;
- return event;
- });
+ scope.setTransactionName(context.functionName);
scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname());
scope.setTag('url', `awslambda:///${context.functionName}`);
}
diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts
index 7ab59670cdf2..28b58a830e61 100644
--- a/packages/aws-serverless/test/sdk.test.ts
+++ b/packages/aws-serverless/test/sdk.test.ts
@@ -18,6 +18,7 @@ const mockScope = {
setTag: jest.fn(),
setContext: jest.fn(),
addEventProcessor: jest.fn(),
+ setTransactionName: jest.fn(),
};
jest.mock('@sentry/node', () => {
@@ -81,12 +82,8 @@ const fakeCallback: Callback = (err, result) => {
};
function expectScopeSettings() {
- expect(mockScope.addEventProcessor).toBeCalledTimes(1);
- // Test than an event processor to add `transaction` is registered for the scope
- const eventProcessor = mockScope.addEventProcessor.mock.calls[0][0];
- const event: Event = {};
- eventProcessor(event);
- expect(event).toEqual({ transaction: 'functionName' });
+ expect(mockScope.setTransactionName).toBeCalledTimes(1);
+ expect(mockScope.setTransactionName).toBeCalledWith('functionName');
expect(mockScope.setTag).toBeCalledWith('server_name', expect.anything());
diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json
index 15d5bde00065..b401a2ef538d 100644
--- a/packages/browser-utils/package.json
+++ b/packages/browser-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/browser-utils",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Browser Utilities for all Sentry JavaScript SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts
index c71b2d70e31d..30bc3a29888e 100644
--- a/packages/browser-utils/src/index.ts
+++ b/packages/browser-utils/src/index.ts
@@ -17,6 +17,8 @@ export {
registerInpInteractionListener,
} from './metrics/browserMetrics';
+export { extractNetworkProtocol } from './metrics/utils';
+
export { addClickKeypressInstrumentationHandler } from './instrument/dom';
export { addHistoryInstrumentationHandler } from './instrument/history';
diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
index aadde247642c..685fba295357 100644
--- a/packages/browser-utils/src/metrics/browserMetrics.ts
+++ b/packages/browser-utils/src/metrics/browserMetrics.ts
@@ -20,7 +20,13 @@ import {
addPerformanceInstrumentationHandler,
addTtfbInstrumentationHandler,
} from './instrument';
-import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
+import {
+ extractNetworkProtocol,
+ getBrowserPerformanceAPI,
+ isMeasurementValue,
+ msToSec,
+ startAndEndSpan,
+} from './utils';
import { getActivationStart } from './web-vitals/lib/getActivationStart';
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
@@ -596,6 +602,10 @@ export function _addResourceSpans(
attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
+ const { name, version } = extractNetworkProtocol(entry.nextHopProtocol);
+ attributes['network.protocol.name'] = name;
+ attributes['network.protocol.version'] = version;
+
const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;
diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts
index b6bc9fc54f2f..bf1517b5a715 100644
--- a/packages/browser-utils/src/metrics/utils.ts
+++ b/packages/browser-utils/src/metrics/utils.ts
@@ -134,3 +134,34 @@ export function getBrowserPerformanceAPI(): Performance | undefined {
export function msToSec(time: number): number {
return time / 1000;
}
+
+/**
+ * Converts ALPN protocol ids to name and version.
+ *
+ * (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
+ * @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
+ */
+export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } {
+ let name = 'unknown';
+ let version = 'unknown';
+ let _name = '';
+ for (const char of nextHopProtocol) {
+ // http/1.1 etc.
+ if (char === '/') {
+ [name, version] = nextHopProtocol.split('/') as [string, string];
+ break;
+ }
+ // h2, h3 etc.
+ if (!isNaN(Number(char))) {
+ name = _name === 'h' ? 'http' : _name;
+ version = nextHopProtocol.split(_name)[1] as string;
+ break;
+ }
+ _name += char;
+ }
+ if (_name === nextHopProtocol) {
+ // webrtc, ftp, etc.
+ name = _name;
+ }
+ return { name, version };
+}
diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts
index 2ff1c2df209a..98a3bb375c00 100644
--- a/packages/browser-utils/test/browser/browserMetrics.test.ts
+++ b/packages/browser-utils/test/browser/browserMetrics.test.ts
@@ -131,6 +131,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 256,
decodedBodySize: 256,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, resourceEntryName, 123, 456, 100);
@@ -150,6 +151,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 256,
decodedBodySize: 256,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 456, 100);
@@ -169,6 +171,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 456,
decodedBodySize: 593,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'http/1.1',
});
const timeOrigin = 100;
@@ -195,6 +198,8 @@ describe('_addResourceSpans', () => {
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.same_origin']: true,
+ ['network.protocol.name']: 'http',
+ ['network.protocol.version']: '1.1',
},
}),
);
@@ -233,6 +238,7 @@ describe('_addResourceSpans', () => {
const { initiatorType, op } = table[i]!;
const entry = mockPerformanceResourceTiming({
initiatorType,
+ nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465);
@@ -254,6 +260,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 0,
decodedBodySize: 0,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'h2',
});
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -271,6 +278,8 @@ describe('_addResourceSpans', () => {
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.same_origin']: true,
+ ['network.protocol.name']: 'http',
+ ['network.protocol.version']: '2',
},
}),
);
@@ -288,6 +297,7 @@ describe('_addResourceSpans', () => {
transferSize: 2147483647,
encodedBodySize: 2147483647,
decodedBodySize: 2147483647,
+ nextHopProtocol: 'h3',
});
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -301,6 +311,8 @@ describe('_addResourceSpans', () => {
'server.address': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
+ ['network.protocol.name']: 'http',
+ ['network.protocol.version']: '3',
},
description: '/assets/to/css',
timestamp: 468,
@@ -325,6 +337,7 @@ describe('_addResourceSpans', () => {
transferSize: null,
encodedBodySize: null,
decodedBodySize: null,
+ nextHopProtocol: 'h3',
} as unknown as PerformanceResourceTiming;
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -338,6 +351,8 @@ describe('_addResourceSpans', () => {
'server.address': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
+ ['network.protocol.name']: 'http',
+ ['network.protocol.version']: '3',
},
description: '/assets/to/css',
timestamp: 468,
@@ -365,6 +380,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 0,
decodedBodySize: 0,
deliveryType,
+ nextHopProtocol: 'h3',
});
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
diff --git a/packages/browser-utils/test/browser/utils.test.ts b/packages/browser-utils/test/browser/utils.test.ts
index bb7a757e4b6a..01fb5da605c4 100644
--- a/packages/browser-utils/test/browser/utils.test.ts
+++ b/packages/browser-utils/test/browser/utils.test.ts
@@ -1,5 +1,5 @@
import { SentrySpan, getCurrentScope, getIsolationScope, setCurrentClient, spanToJSON } from '@sentry/core';
-import { startAndEndSpan } from '../../src/metrics/utils';
+import { extractNetworkProtocol, startAndEndSpan } from '../../src/metrics/utils';
import { TestClient, getDefaultClientOptions } from '../utils/TestClient';
describe('startAndEndSpan()', () => {
@@ -54,3 +54,44 @@ describe('startAndEndSpan()', () => {
expect(spanToJSON(parentSpan).start_timestamp).toEqual(123);
});
});
+
+describe('HTTPTimings', () => {
+ test.each([
+ ['http/0.9', { name: 'http', version: '0.9' }],
+ ['http/1.0', { name: 'http', version: '1.0' }],
+ ['http/1.1', { name: 'http', version: '1.1' }],
+ ['spdy/1', { name: 'spdy', version: '1' }],
+ ['spdy/2', { name: 'spdy', version: '2' }],
+ ['spdy/3', { name: 'spdy', version: '3' }],
+ ['stun.turn', { name: 'stun.turn', version: 'unknown' }],
+ ['stun.nat-discovery', { name: 'stun.nat-discovery', version: 'unknown' }],
+ ['h2', { name: 'http', version: '2' }],
+ ['h2c', { name: 'http', version: '2c' }],
+ ['webrtc', { name: 'webrtc', version: 'unknown' }],
+ ['c-webrtc', { name: 'c-webrtc', version: 'unknown' }],
+ ['ftp', { name: 'ftp', version: 'unknown' }],
+ ['imap', { name: 'imap', version: 'unknown' }],
+ ['pop3', { name: 'pop', version: '3' }],
+ ['managesieve', { name: 'managesieve', version: 'unknown' }],
+ ['coap', { name: 'coap', version: 'unknown' }],
+ ['xmpp-client', { name: 'xmpp-client', version: 'unknown' }],
+ ['xmpp-server', { name: 'xmpp-server', version: 'unknown' }],
+ ['acme-tls/1', { name: 'acme-tls', version: '1' }],
+ ['mqtt', { name: 'mqtt', version: 'unknown' }],
+ ['dot', { name: 'dot', version: 'unknown' }],
+ ['ntske/1', { name: 'ntske', version: '1' }],
+ ['sunrpc', { name: 'sunrpc', version: 'unknown' }],
+ ['h3', { name: 'http', version: '3' }],
+ ['smb', { name: 'smb', version: 'unknown' }],
+ ['irc', { name: 'irc', version: 'unknown' }],
+ ['nntp', { name: 'nntp', version: 'unknown' }],
+ ['nnsp', { name: 'nnsp', version: 'unknown' }],
+ ['doq', { name: 'doq', version: 'unknown' }],
+ ['sip/2', { name: 'sip', version: '2' }],
+ ['tds/8.0', { name: 'tds', version: '8.0' }],
+ ['dicom', { name: 'dicom', version: 'unknown' }],
+ ['', { name: '', version: 'unknown' }],
+ ])('Extracting version from ALPN protocol %s', (protocol, expected) => {
+ expect(extractNetworkProtocol(protocol)).toMatchObject(expected);
+ });
+});
diff --git a/packages/browser/package.json b/packages/browser/package.json
index f588f2801eb0..0716fe0904d1 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/browser",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for browsers",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser",
@@ -39,14 +39,14 @@
"access": "public"
},
"dependencies": {
- "@sentry-internal/browser-utils": "8.45.0",
- "@sentry-internal/feedback": "8.45.0",
- "@sentry-internal/replay": "8.45.0",
- "@sentry-internal/replay-canvas": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry-internal/browser-utils": "8.54.0",
+ "@sentry-internal/feedback": "8.54.0",
+ "@sentry-internal/replay": "8.54.0",
+ "@sentry-internal/replay-canvas": "8.54.0",
+ "@sentry/core": "8.54.0"
},
"devDependencies": {
- "@sentry-internal/integration-shims": "8.45.0",
+ "@sentry-internal/integration-shims": "8.54.0",
"fake-indexeddb": "^4.0.1"
},
"scripts": {
diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs
index f65c27aad6e9..eaf1e1b54a8e 100644
--- a/packages/browser/rollup.bundle.config.mjs
+++ b/packages/browser/rollup.bundle.config.mjs
@@ -37,6 +37,19 @@ reexportedPluggableIntegrationFiles.forEach(integrationName => {
builds.push(...makeBundleConfigVariants(integrationsBundleConfig));
});
+// Bundle config for additional exports we don't want to include in the main SDK bundle
+// if we need more of these, we can generalize the config as for pluggable integrations
+builds.push(
+ ...makeBundleConfigVariants(
+ makeBaseBundleConfig({
+ bundleType: 'addon',
+ entrypoints: ['src/pluggable-exports-bundle/index.multiplexedtransport.ts'],
+ licenseTitle: '@sentry/browser - multiplexedtransport',
+ outputFileBase: () => 'bundles/multiplexedtransport',
+ }),
+ ),
+);
+
const baseBundleConfig = makeBaseBundleConfig({
bundleType: 'standalone',
entrypoints: ['src/index.bundle.ts'],
diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts
index 492f9da23b38..295e6daa36cc 100644
--- a/packages/browser/src/exports.ts
+++ b/packages/browser/src/exports.ts
@@ -62,6 +62,7 @@ export {
spanToJSON,
spanToTraceHeader,
spanToBaggageHeader,
+ updateSpanName,
} from '@sentry/core';
export {
diff --git a/packages/browser/src/feedbackSync.ts b/packages/browser/src/feedbackSync.ts
index b99c9a4b752f..ede41fefb221 100644
--- a/packages/browser/src/feedbackSync.ts
+++ b/packages/browser/src/feedbackSync.ts
@@ -3,11 +3,9 @@ import {
feedbackModalIntegration,
feedbackScreenshotIntegration,
} from '@sentry-internal/feedback';
-import { lazyLoadIntegration } from './utils/lazyLoadIntegration';
/** Add a widget to capture user feedback to your application. */
export const feedbackSyncIntegration = buildFeedbackIntegration({
- lazyLoadIntegration,
getModalIntegration: () => feedbackModalIntegration,
getScreenshotIntegration: () => feedbackScreenshotIntegration,
});
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index e6f57c13fe6b..e6e089cbe04c 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -75,3 +75,4 @@ export {
} from './integrations/featureFlags';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
+export { unleashIntegration } from './integrations/featureFlags/unleash';
diff --git a/packages/browser/src/integrations/featureFlags/unleash/index.ts b/packages/browser/src/integrations/featureFlags/unleash/index.ts
new file mode 100644
index 000000000000..934ff196ee95
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/unleash/index.ts
@@ -0,0 +1 @@
+export { unleashIntegration } from './integration';
diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts
new file mode 100644
index 000000000000..54c5abf159d6
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts
@@ -0,0 +1,87 @@
+import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core';
+
+import { defineIntegration, fill, logger } from '@sentry/core';
+import { DEBUG_BUILD } from '../../../debug-build';
+import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags';
+import type { UnleashClient, UnleashClientClass } from './types';
+
+type UnleashIntegrationOptions = {
+ featureFlagClientClass?: UnleashClientClass;
+
+ /**
+ * @deprecated Use `featureFlagClientClass` instead.
+ */
+ unleashClientClass?: UnleashClientClass;
+};
+
+/**
+ * Sentry integration for capturing feature flag evaluations from the Unleash SDK.
+ *
+ * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information.
+ *
+ * @example
+ * ```
+ * import { UnleashClient } from 'unleash-proxy-client';
+ * import * as Sentry from '@sentry/browser';
+ *
+ * Sentry.init({
+ * dsn: '___PUBLIC_DSN___',
+ * integrations: [Sentry.unleashIntegration({featureFlagClientClass: UnleashClient})],
+ * });
+ *
+ * const unleash = new UnleashClient(...);
+ * unleash.start();
+ *
+ * unleash.isEnabled('my-feature');
+ * Sentry.captureException(new Error('something went wrong'));
+ * ```
+ */
+export const unleashIntegration = defineIntegration(
+ // eslint-disable-next-line deprecation/deprecation
+ ({ featureFlagClientClass, unleashClientClass }: UnleashIntegrationOptions) => {
+ const _unleashClientClass = featureFlagClientClass ?? unleashClientClass;
+ if (!_unleashClientClass) {
+ throw new Error('featureFlagClientClass option is required');
+ }
+
+ return {
+ name: 'Unleash',
+
+ processEvent(event: Event, _hint: EventHint, _client: Client): Event {
+ return copyFlagsFromScopeToEvent(event);
+ },
+
+ setupOnce() {
+ const unleashClientPrototype = _unleashClientClass.prototype as UnleashClient;
+ fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled);
+ },
+ };
+ },
+) satisfies IntegrationFn;
+
+/**
+ * Wraps the UnleashClient.isEnabled method to capture feature flag evaluations. Its only side effect is writing to Sentry scope.
+ *
+ * This wrapper is safe for all isEnabled signatures. If the signature does not match (this: UnleashClient, toggleName: string, ...args: unknown[]) => boolean,
+ * we log an error and return the original result.
+ *
+ * @param original - The original method.
+ * @returns Wrapped method. Results should match the original.
+ */
+function _wrappedIsEnabled(
+ original: (this: UnleashClient, ...args: unknown[]) => unknown,
+): (this: UnleashClient, ...args: unknown[]) => unknown {
+ return function (this: UnleashClient, ...args: unknown[]): unknown {
+ const toggleName = args[0];
+ const result = original.apply(this, args);
+
+ if (typeof toggleName === 'string' && typeof result === 'boolean') {
+ insertFlagToScope(toggleName, result);
+ } else if (DEBUG_BUILD) {
+ logger.error(
+ `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`,
+ );
+ }
+ return result;
+ };
+}
diff --git a/packages/browser/src/integrations/featureFlags/unleash/types.ts b/packages/browser/src/integrations/featureFlags/unleash/types.ts
new file mode 100644
index 000000000000..c87798859911
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/unleash/types.ts
@@ -0,0 +1,23 @@
+export interface IVariant {
+ name: string;
+ enabled: boolean;
+ feature_enabled?: boolean;
+ payload?: {
+ type: string;
+ value: string;
+ };
+}
+
+export interface UnleashClient {
+ isEnabled(this: UnleashClient, featureName: string): boolean;
+ getVariant(this: UnleashClient, featureName: string): IVariant;
+}
+
+export interface IConfig {
+ [key: string]: unknown;
+ appName: string;
+ clientKey: string;
+ url: URL | string;
+}
+
+export type UnleashClientClass = new (config: IConfig) => UnleashClient;
diff --git a/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts
new file mode 100644
index 000000000000..a7d637d9e62f
--- /dev/null
+++ b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts
@@ -0,0 +1 @@
+export { makeMultiplexedTransport } from '@sentry/core';
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index 17030f2f4a43..78ead340ac12 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -465,6 +465,7 @@ export function getMetaContent(metaName: string): string | undefined {
// Can't specify generic to `getDomElement` because tracing can be used
// in a variety of environments, have to disable `no-unsafe-member-access`
// as a result.
+ // eslint-disable-next-line deprecation/deprecation
const metaTag = getDomElement(`meta[name=${metaName}]`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return metaTag ? metaTag.getAttribute('content') : undefined;
diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts
index 5f32b227fa85..5418d5b9f154 100644
--- a/packages/browser/src/tracing/request.ts
+++ b/packages/browser/src/tracing/request.ts
@@ -2,6 +2,7 @@ import {
SENTRY_XHR_DATA_KEY,
addPerformanceInstrumentationHandler,
addXhrInstrumentationHandler,
+ extractNetworkProtocol,
} from '@sentry-internal/browser-utils';
import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/core';
import {
@@ -227,37 +228,6 @@ function addHTTPTimings(span: Span): void {
});
}
-/**
- * Converts ALPN protocol ids to name and version.
- *
- * (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
- * @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
- */
-export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } {
- let name = 'unknown';
- let version = 'unknown';
- let _name = '';
- for (const char of nextHopProtocol) {
- // http/1.1 etc.
- if (char === '/') {
- [name, version] = nextHopProtocol.split('/') as [string, string];
- break;
- }
- // h2, h3 etc.
- if (!isNaN(Number(char))) {
- name = _name === 'h' ? 'http' : _name;
- version = nextHopProtocol.split(_name)[1] as string;
- break;
- }
- _name += char;
- }
- if (_name === nextHopProtocol) {
- // webrtc, ftp, etc.
- name = _name;
- }
- return { name, version };
-}
-
function getAbsoluteTime(time: number = 0): number {
return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000;
}
diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts
index 372c360194c7..5fbf7fa6ffc4 100644
--- a/packages/browser/src/transports/offline.ts
+++ b/packages/browser/src/transports/offline.ts
@@ -1,5 +1,6 @@
import type { BaseTransportOptions, Envelope, OfflineStore, OfflineTransportOptions, Transport } from '@sentry/core';
import { makeOfflineTransport, parseEnvelope, serializeEnvelope } from '@sentry/core';
+import { WINDOW } from '../helpers';
import { makeFetchTransport } from './fetch';
// 'Store', 'promisifyRequest' and 'createStore' were originally copied from the 'idb-keyval' package before being
@@ -158,7 +159,15 @@ function createIndexedDbStore(options: BrowserOfflineTransportOptions): OfflineS
function makeIndexedDbOfflineTransport(
createTransport: (options: T) => Transport,
): (options: T & BrowserOfflineTransportOptions) => Transport {
- return options => createTransport({ ...options, createStore: createIndexedDbStore });
+ return options => {
+ const transport = createTransport({ ...options, createStore: createIndexedDbStore });
+
+ WINDOW.addEventListener('online', async _ => {
+ await transport.flush();
+ });
+
+ return transport;
+ };
}
/**
diff --git a/packages/browser/test/integrations/featureFlags/unleash.test.ts b/packages/browser/test/integrations/featureFlags/unleash.test.ts
new file mode 100644
index 000000000000..659d90fd9b03
--- /dev/null
+++ b/packages/browser/test/integrations/featureFlags/unleash.test.ts
@@ -0,0 +1,7 @@
+import { unleashIntegration } from '../../../src';
+
+describe('Unleash', () => {
+ it('Throws error if given empty options', () => {
+ expect(() => unleashIntegration({})).toThrow('featureFlagClientClass option is required');
+ });
+});
diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts
index 337d08caff24..655d07e53e17 100644
--- a/packages/browser/test/tracing/request.test.ts
+++ b/packages/browser/test/tracing/request.test.ts
@@ -5,7 +5,7 @@ import * as utils from '@sentry/core';
import type { Client } from '@sentry/core';
import { WINDOW } from '../../src/helpers';
-import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request';
+import { instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request';
beforeAll(() => {
// @ts-expect-error need to override global Request because it's not in the vi environment (even with an
@@ -64,57 +64,6 @@ describe('instrumentOutgoingRequests', () => {
});
});
-interface ProtocolInfo {
- name: string;
- version: string;
-}
-
-describe('HTTPTimings', () => {
- test('Extracting version from ALPN protocol', () => {
- const nextHopToNetworkVersion: Record = {
- 'http/0.9': { name: 'http', version: '0.9' },
- 'http/1.0': { name: 'http', version: '1.0' },
- 'http/1.1': { name: 'http', version: '1.1' },
- 'spdy/1': { name: 'spdy', version: '1' },
- 'spdy/2': { name: 'spdy', version: '2' },
- 'spdy/3': { name: 'spdy', version: '3' },
- 'stun.turn': { name: 'stun.turn', version: 'unknown' },
- 'stun.nat-discovery': { name: 'stun.nat-discovery', version: 'unknown' },
- h2: { name: 'http', version: '2' },
- h2c: { name: 'http', version: '2c' },
- webrtc: { name: 'webrtc', version: 'unknown' },
- 'c-webrtc': { name: 'c-webrtc', version: 'unknown' },
- ftp: { name: 'ftp', version: 'unknown' },
- imap: { name: 'imap', version: 'unknown' },
- pop3: { name: 'pop', version: '3' },
- managesieve: { name: 'managesieve', version: 'unknown' },
- coap: { name: 'coap', version: 'unknown' },
- 'xmpp-client': { name: 'xmpp-client', version: 'unknown' },
- 'xmpp-server': { name: 'xmpp-server', version: 'unknown' },
- 'acme-tls/1': { name: 'acme-tls', version: '1' },
- mqtt: { name: 'mqtt', version: 'unknown' },
- dot: { name: 'dot', version: 'unknown' },
- 'ntske/1': { name: 'ntske', version: '1' },
- sunrpc: { name: 'sunrpc', version: 'unknown' },
- h3: { name: 'http', version: '3' },
- smb: { name: 'smb', version: 'unknown' },
- irc: { name: 'irc', version: 'unknown' },
- nntp: { name: 'nntp', version: 'unknown' },
- nnsp: { name: 'nnsp', version: 'unknown' },
- doq: { name: 'doq', version: 'unknown' },
- 'sip/2': { name: 'sip', version: '2' },
- 'tds/8.0': { name: 'tds', version: '8.0' },
- dicom: { name: 'dicom', version: 'unknown' },
- };
-
- const protocols = Object.keys(nextHopToNetworkVersion);
- for (const protocol of protocols) {
- const expected = nextHopToNetworkVersion[protocol]!;
- expect(extractNetworkProtocol(protocol)).toMatchObject(expected);
- }
- });
-});
-
describe('shouldAttachHeaders', () => {
describe('should prefer `tracePropagationTargets` over defaults', () => {
it('should return `true` if the url matches the new tracePropagationTargets', () => {
diff --git a/packages/browser/test/transports/offline.test.ts b/packages/browser/test/transports/offline.test.ts
index a9a396949588..070d6623f967 100644
--- a/packages/browser/test/transports/offline.test.ts
+++ b/packages/browser/test/transports/offline.test.ts
@@ -64,6 +64,7 @@ describe('makeOfflineTransport', () => {
await deleteDatabase('sentry');
(global as any).TextEncoder = TextEncoder;
(global as any).TextDecoder = TextDecoder;
+ (global as any).addEventListener = () => {};
});
it('indexedDb wrappers push, unshift and pop', async () => {
@@ -115,4 +116,32 @@ describe('makeOfflineTransport', () => {
expect(queuedCount).toEqual(1);
expect(getSendCount()).toEqual(2);
});
+
+ it('flush forces retry', async () => {
+ const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }, { statusCode: 200 });
+ let queuedCount = 0;
+ const transport = makeBrowserOfflineTransport(baseTransport)({
+ ...transportOptions,
+ shouldStore: () => {
+ queuedCount += 1;
+ return true;
+ },
+ url: 'http://localhost',
+ });
+ const result = await transport.send(ERROR_ENVELOPE);
+
+ expect(result).toEqual({});
+
+ await delay(MIN_DELAY * 2);
+
+ expect(getSendCount()).toEqual(0);
+ expect(queuedCount).toEqual(1);
+
+ await transport.flush();
+
+ await delay(MIN_DELAY * 2);
+
+ expect(queuedCount).toEqual(1);
+ expect(getSendCount()).toEqual(1);
+ });
});
diff --git a/packages/bun/package.json b/packages/bun/package.json
index ce1c85cbcd0f..00d7eb8b750a 100644
--- a/packages/bun/package.json
+++ b/packages/bun/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/bun",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for bun",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun",
@@ -39,9 +39,9 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0"
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0"
},
"devDependencies": {
"bun-types": "latest"
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 1ba5f2de4786..dcae6e98aa8d 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -141,6 +141,7 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
trpcMiddleware,
+ updateSpanName,
// eslint-disable-next-line deprecation/deprecation
addOpenTelemetryInstrumentation,
zodErrorsIntegration,
diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts
index 862d5bd87212..7abffa058157 100644
--- a/packages/bun/src/integrations/bunserver.ts
+++ b/packages/bun/src/integrations/bunserver.ts
@@ -47,7 +47,18 @@ export function instrumentBunServe(): void {
Bun.serve = new Proxy(Bun.serve, {
apply(serveTarget, serveThisArg, serveArgs: Parameters) {
instrumentBunServeOptions(serveArgs[0]);
- return serveTarget.apply(serveThisArg, serveArgs);
+ const server: ReturnType = serveTarget.apply(serveThisArg, serveArgs);
+
+ // A Bun server can be reloaded, re-wrap any fetch function passed to it
+ // We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we
+ // wrap the Server instance.
+ const originalReload: typeof server.reload = server.reload.bind(server);
+ server.reload = (serveOptions: Parameters[0]) => {
+ instrumentBunServeOptions(serveOptions);
+ return originalReload(serveOptions);
+ };
+
+ return server;
},
});
}
diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts
index b1dc17381ccb..dd1f738a334b 100644
--- a/packages/bun/test/integrations/bunserver.test.ts
+++ b/packages/bun/test/integrations/bunserver.test.ts
@@ -1,67 +1,87 @@
-import { beforeAll, beforeEach, describe, expect, test } from 'bun:test';
+import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
+import type { Span } from '@sentry/core';
import { getDynamicSamplingContextFromSpan, setCurrentClient, spanIsSampled, spanToJSON } from '@sentry/core';
import { BunClient } from '../../src/client';
import { instrumentBunServe } from '../../src/integrations/bunserver';
import { getDefaultBunClientOptions } from '../helpers';
-// Fun fact: Bun = 2 21 14 :)
-const DEFAULT_PORT = 22114;
-
describe('Bun Serve Integration', () => {
let client: BunClient;
+ // Fun fact: Bun = 2 21 14 :)
+ let port: number = 22114;
beforeAll(() => {
instrumentBunServe();
});
beforeEach(() => {
- const options = getDefaultBunClientOptions({ tracesSampleRate: 1, debug: true });
+ const options = getDefaultBunClientOptions({ tracesSampleRate: 1 });
client = new BunClient(options);
setCurrentClient(client);
client.init();
});
+ afterEach(() => {
+ // Don't reuse the port; Bun server stops lazily so tests may accidentally hit a server still closing from a
+ // previous test
+ port += 1;
+ });
+
test('generates a transaction around a request', async () => {
+ let generatedSpan: Span | undefined;
+
client.on('spanEnd', span => {
- expect(spanToJSON(span).status).toBe('ok');
- expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200);
- expect(spanToJSON(span).op).toEqual('http.server');
- expect(spanToJSON(span).description).toEqual('GET /');
+ generatedSpan = span;
});
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
- port: DEFAULT_PORT,
+ port,
});
+ await fetch(`http://localhost:${port}/`);
+ server.stop();
- await fetch('http://localhost:22114/');
+ if (!generatedSpan) {
+ throw 'No span was generated in the test';
+ }
- server.stop();
+ expect(spanToJSON(generatedSpan).status).toBe('ok');
+ expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200);
+ expect(spanToJSON(generatedSpan).op).toEqual('http.server');
+ expect(spanToJSON(generatedSpan).description).toEqual('GET /');
});
test('generates a post transaction', async () => {
+ let generatedSpan: Span | undefined;
+
client.on('spanEnd', span => {
- expect(spanToJSON(span).status).toBe('ok');
- expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200);
- expect(spanToJSON(span).op).toEqual('http.server');
- expect(spanToJSON(span).description).toEqual('POST /');
+ generatedSpan = span;
});
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
- port: DEFAULT_PORT,
+ port,
});
- await fetch('http://localhost:22114/', {
+ await fetch(`http://localhost:${port}/`, {
method: 'POST',
});
server.stop();
+
+ if (!generatedSpan) {
+ throw 'No span was generated in the test';
+ }
+
+ expect(spanToJSON(generatedSpan).status).toBe('ok');
+ expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200);
+ expect(spanToJSON(generatedSpan).op).toEqual('http.server');
+ expect(spanToJSON(generatedSpan).description).toEqual('POST /');
});
test('continues a trace', async () => {
@@ -70,55 +90,93 @@ describe('Bun Serve Integration', () => {
const PARENT_SAMPLED = '1';
const SENTRY_TRACE_HEADER = `${TRACE_ID}-${PARENT_SPAN_ID}-${PARENT_SAMPLED}`;
- const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production';
+ const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-sample_rand=0.42,sentry-environment=production';
- client.on('spanEnd', span => {
- expect(span.spanContext().traceId).toBe(TRACE_ID);
- expect(spanToJSON(span).parent_span_id).toBe(PARENT_SPAN_ID);
- expect(spanIsSampled(span)).toBe(true);
- expect(span.isRecording()).toBe(false);
+ let generatedSpan: Span | undefined;
- expect(getDynamicSamplingContextFromSpan(span)).toStrictEqual({
- version: '1.0',
- environment: 'production',
- });
+ client.on('spanEnd', span => {
+ generatedSpan = span;
});
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
- port: DEFAULT_PORT,
+ port,
});
- await fetch('http://localhost:22114/', {
+ await fetch(`http://localhost:${port}/`, {
headers: { 'sentry-trace': SENTRY_TRACE_HEADER, baggage: SENTRY_BAGGAGE_HEADER },
});
server.stop();
+
+ if (!generatedSpan) {
+ throw 'No span was generated in the test';
+ }
+
+ expect(generatedSpan.spanContext().traceId).toBe(TRACE_ID);
+ expect(spanToJSON(generatedSpan).parent_span_id).toBe(PARENT_SPAN_ID);
+ expect(spanIsSampled(generatedSpan)).toBe(true);
+ expect(generatedSpan.isRecording()).toBe(false);
+
+ expect(getDynamicSamplingContextFromSpan(generatedSpan)).toStrictEqual({
+ version: '1.0',
+ sample_rand: '0.42',
+ environment: 'production',
+ });
});
test('does not create transactions for OPTIONS or HEAD requests', async () => {
- client.on('spanEnd', () => {
- // This will never run, but we want to make sure it doesn't run.
- expect(false).toEqual(true);
+ let generatedSpan: Span | undefined;
+
+ client.on('spanEnd', span => {
+ generatedSpan = span;
});
const server = Bun.serve({
async fetch(_req) {
return new Response('Bun!');
},
- port: DEFAULT_PORT,
+ port,
});
- await fetch('http://localhost:22114/', {
+ await fetch(`http://localhost:${port}/`, {
method: 'OPTIONS',
});
- await fetch('http://localhost:22114/', {
+ await fetch(`http://localhost:${port}/`, {
method: 'HEAD',
});
server.stop();
+
+ expect(generatedSpan).toBeUndefined();
+ });
+
+ test('intruments the server again if it is reloaded', async () => {
+ let serverWasInstrumented = false;
+ client.on('spanEnd', () => {
+ serverWasInstrumented = true;
+ });
+
+ const server = Bun.serve({
+ async fetch(_req) {
+ return new Response('Bun!');
+ },
+ port,
+ });
+
+ server.reload({
+ async fetch(_req) {
+ return new Response('Reloaded Bun!');
+ },
+ });
+
+ await fetch(`http://localhost:${port}/`);
+
+ server.stop();
+
+ expect(serverWasInstrumented).toBeTrue();
});
});
diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts
index a548cc2614c7..11870f30c101 100644
--- a/packages/bun/test/sdk.test.ts
+++ b/packages/bun/test/sdk.test.ts
@@ -1,14 +1,20 @@
-import { expect, test } from 'bun:test';
+import { describe, expect, test } from 'bun:test';
import { init } from '../src/index';
-test("calling init shouldn't fail", () => {
- init({
+describe('Bun SDK', () => {
+ const initOptions = {
dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000',
+ tracesSampleRate: 1,
+ };
+
+ test("calling init shouldn't fail", () => {
+ expect(() => {
+ init(initOptions);
+ }).not.toThrow();
});
- expect(true).toBe(true);
-});
-test('should return client from init', () => {
- expect(init({})).not.toBeUndefined();
+ test('should return client from init', () => {
+ expect(init(initOptions)).not.toBeUndefined();
+ });
});
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index efec51c5c0f5..b1ff74862744 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/cloudflare",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Cloudflare Workers and Pages",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"optionalDependencies": {
"@cloudflare/workers-types": "^4.x"
diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts
index f3c80b8ddf32..fb8c34694282 100644
--- a/packages/cloudflare/src/index.ts
+++ b/packages/cloudflare/src/index.ts
@@ -89,6 +89,7 @@ export {
spanToJSON,
spanToTraceHeader,
spanToBaggageHeader,
+ updateSpanName,
} from '@sentry/core';
export { withSentry } from './handler';
diff --git a/packages/core/package.json b/packages/core/package.json
index ab43b79117b9..887649741bec 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/core",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Base implementation for all Sentry JavaScript SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core",
@@ -41,7 +41,8 @@
"TODO(v9):": "Remove these dependencies",
"devDependencies": {
"@types/array.prototype.flat": "^1.2.1",
- "array.prototype.flat": "^1.3.0"
+ "array.prototype.flat": "^1.3.0",
+ "zod": "^3.24.1"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/core/src/asyncContext/types.ts b/packages/core/src/asyncContext/types.ts
index 7b5bf8acc54c..b830bebb0d9b 100644
--- a/packages/core/src/asyncContext/types.ts
+++ b/packages/core/src/asyncContext/types.ts
@@ -1,6 +1,7 @@
import type { Scope } from '../types-hoist';
import type { getTraceData } from '../utils/traceData';
import type {
+ continueTrace,
startInactiveSpan,
startSpan,
startSpanManual,
@@ -68,4 +69,11 @@ export interface AsyncContextStrategy {
/** Get trace data as serialized string values for propagation via `sentry-trace` and `baggage`. */
getTraceData?: typeof getTraceData;
+
+ /**
+ * Continue a trace from `sentry-trace` and `baggage` values.
+ * These values can be obtained from incoming request headers, or in the browser from ``
+ * and `` HTML tags.
+ */
+ continueTrace?: typeof continueTrace;
}
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index c394a0d77a95..c2dff2122734 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -587,7 +587,7 @@ export abstract class BaseClient implements Client {
/** Updates existing session based on the provided event */
protected _updateSessionFromEvent(session: Session, event: Event): void {
- let crashed = false;
+ let crashed = event.level === 'fatal';
let errored = false;
const exceptions = event.exception && event.exception.values;
@@ -721,11 +721,10 @@ export abstract class BaseClient implements Client {
if (DEBUG_BUILD) {
// If something's gone wrong, log the error as a warning. If it's just us having used a `SentryError` for
// control flow, log just the message (no stack) as a log-level log.
- const sentryError = reason as SentryError;
- if (sentryError.logLevel === 'log') {
- logger.log(sentryError.message);
+ if (reason instanceof SentryError && reason.logLevel === 'log') {
+ logger.log(reason.message);
} else {
- logger.warn(sentryError);
+ logger.warn(reason);
}
}
return undefined;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 77259d2434d4..5baf88f38e1c 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -81,6 +81,7 @@ export {
getActiveSpan,
addChildSpanToSpan,
spanTimeInputToSeconds,
+ updateSpanName,
} from './utils/spanUtils';
export { parseSampleRate } from './utils/parseSampleRate';
export { applySdkMetadata } from './utils/sdkMetadata';
diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts
index fc36925eb0ea..f059a1dc4ced 100644
--- a/packages/core/src/integrations/zoderrors.ts
+++ b/packages/core/src/integrations/zoderrors.ts
@@ -5,33 +5,45 @@ import { truncate } from '../utils-hoist/string';
interface ZodErrorsOptions {
key?: string;
+ /**
+ * Limits the number of Zod errors inlined in each Sentry event.
+ *
+ * @default 10
+ */
limit?: number;
+ /**
+ * Save full list of Zod issues as an attachment in Sentry
+ *
+ * @default false
+ */
+ saveZodIssuesAsAttachment?: boolean;
}
const DEFAULT_LIMIT = 10;
const INTEGRATION_NAME = 'ZodErrors';
-// Simplified ZodIssue type definition
+/**
+ * Simplified ZodIssue type definition
+ */
interface ZodIssue {
path: (string | number)[];
message?: string;
- expected?: string | number;
- received?: string | number;
+ expected?: unknown;
+ received?: unknown;
unionErrors?: unknown[];
keys?: unknown[];
+ invalid_literal?: unknown;
}
interface ZodError extends Error {
issues: ZodIssue[];
-
- get errors(): ZodError['issues'];
}
function originalExceptionIsZodError(originalException: unknown): originalException is ZodError {
return (
isError(originalException) &&
originalException.name === 'ZodError' &&
- Array.isArray((originalException as ZodError).errors)
+ Array.isArray((originalException as ZodError).issues)
);
}
@@ -45,9 +57,18 @@ type SingleLevelZodIssue = {
/**
* Formats child objects or arrays to a string
- * That is preserved when sent to Sentry
+ * that is preserved when sent to Sentry.
+ *
+ * Without this, we end up with something like this in Sentry:
+ *
+ * [
+ * [Object],
+ * [Object],
+ * [Object],
+ * [Object]
+ * ]
*/
-function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue {
+export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue {
return {
...issue,
path: 'path' in issue && Array.isArray(issue.path) ? issue.path.join('.') : undefined,
@@ -56,26 +77,70 @@ function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue {
};
}
+/**
+ * Takes ZodError issue path array and returns a flattened version as a string.
+ * This makes it easier to display paths within a Sentry error message.
+ *
+ * Array indexes are normalized to reduce duplicate entries
+ *
+ * @param path ZodError issue path
+ * @returns flattened path
+ *
+ * @example
+ * flattenIssuePath([0, 'foo', 1, 'bar']) // -> '.foo..bar'
+ */
+export function flattenIssuePath(path: Array): string {
+ return path
+ .map(p => {
+ if (typeof p === 'number') {
+ return '';
+ } else {
+ return p;
+ }
+ })
+ .join('.');
+}
+
/**
* Zod error message is a stringified version of ZodError.issues
* This doesn't display well in the Sentry UI. Replace it with something shorter.
*/
-function formatIssueMessage(zodError: ZodError): string {
+export function formatIssueMessage(zodError: ZodError): string {
const errorKeyMap = new Set();
for (const iss of zodError.issues) {
- if (iss.path && iss.path[0]) {
- errorKeyMap.add(iss.path[0]);
+ const issuePath = flattenIssuePath(iss.path);
+ if (issuePath.length > 0) {
+ errorKeyMap.add(issuePath);
}
}
- const errorKeys = Array.from(errorKeyMap);
+ const errorKeys = Array.from(errorKeyMap);
+ if (errorKeys.length === 0) {
+ // If there are no keys, then we're likely validating the root
+ // variable rather than a key within an object. This attempts
+ // to extract what type it was that failed to validate.
+ // For example, z.string().parse(123) would return "string" here.
+ let rootExpectedType = 'variable';
+ if (zodError.issues.length > 0) {
+ const iss = zodError.issues[0];
+ if (iss !== undefined && 'expected' in iss && typeof iss.expected === 'string') {
+ rootExpectedType = iss.expected;
+ }
+ }
+ return `Failed to validate ${rootExpectedType}`;
+ }
return `Failed to validate keys: ${truncate(errorKeys.join(', '), 100)}`;
}
/**
- * Applies ZodError issues to an event extras and replaces the error message
+ * Applies ZodError issues to an event extra and replaces the error message
*/
-export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventHint): Event {
+export function applyZodErrorsToEvent(
+ limit: number,
+ saveZodIssuesAsAttachment: boolean = false,
+ event: Event,
+ hint: EventHint,
+): Event {
if (
!event.exception ||
!event.exception.values ||
@@ -87,35 +152,72 @@ export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventH
return event;
}
- return {
- ...event,
- exception: {
- ...event.exception,
- values: [
- {
- ...event.exception.values[0],
- value: formatIssueMessage(hint.originalException),
+ try {
+ const issuesToFlatten = saveZodIssuesAsAttachment
+ ? hint.originalException.issues
+ : hint.originalException.issues.slice(0, limit);
+ const flattenedIssues = issuesToFlatten.map(flattenIssue);
+
+ if (saveZodIssuesAsAttachment) {
+ // Sometimes having the full error details can be helpful.
+ // Attachments have much higher limits, so we can include the full list of issues.
+ if (!Array.isArray(hint.attachments)) {
+ hint.attachments = [];
+ }
+ hint.attachments.push({
+ filename: 'zod_issues.json',
+ data: JSON.stringify({
+ issues: flattenedIssues,
+ }),
+ });
+ }
+
+ return {
+ ...event,
+ exception: {
+ ...event.exception,
+ values: [
+ {
+ ...event.exception.values[0],
+ value: formatIssueMessage(hint.originalException),
+ },
+ ...event.exception.values.slice(1),
+ ],
+ },
+ extra: {
+ ...event.extra,
+ 'zoderror.issues': flattenedIssues.slice(0, limit),
+ },
+ };
+ } catch (e) {
+ // Hopefully we never throw errors here, but record it
+ // with the event just in case.
+ return {
+ ...event,
+ extra: {
+ ...event.extra,
+ 'zoderrors sentry integration parse error': {
+ message: 'an exception was thrown while processing ZodError within applyZodErrorsToEvent()',
+ error: e instanceof Error ? `${e.name}: ${e.message}\n${e.stack}` : 'unknown',
},
- ...event.exception.values.slice(1),
- ],
- },
- extra: {
- ...event.extra,
- 'zoderror.issues': hint.originalException.errors.slice(0, limit).map(formatIssueTitle),
- },
- };
+ },
+ };
+ }
}
const _zodErrorsIntegration = ((options: ZodErrorsOptions = {}) => {
- const limit = options.limit || DEFAULT_LIMIT;
+ const limit = typeof options.limit === 'undefined' ? DEFAULT_LIMIT : options.limit;
return {
name: INTEGRATION_NAME,
- processEvent(originalEvent, hint) {
- const processedEvent = applyZodErrorsToEvent(limit, originalEvent, hint);
+ processEvent(originalEvent, hint): Event {
+ const processedEvent = applyZodErrorsToEvent(limit, options.saveZodIssuesAsAttachment, originalEvent, hint);
return processedEvent;
},
};
}) satisfies IntegrationFn;
+/**
+ * Sentry integration to process Zod errors, making them easier to work with in Sentry.
+ */
export const zodErrorsIntegration = defineIntegration(_zodErrorsIntegration);
diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts
index 5bba8615e876..dae18881f483 100644
--- a/packages/core/src/scope.ts
+++ b/packages/core/src/scope.ts
@@ -306,7 +306,15 @@ class ScopeClass implements ScopeInterface {
}
/**
- * @inheritDoc
+ * Sets the transaction name on the scope so that the name of e.g. taken server route or
+ * the page location is attached to future events.
+ *
+ * IMPORTANT: Calling this function does NOT change the name of the currently active
+ * root span. If you want to change the name of the active root span, use
+ * `Sentry.updateSpanName(rootSpan, 'new name')` instead.
+ *
+ * By default, the SDK updates the scope's transaction name automatically on sensible
+ * occasions, such as a page navigation or when handling a new request on the server.
*/
public setTransactionName(name?: string): this {
this._transactionName = name;
@@ -435,9 +443,13 @@ class ScopeClass implements ScopeInterface {
...breadcrumb,
};
- const breadcrumbs = this._breadcrumbs;
- breadcrumbs.push(mergedBreadcrumb);
- this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs;
+ this._breadcrumbs.push(mergedBreadcrumb);
+ if (this._breadcrumbs.length > maxCrumbs) {
+ this._breadcrumbs = this._breadcrumbs.slice(-maxCrumbs);
+ if (this._client) {
+ this._client.recordDroppedEvent('buffer_overflow', 'log_item');
+ }
+ }
this._notifyScopeListeners();
diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts
index 2896bd81f93f..b799f5321a0e 100644
--- a/packages/core/src/semanticAttributes.ts
+++ b/packages/core/src/semanticAttributes.ts
@@ -29,6 +29,15 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_un
/** The value of a measurement, which may be stored as a TimedEvent. */
export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value';
+/**
+ * A custom span name set by users guaranteed to be taken over any automatically
+ * inferred name. This attribute is removed before the span is sent.
+ *
+ * @internal only meant for internal SDK usage
+ * @hidden
+ */
+export const SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME = 'sentry.custom_span_name';
+
/**
* The id of the profile that this span occurred in.
*/
diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts
index 9109e78e0343..9ad01bc98a27 100644
--- a/packages/core/src/tracing/sampling.ts
+++ b/packages/core/src/tracing/sampling.ts
@@ -1,5 +1,6 @@
-import type { Options, SamplingContext } from '../types-hoist';
+import type { Options, RequestEventData, SamplingContext } from '../types-hoist';
+import { getIsolationScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import { logger } from '../utils-hoist/logger';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
@@ -20,13 +21,22 @@ export function sampleSpan(
return [false];
}
+ // Casting this from unknown, as the type of `sdkProcessingMetadata` is only changed in v9 and `normalizedRequest` is set in SentryHttpInstrumentation
+ const normalizedRequest = getIsolationScope().getScopeData().sdkProcessingMetadata
+ .normalizedRequest as RequestEventData;
+
+ const enhancedSamplingContext = {
+ ...samplingContext,
+ normalizedRequest: samplingContext.normalizedRequest || normalizedRequest,
+ };
+
// we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should
// work; prefer the hook if so
let sampleRate;
if (typeof options.tracesSampler === 'function') {
- sampleRate = options.tracesSampler(samplingContext);
- } else if (samplingContext.parentSampled !== undefined) {
- sampleRate = samplingContext.parentSampled;
+ sampleRate = options.tracesSampler(enhancedSamplingContext);
+ } else if (enhancedSamplingContext.parentSampled !== undefined) {
+ sampleRate = enhancedSamplingContext.parentSampled;
} else if (typeof options.tracesSampleRate !== 'undefined') {
sampleRate = options.tracesSampleRate;
} else {
diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts
index 126702dfad2b..9965261970f2 100644
--- a/packages/core/src/tracing/sentrySpan.ts
+++ b/packages/core/src/tracing/sentrySpan.ts
@@ -5,6 +5,7 @@ import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
import {
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
SEMANTIC_ATTRIBUTE_PROFILE_ID,
+ SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
@@ -355,6 +356,14 @@ export class SentrySpan implements Span {
const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined;
+ // remove internal root span attributes we don't need to send.
+ /* eslint-disable @typescript-eslint/no-dynamic-delete */
+ delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
+ spans.forEach(span => {
+ span.data && delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
+ });
+ // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete
+
const transaction: TransactionEvent = {
contexts: {
trace: spanToTransactionTraceContext(this),
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index d44d0b216db3..26fefff68174 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -191,15 +191,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
* be attached to the incoming trace.
*/
export const continueTrace = (
- {
- sentryTrace,
- baggage,
- }: {
+ options: {
sentryTrace: Parameters[0];
baggage: Parameters[1];
},
callback: () => V,
): V => {
+ const carrier = getMainCarrier();
+ const acs = getAsyncContextStrategy(carrier);
+ if (acs.continueTrace) {
+ return acs.continueTrace(options, callback);
+ }
+
+ const { sentryTrace, baggage } = options;
+
return withScope(scope => {
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);
scope.setPropagationContext(propagationContext);
diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts
index cf8902739db7..4cdd0b4a71af 100644
--- a/packages/core/src/transports/offline.ts
+++ b/packages/core/src/transports/offline.ts
@@ -170,7 +170,15 @@ export function makeOfflineTransport(
return {
send,
- flush: t => transport.flush(t),
+ flush: timeout => {
+ // If there's no timeout, we should attempt to flush the offline queue.
+ if (timeout === undefined) {
+ retryDelay = START_DELAY;
+ flushIn(MIN_DELAY);
+ }
+
+ return transport.flush(timeout);
+ },
};
};
}
diff --git a/packages/core/src/types-hoist/clientreport.ts b/packages/core/src/types-hoist/clientreport.ts
index b6ab1766e68c..069adec43c62 100644
--- a/packages/core/src/types-hoist/clientreport.ts
+++ b/packages/core/src/types-hoist/clientreport.ts
@@ -8,7 +8,8 @@ export type EventDropReason =
| 'ratelimit_backoff'
| 'sample_rate'
| 'send_error'
- | 'internal_sdk_error';
+ | 'internal_sdk_error'
+ | 'buffer_overflow';
export type Outcome = {
reason: EventDropReason;
diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts
index 60aa60b38868..ad6879636086 100644
--- a/packages/core/src/types-hoist/context.ts
+++ b/packages/core/src/types-hoist/context.ts
@@ -133,6 +133,6 @@ export interface MissingInstrumentationContext extends Record {
* directly is not recommended. Use the functions in @sentry/browser
* src/utils/featureFlags instead.
*/
-export interface FeatureFlagContext extends Record {
+interface FeatureFlagContext extends Record {
values: FeatureFlag[];
}
diff --git a/packages/core/src/types-hoist/datacategory.ts b/packages/core/src/types-hoist/datacategory.ts
index bd1c0b693e4d..98c4657cf011 100644
--- a/packages/core/src/types-hoist/datacategory.ts
+++ b/packages/core/src/types-hoist/datacategory.ts
@@ -14,7 +14,7 @@ export type DataCategory =
| 'replay'
// Events with `event_type` csp, hpkp, expectct, expectstaple
| 'security'
- // Attachment bytes stored (unused for rate limiting
+ // Attachment bytes stored (unused for rate limiting)
| 'attachment'
// Session update events
| 'session'
@@ -30,5 +30,9 @@ export type DataCategory =
| 'metric_bucket'
// Span
| 'span'
+ // Log event
+ | 'log_item'
+ // Log bytes stored (unused for rate limiting)
+ | 'log_byte'
// Unknown data category
| 'unknown';
diff --git a/packages/core/src/types-hoist/samplingcontext.ts b/packages/core/src/types-hoist/samplingcontext.ts
index ecce87d7fbc7..1cb15490e5b2 100644
--- a/packages/core/src/types-hoist/samplingcontext.ts
+++ b/packages/core/src/types-hoist/samplingcontext.ts
@@ -1,3 +1,4 @@
+import type { RequestEventData } from '../types-hoist/request';
import type { ExtractedNodeRequestData, WorkerLocation } from './misc';
import type { SpanAttributes } from './span';
@@ -35,10 +36,16 @@ export interface SamplingContext extends CustomSamplingContext {
location?: WorkerLocation;
/**
- * Object representing the incoming request to a node server. Passed by default when using the TracingHandler.
+ * Object representing the incoming request to a node server.
+ * @deprecated This attribute is currently never defined and will be removed in v9. Use `normalizedRequest` instead
*/
request?: ExtractedNodeRequestData;
+ /**
+ * Object representing the incoming request to a node server in a normalized format.
+ */
+ normalizedRequest?: RequestEventData;
+
/** The name of the span being sampled. */
name: string;
diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts
index a2ee74fd7cfa..cf0c3086bf88 100644
--- a/packages/core/src/types-hoist/span.ts
+++ b/packages/core/src/types-hoist/span.ts
@@ -234,6 +234,16 @@ export interface Span {
/**
* Update the name of the span.
+ *
+ * **Important:** You most likely want to use `Sentry.updateSpanName(span, name)` instead!
+ *
+ * This method will update the current span name but cannot guarantee that the new name will be
+ * the final name of the span. Instrumentation might still overwrite the name with an automatically
+ * computed name, for example in `http.server` or `db` spans.
+ *
+ * You can ensure that your name is kept and not overwritten by calling `Sentry.updateSpanName(span, name)`
+ *
+ * @param name the new name of the span
*/
updateName(name: string): this;
diff --git a/packages/core/src/utils-hoist/browser.ts b/packages/core/src/utils-hoist/browser.ts
index b3a5220e7c3c..c5d5a91ccb75 100644
--- a/packages/core/src/utils-hoist/browser.ts
+++ b/packages/core/src/utils-hoist/browser.ts
@@ -155,6 +155,8 @@ export function getLocationHref(): string {
* `const element = getDomElement('selector');`
*
* @param selector the selector string passed on to document.querySelector
+ *
+ * @deprecated This method is deprecated and will be removed in the next major version.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getDomElement(selector: string): E | null {
diff --git a/packages/core/src/utils-hoist/error.ts b/packages/core/src/utils-hoist/error.ts
index 622aaff9cf80..5ae28093a8bf 100644
--- a/packages/core/src/utils-hoist/error.ts
+++ b/packages/core/src/utils-hoist/error.ts
@@ -2,9 +2,6 @@ import type { ConsoleLevel } from '../types-hoist';
/** An error emitted by Sentry SDKs and related utilities. */
export class SentryError extends Error {
- /** Display name of this error instance. */
- public name: string;
-
public logLevel: ConsoleLevel;
public constructor(
@@ -13,11 +10,6 @@ export class SentryError extends Error {
) {
super(message);
- this.name = new.target.prototype.constructor.name;
- // This sets the prototype to be `Error`, not `SentryError`. It's unclear why we do this, but commenting this line
- // out causes various (seemingly totally unrelated) playwright tests consistently time out. FYI, this makes
- // instances of `SentryError` fail `obj instanceof SentryError` checks.
- Object.setPrototypeOf(this, new.target.prototype);
this.logLevel = logLevel;
}
}
diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts
index e53cd0edb59b..68a52655fb3a 100644
--- a/packages/core/src/utils-hoist/index.ts
+++ b/packages/core/src/utils-hoist/index.ts
@@ -2,7 +2,13 @@ export { applyAggregateErrorsToEvent } from './aggregate-errors';
// eslint-disable-next-line deprecation/deprecation
export { flatten } from './array';
export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level';
-export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser';
+export {
+ getComponentName,
+ // eslint-disable-next-line deprecation/deprecation
+ getDomElement,
+ getLocationHref,
+ htmlTreeAsString,
+} from './browser';
export { dsnFromString, dsnToString, makeDsn } from './dsn';
export { SentryError } from './error';
export { GLOBAL_OBJ, getGlobalSingleton } from './worldwide';
diff --git a/packages/core/src/utils-hoist/node.ts b/packages/core/src/utils-hoist/node.ts
index 3805248bdedd..489a5c2cb57d 100644
--- a/packages/core/src/utils-hoist/node.ts
+++ b/packages/core/src/utils-hoist/node.ts
@@ -42,14 +42,16 @@ export function dynamicRequire(mod: any, request: string): any {
* That is to mimic the behavior of `require.resolve` exactly.
*
* @param moduleName module name to require
+ * @param existingModule module to use for requiring
* @returns possibly required module
*/
-export function loadModule(moduleName: string): T | undefined {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function loadModule(moduleName: string, existingModule: any = module): T | undefined {
let mod: T | undefined;
try {
// eslint-disable-next-line deprecation/deprecation
- mod = dynamicRequire(module, moduleName);
+ mod = dynamicRequire(existingModule, moduleName);
} catch (e) {
// no-empty
}
@@ -57,9 +59,9 @@ export function loadModule(moduleName: string): T | undefined {
if (!mod) {
try {
// eslint-disable-next-line deprecation/deprecation
- const { cwd } = dynamicRequire(module, 'process');
+ const { cwd } = dynamicRequire(existingModule, 'process');
// eslint-disable-next-line deprecation/deprecation
- mod = dynamicRequire(module, `${cwd()}/node_modules/${moduleName}`) as T;
+ mod = dynamicRequire(existingModule, `${cwd()}/node_modules/${moduleName}`) as T;
} catch (e) {
// no-empty
}
diff --git a/packages/core/src/utils-hoist/requestdata.ts b/packages/core/src/utils-hoist/requestdata.ts
index bff0f3f629bd..582a8954d4c6 100644
--- a/packages/core/src/utils-hoist/requestdata.ts
+++ b/packages/core/src/utils-hoist/requestdata.ts
@@ -295,8 +295,8 @@ export function addNormalizedRequestDataToEvent(
if (Object.keys(extractedUser).length) {
event.user = {
- ...event.user,
...extractedUser,
+ ...event.user,
};
}
}
diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts
index 92018731ff4f..cb819bd92204 100644
--- a/packages/core/src/utils-hoist/worldwide.ts
+++ b/packages/core/src/utils-hoist/worldwide.ts
@@ -49,7 +49,7 @@ type BackwardsCompatibleSentryCarrier = SentryCarrier & {
/** Internal global with common properties and Sentry extensions */
export type InternalGlobal = {
- navigator?: { userAgent?: string };
+ navigator?: { userAgent?: string; maxTouchPoints?: number };
console: Console;
PerformanceObserver?: any;
Sentry?: any;
diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts
index 594a297f9395..09ba9d729449 100644
--- a/packages/core/src/utils/spanUtils.ts
+++ b/packages/core/src/utils/spanUtils.ts
@@ -3,7 +3,12 @@ import { getMainCarrier } from '../carrier';
import { getCurrentScope } from '../currentScopes';
import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary';
import type { MetricType } from '../metrics/types';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '../semanticAttributes';
import type { SentrySpan } from '../tracing/sentrySpan';
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
import type {
@@ -310,3 +315,27 @@ export function showSpanDropWarning(): void {
hasShownSpanDropWarning = true;
}
}
+
+/**
+ * Updates the name of the given span and ensures that the span name is not
+ * overwritten by the Sentry SDK.
+ *
+ * Use this function instead of `span.updateName()` if you want to make sure that
+ * your name is kept. For some spans, for example root `http.server` spans the
+ * Sentry SDK would otherwise overwrite the span name with a high-quality name
+ * it infers when the span ends.
+ *
+ * Use this function in server code or when your span is started on the server
+ * and on the client (browser). If you only update a span name on the client,
+ * you can also use `span.updateName()` the SDK does not overwrite the name.
+ *
+ * @param span - The span to update the name of.
+ * @param name - The name to set on the span.
+ */
+export function updateSpanName(span: Span, name: string): void {
+ span.updateName(name);
+ span.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: name,
+ });
+}
diff --git a/packages/core/test/lib/baseclient.test.ts b/packages/core/test/lib/baseclient.test.ts
index ce480879bb27..338e88ee72e8 100644
--- a/packages/core/test/lib/baseclient.test.ts
+++ b/packages/core/test/lib/baseclient.test.ts
@@ -201,6 +201,22 @@ describe('BaseClient', () => {
expect(isolationScopeBreadcrumbs).toEqual([{ message: 'hello3', timestamp: expect.any(Number) }]);
});
+ test('it records `buffer_overflow` client discard reason when buffer overflows', () => {
+ const options = getDefaultTestClientOptions({ maxBreadcrumbs: 1 });
+ const client = new TestClient(options);
+ const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent');
+ setCurrentClient(client);
+ getIsolationScope().setClient(client);
+ client.init();
+
+ addBreadcrumb({ message: 'hello1' });
+ addBreadcrumb({ message: 'hello2' });
+ addBreadcrumb({ message: 'hello3' });
+
+ expect(recordLostEventSpy).toHaveBeenCalledTimes(2);
+ expect(recordLostEventSpy).toHaveBeenLastCalledWith('buffer_overflow', 'log_item');
+ });
+
test('calls `beforeBreadcrumb` and adds the breadcrumb without any changes', () => {
const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb);
const options = getDefaultTestClientOptions({ beforeBreadcrumb });
diff --git a/packages/core/test/lib/integrations/zoderrrors.test.ts b/packages/core/test/lib/integrations/zoderrrors.test.ts
index d5583fb57380..d7119995ae27 100644
--- a/packages/core/test/lib/integrations/zoderrrors.test.ts
+++ b/packages/core/test/lib/integrations/zoderrrors.test.ts
@@ -1,6 +1,12 @@
+import { z } from 'zod';
import type { Event, EventHint } from '../../../src/types-hoist';
-import { applyZodErrorsToEvent } from '../../../src/integrations/zoderrors';
+import {
+ applyZodErrorsToEvent,
+ flattenIssue,
+ flattenIssuePath,
+ formatIssueMessage,
+} from '../../../src/integrations/zoderrors';
// Simplified type definition
interface ZodIssue {
@@ -44,13 +50,13 @@ describe('applyZodErrorsToEvent()', () => {
test('should not do anything if exception is not a ZodError', () => {
const event: Event = {};
const eventHint: EventHint = { originalException: new Error() };
- applyZodErrorsToEvent(100, event, eventHint);
+ applyZodErrorsToEvent(100, false, event, eventHint);
// no changes
expect(event).toStrictEqual({});
});
- test('should add ZodError issues to extras and format message', () => {
+ test('should add ZodError issues to extra and format message', () => {
const issues = [
{
code: 'invalid_type',
@@ -75,13 +81,13 @@ describe('applyZodErrorsToEvent()', () => {
};
const eventHint: EventHint = { originalException };
- const processedEvent = applyZodErrorsToEvent(100, event, eventHint);
+ const processedEvent = applyZodErrorsToEvent(100, false, event, eventHint);
expect(processedEvent.exception).toStrictEqual({
values: [
{
type: 'Error',
- value: 'Failed to validate keys: names',
+ value: 'Failed to validate keys: names.',
},
],
});
@@ -96,5 +102,421 @@ describe('applyZodErrorsToEvent()', () => {
},
],
});
+
+ // No attachments added
+ expect(eventHint.attachments).toBe(undefined);
+ });
+
+ test('should add all ZodError issues as attachment', () => {
+ const issues = [
+ {
+ code: 'invalid_type',
+ expected: 'string',
+ received: 'number',
+ path: ['names', 1],
+ keys: ['extra'],
+ message: 'Invalid input: expected string, received number',
+ },
+ {
+ code: 'invalid_type',
+ expected: 'string',
+ received: 'number',
+ path: ['foo', 1],
+ keys: ['extra2'],
+ message: 'Invalid input: expected string, received number',
+ },
+ ] satisfies ZodIssue[];
+ const originalException = ZodError.create(issues);
+
+ const event: Event = {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: originalException.message,
+ },
+ ],
+ },
+ };
+
+ const eventHint: EventHint = { originalException };
+ const processedEvent = applyZodErrorsToEvent(1, true, event, eventHint);
+
+ expect(processedEvent.exception).toStrictEqual({
+ values: [
+ {
+ type: 'Error',
+ value: 'Failed to validate keys: names., foo.',
+ },
+ ],
+ });
+
+ // Only adds the first issue to extra due to the limit
+ expect(processedEvent.extra).toStrictEqual({
+ 'zoderror.issues': [
+ {
+ ...issues[0],
+ path: issues[0]?.path.join('.'),
+ keys: JSON.stringify(issues[0]?.keys),
+ unionErrors: undefined,
+ },
+ ],
+ });
+
+ // hint attachments contains the full issue list
+ expect(Array.isArray(eventHint.attachments)).toBe(true);
+ expect(eventHint.attachments?.length).toBe(1);
+ const attachment = eventHint.attachments?.[0];
+ if (attachment === undefined) {
+ throw new Error('attachment is undefined');
+ }
+ expect(attachment.filename).toBe('zod_issues.json');
+ expect(JSON.parse(attachment.data.toString())).toMatchInlineSnapshot(`
+Object {
+ "issues": Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "string",
+ "keys": "[\\"extra\\"]",
+ "message": "Invalid input: expected string, received number",
+ "path": "names.1",
+ "received": "number",
+ },
+ Object {
+ "code": "invalid_type",
+ "expected": "string",
+ "keys": "[\\"extra2\\"]",
+ "message": "Invalid input: expected string, received number",
+ "path": "foo.1",
+ "received": "number",
+ },
+ ],
+}
+`);
+ });
+});
+
+describe('flattenIssue()', () => {
+ it('flattens path field', () => {
+ const zodError = z
+ .object({
+ foo: z.string().min(1),
+ nested: z.object({
+ bar: z.literal('baz'),
+ }),
+ })
+ .safeParse({
+ foo: '',
+ nested: {
+ bar: 'not-baz',
+ },
+ }).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "too_small",
+ "exact": false,
+ "inclusive": true,
+ "message": "String must contain at least 1 character(s)",
+ "minimum": 1,
+ "path": Array [
+ "foo",
+ ],
+ "type": "string",
+ },
+ Object {
+ "code": "invalid_literal",
+ "expected": "baz",
+ "message": "Invalid literal value, expected \\"baz\\"",
+ "path": Array [
+ "nested",
+ "bar",
+ ],
+ "received": "not-baz",
+ },
+]
+`);
+
+ const issues = zodError.issues;
+ expect(issues.length).toBe(2);
+
+ // Format it for use in Sentry
+ expect(issues.map(flattenIssue)).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "too_small",
+ "exact": false,
+ "inclusive": true,
+ "keys": undefined,
+ "message": "String must contain at least 1 character(s)",
+ "minimum": 1,
+ "path": "foo",
+ "type": "string",
+ "unionErrors": undefined,
+ },
+ Object {
+ "code": "invalid_literal",
+ "expected": "baz",
+ "keys": undefined,
+ "message": "Invalid literal value, expected \\"baz\\"",
+ "path": "nested.bar",
+ "received": "not-baz",
+ "unionErrors": undefined,
+ },
+]
+`);
+
+ expect(zodError.flatten(flattenIssue)).toMatchInlineSnapshot(`
+Object {
+ "fieldErrors": Object {
+ "foo": Array [
+ Object {
+ "code": "too_small",
+ "exact": false,
+ "inclusive": true,
+ "keys": undefined,
+ "message": "String must contain at least 1 character(s)",
+ "minimum": 1,
+ "path": "foo",
+ "type": "string",
+ "unionErrors": undefined,
+ },
+ ],
+ "nested": Array [
+ Object {
+ "code": "invalid_literal",
+ "expected": "baz",
+ "keys": undefined,
+ "message": "Invalid literal value, expected \\"baz\\"",
+ "path": "nested.bar",
+ "received": "not-baz",
+ "unionErrors": undefined,
+ },
+ ],
+ },
+ "formErrors": Array [],
+}
+`);
+ });
+
+ it('flattens keys field to string', () => {
+ const zodError = z
+ .object({
+ foo: z.string().min(1),
+ })
+ .strict()
+ .safeParse({
+ foo: 'bar',
+ extra_key_abc: 'hello',
+ extra_key_def: 'world',
+ }).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "unrecognized_keys",
+ "keys": Array [
+ "extra_key_abc",
+ "extra_key_def",
+ ],
+ "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'",
+ "path": Array [],
+ },
+]
+`);
+
+ const issues = zodError.issues;
+ expect(issues.length).toBe(1);
+
+ // Format it for use in Sentry
+ const iss = issues[0];
+ if (iss === undefined) {
+ throw new Error('iss is undefined');
+ }
+ const formattedIssue = flattenIssue(iss);
+
+ // keys is now a string rather than array.
+ // Note: path is an empty string because the issue is at the root.
+ // TODO: Maybe somehow make it clearer that this is at the root?
+ expect(formattedIssue).toMatchInlineSnapshot(`
+Object {
+ "code": "unrecognized_keys",
+ "keys": "[\\"extra_key_abc\\",\\"extra_key_def\\"]",
+ "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'",
+ "path": "",
+ "unionErrors": undefined,
+}
+`);
+ expect(typeof formattedIssue.keys === 'string').toBe(true);
+ });
+});
+
+describe('flattenIssuePath()', () => {
+ it('returns single path', () => {
+ expect(flattenIssuePath(['foo'])).toBe('foo');
+ });
+
+ it('flattens nested string paths', () => {
+ expect(flattenIssuePath(['foo', 'bar'])).toBe('foo.bar');
+ });
+
+ it('uses placeholder for path index within array', () => {
+ expect(flattenIssuePath([0, 'foo', 1, 'bar', 'baz'])).toBe('.foo..bar.baz');
+ });
+});
+
+describe('formatIssueMessage()', () => {
+ it('adds invalid keys to message', () => {
+ const zodError = z
+ .object({
+ foo: z.string().min(1),
+ nested: z.object({
+ bar: z.literal('baz'),
+ }),
+ })
+ .safeParse({
+ foo: '',
+ nested: {
+ bar: 'not-baz',
+ },
+ }).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate keys: foo, nested.bar"');
+ });
+
+ describe('adds expected type if root variable is invalid', () => {
+ test('object', () => {
+ const zodError = z
+ .object({
+ foo: z.string().min(1),
+ })
+ .safeParse(123).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "object",
+ "message": "Expected object, received number",
+ "path": Array [],
+ "received": "number",
+ },
+]
+`);
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate object"');
+ });
+
+ test('number', () => {
+ const zodError = z.number().safeParse('123').error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "number",
+ "message": "Expected number, received string",
+ "path": Array [],
+ "received": "string",
+ },
+]
+`);
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate number"');
+ });
+
+ test('string', () => {
+ const zodError = z.string().safeParse(123).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "string",
+ "message": "Expected string, received number",
+ "path": Array [],
+ "received": "number",
+ },
+]
+`);
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate string"');
+ });
+
+ test('array', () => {
+ const zodError = z.string().array().safeParse('123').error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "array",
+ "message": "Expected array, received string",
+ "path": Array [],
+ "received": "string",
+ },
+]
+`);
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate array"');
+ });
+
+ test('wrong type in array', () => {
+ const zodError = z.string().array().safeParse([123]).error;
+ if (zodError === undefined) {
+ throw new Error('zodError is undefined');
+ }
+
+ // Original zod error
+ expect(zodError.issues).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "code": "invalid_type",
+ "expected": "string",
+ "message": "Expected string, received number",
+ "path": Array [
+ 0,
+ ],
+ "received": "number",
+ },
+]
+`);
+
+ const message = formatIssueMessage(zodError);
+ expect(message).toMatchInlineSnapshot('"Failed to validate keys: "');
+ });
});
});
diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts
index f7187695a025..aa6d4bf4cb2f 100644
--- a/packages/core/test/lib/utils/spanUtils.test.ts
+++ b/packages/core/test/lib/utils/spanUtils.test.ts
@@ -1,6 +1,7 @@
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SPAN_STATUS_ERROR,
SPAN_STATUS_OK,
SPAN_STATUS_UNSET,
@@ -14,8 +15,14 @@ import {
} from '../../../src';
import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist';
import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils';
-import { spanToTraceContext } from '../../../src/utils/spanUtils';
-import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils';
+import {
+ getRootSpan,
+ spanIsSampled,
+ spanTimeInputToSeconds,
+ spanToJSON,
+ spanToTraceContext,
+ updateSpanName,
+} from '../../../src/utils/spanUtils';
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
function createMockedOtelSpan({
@@ -332,3 +339,13 @@ describe('getRootSpan', () => {
});
});
});
+
+describe('updateSpanName', () => {
+ it('updates the span name and source', () => {
+ const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } });
+ updateSpanName(span, 'new-name');
+ const spanJSON = spanToJSON(span);
+ expect(spanJSON.description).toBe('new-name');
+ expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom');
+ });
+});
diff --git a/packages/core/test/utils-hoist/browser.test.ts b/packages/core/test/utils-hoist/browser.test.ts
index c86570ee7fb0..ac76584908d5 100644
--- a/packages/core/test/utils-hoist/browser.test.ts
+++ b/packages/core/test/utils-hoist/browser.test.ts
@@ -78,6 +78,7 @@ describe('htmlTreeAsString', () => {
describe('getDomElement', () => {
it('returns the element for a given query selector', () => {
document.head.innerHTML = 'Hello
';
+ // eslint-disable-next-line deprecation/deprecation
const el = getDomElement('div#mydiv');
expect(el).toBeDefined();
expect(el?.tagName).toEqual('DIV');
diff --git a/packages/deno/README.md b/packages/deno/README.md
index 502778cf8abb..e6c8159cb76b 100644
--- a/packages/deno/README.md
+++ b/packages/deno/README.md
@@ -12,7 +12,6 @@
## Links
-- [SDK on Deno registry](https://deno.land/x/sentry)
- [Official SDK Docs](https://docs.sentry.io/quickstart/)
- [TypeDoc](http://getsentry.github.io/sentry-javascript/)
@@ -21,14 +20,13 @@ The Sentry Deno SDK is in beta. Please help us improve the SDK by
## Usage
+> DEPRECATION NOTICE: The Sentry Deno SDK as published on the Deno registry (deno.land) is deprecated.
+> Import the package from the npm registry instead.
+
To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK
and hook into the environment. Note that you can turn off almost all side effects using the respective options.
```javascript
-// Import from the Deno registry
-import * as Sentry from 'https://deno.land/x/sentry/index.mjs';
-
-// or import from npm registry
import * as Sentry from 'npm:@sentry/deno';
Sentry.init({
diff --git a/packages/deno/package.json b/packages/deno/package.json
index b9f7dbdc8ea8..271620f7cff2 100644
--- a/packages/deno/package.json
+++ b/packages/deno/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/deno",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Deno",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno",
@@ -24,7 +24,7 @@
"/build"
],
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.5",
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
index 892f6ce681c0..cea4effad4bd 100644
--- a/packages/deno/src/index.ts
+++ b/packages/deno/src/index.ts
@@ -89,6 +89,7 @@ export {
spanToJSON,
spanToTraceHeader,
spanToBaggageHeader,
+ updateSpanName,
} from '@sentry/core';
export { DenoClient } from './client';
diff --git a/packages/ember/package.json b/packages/ember/package.json
index 1547eed88d94..c0fc6d4d8273 100644
--- a/packages/ember/package.json
+++ b/packages/ember/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/ember",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Ember.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember",
@@ -32,8 +32,8 @@
"dependencies": {
"@babel/core": "^7.24.4",
"@embroider/macros": "^1.16.0",
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
"ember-auto-import": "^2.7.2",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.1.1",
diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json
index 246f51ed6dcf..f283623524ef 100644
--- a/packages/eslint-config-sdk/package.json
+++ b/packages/eslint-config-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/eslint-config-sdk",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK eslint config",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk",
@@ -22,8 +22,8 @@
"access": "public"
},
"dependencies": {
- "@sentry-internal/eslint-plugin-sdk": "8.45.0",
- "@sentry-internal/typescript": "8.45.0",
+ "@sentry-internal/eslint-plugin-sdk": "8.54.0",
+ "@sentry-internal/typescript": "8.54.0",
"@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0",
"eslint-config-prettier": "^6.11.0",
diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json
index 7a6a729fc0cd..a640a5537b3c 100644
--- a/packages/eslint-plugin-sdk/package.json
+++ b/packages/eslint-plugin-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/eslint-plugin-sdk",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK eslint plugin",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk",
diff --git a/packages/feedback/package.json b/packages/feedback/package.json
index a5e44856378e..3be95be4aa00 100644
--- a/packages/feedback/package.json
+++ b/packages/feedback/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/feedback",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Sentry SDK integration for user feedback",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"devDependencies": {
"preact": "^10.19.4"
diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts
index fb1bd1fc143e..1c2f5655decb 100644
--- a/packages/feedback/src/core/integration.ts
+++ b/packages/feedback/src/core/integration.ts
@@ -5,7 +5,7 @@ import type {
Integration,
IntegrationFn,
} from '@sentry/core';
-import { getClient, isBrowser, logger } from '@sentry/core';
+import { addIntegration, isBrowser, logger } from '@sentry/core';
import {
ADD_SCREENSHOT_LABEL,
CANCEL_BUTTON_LABEL,
@@ -39,16 +39,22 @@ type Unsubscribe = () => void;
* Allow users to capture user feedback and send it to Sentry.
*/
-interface BuilderOptions {
- // The type here should be `keyof typeof LazyLoadableIntegrations`, but that'll cause a cicrular
- // dependency with @sentry/core
- lazyLoadIntegration: (
- name: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration',
- scriptNonce?: string,
- ) => Promise;
- getModalIntegration?: null | (() => IntegrationFn);
- getScreenshotIntegration?: null | (() => IntegrationFn);
-}
+type BuilderOptions =
+ | {
+ lazyLoadIntegration?: never;
+ getModalIntegration: () => IntegrationFn;
+ getScreenshotIntegration: () => IntegrationFn;
+ }
+ | {
+ // The type here should be `keyof typeof LazyLoadableIntegrations`, but that'll cause a cicrular
+ // dependency with @sentry/core
+ lazyLoadIntegration: (
+ name: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration',
+ scriptNonce?: string,
+ ) => Promise;
+ getModalIntegration?: never;
+ getScreenshotIntegration?: never;
+ };
export const buildFeedbackIntegration = ({
lazyLoadIntegration,
@@ -172,45 +178,40 @@ export const buildFeedbackIntegration = ({
return _shadow as ShadowRoot;
};
- const _findIntegration = async (
- integrationName: string,
- getter: undefined | null | (() => IntegrationFn),
- functionMethodName: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration',
- ): Promise => {
- const client = getClient();
- const existing = client && client.getIntegrationByName(integrationName);
- if (existing) {
- return existing as I;
- }
- const integrationFn = (getter && getter()) || (await lazyLoadIntegration(functionMethodName, scriptNonce));
- const integration = integrationFn();
- client && client.addIntegration(integration);
- return integration as I;
- };
-
const _loadAndRenderDialog = async (
options: FeedbackInternalOptions,
): Promise> => {
const screenshotRequired = options.enableScreenshot && isScreenshotSupported();
- const [modalIntegration, screenshotIntegration] = await Promise.all([
- _findIntegration('FeedbackModal', getModalIntegration, 'feedbackModalIntegration'),
- screenshotRequired
- ? _findIntegration(
- 'FeedbackScreenshot',
- getScreenshotIntegration,
- 'feedbackScreenshotIntegration',
- )
- : undefined,
- ]);
- if (!modalIntegration) {
- // TODO: Let the end-user retry async loading
+
+ let modalIntegration: FeedbackModalIntegration;
+ let screenshotIntegration: FeedbackScreenshotIntegration | undefined;
+
+ try {
+ const modalIntegrationFn = getModalIntegration
+ ? getModalIntegration()
+ : await lazyLoadIntegration('feedbackModalIntegration', scriptNonce);
+ modalIntegration = modalIntegrationFn() as FeedbackModalIntegration;
+ addIntegration(modalIntegration);
+ } catch {
DEBUG_BUILD &&
logger.error(
- '[Feedback] Missing feedback modal integration. Try using `feedbackSyncIntegration` in your `Sentry.init`.',
+ '[Feedback] Error when trying to load feedback integrations. Try using `feedbackSyncIntegration` in your `Sentry.init`.',
);
throw new Error('[Feedback] Missing feedback modal integration!');
}
- if (screenshotRequired && !screenshotIntegration) {
+
+ try {
+ const screenshotIntegrationFn = screenshotRequired
+ ? getScreenshotIntegration
+ ? getScreenshotIntegration()
+ : await lazyLoadIntegration('feedbackScreenshotIntegration', scriptNonce)
+ : undefined;
+
+ if (screenshotIntegrationFn) {
+ screenshotIntegration = screenshotIntegrationFn() as FeedbackScreenshotIntegration;
+ addIntegration(screenshotIntegration);
+ }
+ } catch {
DEBUG_BUILD &&
logger.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.');
}
@@ -227,7 +228,7 @@ export const buildFeedbackIntegration = ({
options.onFormSubmitted && options.onFormSubmitted();
},
},
- screenshotIntegration: screenshotRequired ? screenshotIntegration : undefined,
+ screenshotIntegration,
sendFeedback,
shadow: _createShadow(options),
});
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 2c03fae30f14..fd916898588b 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/gatsby",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Gatsby.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby",
@@ -45,8 +45,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/react": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/react": "8.54.0",
"@sentry/webpack-plugin": "2.22.7"
},
"peerDependencies": {
diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json
index 52008df49931..2f6562b3e924 100644
--- a/packages/google-cloud-serverless/package.json
+++ b/packages/google-cloud-serverless/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/google-cloud-serverless",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Google Cloud Functions",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless",
@@ -48,8 +48,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
"@types/express": "^4.17.14"
},
"devDependencies": {
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 6f89769c2a37..95d30ec8b072 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -118,6 +118,7 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
trpcMiddleware,
+ updateSpanName,
// eslint-disable-next-line deprecation/deprecation
addOpenTelemetryInstrumentation,
zodErrorsIntegration,
diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json
index 322922a945a0..8522e54ca34e 100644
--- a/packages/integration-shims/package.json
+++ b/packages/integration-shims/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/integration-shims",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Shims for integrations in Sentry SDK.",
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
@@ -55,7 +55,7 @@
"url": "https://github.com/getsentry/sentry-javascript/issues"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json
index 1bef4cc56c2f..7036be81047b 100644
--- a/packages/nestjs/package.json
+++ b/packages/nestjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nestjs",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for NestJS",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs",
@@ -14,8 +14,8 @@
"/*.d.ts",
"/*.d.ts.map"
],
- "main": "build/cjs/nestjs/index.js",
- "module": "build/esm/nestjs/index.js",
+ "main": "build/cjs/index.js",
+ "module": "build/esm/index.js",
"types": "build/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
@@ -44,8 +44,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0"
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0"
},
"devDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts
index e75054d3391d..a32751c22ae4 100644
--- a/packages/nestjs/src/setup.ts
+++ b/packages/nestjs/src/setup.ts
@@ -27,9 +27,9 @@ import { isExpectedError } from './helpers';
// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41
interface FastifyRequest {
routeOptions?: {
- method?: string;
url?: string;
};
+ method?: string;
}
// Partial extract of ExpressRequest interface
@@ -72,9 +72,7 @@ class SentryTracingInterceptor implements NestInterceptor {
const req = context.switchToHttp().getRequest() as FastifyRequest | ExpressRequest;
if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) {
// fastify case
- getIsolationScope().setTransactionName(
- `${(req.routeOptions.method || 'GET').toUpperCase()} ${req.routeOptions.url}`,
- );
+ getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.routeOptions.url}`);
} else if ('route' in req && req.route && req.route.path) {
// express case
getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.route.path}`);
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index baafbe149a1d..a749a17ec43e 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nextjs",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Next.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs",
@@ -79,12 +79,12 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.28.0",
"@rollup/plugin-commonjs": "28.0.1",
- "@sentry-internal/browser-utils": "8.45.0",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
- "@sentry/react": "8.45.0",
- "@sentry/vercel-edge": "8.45.0",
+ "@sentry-internal/browser-utils": "8.54.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
+ "@sentry/react": "8.54.0",
+ "@sentry/vercel-edge": "8.54.0",
"@sentry/webpack-plugin": "2.22.7",
"chalk": "3.0.0",
"resolve": "1.22.8",
diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
index 3cc5b4d340e5..856e80ee72f8 100644
--- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
@@ -3,6 +3,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
captureException,
continueTrace,
+ getActiveSpan,
httpRequestToRequestData,
isString,
logger,
@@ -59,7 +60,13 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
req.__withSentry_applied__ = true;
return withIsolationScope(isolationScope => {
- return continueTrace(
+ // Normally, there is an active span here (from Next.js OTEL) and we just use that as parent
+ // Else, we manually continueTrace from the incoming headers
+ const continueTraceIfNoActiveSpan = getActiveSpan()
+ ? (_opts: unknown, callback: () => T) => callback()
+ : continueTrace;
+
+ return continueTraceIfNoActiveSpan(
{
sentryTrace:
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts
index 8d5ab14c77c3..b04d94417204 100644
--- a/packages/nextjs/src/common/withServerActionInstrumentation.ts
+++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts
@@ -1,4 +1,5 @@
import type { RequestEventData } from '@sentry/core';
+import { getActiveSpan } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
SPAN_STATUS_ERROR,
@@ -95,7 +96,13 @@ async function withServerActionInstrumentationImplementation(_opts: unknown, callback: () => T) => callback()
+ : continueTrace;
+
+ return continueTraceIfNoActiveSpan(
{
sentryTrace: sentryTraceHeader,
baggage: baggageHeader,
diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts
index 9fb3bef49e67..432459004b68 100644
--- a/packages/nextjs/src/config/webpack.ts
+++ b/packages/nextjs/src/config/webpack.ts
@@ -332,7 +332,7 @@ export function constructWebpackConfigFunction(
// Symbolication for dev-mode errors is done elsewhere.
if (!isDev) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const { sentryWebpackPlugin } = loadModule<{ sentryWebpackPlugin: any }>('@sentry/webpack-plugin') ?? {};
+ const { sentryWebpackPlugin } = loadModule<{ sentryWebpackPlugin: any }>('@sentry/webpack-plugin', module) ?? {};
if (sentryWebpackPlugin) {
if (!userSentryOptions.sourcemaps?.disable) {
@@ -340,7 +340,7 @@ export function constructWebpackConfigFunction(
if (!isServer && !userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload) {
// eslint-disable-next-line no-console
console.warn(
- "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `deleteSourceMapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `deleteSourceMapsAfterUpload` will default to `true`. If you do not want to generate and upload sourcemaps, set the `sourcemaps.disable` option in `withSentryConfig()`.",
+ "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `sourcemaps.deleteSourcemapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `sourcemaps.deleteSourcemapsAfterUpload` will default to `true`. If you do not want to generate and upload sourcemaps, set the `sourcemaps.disable` option in `withSentryConfig()`.",
);
}
@@ -716,6 +716,7 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
// We provide these objects in addition to the hook above to provide redundancy in case the hook fails.
{ module: /@opentelemetry\/instrumentation/, message: /Critical dependency/ },
{ module: /@prisma\/instrumentation/, message: /Critical dependency/ },
+ { module: /require-in-the-middle/, message: /Critical dependency/ },
] satisfies IgnoreWarningsOption;
if (newConfig.ignoreWarnings === undefined) {
diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts
index 1b6a0e09ed85..83a5562fde35 100644
--- a/packages/nextjs/src/index.types.ts
+++ b/packages/nextjs/src/index.types.ts
@@ -19,10 +19,6 @@ export declare function init(
options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions | edgeSdk.EdgeOptions,
): Client | undefined;
-export declare const getClient: typeof clientSdk.getClient;
-export declare const getRootSpan: typeof serverSdk.getRootSpan;
-export declare const continueTrace: typeof clientSdk.continueTrace;
-
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
diff --git a/packages/nitro-utils/package.json b/packages/nitro-utils/package.json
index 06ec390ef87f..541fda472b7a 100644
--- a/packages/nitro-utils/package.json
+++ b/packages/nitro-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/nitro-utils",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Utilities for all Sentry SDKs with Nitro on the server-side",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro-utils",
@@ -37,7 +37,7 @@
}
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"devDependencies": {
"rollup": "^4.24.4"
diff --git a/packages/node/package.json b/packages/node/package.json
index 618b9aa89725..f4a0c2f4bc2f 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/node",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Sentry Node SDK using OpenTelemetry for performance instrumentation",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node",
@@ -66,39 +66,39 @@
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
- "@opentelemetry/context-async-hooks": "^1.29.0",
- "@opentelemetry/core": "^1.29.0",
- "@opentelemetry/instrumentation": "^0.56.0",
- "@opentelemetry/instrumentation-amqplib": "^0.45.0",
- "@opentelemetry/instrumentation-connect": "0.42.0",
- "@opentelemetry/instrumentation-dataloader": "0.15.0",
- "@opentelemetry/instrumentation-express": "0.46.0",
- "@opentelemetry/instrumentation-fastify": "0.43.0",
- "@opentelemetry/instrumentation-fs": "0.18.0",
- "@opentelemetry/instrumentation-generic-pool": "0.42.0",
- "@opentelemetry/instrumentation-graphql": "0.46.0",
- "@opentelemetry/instrumentation-hapi": "0.44.0",
- "@opentelemetry/instrumentation-http": "0.56.0",
- "@opentelemetry/instrumentation-ioredis": "0.46.0",
- "@opentelemetry/instrumentation-kafkajs": "0.6.0",
- "@opentelemetry/instrumentation-knex": "0.43.0",
- "@opentelemetry/instrumentation-koa": "0.46.0",
- "@opentelemetry/instrumentation-lru-memoizer": "0.43.0",
- "@opentelemetry/instrumentation-mongodb": "0.50.0",
- "@opentelemetry/instrumentation-mongoose": "0.45.0",
- "@opentelemetry/instrumentation-mysql": "0.44.0",
- "@opentelemetry/instrumentation-mysql2": "0.44.0",
- "@opentelemetry/instrumentation-nestjs-core": "0.43.0",
- "@opentelemetry/instrumentation-pg": "0.49.0",
- "@opentelemetry/instrumentation-redis-4": "0.45.0",
- "@opentelemetry/instrumentation-tedious": "0.17.0",
- "@opentelemetry/instrumentation-undici": "0.9.0",
- "@opentelemetry/resources": "^1.29.0",
- "@opentelemetry/sdk-trace-base": "^1.29.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/instrumentation-amqplib": "^0.46.0",
+ "@opentelemetry/instrumentation-connect": "0.43.0",
+ "@opentelemetry/instrumentation-dataloader": "0.16.0",
+ "@opentelemetry/instrumentation-express": "0.47.0",
+ "@opentelemetry/instrumentation-fastify": "0.44.1",
+ "@opentelemetry/instrumentation-fs": "0.19.0",
+ "@opentelemetry/instrumentation-generic-pool": "0.43.0",
+ "@opentelemetry/instrumentation-graphql": "0.47.0",
+ "@opentelemetry/instrumentation-hapi": "0.45.1",
+ "@opentelemetry/instrumentation-http": "0.57.1",
+ "@opentelemetry/instrumentation-ioredis": "0.47.0",
+ "@opentelemetry/instrumentation-kafkajs": "0.7.0",
+ "@opentelemetry/instrumentation-knex": "0.44.0",
+ "@opentelemetry/instrumentation-koa": "0.47.0",
+ "@opentelemetry/instrumentation-lru-memoizer": "0.44.0",
+ "@opentelemetry/instrumentation-mongodb": "0.51.0",
+ "@opentelemetry/instrumentation-mongoose": "0.46.0",
+ "@opentelemetry/instrumentation-mysql": "0.45.0",
+ "@opentelemetry/instrumentation-mysql2": "0.45.0",
+ "@opentelemetry/instrumentation-nestjs-core": "0.44.0",
+ "@opentelemetry/instrumentation-pg": "0.50.0",
+ "@opentelemetry/instrumentation-redis-4": "0.46.0",
+ "@opentelemetry/instrumentation-tedious": "0.18.0",
+ "@opentelemetry/instrumentation-undici": "0.10.0",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.28.0",
- "@prisma/instrumentation": "5.19.1",
- "@sentry/core": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
+ "@prisma/instrumentation": "5.22.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
"import-in-the-middle": "^1.11.2"
},
"devDependencies": {
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index fa16ac4e6b3d..7f1bc6c735f3 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -63,9 +63,6 @@ export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from
export {
// eslint-disable-next-line deprecation/deprecation
addOpenTelemetryInstrumentation,
- // These are custom variants that need to be used instead of the core one
- // As they have slightly different implementations
- continueTrace,
// This needs exporting so the NodeClient can be used without calling init
setOpenTelemetryContextAsyncContextStrategy as setNodeAsyncContextStrategy,
} from '@sentry/opentelemetry';
@@ -110,6 +107,7 @@ export {
getIsolationScope,
getTraceData,
getTraceMetaTags,
+ continueTrace,
withScope,
withIsolationScope,
captureException,
@@ -142,6 +140,7 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
trpcMiddleware,
+ updateSpanName,
zodErrorsIntegration,
profiler,
} from '@sentry/core';
diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts
index e2666e3ecd3e..fc1b23e35b1d 100644
--- a/packages/node/src/integrations/anr/common.ts
+++ b/packages/node/src/integrations/anr/common.ts
@@ -21,6 +21,12 @@ export interface AnrIntegrationOptions {
* This uses the node debugger which enables the inspector API and opens the required ports.
*/
captureStackTrace: boolean;
+ /**
+ * Maximum number of ANR events to send.
+ *
+ * Defaults to 1.
+ */
+ maxAnrEvents: number;
/**
* Tags to include with ANR events.
*/
diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts
index f0cef4d60831..aa903789ad12 100644
--- a/packages/node/src/integrations/anr/index.ts
+++ b/packages/node/src/integrations/anr/index.ts
@@ -160,6 +160,7 @@ async function _startWorker(
pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL,
anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD,
captureStackTrace: !!integrationOptions.captureStackTrace,
+ maxAnrEvents: integrationOptions.maxAnrEvents || 1,
staticTags: integrationOptions.staticTags || {},
contexts,
};
@@ -175,6 +176,7 @@ async function _startWorker(
workerData: options,
// We don't want any Node args to be passed to the worker
execArgv: [],
+ env: { ...process.env, NODE_OPTIONS: undefined },
});
process.on('exit', () => {
diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts
index 354cea514618..117cccfe8904 100644
--- a/packages/node/src/integrations/anr/worker.ts
+++ b/packages/node/src/integrations/anr/worker.ts
@@ -23,7 +23,7 @@ type VoidFunction = () => void;
const options: WorkerStartData = workerData;
let session: Session | undefined;
-let hasSentAnrEvent = false;
+let sentAnrEvents = 0;
let mainDebugImages: Record = {};
function log(msg: string): void {
@@ -91,24 +91,31 @@ function applyDebugMeta(event: Event): void {
return;
}
+ const normalisedDebugImages = options.appRootPath ? {} : mainDebugImages;
+ if (options.appRootPath) {
+ for (const [path, debugId] of Object.entries(mainDebugImages)) {
+ normalisedDebugImages[normalizeUrlToBase(path, options.appRootPath)] = debugId;
+ }
+ }
+
const filenameToDebugId = new Map();
for (const exception of event.exception?.values || []) {
for (const frame of exception.stacktrace?.frames || []) {
const filename = frame.abs_path || frame.filename;
- if (filename && mainDebugImages[filename]) {
- filenameToDebugId.set(filename, mainDebugImages[filename] as string);
+ if (filename && normalisedDebugImages[filename]) {
+ filenameToDebugId.set(filename, normalisedDebugImages[filename] as string);
}
}
}
if (filenameToDebugId.size > 0) {
const images: DebugImage[] = [];
- for (const [filename, debugId] of filenameToDebugId.entries()) {
+ for (const [code_file, debug_id] of filenameToDebugId.entries()) {
images.push({
type: 'sourcemap',
- code_file: filename,
- debug_id: debugId,
+ code_file,
+ debug_id,
});
}
event.debug_meta = { images };
@@ -134,11 +141,11 @@ function applyScopeToEvent(event: Event, scope: ScopeData): void {
}
async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise {
- if (hasSentAnrEvent) {
+ if (sentAnrEvents >= options.maxAnrEvents) {
return;
}
- hasSentAnrEvent = true;
+ sentAnrEvents += 1;
await sendAbnormalSession();
@@ -179,11 +186,13 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise {
- process.exit(0);
- }, 5_000);
+ if (sentAnrEvents >= options.maxAnrEvents) {
+ // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts.
+ // This is mainly for the benefit of logging or debugging.
+ setTimeout(() => {
+ process.exit(0);
+ }, 5_000);
+ }
}
let debuggerPause: VoidFunction | undefined;
diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts
index 5e1bd75913c9..2d00ad5d5c61 100644
--- a/packages/node/src/integrations/contextlines.ts
+++ b/packages/node/src/integrations/contextlines.ts
@@ -142,13 +142,21 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
input: stream,
});
+ // We need to explicitly destroy the stream to prevent memory leaks,
+ // removing the listeners on the readline interface is not enough.
+ // See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
+ function destroyStreamAndResolve(): void {
+ stream.destroy();
+ resolve();
+ }
+
// Init at zero and increment at the start of the loop because lines are 1 indexed.
let lineNumber = 0;
let currentRangeIndex = 0;
const range = ranges[currentRangeIndex];
if (range === undefined) {
// We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
- resolve();
+ destroyStreamAndResolve();
return;
}
let rangeStart = range[0];
@@ -162,14 +170,14 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
DEBUG_BUILD && logger.error(`Failed to read file: ${path}. Error: ${e}`);
lineReaded.close();
lineReaded.removeAllListeners();
- resolve();
+ destroyStreamAndResolve();
}
// We need to handle the error event to prevent the process from crashing in < Node 16
// https://github.com/nodejs/node/pull/31603
stream.on('error', onStreamError);
lineReaded.on('error', onStreamError);
- lineReaded.on('close', resolve);
+ lineReaded.on('close', destroyStreamAndResolve);
lineReaded.on('line', line => {
lineNumber++;
diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts
index e1e0ebadf755..c3dcb1d12450 100644
--- a/packages/node/src/integrations/local-variables/local-variables-async.ts
+++ b/packages/node/src/integrations/local-variables/local-variables-async.ts
@@ -81,6 +81,7 @@ export const localVariablesAsyncIntegration = defineIntegration(((
workerData: options,
// We don't want any Node args to be passed to the worker
execArgv: [],
+ env: { ...process.env, NODE_OPTIONS: undefined },
});
process.on('exit', () => {
diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts
index e6cdcf514b2c..18e50edb8e4a 100644
--- a/packages/node/src/integrations/tracing/express.ts
+++ b/packages/node/src/integrations/tracing/express.ts
@@ -93,7 +93,9 @@ interface MiddlewareError extends Error {
};
}
-type ExpressMiddleware = (
+type ExpressMiddleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void;
+
+type ExpressErrorMiddleware = (
error: MiddlewareError,
req: http.IncomingMessage,
res: http.ServerResponse,
@@ -111,13 +113,17 @@ interface ExpressHandlerOptions {
/**
* An Express-compatible error handler.
*/
-export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMiddleware {
+export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware {
return function sentryErrorMiddleware(
error: MiddlewareError,
- _req: http.IncomingMessage,
+ request: http.IncomingMessage,
res: http.ServerResponse,
next: (error: MiddlewareError) => void,
): void {
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
+ // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too
+ getIsolationScope().setSDKProcessingMetadata({ request });
+
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
if (shouldHandleError(error)) {
@@ -152,6 +158,19 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid
};
}
+function expressRequestHandler(): ExpressMiddleware {
+ return function sentryRequestMiddleware(
+ request: http.IncomingMessage,
+ _res: http.ServerResponse,
+ next: () => void,
+ ): void {
+ // Ensure we use the express-enhanced request here, instead of the plain HTTP one
+ getIsolationScope().setSDKProcessingMetadata({ request });
+
+ next();
+ };
+}
+
/**
* Add an Express error handler to capture errors to Sentry.
*
@@ -177,9 +196,10 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid
* ```
*/
export function setupExpressErrorHandler(
- app: { use: (middleware: ExpressMiddleware) => unknown },
+ app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown },
options?: ExpressHandlerOptions,
): void {
+ app.use(expressRequestHandler());
app.use(expressErrorHandler(options));
ensureIsWrapped(app.use, 'express');
}
diff --git a/packages/node/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts
index a87980045b54..92936b5d6ee8 100644
--- a/packages/node/src/integrations/tracing/fastify.ts
+++ b/packages/node/src/integrations/tracing/fastify.ts
@@ -25,10 +25,10 @@ interface Fastify {
* Works for Fastify 3, 4 and presumably 5.
*/
interface FastifyRequestRouteInfo {
+ method?: string;
// since fastify@4.10.0
routeOptions?: {
url?: string;
- method?: string;
};
routerPath?: string;
}
@@ -107,7 +107,7 @@ export function setupFastifyErrorHandler(fastify: Fastify): void {
// Taken from Otel Fastify instrumentation:
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
- const method = reqWithRouteInfo.routeOptions?.method || 'GET';
+ const method = reqWithRouteInfo.method || 'GET';
getIsolationScope().setTransactionName(`${method} ${routeName}`);
});
diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts
index b5c9ea4bb61f..0b5be4cc116a 100644
--- a/packages/node/src/integrations/tracing/nest/nest.ts
+++ b/packages/node/src/integrations/tracing/nest/nest.ts
@@ -91,9 +91,7 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE
const req = context.switchToHttp().getRequest();
if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) {
// fastify case
- getIsolationScope().setTransactionName(
- `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`,
- );
+ getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`);
} else if ('route' in req && req.route && req.route.path) {
// express case
getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts
index a983832ac8c6..8283e652edfb 100644
--- a/packages/node/src/integrations/tracing/nest/types.ts
+++ b/packages/node/src/integrations/tracing/nest/types.ts
@@ -4,9 +4,9 @@
// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41
interface FastifyRequest {
routeOptions?: {
- method?: string;
url?: string;
};
+ method?: string;
}
// Partial extract of ExpressRequest interface
diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts
index a42d41a6b5ec..930d34d602b9 100644
--- a/packages/node/src/integrations/tracing/prisma.ts
+++ b/packages/node/src/integrations/tracing/prisma.ts
@@ -1,51 +1,78 @@
+import type { Instrumentation } from '@opentelemetry/instrumentation';
// When importing CJS modules into an ESM module, we cannot import the named exports directly.
import * as prismaInstrumentation from '@prisma/instrumentation';
-import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core';
-import type { IntegrationFn } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, consoleSandbox, defineIntegration, spanToJSON } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
+import type { PrismaV5TracingHelper } from './prisma/vendor/v5-tracing-helper';
+import type { PrismaV6TracingHelper } from './prisma/vendor/v6-tracing-helper';
const INTEGRATION_NAME = 'Prisma';
-export const instrumentPrisma = generateInstrumentOnce(INTEGRATION_NAME, () => {
- const EsmInteropPrismaInstrumentation: typeof prismaInstrumentation.PrismaInstrumentation =
- // @ts-expect-error We need to do the following for interop reasons
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- prismaInstrumentation.default?.PrismaInstrumentation || prismaInstrumentation.PrismaInstrumentation;
+const EsmInteropPrismaInstrumentation: typeof prismaInstrumentation.PrismaInstrumentation =
+ // @ts-expect-error We need to do the following for interop reasons
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ prismaInstrumentation.default?.PrismaInstrumentation || prismaInstrumentation.PrismaInstrumentation;
- return new EsmInteropPrismaInstrumentation({});
-});
+type CompatibilityLayerTraceHelper = PrismaV5TracingHelper & PrismaV6TracingHelper;
-const _prismaIntegration = (() => {
- return {
- name: INTEGRATION_NAME,
- setupOnce() {
- instrumentPrisma();
- },
+function isPrismaV5TracingHelper(helper: unknown): helper is PrismaV5TracingHelper {
+ return !!helper && typeof helper === 'object' && 'createEngineSpan' in helper;
+}
- setup(client) {
- client.on('spanStart', span => {
- const spanJSON = spanToJSON(span);
- if (spanJSON.description?.startsWith('prisma:')) {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma');
- }
+class SentryPrismaInteropInstrumentation extends EsmInteropPrismaInstrumentation {
+ public constructor() {
+ super();
+ }
- if (spanJSON.description === 'prisma:engine:db_query') {
- span.setAttribute('db.system', 'prisma');
- }
- });
- },
- };
-}) satisfies IntegrationFn;
+ public enable(): void {
+ super.enable();
+
+ // The PrismaIntegration (super class) defines a global variable `global["PRISMA_INSTRUMENTATION"]` when `enable()` is called. This global variable holds a "TracingHelper" which Prisma uses internally to create tracing data. It's their way of not depending on OTEL with their main package. The sucky thing is, prisma broke the interface of the tracing helper with the v6 major update. This means that if you use Prisma 6 with the v5 instrumentation (or vice versa) Prisma just blows up, because tries to call methods on the helper that no longer exist.
+ // Because we actually want to use the v6 instrumentation and not blow up in Prisma 5 user's faces, what we're doing here is backfilling the v5 method (`createEngineSpan`) with a noop so that no longer crashes when it attempts to call that function.
+ // We still won't fully emit all the spans, but this could potentially be implemented in the future.
+ const prismaInstrumentationObject = (globalThis as Record).PRISMA_INSTRUMENTATION;
+ const prismaTracingHelper =
+ prismaInstrumentationObject &&
+ typeof prismaInstrumentationObject === 'object' &&
+ 'helper' in prismaInstrumentationObject
+ ? prismaInstrumentationObject.helper
+ : undefined;
+
+ let emittedWarning = false;
+
+ if (isPrismaV5TracingHelper(prismaTracingHelper)) {
+ (prismaTracingHelper as CompatibilityLayerTraceHelper).dispatchEngineSpans = () => {
+ consoleSandbox(() => {
+ if (!emittedWarning) {
+ emittedWarning = true;
+ // eslint-disable-next-line no-console
+ console.warn(
+ '[Sentry] This version (v8) of the Sentry SDK does not support tracing with Prisma version 6 out of the box. To trace Prisma version 6, pass a `prismaInstrumentation` for version 6 to the Sentry `prismaIntegration`. Read more: https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/',
+ );
+ }
+ });
+ };
+ }
+ }
+}
+
+export const instrumentPrisma = generateInstrumentOnce<{ prismaInstrumentation?: Instrumentation }>(
+ INTEGRATION_NAME,
+ options => {
+ // Use a passed instrumentation instance to support older Prisma versions
+ if (options?.prismaInstrumentation) {
+ return options.prismaInstrumentation;
+ }
+
+ return new SentryPrismaInteropInstrumentation();
+ },
+);
/**
- * Adds Sentry tracing instrumentation for the [prisma](https://www.npmjs.com/package/prisma) library.
- *
+ * Adds Sentry tracing instrumentation for the [Prisma](https://www.npmjs.com/package/prisma) ORM.
* For more information, see the [`prismaIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/).
*
- * @example
- *
- * Make sure `previewFeatures = ["tracing"]` is set in the prisma client generator block. See the
- * [prisma docs](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing) for more details.
+ * Make sure `previewFeatures = ["tracing"]` is added to the generator block in your Prisma schema.
*
* ```prisma
* generator client {
@@ -54,14 +81,63 @@ const _prismaIntegration = (() => {
* }
* ```
*
- * Then you can use the integration like this:
+ * NOTE: By default, this integration works with Prisma version 5.
+ * To get performance instrumentation for other Prisma versions,
+ * 1. Install the `@prisma/instrumentation` package with the desired version.
+ * 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option of this integration:
*
- * ```javascript
- * const Sentry = require('@sentry/node');
+ * ```js
+ * import { PrismaInstrumentation } from '@prisma/instrumentation'
*
- * Sentry.init({
- * integrations: [Sentry.prismaIntegration()],
- * });
- * ```
+ * Sentry.init({
+ * integrations: [
+ * prismaIntegration({
+ * // Override the default instrumentation that Sentry uses
+ * prismaInstrumentation: new PrismaInstrumentation()
+ * })
+ * ]
+ * })
+ * ```
+ *
+ * The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions.
*/
-export const prismaIntegration = defineIntegration(_prismaIntegration);
+export const prismaIntegration = defineIntegration(
+ ({
+ prismaInstrumentation,
+ }: {
+ /**
+ * Overrides the instrumentation used by the Sentry SDK with the passed in instrumentation instance.
+ *
+ * NOTE: By default, the Sentry SDK uses the Prisma v5 instrumentation. Use this option if you need performance instrumentation different Prisma versions.
+ *
+ * For more information refer to the documentation of `prismaIntegration()` or see https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/
+ */
+ prismaInstrumentation?: Instrumentation;
+ } = {}) => {
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ instrumentPrisma({ prismaInstrumentation });
+ },
+ setup(client) {
+ client.on('spanStart', span => {
+ const spanJSON = spanToJSON(span);
+ if (spanJSON.description?.startsWith('prisma:')) {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma');
+ }
+
+ // Make sure we use the query text as the span name, for ex. SELECT * FROM "User" WHERE "id" = $1
+ if (spanJSON.description === 'prisma:engine:db_query' && spanJSON.data?.['db.query.text']) {
+ span.updateName(spanJSON.data['db.query.text'] as string);
+ }
+
+ // In Prisma v5.22+, the `db.system` attribute is automatically set
+ // On older versions, this is missing, so we add it here
+ if (spanJSON.description === 'prisma:engine:db_query' && !spanJSON.data?.['db.system']) {
+ span.setAttribute('db.system', 'prisma');
+ }
+ });
+ },
+ };
+ },
+);
diff --git a/packages/node/src/integrations/tracing/prisma/vendor/v5-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendor/v5-tracing-helper.ts
new file mode 100644
index 000000000000..8823a8ca7728
--- /dev/null
+++ b/packages/node/src/integrations/tracing/prisma/vendor/v5-tracing-helper.ts
@@ -0,0 +1,41 @@
+// Vendored from https://github.com/prisma/prisma/blob/718358aa37975c18e5ea62f5b659fb47630b7609/packages/internals/src/tracing/types.ts#L1
+
+import type { Context, Span, SpanOptions } from '@opentelemetry/api';
+
+type V5SpanCallback = (span?: Span, context?: Context) => R;
+
+type V5ExtendedSpanOptions = SpanOptions & {
+ name: string;
+ internal?: boolean;
+ middleware?: boolean;
+ active?: boolean;
+ context?: Context;
+};
+
+type EngineSpanEvent = {
+ span: boolean;
+ spans: V5EngineSpan[];
+};
+
+type V5EngineSpanKind = 'client' | 'internal';
+
+type V5EngineSpan = {
+ span: boolean;
+ name: string;
+ trace_id: string;
+ span_id: string;
+ parent_span_id: string;
+ start_time: [number, number];
+ end_time: [number, number];
+ attributes?: Record;
+ links?: { trace_id: string; span_id: string }[];
+ kind: V5EngineSpanKind;
+};
+
+export interface PrismaV5TracingHelper {
+ isEnabled(): boolean;
+ getTraceParent(context?: Context): string;
+ createEngineSpan(engineSpanEvent: EngineSpanEvent): void;
+ getActiveContext(): Context | undefined;
+ runInChildSpan(nameOrOptions: string | V5ExtendedSpanOptions, callback: V5SpanCallback): R;
+}
diff --git a/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts
new file mode 100644
index 000000000000..2ad1482a2e1a
--- /dev/null
+++ b/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts
@@ -0,0 +1,38 @@
+// https://github.com/prisma/prisma/blob/d45607dfa10c4ef08cb8f79f18fa84ef33910150/packages/internals/src/tracing/types.ts#L1
+
+import type { Context, Span, SpanOptions } from '@opentelemetry/api';
+
+type V6SpanCallback = (span?: Span, context?: Context) => R;
+
+type V6ExtendedSpanOptions = SpanOptions & {
+ name: string;
+ internal?: boolean;
+ middleware?: boolean;
+ active?: boolean;
+ context?: Context;
+};
+
+type V6EngineSpanId = string;
+
+type V6HrTime = [number, number];
+
+type EngineSpanKind = 'client' | 'internal';
+
+type PrismaV6EngineSpan = {
+ id: V6EngineSpanId;
+ parentId: string | null;
+ name: string;
+ startTime: V6HrTime;
+ endTime: V6HrTime;
+ kind: EngineSpanKind;
+ attributes?: Record;
+ links?: V6EngineSpanId[];
+};
+
+export interface PrismaV6TracingHelper {
+ isEnabled(): boolean;
+ getTraceParent(context?: Context): string;
+ dispatchEngineSpans(spans: PrismaV6EngineSpan[]): void;
+ getActiveContext(): Context | undefined;
+ runInChildSpan(nameOrOptions: string | V6ExtendedSpanOptions, callback: V6SpanCallback): R;
+}
diff --git a/packages/node/src/sdk/api.ts b/packages/node/src/sdk/api.ts
index dd7ccc8ca75d..7e4f200d1e06 100644
--- a/packages/node/src/sdk/api.ts
+++ b/packages/node/src/sdk/api.ts
@@ -68,6 +68,8 @@ export function getSentryRelease(fallback?: string): string | undefined {
process.env['HEROKU_TEST_RUN_COMMIT_VERSION'] ||
// Heroku #2 https://docs.sentry.io/product/integrations/deployment/heroku/#configure-releases
process.env['HEROKU_SLUG_COMMIT'] ||
+ // Railway - https://docs.railway.app/reference/variables#git-variables
+ process.env['RAILWAY_GIT_COMMIT_SHA'] ||
// Render - https://render.com/docs/environment-variables
process.env['RENDER_GIT_COMMIT'] ||
// Semaphore CI - https://docs.semaphoreci.com/ci-cd-environment/environment-variables
diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts
index 2ce75908168d..f0e2739f2629 100644
--- a/packages/node/src/sdk/index.ts
+++ b/packages/node/src/sdk/index.ts
@@ -169,7 +169,9 @@ function _init(
// If users opt-out of this, they _have_ to set up OpenTelemetry themselves
// There is no way to use this SDK without OpenTelemetry!
if (!options.skipOpenTelemetrySetup) {
- initOpenTelemetry(client);
+ initOpenTelemetry(client, {
+ spanProcessors: options.openTelemetrySpanProcessors,
+ });
validateOpenTelemetrySetup();
}
diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts
index 4f0bb444d83d..b268314485a6 100644
--- a/packages/node/src/sdk/initOtel.ts
+++ b/packages/node/src/sdk/initOtel.ts
@@ -1,6 +1,7 @@
import moduleModule from 'module';
import { DiagLogLevel, diag } from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
+import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
import {
ATTR_SERVICE_NAME,
@@ -22,15 +23,20 @@ declare const __IMPORT_META_URL_REPLACEMENT__: string;
// About 277h - this must fit into new Array(len)!
const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000;
+interface AdditionalOpenTelemetryOptions {
+ /** Additional SpanProcessor instances that should be used. */
+ spanProcessors?: SpanProcessor[];
+}
+
/**
* Initialize OpenTelemetry for Node.
*/
-export function initOpenTelemetry(client: NodeClient): void {
+export function initOpenTelemetry(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): void {
if (client.getOptions().debug) {
setupOpenTelemetryLogger();
}
- const provider = setupOtel(client);
+ const provider = setupOtel(client, options);
client.traceProvider = provider;
}
@@ -129,7 +135,7 @@ function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: s
}
/** Just exported for tests. */
-export function setupOtel(client: NodeClient): BasicTracerProvider {
+export function setupOtel(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): BasicTracerProvider {
// Create and configure NodeTracerProvider
const provider = new BasicTracerProvider({
sampler: new SentrySampler(client),
@@ -144,6 +150,7 @@ export function setupOtel(client: NodeClient): BasicTracerProvider {
new SentrySpanProcessor({
timeout: _clampSpanProcessorTimeout(client.getOptions().maxSpanWaitDuration),
}),
+ ...(options.spanProcessors || []),
],
});
diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts
index ebcdee869523..c7f166ed9b4d 100644
--- a/packages/node/src/types.ts
+++ b/packages/node/src/types.ts
@@ -1,6 +1,6 @@
import type { Span as WriteableSpan } from '@opentelemetry/api';
import type { Instrumentation } from '@opentelemetry/instrumentation';
-import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
+import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core';
import type { NodeTransportOptions } from './transports';
@@ -121,6 +121,11 @@ export interface BaseNodeOptions {
*/
openTelemetryInstrumentations?: Instrumentation[];
+ /**
+ * Provide an array of additional OpenTelemetry SpanProcessors that should be registered.
+ */
+ openTelemetrySpanProcessors?: SpanProcessor[];
+
/**
* The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span.
* The SDK will automatically clean up spans that have no finished parent after this duration.
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index 86ce75ba76f8..7f59381d2778 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nuxt",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt",
@@ -43,13 +43,13 @@
},
"dependencies": {
"@nuxt/kit": "^3.13.2",
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
"@sentry/rollup-plugin": "2.22.7",
"@sentry/vite-plugin": "2.22.6",
- "@sentry/vue": "8.45.0"
+ "@sentry/vue": "8.54.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.8.4",
diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts
index 93ca94016924..8a9a453ff7db 100644
--- a/packages/nuxt/src/common/types.ts
+++ b/packages/nuxt/src/common/types.ts
@@ -32,6 +32,13 @@ type SourceMapsOptions = {
*/
org?: string;
+ /**
+ * The URL of your Sentry instance if you're using self-hosted Sentry.
+ *
+ * @default https://sentry.io by default the plugin will point towards the Sentry SaaS URL
+ */
+ url?: string;
+
/**
* The project slug of your Sentry project.
* Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable.
diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts
index fd4bd00856be..2802d3158d41 100644
--- a/packages/nuxt/src/index.types.ts
+++ b/packages/nuxt/src/index.types.ts
@@ -14,6 +14,5 @@ export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsInteg
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
-export declare const continueTrace: typeof clientSdk.continueTrace;
// eslint-disable-next-line deprecation/deprecation
export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics;
diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts
index b748115f5c81..15992be5d0b8 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts
@@ -52,7 +52,11 @@ export default defineNitroPlugin(nitroApp => {
});
async function flushIfServerless(): Promise {
- const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY;
+ const isServerless =
+ !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions
+ !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda
+ !!process.env.VERCEL ||
+ !!process.env.NETLIFY;
// @ts-expect-error This is not typed
if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) {
diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts
index 2f90094e6138..0b264e822bcc 100644
--- a/packages/nuxt/src/vite/sourceMaps.ts
+++ b/packages/nuxt/src/vite/sourceMaps.ts
@@ -91,6 +91,7 @@ export function getPluginOptions(
project: sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT,
authToken: sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN,
telemetry: sourceMapsUploadOptions.telemetry ?? true,
+ url: sourceMapsUploadOptions.url ?? process.env.SENTRY_URL,
debug: moduleOptions.debug ?? false,
_metaOptions: {
telemetry: {
diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts
index 0c90429fa8d5..b33d314f5166 100644
--- a/packages/nuxt/test/vite/sourceMaps.test.ts
+++ b/packages/nuxt/test/vite/sourceMaps.test.ts
@@ -20,6 +20,7 @@ describe('getPluginOptions', () => {
SENTRY_ORG: 'default-org',
SENTRY_PROJECT: 'default-project',
SENTRY_AUTH_TOKEN: 'default-token',
+ SENTRY_URL: 'https://santry.io',
};
process.env = { ...defaultEnv };
@@ -31,6 +32,7 @@ describe('getPluginOptions', () => {
org: 'default-org',
project: 'default-project',
authToken: 'default-token',
+ url: 'https://santry.io',
telemetry: true,
sourcemaps: expect.objectContaining({
rewriteSources: expect.any(Function),
@@ -114,6 +116,7 @@ describe('getPluginOptions', () => {
assets: ['custom-assets/**/*'],
filesToDeleteAfterUpload: ['delete-this.js'],
},
+ url: 'https://santry.io',
},
debug: true,
unstable_sentryBundlerPluginOptions: {
@@ -124,6 +127,7 @@ describe('getPluginOptions', () => {
release: {
name: 'test-release',
},
+ url: 'https://suntry.io',
},
};
const options = getPluginOptions(customOptions, false);
@@ -140,6 +144,7 @@ describe('getPluginOptions', () => {
release: expect.objectContaining({
name: 'test-release',
}),
+ url: 'https://suntry.io',
}),
);
});
diff --git a/packages/nuxt/tsconfig.types.json b/packages/nuxt/tsconfig.types.json
index 65455f66bd75..cab81135cd7a 100644
--- a/packages/nuxt/tsconfig.types.json
+++ b/packages/nuxt/tsconfig.types.json
@@ -1,6 +1,6 @@
{
"extends": "./tsconfig.json",
-
+ "exclude": ["build.config.ts"],
"compilerOptions": {
"declaration": true,
"declarationMap": true,
diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json
index e342a8d34be8..db8e16c2223c 100644
--- a/packages/opentelemetry/package.json
+++ b/packages/opentelemetry/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/opentelemetry",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry utilities for OpenTelemetry",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry",
@@ -39,20 +39,22 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.54.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.9.0",
- "@opentelemetry/core": "^1.29.0",
- "@opentelemetry/instrumentation": "^0.56.0",
- "@opentelemetry/sdk-trace-base": "^1.29.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.28.0"
},
"devDependencies": {
"@opentelemetry/api": "^1.9.0",
- "@opentelemetry/context-async-hooks": "^1.29.0",
- "@opentelemetry/core": "^1.29.0",
- "@opentelemetry/sdk-trace-base": "^1.29.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1",
+ "@opentelemetry/core": "^1.30.1",
+ "@opentelemetry/instrumentation": "^0.57.1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.28.0"
},
"scripts": {
diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts
index cfc4254819d7..695175bc3fa1 100644
--- a/packages/opentelemetry/src/asyncContextStrategy.ts
+++ b/packages/opentelemetry/src/asyncContextStrategy.ts
@@ -6,7 +6,7 @@ import {
SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY,
SENTRY_FORK_SET_SCOPE_CONTEXT_KEY,
} from './constants';
-import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace';
+import { continueTrace, startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace';
import type { CurrentScopes } from './types';
import { getScopesFromContext } from './utils/contextData';
import { getActiveSpan } from './utils/getActiveSpan';
@@ -103,6 +103,7 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void {
getActiveSpan,
suppressTracing,
getTraceData,
+ continueTrace,
// The types here don't fully align, because our own `Span` type is narrower
// than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around
withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan,
diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts
index a8d5affa4646..bff6518eb27d 100644
--- a/packages/opentelemetry/src/spanExporter.ts
+++ b/packages/opentelemetry/src/spanExporter.ts
@@ -12,6 +12,7 @@ import type {
TransactionSource,
} from '@sentry/core';
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
@@ -392,6 +393,7 @@ function removeSentryAttributes(data: Record): Record(options: Parameters[0], callback: () => T): T {
- return baseContinueTrace(options, () => {
- return continueTraceAsRemoteSpan(context.active(), options, callback);
- });
+ return continueTraceAsRemoteSpan(context.active(), options, callback);
}
/**
diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts
index a1aa47e5b6ce..3136b94e6bf7 100644
--- a/packages/opentelemetry/src/utils/parseSpanDescription.ts
+++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts
@@ -15,8 +15,10 @@ import {
} from '@opentelemetry/semantic-conventions';
import type { SpanAttributes, TransactionSource } from '@sentry/core';
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getSanitizedUrlString,
parseUrl,
stripUrlQueryAndFragment,
@@ -36,12 +38,12 @@ interface SpanDescription {
/**
* Infer the op & description for a set of name, attributes and kind of a span.
*/
-export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
+export function inferSpanData(spanName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription {
// if http.method exists, this is an http request span
// eslint-disable-next-line deprecation/deprecation
const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD];
if (httpMethod) {
- return descriptionForHttpMethod({ attributes, name, kind }, httpMethod);
+ return descriptionForHttpMethod({ attributes, name: spanName, kind }, httpMethod);
}
// eslint-disable-next-line deprecation/deprecation
@@ -53,17 +55,18 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
// If db.type exists then this is a database call span
// If the Redis DB is used as a cache, the span description should not be changed
if (dbSystem && !opIsCache) {
- return descriptionForDbSystem({ attributes, name });
+ return descriptionForDbSystem({ attributes, name: spanName });
}
+ const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route';
+
// If rpc.service exists then this is a rpc call span.
// eslint-disable-next-line deprecation/deprecation
const rpcService = attributes[SEMATTRS_RPC_SERVICE];
if (rpcService) {
return {
+ ...getUserUpdatedNameAndSource(spanName, attributes, 'route'),
op: 'rpc',
- description: name,
- source: 'route',
};
}
@@ -72,9 +75,8 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
const messagingSystem = attributes[SEMATTRS_MESSAGING_SYSTEM];
if (messagingSystem) {
return {
+ ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute),
op: 'message',
- description: name,
- source: 'route',
};
}
@@ -82,15 +84,22 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp
// eslint-disable-next-line deprecation/deprecation
const faasTrigger = attributes[SEMATTRS_FAAS_TRIGGER];
if (faasTrigger) {
- return { op: faasTrigger.toString(), description: name, source: 'route' };
+ return {
+ ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute),
+ op: faasTrigger.toString(),
+ };
}
- return { op: undefined, description: name, source: 'custom' };
+ return { op: undefined, description: spanName, source: 'custom' };
}
/**
* Extract better op/description from an otel span.
*
+ * Does not overwrite the span name if the source is already set to custom to ensure
+ * that user-updated span names are preserved. In this case, we only adjust the op but
+ * leave span description and source unchanged.
+ *
* Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306
*/
export function parseSpanDescription(span: AbstractSpan): SpanDescription {
@@ -102,6 +111,21 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription {
}
function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription {
+ // if we already have a custom name, we don't overwrite it but only set the op
+ const userDefinedName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
+ if (typeof userDefinedName === 'string') {
+ return {
+ op: 'db',
+ description: userDefinedName,
+ source: (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || 'custom',
+ };
+ }
+
+ // if we already have the source set to custom, we don't overwrite the span description but only set the op
+ if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') {
+ return { op: 'db', description: name, source: 'custom' };
+ }
+
// Use DB statement (Ex "SELECT * FROM table") if possible as description.
// eslint-disable-next-line deprecation/deprecation
const statement = attributes[SEMATTRS_DB_STATEMENT];
@@ -135,7 +159,7 @@ export function descriptionForHttpMethod(
const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes, kind);
if (!urlPath) {
- return { op: opParts.join('.'), description: name, source: 'custom' };
+ return { ...getUserUpdatedNameAndSource(name, attributes), op: opParts.join('.') };
}
const graphqlOperationsAttribute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION];
@@ -145,12 +169,12 @@ export function descriptionForHttpMethod(
// When the http span has a graphql operation, append it to the description
// We add these in the graphqlIntegration
- const description = graphqlOperationsAttribute
+ const inferredDescription = graphqlOperationsAttribute
? `${baseDescription} (${getGraphqlOperationNamesFromAttribute(graphqlOperationsAttribute)})`
: baseDescription;
// If `httpPath` is a root path, then we can categorize the transaction source as route.
- const source: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url';
+ const inferredSource: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url';
const data: Record = {};
@@ -174,12 +198,21 @@ export function descriptionForHttpMethod(
const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual';
const isManualSpan = !`${origin}`.startsWith('auto');
- const useInferredDescription = isClientOrServerKind || !isManualSpan;
+ // If users (or in very rare occasions we) set the source to custom, we don't overwrite the name
+ const alreadyHasCustomSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom';
+ const customSpanName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
+
+ const useInferredDescription =
+ !alreadyHasCustomSource && customSpanName == null && (isClientOrServerKind || !isManualSpan);
+
+ const { description, source } = useInferredDescription
+ ? { description: inferredDescription, source: inferredSource }
+ : getUserUpdatedNameAndSource(name, attributes);
return {
op: opParts.join('.'),
- description: useInferredDescription ? description : name,
- source: useInferredDescription ? source : 'custom',
+ description,
+ source,
data,
};
}
@@ -244,3 +277,36 @@ export function getSanitizedUrl(
return { urlPath: undefined, url, query, fragment, hasRoute: false };
}
+
+/**
+ * Because Otel instrumentation sometimes mutates span names via `span.updateName`, the only way
+ * to ensure that a user-set span name is preserved is to store it as a tmp attribute on the span.
+ * We delete this attribute once we're done with it when preparing the event envelope.
+ *
+ * This temp attribute always takes precedence over the original name.
+ *
+ * We also need to take care of setting the correct source. Users can always update the source
+ * after updating the name, so we need to respect that.
+ *
+ * @internal exported only for testing
+ */
+export function getUserUpdatedNameAndSource(
+ originalName: string,
+ attributes: Attributes,
+ fallbackSource: TransactionSource = 'custom',
+): {
+ description: string;
+ source: TransactionSource;
+} {
+ const source = (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || fallbackSource;
+ const description = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
+
+ if (description && typeof description === 'string') {
+ return {
+ description,
+ source,
+ };
+ }
+
+ return { description: originalName, source };
+}
diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts
index 2c22318ec977..43ce7b0e347b 100644
--- a/packages/opentelemetry/test/trace.test.ts
+++ b/packages/opentelemetry/test/trace.test.ts
@@ -1574,11 +1574,8 @@ describe('continueTrace', () => {
);
expect(scope.getPropagationContext()).toEqual({
- dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace
- sampled: false,
- parentSpanId: '1121201211212012',
spanId: expect.any(String),
- traceId: '12312012123120121231201212312012',
+ traceId: expect.any(String),
});
expect(scope.getScopeData().sdkProcessingMetadata).toEqual({});
@@ -1605,14 +1602,8 @@ describe('continueTrace', () => {
);
expect(scope.getPropagationContext()).toEqual({
- dsc: {
- environment: 'production',
- version: '1.0',
- },
- sampled: true,
- parentSpanId: '1121201211212012',
spanId: expect.any(String),
- traceId: '12312012123120121231201212312012',
+ traceId: expect.any(String),
});
expect(scope.getScopeData().sdkProcessingMetadata).toEqual({});
@@ -1639,16 +1630,9 @@ describe('continueTrace', () => {
);
expect(scope.getPropagationContext()).toEqual({
- dsc: {
- environment: 'production',
- version: '1.0',
- },
- sampled: true,
- parentSpanId: '1121201211212012',
spanId: expect.any(String),
- traceId: '12312012123120121231201212312012',
+ traceId: expect.any(String),
});
-
expect(scope.getScopeData().sdkProcessingMetadata).toEqual({});
});
diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts
index c44645c62888..d43dfcd9f587 100644
--- a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts
+++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts
@@ -15,7 +15,13 @@ import {
SEMATTRS_RPC_SERVICE,
} from '@opentelemetry/semantic-conventions';
-import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription';
+import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import {
+ descriptionForHttpMethod,
+ getSanitizedUrl,
+ getUserUpdatedNameAndSource,
+ parseSpanDescription,
+} from '../../src/utils/parseSpanDescription';
describe('parseSpanDescription', () => {
it.each([
@@ -81,6 +87,53 @@ describe('parseSpanDescription', () => {
source: 'task',
},
],
+ [
+ 'works with db system and custom source',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_DB_SYSTEM]: 'mysql',
+ [SEMATTRS_DB_STATEMENT]: 'SELECT * from users',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ description: 'test name',
+ op: 'db',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with db system and custom source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_DB_SYSTEM]: 'mysql',
+ [SEMATTRS_DB_STATEMENT]: 'SELECT * from users',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ description: 'custom name',
+ op: 'db',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with db system and component source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMATTRS_DB_SYSTEM]: 'mysql',
+ [SEMATTRS_DB_STATEMENT]: 'SELECT * from users',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ description: 'custom name',
+ op: 'db',
+ source: 'component',
+ },
+ ],
[
'works with db system without statement',
{
@@ -107,6 +160,50 @@ describe('parseSpanDescription', () => {
source: 'route',
},
],
+ [
+ 'works with rpc service and custom source',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_RPC_SERVICE]: 'rpc-test-service',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'test name',
+ op: 'rpc',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with rpc service and custom source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_RPC_SERVICE]: 'rpc-test-service',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'rpc',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with rpc service and component source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMATTRS_RPC_SERVICE]: 'rpc-test-service',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'rpc',
+ source: 'component',
+ },
+ ],
[
'works with messaging system',
{
@@ -120,6 +217,50 @@ describe('parseSpanDescription', () => {
source: 'route',
},
],
+ [
+ 'works with messaging system and custom source',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'test name',
+ op: 'message',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with messaging system and custom source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'message',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with messaging system and component source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'message',
+ source: 'component',
+ },
+ ],
[
'works with faas trigger',
{
@@ -133,6 +274,50 @@ describe('parseSpanDescription', () => {
source: 'route',
},
],
+ [
+ 'works with faas trigger and custom source',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'test name',
+ op: 'test-faas-trigger',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with faas trigger and custom source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'test-faas-trigger',
+ source: 'custom',
+ },
+ ],
+ [
+ 'works with faas trigger and component source and custom name',
+ {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ undefined,
+ {
+ description: 'custom name',
+ op: 'test-faas-trigger',
+ source: 'component',
+ },
+ ],
])('%s', (_, attributes, name, kind, expected) => {
const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span);
expect(actual).toEqual(expected);
@@ -172,6 +357,26 @@ describe('descriptionForHttpMethod', () => {
source: 'url',
},
],
+ [
+ 'works with prefetch request',
+ 'GET',
+ {
+ [SEMATTRS_HTTP_METHOD]: 'GET',
+ [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path',
+ [SEMATTRS_HTTP_TARGET]: '/my-path',
+ 'sentry.http.prefetch': true,
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ op: 'http.client.prefetch',
+ description: 'GET https://www.example.com/my-path',
+ data: {
+ url: 'https://www.example.com/my-path',
+ },
+ source: 'url',
+ },
+ ],
[
'works with basic server POST',
'POST',
@@ -230,6 +435,71 @@ describe('descriptionForHttpMethod', () => {
source: 'custom',
},
],
+ [
+ "doesn't overwrite span name with source custom",
+ 'GET',
+ {
+ [SEMATTRS_HTTP_METHOD]: 'GET',
+ [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123',
+ [SEMATTRS_HTTP_TARGET]: '/my-path/123',
+ [ATTR_HTTP_ROUTE]: '/my-path/:id',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ op: 'http.client',
+ description: 'test name',
+ data: {
+ url: 'https://www.example.com/my-path/123',
+ },
+ source: 'custom',
+ },
+ ],
+ [
+ 'takes user-passed span name (with source custom)',
+ 'GET',
+ {
+ [SEMATTRS_HTTP_METHOD]: 'GET',
+ [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123',
+ [SEMATTRS_HTTP_TARGET]: '/my-path/123',
+ [ATTR_HTTP_ROUTE]: '/my-path/:id',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ op: 'http.client',
+ description: 'custom name',
+ data: {
+ url: 'https://www.example.com/my-path/123',
+ },
+ source: 'custom',
+ },
+ ],
+ [
+ 'takes user-passed span name (with source component)',
+ 'GET',
+ {
+ [SEMATTRS_HTTP_METHOD]: 'GET',
+ [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123',
+ [SEMATTRS_HTTP_TARGET]: '/my-path/123',
+ [ATTR_HTTP_ROUTE]: '/my-path/:id',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ },
+ 'test name',
+ SpanKind.CLIENT,
+ {
+ op: 'http.client',
+ description: 'custom name',
+ data: {
+ url: 'https://www.example.com/my-path/123',
+ },
+ source: 'component',
+ },
+ ],
])('%s', (_, httpMethod, attributes, name, kind, expected) => {
const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod);
expect(actual).toEqual(expected);
@@ -383,3 +653,38 @@ describe('getSanitizedUrl', () => {
expect(actual).toEqual(expected);
});
});
+
+describe('getUserUpdatedNameAndSource', () => {
+ it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => {
+ expect(getUserUpdatedNameAndSource('base name', {})).toEqual({ description: 'base name', source: 'custom' });
+ });
+
+ it('returns param name with custom fallback source if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => {
+ expect(getUserUpdatedNameAndSource('base name', {}, 'route')).toEqual({
+ description: 'base name',
+ source: 'route',
+ });
+ });
+
+ it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not a string', () => {
+ expect(getUserUpdatedNameAndSource('base name', { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 123 })).toEqual({
+ description: 'base name',
+ source: 'custom',
+ });
+ });
+
+ it.each(['custom', 'task', 'url', 'route'])(
+ 'returns `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute if is a string and source is %s',
+ source => {
+ expect(
+ getUserUpdatedNameAndSource('base name', {
+ [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
+ }),
+ ).toEqual({
+ description: 'custom name',
+ source,
+ });
+ },
+ );
+});
diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json
index 19ecb062875e..448b2d4d94d8 100644
--- a/packages/profiling-node/package.json
+++ b/packages/profiling-node/package.json
@@ -1,12 +1,13 @@
{
"name": "@sentry/profiling-node",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Node.js Profiling",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node",
"author": "Sentry",
"license": "MIT",
"main": "lib/cjs/index.js",
+ "module": "lib/esm/index.js",
"types": "lib/types/index.d.ts",
"exports": {
"./package.json": "./package.json",
@@ -75,8 +76,8 @@
"test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
"detect-libc": "^2.0.2",
"node-abi": "^3.61.0"
},
diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs
index 12492b7c83e8..a9c148306709 100644
--- a/packages/profiling-node/rollup.npm.config.mjs
+++ b/packages/profiling-node/rollup.npm.config.mjs
@@ -1,49 +1,20 @@
import commonjs from '@rollup/plugin-commonjs';
+import replace from '@rollup/plugin-replace';
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
-export const ESMShim = `
-import cjsUrl from 'node:url';
-import cjsPath from 'node:path';
-import cjsModule from 'node:module';
-
-if(typeof __filename === 'undefined'){
- globalThis.__filename = cjsUrl.fileURLToPath(import.meta.url);
-}
-
-if(typeof __dirname === 'undefined'){
- globalThis.__dirname = cjsPath.dirname(__filename);
-}
-
-if(typeof require === 'undefined'){
- globalThis.require = cjsModule.createRequire(import.meta.url);
-}
-`;
-
-function makeESMShimPlugin(shim) {
- return {
- transform(code) {
- const SHIM_REGEXP = /\/\/ #START_SENTRY_ESM_SHIM[\s\S]*?\/\/ #END_SENTRY_ESM_SHIM/;
- return code.replace(SHIM_REGEXP, shim);
- },
- };
-}
-
-const variants = makeNPMConfigVariants(
+export default makeNPMConfigVariants(
makeBaseNPMConfig({
packageSpecificConfig: {
output: { dir: 'lib', preserveModules: false },
- plugins: [commonjs()],
+ plugins: [
+ commonjs(),
+ replace({
+ preventAssignment: false,
+ values: {
+ __IMPORT_META_URL_REPLACEMENT__: 'import.meta.url',
+ },
+ }),
+ ],
},
}),
);
-
-for (const variant of variants) {
- if (variant.output.format === 'esm') {
- variant.plugins.push(makeESMShimPlugin(ESMShim));
- } else {
- // Remove the ESM shim comment
- variant.plugins.push(makeESMShimPlugin(''));
- }
-}
-
-export default variants;
diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts
index ed4ad83e7b31..a9a6d65ce191 100644
--- a/packages/profiling-node/src/cpu_profiler.ts
+++ b/packages/profiling-node/src/cpu_profiler.ts
@@ -1,6 +1,9 @@
+import { createRequire } from 'node:module';
import { arch as _arch, platform as _platform } from 'node:os';
import { join, resolve } from 'node:path';
+import { dirname } from 'node:path';
import { env, versions } from 'node:process';
+import { fileURLToPath, pathToFileURL } from 'node:url';
import { threadId } from 'node:worker_threads';
import { familySync } from 'detect-libc';
import { getAbi } from 'node-abi';
@@ -15,11 +18,7 @@ import type {
} from './types';
import type { ProfileFormat } from './types';
-// #START_SENTRY_ESM_SHIM
-// When building for ESM, we shim require to use createRequire and __dirname.
-// We need to do this because .node extensions in esm are not supported.
-// The comment below this line exists as a placeholder for where to insert the shim.
-// #END_SENTRY_ESM_SHIM
+declare const __IMPORT_META_URL_REPLACEMENT__: string;
const stdlib = familySync();
const platform = process.env['BUILD_PLATFORM'] || _platform();
@@ -27,23 +26,32 @@ const arch = process.env['BUILD_ARCH'] || _arch();
const abi = getAbi(versions.node, 'node');
const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');
-const built_from_source_path = resolve(__dirname, '..', `./sentry_cpu_profiler-${identifier}`);
-
/**
* Imports cpp bindings based on the current platform and architecture.
*/
// eslint-disable-next-line complexity
export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
+ // We need to work around using import.meta.url directly with __IMPORT_META_URL_REPLACEMENT__ because jest complains about it.
+ const importMetaUrl =
+ typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined'
+ ? // This case is always hit when the SDK is built
+ __IMPORT_META_URL_REPLACEMENT__
+ : // This case is hit when the tests are run
+ pathToFileURL(__filename).href;
+
+ const createdRequire = createRequire(importMetaUrl);
+ const esmCompatibleDirname = dirname(fileURLToPath(importMetaUrl));
+
// If a binary path is specified, use that.
if (env['SENTRY_PROFILER_BINARY_PATH']) {
const envPath = env['SENTRY_PROFILER_BINARY_PATH'];
- return require(envPath);
+ return createdRequire(envPath);
}
// If a user specifies a different binary dir, they are in control of the binaries being moved there
if (env['SENTRY_PROFILER_BINARY_DIR']) {
const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`);
- return require(`${binaryPath}.node`);
+ return createdRequire(`${binaryPath}.node`);
}
// We need the fallthrough so that in the end, we can fallback to the dynamic require.
@@ -51,31 +59,31 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
if (platform === 'darwin') {
if (arch === 'x64') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-darwin-x64-93.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-x64-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-darwin-x64-108.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-x64-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-darwin-x64-115.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-x64-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-darwin-x64-127.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-x64-127.node');
}
}
if (arch === 'arm64') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-darwin-arm64-93.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-arm64-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-darwin-arm64-108.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-arm64-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-darwin-arm64-115.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-arm64-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-darwin-arm64-127.node');
+ return createdRequire('../sentry_cpu_profiler-darwin-arm64-127.node');
}
}
}
@@ -83,16 +91,16 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
if (platform === 'win32') {
if (arch === 'x64') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-win32-x64-93.node');
+ return createdRequire('../sentry_cpu_profiler-win32-x64-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-win32-x64-108.node');
+ return createdRequire('../sentry_cpu_profiler-win32-x64-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-win32-x64-115.node');
+ return createdRequire('../sentry_cpu_profiler-win32-x64-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-win32-x64-127.node');
+ return createdRequire('../sentry_cpu_profiler-win32-x64-127.node');
}
}
}
@@ -101,66 +109,68 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
if (arch === 'x64') {
if (stdlib === 'musl') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-linux-x64-musl-93.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-musl-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-linux-x64-musl-108.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-musl-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-linux-x64-musl-115.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-musl-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-linux-x64-musl-127.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-musl-127.node');
}
}
if (stdlib === 'glibc') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-linux-x64-glibc-93.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-linux-x64-glibc-108.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-linux-x64-glibc-115.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-linux-x64-glibc-127.node');
+ return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-127.node');
}
}
}
if (arch === 'arm64') {
if (stdlib === 'musl') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-linux-arm64-musl-93.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-linux-arm64-musl-108.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-linux-arm64-musl-115.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-linux-arm64-musl-127.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-127.node');
}
}
if (stdlib === 'glibc') {
if (abi === '93') {
- return require('../sentry_cpu_profiler-linux-arm64-glibc-93.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-93.node');
}
if (abi === '108') {
- return require('../sentry_cpu_profiler-linux-arm64-glibc-108.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-108.node');
}
if (abi === '115') {
- return require('../sentry_cpu_profiler-linux-arm64-glibc-115.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-115.node');
}
if (abi === '127') {
- return require('../sentry_cpu_profiler-linux-arm64-glibc-127.node');
+ return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-127.node');
}
}
}
}
- return require(`${built_from_source_path}.node`);
+
+ const built_from_source_path = resolve(esmCompatibleDirname, '..', `sentry_cpu_profiler-${identifier}`);
+ return createdRequire(`${built_from_source_path}.node`);
}
const PrivateCpuProfilerBindings: PrivateV8CpuProfilerBindings = importCppBindingsModule();
diff --git a/packages/profiling-node/tsconfig.json b/packages/profiling-node/tsconfig.json
index c53d22cf5270..68bd9a52df2a 100644
--- a/packages/profiling-node/tsconfig.json
+++ b/packages/profiling-node/tsconfig.json
@@ -8,4 +8,3 @@
},
"include": ["src/**/*"]
}
-
diff --git a/packages/react/package.json b/packages/react/package.json
index d14ad2f112a8..0e7ebf5c837e 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/react",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for React.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 91cc0e2cdc17..f500d79466cc 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -35,6 +35,12 @@ export type ErrorBoundaryProps = {
*
*/
fallback?: React.ReactElement | FallbackRender | undefined;
+ /**
+ * If set to `true` or `false`, the error `handled` property will be set to the given value.
+ * If unset, the default behaviour is to rely on the presence of the `fallback` prop to determine
+ * if the error was handled or not.
+ */
+ handled?: boolean | undefined;
/** Called when the error boundary encounters an error */
onError?: ((error: unknown, componentStack: string | undefined, eventId: string) => void) | undefined;
/** Called on componentDidMount() */
@@ -107,7 +113,8 @@ class ErrorBoundary extends React.Component & { basename?: string }): TRouter {
+ return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter {
const router = createRouterFunction(routes, opts);
const basename = opts && opts.basename;
@@ -114,6 +111,78 @@ export function createV6CompatibleWrapCreateBrowserRouter<
};
}
+/**
+ * Creates a wrapCreateMemoryRouter function that can be used with all React Router v6 compatible versions.
+ */
+export function createV6CompatibleWrapCreateMemoryRouter<
+ TState extends RouterState = RouterState,
+ TRouter extends Router = Router,
+>(
+ createRouterFunction: CreateRouterFunction,
+ version: V6CompatibleVersion,
+): CreateRouterFunction {
+ if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) {
+ DEBUG_BUILD &&
+ logger.warn(
+ `reactRouterV${version}Instrumentation was unable to wrap the \`createMemoryRouter\` function because of one or more missing parameters.`,
+ );
+
+ return createRouterFunction;
+ }
+
+ return function (
+ routes: RouteObject[],
+ opts?: Record & {
+ basename?: string;
+ initialEntries?: (string | { pathname: string })[];
+ initialIndex?: number;
+ },
+ ): TRouter {
+ const router = createRouterFunction(routes, opts);
+ const basename = opts ? opts.basename : undefined;
+
+ const activeRootSpan = getActiveRootSpan();
+ let initialEntry = undefined;
+
+ const initialEntries = opts ? opts.initialEntries : undefined;
+ const initialIndex = opts ? opts.initialIndex : undefined;
+
+ const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1;
+ const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex];
+
+ initialEntry = hasOnlyOneInitialEntry
+ ? initialEntries[0]
+ : hasIndexedEntry
+ ? initialEntries[initialIndex]
+ : undefined;
+
+ const location = initialEntry
+ ? typeof initialEntry === 'string'
+ ? { pathname: initialEntry }
+ : initialEntry
+ : router.state.location;
+
+ if (router.state.historyAction === 'POP' && activeRootSpan) {
+ updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename);
+ }
+
+ router.subscribe((state: RouterState) => {
+ const location = state.location;
+ if (state.historyAction === 'PUSH' || state.historyAction === 'POP') {
+ handleNavigation({
+ location,
+ routes,
+ navigationType: state.historyAction,
+ version,
+ basename,
+ });
+ }
+ });
+
+ return router;
+ };
+}
+
/**
* Creates a browser tracing integration that can be used with all React Router v6 compatible versions.
*/
@@ -180,7 +249,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
return origUseRoutes;
}
- const allRoutes: RouteObject[] = [];
+ const allRoutes: Set = new Set();
const SentryRoutes: React.FC<{
children?: React.ReactNode;
@@ -207,10 +276,21 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
if (isMountRenderPass.current) {
routes.forEach(route => {
- allRoutes.push(...getChildRoutesRecursively(route));
+ const extractedChildRoutes = getChildRoutesRecursively(route);
+
+ extractedChildRoutes.forEach(r => {
+ allRoutes.add(r);
+ });
});
- updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, allRoutes);
+ updatePageloadTransaction(
+ getActiveRootSpan(),
+ normalizedLocation,
+ routes,
+ undefined,
+ undefined,
+ Array.from(allRoutes),
+ );
isMountRenderPass.current = false;
} else {
handleNavigation({
@@ -218,7 +298,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
routes,
navigationType,
version,
- allRoutes,
+ allRoutes: Array.from(allRoutes),
});
}
}, [navigationType, stableLocationParam]);
@@ -343,14 +423,18 @@ function locationIsInsideDescendantRoute(location: Location, routes: RouteObject
return false;
}
-function getChildRoutesRecursively(route: RouteObject, allRoutes: RouteObject[] = []): RouteObject[] {
- if (route.children && !route.index) {
- route.children.forEach(child => {
- allRoutes.push(...getChildRoutesRecursively(child, allRoutes));
- });
- }
+function getChildRoutesRecursively(route: RouteObject, allRoutes: Set = new Set()): Set {
+ if (!allRoutes.has(route)) {
+ allRoutes.add(route);
+
+ if (route.children && !route.index) {
+ route.children.forEach(child => {
+ const childRoutes = getChildRoutesRecursively(child, allRoutes);
- allRoutes.push(route);
+ childRoutes.forEach(r => allRoutes.add(r));
+ });
+ }
+ }
return allRoutes;
}
@@ -428,10 +512,10 @@ function getNormalizedName(
// If path is not a wildcard and has no child routes, append the path
if (path && !pathIsWildcardAndHasChildren(path, branch)) {
const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`;
- pathBuilder += newPath;
+ pathBuilder = trimSlash(pathBuilder) + prefixWithSlash(newPath);
// If the path matches the current location, return the path
- if (location.pathname.endsWith(basename + branch.pathname)) {
+ if (trimSlash(location.pathname) === trimSlash(basename + branch.pathname)) {
if (
// If the route defined on the element is something like
// Product} />
@@ -513,7 +597,7 @@ export function createV6CompatibleWithSentryReactRouterRouting = new Set();
const SentryRoutes: React.FC
= (props: P) => {
const isMountRenderPass = React.useRef(true);
@@ -527,10 +611,14 @@ export function createV6CompatibleWithSentryReactRouterRouting
{
- allRoutes.push(...getChildRoutesRecursively(route));
+ const extractedChildRoutes = getChildRoutesRecursively(route);
+
+ extractedChildRoutes.forEach(r => {
+ allRoutes.add(r);
+ });
});
- updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes);
+ updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, Array.from(allRoutes));
isMountRenderPass.current = false;
} else {
handleNavigation({
@@ -538,7 +626,7 @@ export function createV6CompatibleWithSentryReactRouterRouting
= Router,
+>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction {
+ return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '6');
+}
+
/**
* A higher-order component that adds Sentry routing instrumentation to a React Router v6 Route component.
* This is used to automatically capture route changes as transactions.
diff --git a/packages/react/src/reactrouterv7.tsx b/packages/react/src/reactrouterv7.tsx
index df2badd35e44..5a80482cd2c3 100644
--- a/packages/react/src/reactrouterv7.tsx
+++ b/packages/react/src/reactrouterv7.tsx
@@ -6,6 +6,7 @@ import {
createReactRouterV6CompatibleTracingIntegration,
createV6CompatibleWithSentryReactRouterRouting,
createV6CompatibleWrapCreateBrowserRouter,
+ createV6CompatibleWrapCreateMemoryRouter,
createV6CompatibleWrapUseRoutes,
} from './reactrouterv6-compat-utils';
import type { CreateRouterFunction, Router, RouterState, UseRoutes } from './types';
@@ -40,6 +41,19 @@ export function wrapCreateBrowserRouterV7<
return createV6CompatibleWrapCreateBrowserRouter(createRouterFunction, '7');
}
+/**
+ * A wrapper function that adds Sentry routing instrumentation to a React Router v7 createMemoryRouter function.
+ * This is used to automatically capture route changes as transactions when using the createMemoryRouter API.
+ * The difference between createBrowserRouter and createMemoryRouter is that with createMemoryRouter,
+ * optional `initialEntries` are also taken into account.
+ */
+export function wrapCreateMemoryRouterV7<
+ TState extends RouterState = RouterState,
+ TRouter extends Router = Router,
+>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction {
+ return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '7');
+}
+
/**
* A wrapper function that adds Sentry routing instrumentation to a React Router v7 useRoutes hook.
* This is used to automatically capture route changes as transactions when using the useRoutes hook.
diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts
index 2f5467ee1640..2fb55afc0c4e 100644
--- a/packages/react/src/tanstackrouter.ts
+++ b/packages/react/src/tanstackrouter.ts
@@ -64,8 +64,9 @@ export function tanstackRouterBrowserTracingIntegration(
if (instrumentNavigation) {
// The onBeforeNavigate hook is called at the very beginning of a navigation and is only called once per navigation, even when the user is redirected
castRouterInstance.subscribe('onBeforeNavigate', onBeforeNavigateArgs => {
+ const fromLocationState = onBeforeNavigateArgs.fromLocation && onBeforeNavigateArgs.fromLocation.state;
// onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by comparing the states of the to and from arguments.
- if (onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation.state) {
+ if (onBeforeNavigateArgs.toLocation.state === fromLocationState) {
return;
}
diff --git a/packages/react/src/vendor/tanstackrouter-types.ts b/packages/react/src/vendor/tanstackrouter-types.ts
index e5eeba71aa87..417d2b1447b1 100644
--- a/packages/react/src/vendor/tanstackrouter-types.ts
+++ b/packages/react/src/vendor/tanstackrouter-types.ts
@@ -46,7 +46,7 @@ export interface VendoredTanstackRouter {
eventType: 'onResolved' | 'onBeforeNavigate',
callback: (stateUpdate: {
toLocation: VendoredTanstackRouterLocation;
- fromLocation: VendoredTanstackRouterLocation;
+ fromLocation?: VendoredTanstackRouterLocation;
}) => void,
): () => void;
}
diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx
index e0a7328995e0..81f276255a96 100644
--- a/packages/react/test/errorboundary.test.tsx
+++ b/packages/react/test/errorboundary.test.tsx
@@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
import { useState } from 'react';
-import type { ErrorBoundaryProps } from '../src/errorboundary';
+import type { ErrorBoundaryProps, FallbackRender } from '../src/errorboundary';
import { ErrorBoundary, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';
const mockCaptureException = jest.fn();
@@ -537,47 +537,47 @@ describe('ErrorBoundary', () => {
expect(mockOnReset).toHaveBeenCalledTimes(1);
expect(mockOnReset).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String));
});
+ it.each`
+ fallback | handled | expected
+ ${true} | ${undefined} | ${true}
+ ${false} | ${undefined} | ${false}
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${false}
+ `(
+ 'sets `handled: $expected` when `handled` is $handled and `fallback` is $fallback',
+ async ({
+ fallback,
+ handled,
+ expected,
+ }: {
+ fallback: boolean;
+ handled: boolean | undefined;
+ expected: boolean;
+ }) => {
+ const fallbackComponent: FallbackRender | undefined = fallback
+ ? ({ resetError }) =>
+ : undefined;
+ render(
+
+ children
+ ,
+ );
- it('sets `handled: true` when a fallback is provided', async () => {
- render(
- }>
- children
- ,
- );
-
- expect(mockCaptureException).toHaveBeenCalledTimes(0);
-
- const btn = screen.getByTestId('errorBtn');
- fireEvent.click(btn);
-
- expect(mockCaptureException).toHaveBeenCalledTimes(1);
- expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Object), {
- captureContext: {
- contexts: { react: { componentStack: expect.any(String) } },
- },
- mechanism: { handled: true },
- });
- });
-
- it('sets `handled: false` when no fallback is provided', async () => {
- render(
-
- children
- ,
- );
-
- expect(mockCaptureException).toHaveBeenCalledTimes(0);
-
- const btn = screen.getByTestId('errorBtn');
- fireEvent.click(btn);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
- expect(mockCaptureException).toHaveBeenCalledTimes(1);
- expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Object), {
- captureContext: {
- contexts: { react: { componentStack: expect.any(String) } },
- },
- mechanism: { handled: false },
- });
- });
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Object), {
+ captureContext: {
+ contexts: { react: { componentStack: expect.any(String) } },
+ },
+ mechanism: { handled: expected },
+ });
+ },
+ );
});
});
diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx
new file mode 100644
index 000000000000..dcc73a2275df
--- /dev/null
+++ b/packages/react/test/reactrouter-descendant-routes.test.tsx
@@ -0,0 +1,397 @@
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ createTransport,
+ getCurrentScope,
+ setCurrentClient,
+} from '@sentry/core';
+import { render } from '@testing-library/react';
+import * as React from 'react';
+import {
+ MemoryRouter,
+ Navigate,
+ Outlet,
+ Route,
+ Routes,
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+ useRoutes,
+} from 'react-router-6';
+
+import { BrowserClient } from '../src';
+import {
+ reactRouterV6BrowserTracingIntegration,
+ withSentryReactRouterV6Routing,
+ wrapUseRoutesV6,
+} from '../src/reactrouterv6';
+
+const mockStartBrowserTracingPageLoadSpan = jest.fn();
+const mockStartBrowserTracingNavigationSpan = jest.fn();
+
+const mockRootSpan = {
+ updateName: jest.fn(),
+ setAttribute: jest.fn(),
+ getSpanJSON() {
+ return { op: 'pageload' };
+ },
+};
+
+jest.mock('@sentry/browser', () => {
+ const actual = jest.requireActual('@sentry/browser');
+ return {
+ ...actual,
+ startBrowserTracingNavigationSpan: (...args: unknown[]) => {
+ mockStartBrowserTracingNavigationSpan(...args);
+ return actual.startBrowserTracingNavigationSpan(...args);
+ },
+ startBrowserTracingPageLoadSpan: (...args: unknown[]) => {
+ mockStartBrowserTracingPageLoadSpan(...args);
+ return actual.startBrowserTracingPageLoadSpan(...args);
+ },
+ };
+});
+
+jest.mock('@sentry/core', () => {
+ const actual = jest.requireActual('@sentry/core');
+ return {
+ ...actual,
+ getRootSpan: () => {
+ return mockRootSpan;
+ },
+ };
+});
+
+describe('React Router Descendant Routes', () => {
+ function createMockBrowserClient(): BrowserClient {
+ return new BrowserClient({
+ integrations: [],
+ tracesSampleRate: 1,
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
+ });
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getCurrentScope().setClient(undefined);
+ });
+
+ describe('withSentryReactRouterV6Routing', () => {
+ it('works with descendant wildcard routes - pageload', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ const DetailsRoutes = () => (
+
+ Details} />
+
+ );
+
+ const ViewsRoutes = () => (
+
+ Views} />
+ } />
+
+ );
+
+ const ProjectsRoutes = () => (
+
+ }>
+ No Match Page} />
+
+ );
+
+ const { container } = render(
+
+
+ }>
+
+ ,
+ );
+
+ expect(container.innerHTML).toContain('Details');
+
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId');
+ expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ });
+
+ it('works with descendant wildcard routes - navigation', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ const DetailsRoutes = () => (
+
+ Details} />
+
+ );
+
+ const ViewsRoutes = () => (
+
+ Views} />
+ } />
+
+ );
+
+ const ProjectsRoutes = () => (
+
+ }>
+ No Match Page} />
+
+ );
+
+ const { container } = render(
+
+
+ } />
+ }>
+
+ ,
+ );
+
+ expect(container.innerHTML).toContain('Details');
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/projects/:projectId/views/:viewId/:detailId',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+
+ it('works with descendant wildcard routes with outlets', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ const DetailsRoutes = () => (
+
+ Details} />
+
+ );
+
+ const ViewsRoutes = () => (
+
+ Views} />
+ } />
+
+ );
+
+ const ProjectsRoutes = () => (
+
+ }>
+ Project Page Root} />
+ }>
+ } />
+
+
+
+ );
+
+ const { container } = render(
+
+
+ } />
+ }>
+
+ ,
+ );
+
+ expect(container.innerHTML).toContain('Details');
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/projects/:projectId/views/:viewId/:detailId',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+ });
+
+ describe('wrapUseRoutesV6', () => {
+ it('works with descendant wildcard routes - pageload', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const wrappedUseRoutes = wrapUseRoutesV6(useRoutes);
+
+ const DetailsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ path: ':detailId',
+ element: Details
,
+ },
+ ]);
+
+ const ViewsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ index: true,
+ element: Views
,
+ },
+ {
+ path: 'views/:viewId/*',
+ element: ,
+ },
+ ]);
+
+ const ProjectsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ path: 'projects/:projectId/*',
+ element: ,
+ },
+ {
+ path: '*',
+ element: No Match Page
,
+ },
+ ]);
+
+ const Routes = () =>
+ wrappedUseRoutes([
+ {
+ path: '/*',
+ element: ,
+ },
+ ]);
+
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.innerHTML).toContain('Details');
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId');
+ expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ });
+
+ it('works with descendant wildcard routes - navigation', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const wrappedUseRoutes = wrapUseRoutesV6(useRoutes);
+
+ const DetailsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ path: ':detailId',
+ element: Details
,
+ },
+ ]);
+
+ const ViewsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ index: true,
+ element: Views
,
+ },
+ {
+ path: 'views/:viewId/*',
+ element: ,
+ },
+ ]);
+
+ const ProjectsRoutes = () =>
+ wrappedUseRoutes([
+ {
+ path: 'projects/:projectId/*',
+ element: ,
+ },
+ {
+ path: '*',
+ element: No Match Page
,
+ },
+ ]);
+
+ const Routes = () =>
+ wrappedUseRoutes([
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: '/*',
+ element: ,
+ },
+ ]);
+
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.innerHTML).toContain('Details');
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/projects/:projectId/views/:viewId/:detailId',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+ });
+});
diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx
index 815b562f08f7..a1207a818e70 100644
--- a/packages/react/test/reactrouterv6.test.tsx
+++ b/packages/react/test/reactrouterv6.test.tsx
@@ -13,7 +13,9 @@ import {
Navigate,
Outlet,
Route,
+ RouterProvider,
Routes,
+ createMemoryRouter,
createRoutesFromChildren,
matchRoutes,
useLocation,
@@ -21,10 +23,13 @@ import {
useRoutes,
} from 'react-router-6';
+import type { RouteObject } from 'react-router-6';
+
import { BrowserClient } from '../src';
import {
reactRouterV6BrowserTracingIntegration,
withSentryReactRouterV6Routing,
+ wrapCreateMemoryRouterV6,
wrapUseRoutesV6,
} from '../src/reactrouterv6';
@@ -79,6 +84,99 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
getCurrentScope().setClient(undefined);
});
+ it('wrapCreateMemoryRouterV6 starts and updates a pageload transaction - single initialEntry', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: Home
,
+ },
+ {
+ path: '/about',
+ element: About
,
+ },
+ ];
+
+ const wrappedCreateMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter);
+
+ const router = wrappedCreateMemoryRouter(routes, {
+ initialEntries: ['/about'],
+ });
+
+ render();
+
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
+ },
+ });
+
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about');
+ });
+
+ it('wrapCreateMemoryRouterV6 starts and updates a pageload transaction - multiple initialEntries', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: Home
,
+ },
+ {
+ path: '/about',
+ element: About
,
+ },
+ ];
+
+ const wrappedCreateMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter);
+
+ const router = wrappedCreateMemoryRouter(routes, {
+ initialEntries: ['/', '/about'],
+ initialIndex: 1,
+ });
+
+ render();
+
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
+ },
+ });
+
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about');
+ });
+
describe('withSentryReactRouterV6Routing', () => {
it('starts a pageload transaction', () => {
const client = createMockBrowserClient();
@@ -491,7 +589,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
});
});
- it('works with descendant wildcard routes - pageload', () => {
+ it('works under a slash route with a trailing slash', () => {
const client = createMockBrowserClient();
setCurrentClient(client);
@@ -506,40 +604,70 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
);
const SentryRoutes = withSentryReactRouterV6Routing(Routes);
- const DetailsRoutes = () => (
-
- Details} />
-
+ render(
+
+
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
+
+ ,
);
- const ViewsRoutes = () => (
-
- Views} />
- } />
-
- );
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/issues/:groupId/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
- const ProjectsRoutes = () => (
-
- }>
- No Match Page} />
-
+ it('works nested under a slash root without a trailing slash', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
);
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
render(
-
+
- }>
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
,
);
- expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
- expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId');
- expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/issues/:groupId/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
});
- it('works with descendant wildcard routes - navigation', () => {
+ it('works under a slash route with a trailing slash', () => {
const client = createMockBrowserClient();
setCurrentClient(client);
@@ -554,38 +682,61 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
);
const SentryRoutes = withSentryReactRouterV6Routing(Routes);
- const DetailsRoutes = () => (
-
- Details} />
-
+ render(
+
+
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
+
+ ,
);
- const ViewsRoutes = () => (
-
- Views} />
- } />
-
- );
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/issues/:groupId/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+
+ it('works nested under a slash root without a trailing slash', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
- const ProjectsRoutes = () => (
-
- }>
- No Match Page} />
-
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
);
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
render(
- } />
- }>
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
,
);
expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/projects/:projectId/views/:viewId/:detailId',
+ name: '/issues/:groupId/',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
@@ -1136,150 +1287,6 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
});
});
- it('works with descendant wildcard routes - pageload', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
-
- const wrappedUseRoutes = wrapUseRoutesV6(useRoutes);
-
- const DetailsRoutes = () =>
- wrappedUseRoutes([
- {
- path: ':detailId',
- element: Details
,
- },
- ]);
-
- const ViewsRoutes = () =>
- wrappedUseRoutes([
- {
- index: true,
- element: Views
,
- },
- {
- path: 'views/:viewId/*',
- element: ,
- },
- ]);
-
- const ProjectsRoutes = () =>
- wrappedUseRoutes([
- {
- path: 'projects/:projectId/*',
- element: ,
- },
- {
- path: '*',
- element: No Match Page
,
- },
- ]);
-
- const Routes = () =>
- wrappedUseRoutes([
- {
- path: '/*',
- element: ,
- },
- ]);
-
- render(
-
-
- ,
- );
-
- expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1);
- expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId');
- expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
- });
-
- it('works with descendant wildcard routes - navigation', () => {
- const client = createMockBrowserClient();
- setCurrentClient(client);
-
- client.addIntegration(
- reactRouterV6BrowserTracingIntegration({
- useEffect: React.useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- }),
- );
-
- const wrappedUseRoutes = wrapUseRoutesV6(useRoutes);
-
- const DetailsRoutes = () =>
- wrappedUseRoutes([
- {
- path: ':detailId',
- element: Details
,
- },
- ]);
-
- const ViewsRoutes = () =>
- wrappedUseRoutes([
- {
- index: true,
- element: Views
,
- },
- {
- path: 'views/:viewId/*',
- element: ,
- },
- ]);
-
- const ProjectsRoutes = () =>
- wrappedUseRoutes([
- {
- path: 'projects/:projectId/*',
- element: ,
- },
- {
- path: '*',
- element: No Match Page
,
- },
- ]);
-
- const Routes = () =>
- wrappedUseRoutes([
- {
- index: true,
- element: ,
- },
- {
- path: '/*',
- element: ,
- },
- ]);
-
- render(
-
-
- ,
- );
-
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
- expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
- name: '/projects/:projectId/views/:viewId/:detailId',
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- });
-
it('does not add double slashes to URLS', () => {
const client = createMockBrowserClient();
setCurrentClient(client);
@@ -1397,6 +1404,84 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
});
+ it('works under a slash route with a trailing slash', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ render(
+
+
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
+
+ ,
+ );
+
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/issues/:groupId/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+
+ it('works nested under a slash root without a trailing slash', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+ const SentryRoutes = withSentryReactRouterV6Routing(Routes);
+
+ render(
+
+
+ } />
+ root}>
+ issues group}>
+ index} />
+
+
+
+ ,
+ );
+
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1);
+ expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), {
+ name: '/issues/:groupId/',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
+ },
+ });
+ });
+
it("updates the scope's `transactionName` on a navigation", () => {
const client = createMockBrowserClient();
setCurrentClient(client);
diff --git a/packages/remix/package.json b/packages/remix/package.json
index 9967c77676bc..806217abdfcc 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/remix",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Remix",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix",
@@ -55,10 +55,10 @@
"@opentelemetry/api": "^1.9.0",
"@remix-run/router": "1.x",
"@sentry/cli": "^2.39.1",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
- "@sentry/react": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
+ "@sentry/react": "8.54.0",
"glob": "^10.3.4",
"opentelemetry-instrumentation-remix": "0.8.0",
"yargs": "^17.6.0"
@@ -67,8 +67,7 @@
"@remix-run/node": "^1.4.3",
"@remix-run/react": "^1.4.3",
"@types/express": "^4.17.14",
- "vite": "^5.4.10",
- "vitest": "^1.6.0"
+ "vite": "^5.4.11"
},
"peerDependencies": {
"@remix-run/node": "1.x || 2.x",
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index f6a5f5060dd9..4bb6539dbd33 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -134,6 +134,7 @@ export {
startSpanManual,
tediousIntegration,
trpcMiddleware,
+ updateSpanName,
withActiveSpan,
withIsolationScope,
withMonitor,
diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts
index cfba5f67e781..e306bbb47056 100644
--- a/packages/remix/src/index.types.ts
+++ b/packages/remix/src/index.types.ts
@@ -32,8 +32,6 @@ declare const runtime: 'client' | 'server';
// eslint-disable-next-line deprecation/deprecation
export declare const getCurrentHub: typeof clientSdk.getCurrentHub;
-export declare const getClient: typeof clientSdk.getClient;
-export declare const continueTrace: typeof clientSdk.continueTrace;
export const close = runtime === 'client' ? clientSdk.close : serverSdk.close;
export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush;
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts
index 797c295b0abf..8a51582eda21 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/utils/instrumentServer.ts
@@ -4,6 +4,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ continueTrace,
fill,
getActiveSpan,
getClient,
@@ -19,7 +20,6 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
-import { continueTrace } from '@sentry/opentelemetry';
import { DEBUG_BUILD } from './debug-build';
import { captureRemixServerException, errorHandleDataFunction, errorHandleDocumentRequestFunction } from './errors';
import { getFutureFlagsServer, getRemixVersionFromBuild } from './futureFlags';
@@ -448,7 +448,7 @@ const makeWrappedCreateRequestHandler = (options: RemixOptions) =>
export function instrumentServer(options: RemixOptions): void {
const pkg = loadModule<{
createRequestHandler: CreateRequestHandlerFunction;
- }>('@remix-run/server-runtime');
+ }>('@remix-run/server-runtime', module);
if (!pkg) {
DEBUG_BUILD && logger.warn('Remix SDK was unable to require `@remix-run/server-runtime` package.');
diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json
index 4faeb7db1ef7..8ff0219d4c1f 100644
--- a/packages/replay-canvas/package.json
+++ b/packages/replay-canvas/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay-canvas",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Replay canvas integration",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -68,8 +68,8 @@
"@sentry-internal/rrweb": "2.31.0"
},
"dependencies": {
- "@sentry-internal/replay": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry-internal/replay": "8.54.0",
+ "@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json
index 4267723b8b67..f7bd8c287375 100644
--- a/packages/replay-internal/package.json
+++ b/packages/replay-internal/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "User replays for Sentry",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -68,7 +68,7 @@
"homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
"devDependencies": {
"@babel/core": "^7.17.5",
- "@sentry-internal/replay-worker": "8.45.0",
+ "@sentry-internal/replay-worker": "8.54.0",
"@sentry-internal/rrweb": "2.31.0",
"@sentry-internal/rrweb-snapshot": "2.31.0",
"fflate": "^0.8.1",
@@ -76,8 +76,8 @@
"jsdom-worker": "^0.2.1"
},
"dependencies": {
- "@sentry-internal/browser-utils": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry-internal/browser-utils": "8.54.0",
+ "@sentry/core": "8.54.0"
},
"engines": {
"node": ">=14.18"
diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts
index f3169106d458..d8aaa247d917 100644
--- a/packages/replay-internal/src/replay.ts
+++ b/packages/replay-internal/src/replay.ts
@@ -47,6 +47,7 @@ import { createBreadcrumb } from './util/createBreadcrumb';
import { createPerformanceEntries } from './util/createPerformanceEntries';
import { createPerformanceSpans } from './util/createPerformanceSpans';
import { debounce } from './util/debounce';
+import { getRecordingSamplingOptions } from './util/getRecordingSamplingOptions';
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
import { isExpired } from './util/isExpired';
import { isSessionExpired } from './util/isSessionExpired';
@@ -394,6 +395,7 @@ export class ReplayContainer implements ReplayContainerInterface {
checkoutEveryNms: Math.max(360_000, this._options._experiments.continuousCheckout),
}),
emit: getHandleRecordingEmit(this),
+ ...getRecordingSamplingOptions(),
onMutation: this._onMutationHandler,
...(canvasOptions
? {
diff --git a/packages/replay-internal/src/util/getPrivacyOptions.ts b/packages/replay-internal/src/util/getPrivacyOptions.ts
index ba35ec21476d..a5aa3d392632 100644
--- a/packages/replay-internal/src/util/getPrivacyOptions.ts
+++ b/packages/replay-internal/src/util/getPrivacyOptions.ts
@@ -25,7 +25,7 @@ function getOption(selectors: string[], defaultSelectors: string[]): string {
* Returns privacy related configuration for use in rrweb
*/
export function getPrivacyOptions({ mask, unmask, block, unblock, ignore }: GetPrivacyOptions): GetPrivacyReturn {
- const defaultBlockedElements = ['base[href="/"]'];
+ const defaultBlockedElements = ['base', 'iframe[srcdoc]:not([src])'];
const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]']);
const unmaskSelector = getOption(unmask, []);
diff --git a/packages/replay-internal/src/util/getRecordingSamplingOptions.ts b/packages/replay-internal/src/util/getRecordingSamplingOptions.ts
new file mode 100644
index 000000000000..4c7a78ed8ca3
--- /dev/null
+++ b/packages/replay-internal/src/util/getRecordingSamplingOptions.ts
@@ -0,0 +1,25 @@
+import { GLOBAL_OBJ } from '@sentry/core';
+
+const NAVIGATOR = GLOBAL_OBJ.navigator;
+
+/**
+ * Disable sampling mousemove events on iOS browsers as this can cause blocking the main thread
+ * https://github.com/getsentry/sentry-javascript/issues/14534
+ */
+export function getRecordingSamplingOptions(): Partial<{ sampling: { mousemove: boolean } }> {
+ if (
+ /iPhone|iPad|iPod/i.test((NAVIGATOR && NAVIGATOR.userAgent) || '') ||
+ (/Macintosh/i.test((NAVIGATOR && NAVIGATOR.userAgent) || '') &&
+ NAVIGATOR &&
+ NAVIGATOR.maxTouchPoints &&
+ NAVIGATOR.maxTouchPoints > 1)
+ ) {
+ return {
+ sampling: {
+ mousemove: false,
+ },
+ };
+ }
+
+ return {};
+}
diff --git a/packages/replay-internal/test/integration/integrationSettings.test.ts b/packages/replay-internal/test/integration/integrationSettings.test.ts
index 62dc2a4a6588..8f7f39fdcf1a 100644
--- a/packages/replay-internal/test/integration/integrationSettings.test.ts
+++ b/packages/replay-internal/test/integration/integrationSettings.test.ts
@@ -17,7 +17,9 @@ describe('Integration | integrationSettings', () => {
it('sets the correct configuration when `blockAllMedia` is disabled', async () => {
const { replay } = await mockSdk({ replayOptions: { blockAllMedia: false } });
- expect(replay['_recordingOptions'].blockSelector).toBe('.sentry-block,[data-sentry-block],base[href="/"]');
+ expect(replay['_recordingOptions'].blockSelector).toBe(
+ '.sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src])',
+ );
});
});
diff --git a/packages/replay-internal/test/integration/rrweb.test.ts b/packages/replay-internal/test/integration/rrweb.test.ts
index 4327ddb21de1..7f156c542f08 100644
--- a/packages/replay-internal/test/integration/rrweb.test.ts
+++ b/packages/replay-internal/test/integration/rrweb.test.ts
@@ -23,7 +23,7 @@ describe('Integration | rrweb', () => {
});
expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(`
{
- "blockSelector": ".sentry-block,[data-sentry-block],base[href="/"],img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
+ "blockSelector": ".sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src]),img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
"collectFonts": true,
"emit": [Function],
"errorHandler": [Function],
@@ -62,7 +62,7 @@ describe('Integration | rrweb', () => {
expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(`
{
- "blockSelector": ".sentry-block,[data-sentry-block],base[href="/"],img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
+ "blockSelector": ".sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src]),img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
"checkoutEveryNms": 360000,
"collectFonts": true,
"emit": [Function],
@@ -86,4 +86,58 @@ describe('Integration | rrweb', () => {
}
`);
});
+
+ it('calls rrweb.record with updated sampling options on iOS', async () => {
+ // Mock iOS user agent
+ const originalNavigator = global.navigator;
+ Object.defineProperty(global, 'navigator', {
+ value: {
+ userAgent:
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
+ },
+ configurable: true,
+ });
+
+ const { mockRecord } = await resetSdkMock({
+ replayOptions: {},
+ sentryOptions: {
+ replaysOnErrorSampleRate: 1.0,
+ replaysSessionSampleRate: 1.0,
+ },
+ });
+
+ // Restore original navigator
+ Object.defineProperty(global, 'navigator', {
+ value: originalNavigator,
+ configurable: true,
+ });
+
+ expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(`
+ {
+ "blockSelector": ".sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src]),img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]",
+ "collectFonts": true,
+ "emit": [Function],
+ "errorHandler": [Function],
+ "ignoreSelector": ".sentry-ignore,[data-sentry-ignore],input[type="file"]",
+ "inlineImages": false,
+ "inlineStylesheet": true,
+ "maskAllInputs": true,
+ "maskAllText": true,
+ "maskAttributeFn": [Function],
+ "maskInputFn": undefined,
+ "maskInputOptions": {
+ "password": true,
+ },
+ "maskTextFn": undefined,
+ "maskTextSelector": ".sentry-mask,[data-sentry-mask]",
+ "onMutation": [Function],
+ "sampling": {
+ "mousemove": false,
+ },
+ "slimDOMOptions": "all",
+ "unblockSelector": "",
+ "unmaskTextSelector": "",
+ }
+ `);
+ });
});
diff --git a/packages/replay-internal/test/unit/util/getPrivacyOptions.test.ts b/packages/replay-internal/test/unit/util/getPrivacyOptions.test.ts
index 8595ca6aa1c4..3123e3efaa7c 100644
--- a/packages/replay-internal/test/unit/util/getPrivacyOptions.test.ts
+++ b/packages/replay-internal/test/unit/util/getPrivacyOptions.test.ts
@@ -21,7 +21,7 @@ describe('Unit | util | getPrivacyOptions', () => {
}),
).toMatchInlineSnapshot(`
{
- "blockSelector": ".custom-block,.sentry-block,[data-sentry-block],base[href="/"]",
+ "blockSelector": ".custom-block,.sentry-block,[data-sentry-block],base,iframe[srcdoc]:not([src])",
"ignoreSelector": ".custom-ignore,.sentry-ignore,[data-sentry-ignore],input[type="file"]",
"maskTextSelector": ".custom-mask,.sentry-mask,[data-sentry-mask]",
"unblockSelector": ".custom-unblock",
diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json
index 7a1596319e4f..d389b7d51dcd 100644
--- a/packages/replay-worker/package.json
+++ b/packages/replay-worker/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay-worker",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Worker for @sentry-internal/replay",
"main": "build/esm/index.js",
"module": "build/esm/index.js",
diff --git a/packages/solid/package.json b/packages/solid/package.json
index f718a1374a11..053841eb350b 100644
--- a/packages/solid/package.json
+++ b/packages/solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solid",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Solid",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid",
@@ -44,8 +44,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0"
},
"peerDependencies": {
"@solidjs/router": "^0.13.4",
diff --git a/packages/solidstart/.eslintrc.js b/packages/solidstart/.eslintrc.js
index d567b12530d0..0fe78630b548 100644
--- a/packages/solidstart/.eslintrc.js
+++ b/packages/solidstart/.eslintrc.js
@@ -11,7 +11,7 @@ module.exports = {
},
},
{
- files: ['src/vite/**', 'src/server/**'],
+ files: ['src/vite/**', 'src/server/**', 'src/config/**'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json
index dba27d321153..30ecfff56ab9 100644
--- a/packages/solidstart/package.json
+++ b/packages/solidstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solidstart",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Solid Start",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart",
@@ -66,10 +66,10 @@
}
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
- "@sentry/solid": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
+ "@sentry/solid": "8.54.0",
"@sentry/vite-plugin": "2.22.6"
},
"devDependencies": {
diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts
new file mode 100644
index 000000000000..74b72a12b4de
--- /dev/null
+++ b/packages/solidstart/src/config/addInstrumentation.ts
@@ -0,0 +1,182 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { consoleSandbox } from '@sentry/core';
+import type { Nitro } from 'nitropack';
+import type { SentrySolidStartPluginOptions } from '../vite/types';
+import type { RollupConfig } from './types';
+import { wrapServerEntryWithDynamicImport } from './wrapServerEntryWithDynamicImport';
+
+// Nitro presets for hosts that only host static files
+export const staticHostPresets = ['github_pages'];
+// Nitro presets for hosts that use `server.mjs` as opposed to `index.mjs`
+export const serverFilePresets = ['netlify'];
+
+/**
+ * Adds the built `instrument.server.js` file to the output directory.
+ *
+ * As Sentry also imports the release injection file, this needs to be copied over manually as well.
+ * TODO: The mechanism of manually copying those files could maybe be improved
+ *
+ * This will no-op if no `instrument.server.js` file was found in the
+ * build directory.
+ */
+export async function addInstrumentationFileToBuild(nitro: Nitro): Promise {
+ nitro.hooks.hook('close', async () => {
+ // Static file hosts have no server component so there's nothing to do
+ if (staticHostPresets.includes(nitro.options.preset)) {
+ return;
+ }
+
+ const buildDir = nitro.options.buildDir;
+ const serverDir = nitro.options.output.serverDir;
+
+ try {
+ // 1. Create assets directory first (for release-injection-file)
+ const assetsServerDir = path.join(serverDir, 'assets');
+ if (!fs.existsSync(assetsServerDir)) {
+ await fs.promises.mkdir(assetsServerDir, { recursive: true });
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[Sentry SolidStart withSentry] Successfully created directory ${assetsServerDir}.`);
+ });
+ }
+
+ // 2. Copy release injection file if available
+ try {
+ const ssrAssetsPath = path.resolve(buildDir, 'build', 'ssr', 'assets');
+ const assetsBuildDir = await fs.promises.readdir(ssrAssetsPath);
+ const releaseInjectionFile = assetsBuildDir.find(file => file.startsWith('_sentry-release-injection-file-'));
+
+ if (releaseInjectionFile) {
+ const releaseSource = path.resolve(ssrAssetsPath, releaseInjectionFile);
+ const releaseDestination = path.resolve(assetsServerDir, releaseInjectionFile);
+
+ await fs.promises.copyFile(releaseSource, releaseDestination);
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[Sentry SolidStart withSentry] Successfully created ${releaseDestination}.`);
+ });
+ }
+ } catch (err) {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('[Sentry SolidStart withSentry] Failed to copy release injection file.', err);
+ });
+ }
+
+ // 3. Copy Sentry server instrumentation file
+ const instrumentSource = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js');
+ const instrumentDestination = path.resolve(serverDir, 'instrument.server.mjs');
+
+ await fs.promises.copyFile(instrumentSource, instrumentDestination);
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[Sentry SolidStart withSentry] Successfully created ${instrumentDestination}.`);
+ });
+ } catch (error) {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('[Sentry SolidStart withSentry] Failed to add instrumentation file to build.', error);
+ });
+ }
+ });
+}
+
+/**
+ * Adds an `instrument.server.mjs` import to the top of the server entry file.
+ *
+ * This is meant as an escape hatch and should only be used in environments where
+ * it's not possible to `--import` the file instead as it comes with a limited
+ * tracing experience, only collecting http traces.
+ */
+export async function addSentryTopImport(nitro: Nitro): Promise {
+ nitro.hooks.hook('close', async () => {
+ const buildPreset = nitro.options.preset;
+ const serverDir = nitro.options.output.serverDir;
+
+ // Static file hosts have no server component so there's nothing to do
+ if (staticHostPresets.includes(buildPreset)) {
+ return;
+ }
+
+ const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs');
+ const serverEntryFileName = serverFilePresets.includes(buildPreset) ? 'server.mjs' : 'index.mjs';
+ const serverEntryFile = path.resolve(serverDir, serverEntryFileName);
+
+ try {
+ await fs.promises.access(instrumentationFile, fs.constants.F_OK);
+ } catch (error) {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[Sentry SolidStart withSentry] Failed to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
+ error,
+ );
+ });
+ return;
+ }
+
+ try {
+ const content = await fs.promises.readFile(serverEntryFile, 'utf-8');
+ const updatedContent = `import './instrument.server.mjs';\n${content}`;
+ await fs.promises.writeFile(serverEntryFile, updatedContent);
+
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.log(
+ `[Sentry SolidStart withSentry] Added \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
+ );
+ });
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[Sentry SolidStart withSentry] An error occurred when trying to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`,
+ error,
+ );
+ }
+ });
+}
+
+/**
+ * This function modifies the Rollup configuration to include a plugin that wraps the entry file with a dynamic import (`import()`)
+ * and adds the Sentry server config with the static `import` declaration.
+ *
+ * With this, the Sentry server config can be loaded before all other modules of the application (which is needed for import-in-the-middle).
+ * See: https://nodejs.org/api/module.html#enabling
+ */
+export async function addDynamicImportEntryFileWrapper({
+ nitro,
+ rollupConfig,
+ sentryPluginOptions,
+}: {
+ nitro: Nitro;
+ rollupConfig: RollupConfig;
+ sentryPluginOptions: Omit &
+ Required>;
+}): Promise {
+ // Static file hosts have no server component so there's nothing to do
+ if (staticHostPresets.includes(nitro.options.preset)) {
+ return;
+ }
+
+ const srcDir = nitro.options.srcDir;
+ // todo allow other instrumentation paths
+ const serverInstrumentationPath = path.resolve(srcDir, 'src', 'instrument.server.ts');
+
+ const instrumentationFileName = sentryPluginOptions.instrumentation
+ ? path.basename(sentryPluginOptions.instrumentation)
+ : '';
+
+ rollupConfig.plugins.push(
+ wrapServerEntryWithDynamicImport({
+ serverConfigFileName: sentryPluginOptions.instrumentation
+ ? path.join(path.dirname(instrumentationFileName), path.parse(instrumentationFileName).name)
+ : 'instrument.server',
+ serverEntrypointFileName: sentryPluginOptions.serverEntrypointFileName || nitro.options.preset,
+ resolvedServerConfigPath: serverInstrumentationPath,
+ entrypointWrappedFunctions: sentryPluginOptions.experimental_entrypointWrappedFunctions,
+ additionalImports: ['import-in-the-middle/hook.mjs'],
+ debug: sentryPluginOptions.debug,
+ }),
+ );
+}
diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts
new file mode 100644
index 000000000000..4949f4bdf523
--- /dev/null
+++ b/packages/solidstart/src/config/index.ts
@@ -0,0 +1 @@
+export * from './withSentry';
diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts
new file mode 100644
index 000000000000..0d6ea9bdf4f4
--- /dev/null
+++ b/packages/solidstart/src/config/types.ts
@@ -0,0 +1,16 @@
+import type { defineConfig } from '@solidjs/start/config';
+import type { Nitro } from 'nitropack';
+
+// Nitro does not export this type
+export type RollupConfig = {
+ plugins: unknown[];
+};
+
+export type SolidStartInlineConfig = Parameters[0];
+
+export type SolidStartInlineServerConfig = {
+ hooks?: {
+ close?: () => unknown;
+ 'rollup:before'?: (nitro: Nitro) => unknown;
+ };
+};
diff --git a/packages/solidstart/src/config/utils.ts b/packages/solidstart/src/config/utils.ts
new file mode 100644
index 000000000000..fd4b70d508d0
--- /dev/null
+++ b/packages/solidstart/src/config/utils.ts
@@ -0,0 +1,82 @@
+export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
+export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions=';
+export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions=';
+export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END';
+
+/**
+ * Strips the Sentry query part from a path.
+ * Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path
+ *
+ * Only exported for testing.
+ */
+export function removeSentryQueryFromPath(url: string): string {
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`);
+ return url.replace(regex, '');
+}
+
+/**
+ * Extracts and sanitizes function re-export and function wrap query parameters from a query string.
+ * If it is a default export, it is not considered for re-exporting.
+ *
+ * Only exported for testing.
+ */
+export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } {
+ // Regex matches the comma-separated params between the functions query
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const wrapRegex = new RegExp(
+ `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`,
+ );
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`);
+
+ const wrapMatch = query.match(wrapRegex);
+ const reexportMatch = query.match(reexportRegex);
+
+ const wrap =
+ wrapMatch && wrapMatch[1]
+ ? wrapMatch[1]
+ .split(',')
+ .filter(param => param !== '')
+ // Sanitize, as code could be injected with another rollup plugin
+ .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ : [];
+
+ const reexport =
+ reexportMatch && reexportMatch[1]
+ ? reexportMatch[1]
+ .split(',')
+ .filter(param => param !== '' && param !== 'default')
+ // Sanitize, as code could be injected with another rollup plugin
+ .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ : [];
+
+ return { wrap, reexport };
+}
+
+/**
+ * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`)
+ */
+export function constructFunctionReExport(pathWithQuery: string, entryId: string): string {
+ const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery);
+
+ return wrapFunctions
+ .reduce(
+ (functionsCode, currFunctionName) =>
+ functionsCode.concat(
+ `async function ${currFunctionName}_sentryWrapped(...args) {\n` +
+ ` const res = await import(${JSON.stringify(entryId)});\n` +
+ ` return res.${currFunctionName}.call(this, ...args);\n` +
+ '}\n' +
+ `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`,
+ ),
+ '',
+ )
+ .concat(
+ reexportFunctions.reduce(
+ (functionsCode, currFunctionName) =>
+ functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`),
+ '',
+ ),
+ );
+}
diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts
new file mode 100644
index 000000000000..c1050f0da1cc
--- /dev/null
+++ b/packages/solidstart/src/config/withSentry.ts
@@ -0,0 +1,76 @@
+import { logger } from '@sentry/core';
+import type { Nitro } from 'nitropack';
+import { addSentryPluginToVite } from '../vite';
+import type { SentrySolidStartPluginOptions } from '../vite/types';
+import {
+ addDynamicImportEntryFileWrapper,
+ addInstrumentationFileToBuild,
+ addSentryTopImport,
+} from './addInstrumentation';
+import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types';
+
+const defaultSentrySolidStartPluginOptions: Omit<
+ SentrySolidStartPluginOptions,
+ 'experimental_entrypointWrappedFunctions'
+> &
+ Required> = {
+ experimental_entrypointWrappedFunctions: ['default', 'handler', 'server'],
+};
+
+/**
+ * Modifies the passed in Solid Start configuration with build-time enhancements such as
+ * building the `instrument.server.ts` file into the appropriate build folder based on
+ * build preset.
+ *
+ * @param solidStartConfig A Solid Start configuration object, as usually passed to `defineConfig` in `app.config.ts|js`
+ * @param sentrySolidStartPluginOptions Options to configure the plugin
+ * @returns The modified config to be exported and passed back into `defineConfig`
+ */
+export function withSentry(
+ solidStartConfig: SolidStartInlineConfig = {},
+ sentrySolidStartPluginOptions: SentrySolidStartPluginOptions,
+): SolidStartInlineConfig {
+ const sentryPluginOptions = {
+ ...sentrySolidStartPluginOptions,
+ ...defaultSentrySolidStartPluginOptions,
+ };
+
+ const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig;
+ const hooks = server.hooks || {};
+ const vite =
+ typeof solidStartConfig.vite === 'function'
+ ? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentryPluginOptions)
+ : addSentryPluginToVite(solidStartConfig.vite, sentryPluginOptions);
+
+ return {
+ ...solidStartConfig,
+ vite,
+ server: {
+ ...server,
+ hooks: {
+ ...hooks,
+ async 'rollup:before'(nitro: Nitro, config: RollupConfig) {
+ if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'experimental_dynamic-import') {
+ await addDynamicImportEntryFileWrapper({ nitro, rollupConfig: config, sentryPluginOptions });
+
+ sentrySolidStartPluginOptions.debug &&
+ logger.log(
+ 'Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.',
+ );
+ } else {
+ await addInstrumentationFileToBuild(nitro);
+
+ if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'top-level-import') {
+ await addSentryTopImport(nitro);
+ }
+ }
+
+ // Run user provided hook
+ if (hooks['rollup:before']) {
+ hooks['rollup:before'](nitro);
+ }
+ },
+ },
+ },
+ };
+}
diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts
new file mode 100644
index 000000000000..6d069220e1ae
--- /dev/null
+++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts
@@ -0,0 +1,245 @@
+import { consoleSandbox } from '@sentry/core';
+import type { InputPluginOption } from 'rollup';
+
+/** THIS FILE IS AN UTILITY FOR NITRO-BASED PACKAGES AND SHOULD BE KEPT IN SYNC IN NUXT, SOLIDSTART, ETC. */
+
+export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
+export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions=';
+export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions=';
+export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END';
+
+export type WrapServerEntryPluginOptions = {
+ serverEntrypointFileName: string;
+ serverConfigFileName: string;
+ resolvedServerConfigPath: string;
+ entrypointWrappedFunctions: string[];
+ additionalImports?: string[];
+ debug?: boolean;
+};
+
+/**
+ * A Rollup plugin which wraps the server entry with a dynamic `import()`. This makes it possible to initialize Sentry first
+ * by using a regular `import` and load the server after that.
+ * This also works with serverless `handler` functions, as it re-exports the `handler`.
+ *
+ * @param config Configuration options for the Rollup Plugin
+ * @param config.serverConfigFileName Name of the Sentry server config (without file extension). E.g. 'sentry.server.config'
+ * @param config.serverEntrypointFileName The server entrypoint (with file extension). Usually, this is defined by the Nitro preset and is something like 'node-server.mjs'
+ * @param config.resolvedServerConfigPath Resolved path of the Sentry server config (based on `src` directory)
+ * @param config.entryPointWrappedFunctions Exported bindings of the server entry file, which are wrapped as async function. E.g. ['default', 'handler', 'server']
+ * @param config.additionalImports Adds additional imports to the entry file. Can be e.g. 'import-in-the-middle/hook.mjs'
+ * @param config.debug Whether debug logs are enabled in the build time environment
+ */
+export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOptions): InputPluginOption {
+ const {
+ serverConfigFileName,
+ serverEntrypointFileName,
+ resolvedServerConfigPath,
+ entrypointWrappedFunctions,
+ additionalImports,
+ debug,
+ } = config;
+
+ // In order to correctly import the server config file
+ // and dynamically import the nitro runtime, we need to
+ // mark the resolutionId with '\0raw' to fall into the
+ // raw chunk group, c.f. https://github.com/nitrojs/nitro/commit/8b4a408231bdc222569a32ce109796a41eac4aa6#diff-e58102d2230f95ddeef2662957b48d847a6e891e354cfd0ae6e2e03ce848d1a2R142
+ const resolutionIdPrefix = '\0raw';
+
+ return {
+ name: 'sentry-wrap-server-entry-with-dynamic-import',
+ async resolveId(source, importer, options) {
+ if (source.includes(`/${serverConfigFileName}`)) {
+ return { id: source, moduleSideEffects: true };
+ }
+
+ if (additionalImports && additionalImports.includes(source)) {
+ // When importing additional imports like "import-in-the-middle/hook.mjs" in the returned code of the `load()` function below:
+ // By setting `moduleSideEffects` to `true`, the import is added to the bundle, although nothing is imported from it
+ // By importing "import-in-the-middle/hook.mjs", we can make sure this file is included, as not all node builders are including files imported with `module.register()`.
+ // Prevents the error "Failed to register ESM hook Error: Cannot find module 'import-in-the-middle/hook.mjs'"
+ return { id: source, moduleSideEffects: true, external: true };
+ }
+
+ if (
+ options.isEntry &&
+ source.includes(serverEntrypointFileName) &&
+ source.includes('.mjs') &&
+ !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
+ ) {
+ const resolution = await this.resolve(source, importer, options);
+
+ // If it cannot be resolved or is external, just return it so that Rollup can display an error
+ if (!resolution || (resolution && resolution.external)) return resolution;
+
+ const moduleInfo = await this.load(resolution);
+
+ moduleInfo.moduleSideEffects = true;
+
+ // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix
+ return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
+ ? resolution.id
+ : `${resolutionIdPrefix}${resolution.id
+ // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler)
+ .concat(SENTRY_WRAPPED_ENTRY)
+ .concat(
+ constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
+ )
+ .concat(QUERY_END_INDICATOR)}`;
+ }
+ return null;
+ },
+ load(id: string) {
+ if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) {
+ const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length);
+
+ // Mostly useful for serverless `handler` functions
+ const reExportedFunctions =
+ id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS)
+ ? constructFunctionReExport(id, entryId)
+ : '';
+
+ return (
+ // Regular `import` of the Sentry config
+ `import ${JSON.stringify(resolvedServerConfigPath)};\n` +
+ // Dynamic `import()` for the previous, actual entry point.
+ // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling)
+ `import(${JSON.stringify(entryId)});\n` +
+ // By importing additional imports like "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`.
+ `${additionalImports ? additionalImports.map(importPath => `import "${importPath}";\n`) : ''}` +
+ `${reExportedFunctions}\n`
+ );
+ }
+
+ return null;
+ },
+ };
+}
+
+/**
+ * Strips the Sentry query part from a path.
+ * Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path
+ *
+ * **Only exported for testing**
+ */
+export function removeSentryQueryFromPath(url: string): string {
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`);
+ return url.replace(regex, '');
+}
+
+/**
+ * Extracts and sanitizes function re-export and function wrap query parameters from a query string.
+ * If it is a default export, it is not considered for re-exporting.
+ *
+ * **Only exported for testing**
+ */
+export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } {
+ // Regex matches the comma-separated params between the functions query
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const wrapRegex = new RegExp(
+ `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`,
+ );
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
+ const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`);
+
+ const wrapMatch = query.match(wrapRegex);
+ const reexportMatch = query.match(reexportRegex);
+
+ const wrap =
+ wrapMatch && wrapMatch[1]
+ ? wrapMatch[1]
+ .split(',')
+ .filter(param => param !== '')
+ // Sanitize, as code could be injected with another rollup plugin
+ .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ : [];
+
+ const reexport =
+ reexportMatch && reexportMatch[1]
+ ? reexportMatch[1]
+ .split(',')
+ .filter(param => param !== '' && param !== 'default')
+ // Sanitize, as code could be injected with another rollup plugin
+ .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ : [];
+
+ return { wrap, reexport };
+}
+
+/**
+ * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry.
+ * It uses Rollup's `exportedBindings` to determine the functions to re-export. Functions which should be wrapped
+ * (e.g. serverless handlers) are wrapped by Sentry.
+ *
+ * **Only exported for testing**
+ */
+export function constructWrappedFunctionExportQuery(
+ exportedBindings: Record | null,
+ entrypointWrappedFunctions: string[],
+ debug?: boolean,
+): string {
+ const functionsToExport: { wrap: string[]; reexport: string[] } = {
+ wrap: [],
+ reexport: [],
+ };
+
+ // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }`
+ // The key `.` refers to exports within the current file, while other keys show from where exports were imported first.
+ Object.values(exportedBindings || {}).forEach(functions =>
+ functions.forEach(fn => {
+ if (entrypointWrappedFunctions.includes(fn)) {
+ functionsToExport.wrap.push(fn);
+ } else {
+ functionsToExport.reexport.push(fn);
+ }
+ }),
+ );
+
+ if (debug && functionsToExport.wrap.length === 0) {
+ consoleSandbox(() =>
+ // eslint-disable-next-line no-console
+ console.warn(
+ '[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to `entrypointWrappedFunctions`.',
+ ),
+ );
+ }
+
+ const wrapQuery = functionsToExport.wrap.length
+ ? `${SENTRY_WRAPPED_FUNCTIONS}${functionsToExport.wrap.join(',')}`
+ : '';
+ const reexportQuery = functionsToExport.reexport.length
+ ? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}`
+ : '';
+
+ return [wrapQuery, reexportQuery].join('');
+}
+
+/**
+ * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`)
+ *
+ * **Only exported for testing**
+ */
+export function constructFunctionReExport(pathWithQuery: string, entryId: string): string {
+ const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery);
+
+ return wrapFunctions
+ .reduce(
+ (functionsCode, currFunctionName) =>
+ functionsCode.concat(
+ `async function ${currFunctionName}_sentryWrapped(...args) {\n` +
+ ` const res = await import(${JSON.stringify(entryId)});\n` +
+ ` return res.${currFunctionName}.call(this, ...args);\n` +
+ '}\n' +
+ `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`,
+ ),
+ '',
+ )
+ .concat(
+ reexportFunctions.reduce(
+ (functionsCode, currFunctionName) =>
+ functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`),
+ '',
+ ),
+ );
+}
diff --git a/packages/solidstart/src/index.server.ts b/packages/solidstart/src/index.server.ts
index d675a1c72820..a20a0367f557 100644
--- a/packages/solidstart/src/index.server.ts
+++ b/packages/solidstart/src/index.server.ts
@@ -1,2 +1,3 @@
export * from './server';
export * from './vite';
+export * from './config';
diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts
index 85b712281f38..5b43674d959a 100644
--- a/packages/solidstart/src/index.types.ts
+++ b/packages/solidstart/src/index.types.ts
@@ -4,6 +4,7 @@
export * from './client';
export * from './server';
export * from './vite';
+export * from './config';
import type { Client, Integration, Options, StackParser } from '@sentry/core';
@@ -19,13 +20,9 @@ export declare const contextLinesIntegration: typeof clientSdk.contextLinesInteg
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
-export declare const getClient: typeof clientSdk.getClient;
-
export declare function close(timeout?: number | undefined): PromiseLike;
export declare function flush(timeout?: number | undefined): PromiseLike;
export declare function lastEventId(): string | undefined;
-export declare const continueTrace: typeof clientSdk.continueTrace;
-
// eslint-disable-next-line deprecation/deprecation
export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics;
diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts
index 450420a2b586..4c1f192b0c36 100644
--- a/packages/solidstart/src/server/index.ts
+++ b/packages/solidstart/src/server/index.ts
@@ -126,6 +126,7 @@ export {
startSpanManual,
tediousIntegration,
trpcMiddleware,
+ updateSpanName,
withActiveSpan,
withIsolationScope,
withMonitor,
diff --git a/packages/solidstart/src/vite/buildInstrumentationFile.ts b/packages/solidstart/src/vite/buildInstrumentationFile.ts
new file mode 100644
index 000000000000..81bcef7a5bf7
--- /dev/null
+++ b/packages/solidstart/src/vite/buildInstrumentationFile.ts
@@ -0,0 +1,55 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { consoleSandbox } from '@sentry/core';
+import type { Plugin, UserConfig } from 'vite';
+import type { SentrySolidStartPluginOptions } from './types';
+
+/**
+ * A Sentry plugin for SolidStart to build the server
+ * `instrument.server.ts` file.
+ */
+export function makeBuildInstrumentationFilePlugin(options: SentrySolidStartPluginOptions = {}): Plugin {
+ return {
+ name: 'sentry-solidstart-build-instrumentation-file',
+ apply: 'build',
+ enforce: 'post',
+ async config(config: UserConfig, { command }) {
+ const instrumentationFilePath = options.instrumentation || './src/instrument.server.ts';
+ const router = (config as UserConfig & { router: { target: string; name: string; root: string } }).router;
+ const build = config.build || {};
+ const rollupOptions = build.rollupOptions || {};
+ const input = [...((rollupOptions.input || []) as string[])];
+
+ // plugin runs for client, server and sever-fns, we only want to run it for the server once.
+ if (command !== 'build' || router.target !== 'server' || router.name === 'server-fns') {
+ return config;
+ }
+
+ try {
+ await fs.promises.access(instrumentationFilePath, fs.constants.F_OK);
+ } catch (error) {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[Sentry SolidStart Plugin] Could not access \`${instrumentationFilePath}\`, please make sure it exists.`,
+ error,
+ );
+ });
+ return config;
+ }
+
+ input.push(path.resolve(router.root, instrumentationFilePath));
+
+ return {
+ ...config,
+ build: {
+ ...build,
+ rollupOptions: {
+ ...rollupOptions,
+ input,
+ },
+ },
+ };
+ },
+ };
+}
diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts
index 59435f919071..da0a3e116a0a 100644
--- a/packages/solidstart/src/vite/sentrySolidStartVite.ts
+++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts
@@ -1,13 +1,36 @@
-import type { Plugin } from 'vite';
+import type { Plugin, UserConfig } from 'vite';
+import { makeBuildInstrumentationFilePlugin } from './buildInstrumentationFile';
import { makeSourceMapsVitePlugin } from './sourceMaps';
import type { SentrySolidStartPluginOptions } from './types';
+// todo(v9): Don't export to users anymore and remove deprecation (and eslint warning silencing) when it's not exported anymore
/**
* Various Sentry vite plugins to be used for SolidStart.
+ *
+ * @deprecated This plugin will be removed in v9. Instead, use `withSentry` to wrap your SolidStart config. Example:
+ * ```
+ * export default defineConfig(
+ * withSentry(
+ * {
+ * // SolidStart config...
+ * },
+ * {
+ * // Sentry config
+ * org: process.env.SENTRY_ORG,
+ * project: process.env.SENTRY_PROJECT,
+ * authToken: process.env.SENTRY_AUTH_TOKEN,
+ * },
+ * ),
+ * );
+ * ```
*/
export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => {
const sentryPlugins: Plugin[] = [];
+ if (options.autoInjectServerSentry !== 'experimental_dynamic-import') {
+ sentryPlugins.push(makeBuildInstrumentationFilePlugin(options));
+ }
+
if (process.env.NODE_ENV !== 'development') {
if (options.sourceMapsUploadOptions?.enabled ?? true) {
sentryPlugins.push(...makeSourceMapsVitePlugin(options));
@@ -16,3 +39,17 @@ export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}
return sentryPlugins;
};
+
+/**
+ * Helper to add the Sentry SolidStart vite plugin to a vite config.
+ */
+export const addSentryPluginToVite = (config: UserConfig = {}, options: SentrySolidStartPluginOptions): UserConfig => {
+ const plugins = Array.isArray(config.plugins) ? [...config.plugins] : [];
+ // eslint-disable-next-line deprecation/deprecation
+ plugins.unshift(sentrySolidStartVite(options));
+
+ return {
+ ...config,
+ plugins,
+ };
+};
diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts
index 4a64e4856b5d..1ae73777c6a4 100644
--- a/packages/solidstart/src/vite/types.ts
+++ b/packages/solidstart/src/vite/types.ts
@@ -85,7 +85,7 @@ type BundleSizeOptimizationOptions = {
};
/**
- * Build options for the Sentry module. These options are used during build-time by the Sentry SDK.
+ * Build options for the Sentry plugin. These options are used during build-time by the Sentry SDK.
*/
export type SentrySolidStartPluginOptions = {
/**
@@ -125,4 +125,59 @@ export type SentrySolidStartPluginOptions = {
* Enabling this will give you, for example logs about source maps.
*/
debug?: boolean;
+
+ /**
+ * The path to your `instrument.server.ts|js` file.
+ * e.g. `./src/instrument.server.ts`
+ *
+ * Defaults to: `./src/instrument.server.ts`
+ */
+ instrumentation?: string;
+
+ /**
+ * The server entrypoint filename is automatically set by the Sentry SDK depending on the Nitro present.
+ * In case the server entrypoint has a different filename, you can overwrite it here.
+ */
+ serverEntrypointFileName?: string;
+
+ /**
+ *
+ * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible.
+ *
+ * **DO NOT** add the node CLI flag `--import` in your node start script, when auto-injecting Sentry.
+ * This would initialize Sentry twice on the server-side and this leads to unexpected issues.
+ *
+ * ---
+ *
+ * **"top-level-import"**
+ *
+ * Enabling basic server tracing with top-level import can be used for environments where modifying the node option `--import` is not possible.
+ * However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
+ *
+ * If `"top-level-import"` is enabled, the Sentry SDK will import the Sentry server config at the top of the server entry file to load the SDK on the server.
+ *
+ * ---
+ * **"experimental_dynamic-import"**
+ *
+ * Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
+ * necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
+ *
+ * If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`.
+ *
+ * @default undefined
+ */
+ autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import';
+
+ /**
+ * When `autoInjectServerSentry` is set to `"experimental_dynamic-import"`, the SDK will wrap your Nitro server entrypoint
+ * with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported.
+ * Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is.
+ *
+ * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
+ * If your server has a different main export that is used to run the server, you can overwrite this by providing an array of export names to wrap.
+ * Any wrapped export is expected to be an async function.
+ *
+ * @default ['default', 'handler', 'server']
+ */
+ experimental_entrypointWrappedFunctions?: string[];
};
diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts
new file mode 100644
index 000000000000..012bca76c9ca
--- /dev/null
+++ b/packages/solidstart/test/config/addInstrumentation.test.ts
@@ -0,0 +1,222 @@
+import type { Nitro } from 'nitropack';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+ addDynamicImportEntryFileWrapper,
+ addInstrumentationFileToBuild,
+ staticHostPresets,
+} from '../../src/config/addInstrumentation';
+import type { RollupConfig } from '../../src/config/types';
+
+const consoleLogSpy = vi.spyOn(console, 'log');
+const consoleWarnSpy = vi.spyOn(console, 'warn');
+const fsAccessMock = vi.fn();
+const fsCopyFileMock = vi.fn();
+const fsReadFile = vi.fn();
+const fsWriteFileMock = vi.fn();
+const fsMkdirMock = vi.fn();
+const fsReaddirMock = vi.fn();
+const fsExistsSyncMock = vi.fn();
+
+vi.mock('fs', async () => {
+ const actual = await vi.importActual('fs');
+ return {
+ ...actual,
+ existsSync: (...args: unknown[]) => fsExistsSyncMock(...args),
+ promises: {
+ // @ts-expect-error this exists
+ ...actual.promises,
+ access: (...args: unknown[]) => fsAccessMock(...args),
+ copyFile: (...args: unknown[]) => fsCopyFileMock(...args),
+ readFile: (...args: unknown[]) => fsReadFile(...args),
+ writeFile: (...args: unknown[]) => fsWriteFileMock(...args),
+ mkdir: (...args: unknown[]) => fsMkdirMock(...args),
+ readdir: (...args: unknown[]) => fsReaddirMock(...args),
+ },
+ };
+});
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('addInstrumentationFileToBuild()', () => {
+ const nitroOptions: Nitro = {
+ hooks: {
+ hook: vi.fn(),
+ },
+ options: {
+ buildDir: '/path/to/buildDir',
+ output: {
+ serverDir: '/path/to/serverDir',
+ },
+ preset: 'vercel',
+ },
+ };
+
+ const callNitroCloseHook = async () => {
+ const hookCallback = nitroOptions.hooks.hook.mock.calls[0][1];
+ await hookCallback();
+ };
+
+ it('adds `instrument.server.mjs` to the server output directory', async () => {
+ fsCopyFileMock.mockResolvedValueOnce(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/instrument.server.js',
+ '/path/to/serverDir/instrument.server.mjs',
+ );
+ });
+
+ it('warns when `instrument.server.js` cannot be copied to the server output directory', async () => {
+ const error = new Error('Failed to copy file.');
+ fsCopyFileMock.mockRejectedValueOnce(error);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/instrument.server.js',
+ '/path/to/serverDir/instrument.server.mjs',
+ );
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Failed to add instrumentation file to build.',
+ error,
+ );
+ });
+
+ it.each(staticHostPresets)("doesn't add `instrument.server.mjs` for static host `%s`", async preset => {
+ const staticNitroOptions = {
+ ...nitroOptions,
+ options: {
+ ...nitroOptions.options,
+ preset,
+ },
+ };
+
+ await addInstrumentationFileToBuild(staticNitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).not.toHaveBeenCalled();
+ });
+
+ it('creates assets directory if it does not exist', async () => {
+ fsExistsSyncMock.mockReturnValue(false);
+ fsMkdirMock.mockResolvedValueOnce(true);
+ fsCopyFileMock.mockResolvedValueOnce(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsMkdirMock).toHaveBeenCalledWith('/path/to/serverDir/assets', { recursive: true });
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Successfully created directory /path/to/serverDir/assets.',
+ );
+ });
+
+ it('does not create assets directory if it already exists', async () => {
+ fsExistsSyncMock.mockReturnValue(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsMkdirMock).not.toHaveBeenCalled();
+ });
+
+ it('copies release injection file if available', async () => {
+ fsExistsSyncMock.mockReturnValue(true);
+ fsReaddirMock.mockResolvedValueOnce(['_sentry-release-injection-file-test.js']);
+ fsCopyFileMock.mockResolvedValueOnce(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/assets/_sentry-release-injection-file-test.js',
+ '/path/to/serverDir/assets/_sentry-release-injection-file-test.js',
+ );
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Successfully created /path/to/serverDir/assets/_sentry-release-injection-file-test.js.',
+ );
+ });
+
+ it('warns when release injection file cannot be copied', async () => {
+ const error = new Error('Failed to copy release injection file.');
+ fsExistsSyncMock.mockReturnValue(true);
+ fsReaddirMock.mockResolvedValueOnce(['_sentry-release-injection-file-test.js']);
+ fsCopyFileMock.mockRejectedValueOnce(error);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/assets/_sentry-release-injection-file-test.js',
+ '/path/to/serverDir/assets/_sentry-release-injection-file-test.js',
+ );
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Failed to copy release injection file.',
+ error,
+ );
+ });
+
+ it('does not copy release injection file if not found', async () => {
+ fsExistsSyncMock.mockReturnValue(true);
+ fsReaddirMock.mockResolvedValueOnce([]);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).not.toHaveBeenCalledWith(
+ expect.stringContaining('_sentry-release-injection-file-'),
+ expect.any(String),
+ );
+ });
+
+ it('warns when `instrument.server.js` is not found', async () => {
+ const error = new Error('File not found');
+ fsCopyFileMock.mockRejectedValueOnce(error);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/instrument.server.js',
+ '/path/to/serverDir/instrument.server.mjs',
+ );
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Failed to add instrumentation file to build.',
+ error,
+ );
+ });
+});
+
+describe('addAutoInstrumentation()', () => {
+ const nitroOptions: Nitro = {
+ options: {
+ srcDir: 'path/to/srcDir',
+ buildDir: '/path/to/buildDir',
+ output: {
+ serverDir: '/path/to/serverDir',
+ },
+ preset: 'vercel',
+ },
+ };
+
+ it('adds the `sentry-wrap-server-entry-with-dynamic-import` rollup plugin to the rollup config', async () => {
+ const rollupConfig: RollupConfig = {
+ plugins: [],
+ };
+
+ await addDynamicImportEntryFileWrapper({
+ nitro: nitroOptions,
+ rollupConfig,
+ sentryPluginOptions: { experimental_entrypointWrappedFunctions: [] },
+ });
+ expect(
+ rollupConfig.plugins.find(plugin => plugin.name === 'sentry-wrap-server-entry-with-dynamic-import'),
+ ).toBeTruthy();
+ });
+});
diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts
new file mode 100644
index 000000000000..e554db45124f
--- /dev/null
+++ b/packages/solidstart/test/config/withSentry.test.ts
@@ -0,0 +1,152 @@
+import type { Nitro } from 'nitropack';
+import type { Plugin } from 'vite';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { withSentry } from '../../src/config';
+
+const userDefinedNitroRollupBeforeHookMock = vi.fn();
+const userDefinedNitroCloseHookMock = vi.fn();
+const addInstrumentationFileToBuildMock = vi.fn();
+const addSentryTopImportMock = vi.fn();
+
+vi.mock('../../src/config/addInstrumentation', () => ({
+ addInstrumentationFileToBuild: (...args: unknown[]) => addInstrumentationFileToBuildMock(...args),
+ addSentryTopImport: (...args: unknown[]) => addSentryTopImportMock(...args),
+}));
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('withSentry()', () => {
+ const solidStartConfig = {
+ middleware: './src/middleware.ts',
+ server: {
+ hooks: {
+ close: userDefinedNitroCloseHookMock,
+ 'rollup:before': userDefinedNitroRollupBeforeHookMock,
+ },
+ },
+ };
+ const nitroOptions: Nitro = {
+ options: {
+ buildDir: '/path/to/buildDir',
+ output: {
+ serverDir: '/path/to/serverDir',
+ },
+ preset: 'vercel',
+ },
+ };
+
+ it('adds a nitro hook to add the instrumentation file to the build if no plugin options are provided', async () => {
+ const config = withSentry(solidStartConfig, {});
+ await config?.server.hooks['rollup:before'](nitroOptions);
+ expect(addInstrumentationFileToBuildMock).toHaveBeenCalledWith(nitroOptions);
+ expect(userDefinedNitroRollupBeforeHookMock).toHaveBeenCalledWith(nitroOptions);
+ });
+
+ it('adds a nitro hook to add the instrumentation file as top level import to the server entry file when configured in autoInjectServerSentry', async () => {
+ const config = withSentry(solidStartConfig, { autoInjectServerSentry: 'top-level-import' });
+ await config?.server.hooks['rollup:before'](nitroOptions);
+ await config?.server.hooks['close'](nitroOptions);
+ expect(addSentryTopImportMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: {
+ buildDir: '/path/to/buildDir',
+ output: {
+ serverDir: '/path/to/serverDir',
+ },
+ preset: 'vercel',
+ },
+ }),
+ );
+ expect(userDefinedNitroCloseHookMock).toHaveBeenCalled();
+ });
+
+ it('does not add the instrumentation file as top level import if autoInjectServerSentry is undefined', async () => {
+ const config = withSentry(solidStartConfig, { autoInjectServerSentry: undefined });
+ await config?.server.hooks['rollup:before'](nitroOptions);
+ await config?.server.hooks['close'](nitroOptions);
+ expect(addSentryTopImportMock).not.toHaveBeenCalled();
+ expect(userDefinedNitroCloseHookMock).toHaveBeenCalled();
+ });
+
+ it('adds the sentry solidstart vite plugin', () => {
+ const config = withSentry(solidStartConfig, {
+ project: 'project',
+ org: 'org',
+ authToken: 'token',
+ });
+ const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name);
+ expect(names).toEqual([
+ 'sentry-solidstart-build-instrumentation-file',
+ 'sentry-solidstart-source-maps',
+ 'sentry-telemetry-plugin',
+ 'sentry-vite-release-injection-plugin',
+ 'sentry-debug-id-upload-plugin',
+ 'sentry-vite-debug-id-injection-plugin',
+ 'sentry-vite-debug-id-upload-plugin',
+ 'sentry-file-deletion-plugin',
+ ]);
+ });
+
+ it('extends the passed in vite config object', () => {
+ const config = withSentry(
+ {
+ ...solidStartConfig,
+ vite: {
+ plugins: [{ name: 'my-test-plugin' }],
+ },
+ },
+ {
+ project: 'project',
+ org: 'org',
+ authToken: 'token',
+ },
+ );
+
+ const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name);
+ expect(names).toEqual([
+ 'sentry-solidstart-build-instrumentation-file',
+ 'sentry-solidstart-source-maps',
+ 'sentry-telemetry-plugin',
+ 'sentry-vite-release-injection-plugin',
+ 'sentry-debug-id-upload-plugin',
+ 'sentry-vite-debug-id-injection-plugin',
+ 'sentry-vite-debug-id-upload-plugin',
+ 'sentry-file-deletion-plugin',
+ 'my-test-plugin',
+ ]);
+ });
+
+ it('extends the passed in vite function config', () => {
+ const config = withSentry(
+ {
+ ...solidStartConfig,
+ vite() {
+ return { plugins: [{ name: 'my-test-plugin' }] };
+ },
+ },
+ {
+ project: 'project',
+ org: 'org',
+ authToken: 'token',
+ },
+ );
+
+ const names = config
+ ?.vite()
+ .plugins.flat()
+ .map((plugin: Plugin) => plugin.name);
+ expect(names).toEqual([
+ 'sentry-solidstart-build-instrumentation-file',
+ 'sentry-solidstart-source-maps',
+ 'sentry-telemetry-plugin',
+ 'sentry-vite-release-injection-plugin',
+ 'sentry-debug-id-upload-plugin',
+ 'sentry-vite-debug-id-injection-plugin',
+ 'sentry-vite-debug-id-upload-plugin',
+ 'sentry-file-deletion-plugin',
+ 'my-test-plugin',
+ ]);
+ });
+});
diff --git a/packages/solidstart/test/vite/buildInstrumentation.test.ts b/packages/solidstart/test/vite/buildInstrumentation.test.ts
new file mode 100644
index 000000000000..52378a668870
--- /dev/null
+++ b/packages/solidstart/test/vite/buildInstrumentation.test.ts
@@ -0,0 +1,130 @@
+import type { UserConfig } from 'vite';
+import { describe, expect, it, vi } from 'vitest';
+import { makeBuildInstrumentationFilePlugin } from '../../src/vite/buildInstrumentationFile';
+
+const fsAccessMock = vi.fn();
+
+vi.mock('fs', async () => {
+ const actual = await vi.importActual('fs');
+ return {
+ ...actual,
+ promises: {
+ // @ts-expect-error this exists
+ ...actual.promises,
+ access: () => fsAccessMock(),
+ },
+ };
+});
+
+const consoleWarnSpy = vi.spyOn(console, 'warn');
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('makeBuildInstrumentationFilePlugin()', () => {
+ const viteConfig: UserConfig & { router: { target: string; name: string; root: string } } = {
+ router: {
+ target: 'server',
+ name: 'ssr',
+ root: '/some/project/path',
+ },
+ build: {
+ rollupOptions: {
+ input: ['/path/to/entry1.js', '/path/to/entry2.js'],
+ },
+ },
+ };
+
+ it('returns a plugin to set `sourcemaps` to `true`', () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+
+ expect(buildInstrumentationFilePlugin.name).toEqual('sentry-solidstart-build-instrumentation-file');
+ expect(buildInstrumentationFilePlugin.apply).toEqual('build');
+ expect(buildInstrumentationFilePlugin.enforce).toEqual('post');
+ expect(buildInstrumentationFilePlugin.config).toEqual(expect.any(Function));
+ });
+
+ it('adds the instrumentation file for server builds', async () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' });
+ expect(config.build.rollupOptions.input).toContain('/some/project/path/src/instrument.server.ts');
+ });
+
+ it('adds the correct instrumentation file', async () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin({
+ instrumentation: './src/myapp/instrument.server.ts',
+ });
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' });
+ expect(config.build.rollupOptions.input).toContain('/some/project/path/src/myapp/instrument.server.ts');
+ });
+
+ it("doesn't add the instrumentation file for server function builds", async () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(
+ {
+ ...viteConfig,
+ router: {
+ ...viteConfig.router,
+ name: 'server-fns',
+ },
+ },
+ { command: 'build' },
+ );
+ expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts');
+ });
+
+ it("doesn't add the instrumentation file for client builds", async () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(
+ {
+ ...viteConfig,
+ router: {
+ ...viteConfig.router,
+ target: 'client',
+ },
+ },
+ { command: 'build' },
+ );
+ expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts');
+ });
+
+ it("doesn't add the instrumentation file when serving", async () => {
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'serve' });
+ expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts');
+ });
+
+ it("doesn't modify the config if the instrumentation file doesn't exist", async () => {
+ fsAccessMock.mockRejectedValueOnce(undefined);
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' });
+ expect(config).toEqual(viteConfig);
+ });
+
+ it("logs a warning if the instrumentation file doesn't exist", async () => {
+ const error = new Error("File doesn't exist.");
+ fsAccessMock.mockRejectedValueOnce(error);
+ const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin();
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore - this is always defined and always a function
+ const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' });
+ expect(config).toEqual(viteConfig);
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart Plugin] Could not access `./src/instrument.server.ts`, please make sure it exists.',
+ error,
+ );
+ });
+});
diff --git a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts
index d3f905313859..880d7dff4f69 100644
--- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts
+++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts
@@ -9,7 +9,9 @@ vi.spyOn(console, 'warn').mockImplementation(() => {
/* noop */
});
+// eslint-disable-next-line deprecation/deprecation
function getSentrySolidStartVitePlugins(options?: Parameters[0]): Plugin[] {
+ // eslint-disable-next-line deprecation/deprecation
return sentrySolidStartVite({
project: 'project',
org: 'org',
@@ -23,6 +25,7 @@ describe('sentrySolidStartVite()', () => {
const plugins = getSentrySolidStartVitePlugins();
const names = plugins.map(plugin => plugin.name);
expect(names).toEqual([
+ 'sentry-solidstart-build-instrumentation-file',
'sentry-solidstart-source-maps',
'sentry-telemetry-plugin',
'sentry-vite-release-injection-plugin',
@@ -33,17 +36,19 @@ describe('sentrySolidStartVite()', () => {
]);
});
- it("returns an empty array if source maps upload isn't enabled", () => {
+ it("returns only build-instrumentation-file plugin if source maps upload isn't enabled", () => {
const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: false } });
- expect(plugins).toHaveLength(0);
+ const names = plugins.map(plugin => plugin.name);
+ expect(names).toEqual(['sentry-solidstart-build-instrumentation-file']);
});
- it('returns an empty array if `NODE_ENV` is development', async () => {
+ it('returns only build-instrumentation-file plugin if `NODE_ENV` is development', async () => {
const previousEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: true } });
- expect(plugins).toHaveLength(0);
+ const names = plugins.map(plugin => plugin.name);
+ expect(names).toEqual(['sentry-solidstart-build-instrumentation-file']);
process.env.NODE_ENV = previousEnv;
});
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index fb95251cf35b..d19282e51848 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/svelte",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for Svelte",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.54.0",
+ "@sentry/core": "8.54.0",
"magic-string": "^0.30.0"
},
"peerDependencies": {
diff --git a/packages/svelte/src/config.ts b/packages/svelte/src/config.ts
index 4c265ad57fc7..9231ef46426e 100644
--- a/packages/svelte/src/config.ts
+++ b/packages/svelte/src/config.ts
@@ -22,30 +22,23 @@ export function withSentryConfig(
const mergedOptions = {
...DEFAULT_SENTRY_OPTIONS,
...sentryOptions,
+ componentTracking: {
+ ...DEFAULT_SENTRY_OPTIONS.componentTracking,
+ ...(sentryOptions && sentryOptions.componentTracking),
+ },
};
const originalPreprocessors = getOriginalPreprocessorArray(originalConfig);
- // Map is insertion-order-preserving. It's important to add preprocessors
- // to this map in the right order we want to see them being executed.
- // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
- const sentryPreprocessors = new Map();
-
- const shouldTrackComponents = mergedOptions.componentTracking && mergedOptions.componentTracking.trackComponents;
- if (shouldTrackComponents) {
- const firstPassPreproc: SentryPreprocessorGroup = componentTrackingPreprocessor(mergedOptions.componentTracking);
- sentryPreprocessors.set(firstPassPreproc.sentryId || '', firstPassPreproc);
+ // Bail if users already added the preprocessor
+ if (originalPreprocessors.find((p: PreprocessorGroup) => !!(p as SentryPreprocessorGroup).sentryId)) {
+ return originalConfig;
}
- // We prioritize user-added preprocessors, so we don't insert sentry processors if they
- // have already been added by users.
- originalPreprocessors.forEach((p: SentryPreprocessorGroup) => {
- if (p.sentryId) {
- sentryPreprocessors.delete(p.sentryId);
- }
- });
-
- const mergedPreprocessors = [...sentryPreprocessors.values(), ...originalPreprocessors];
+ const mergedPreprocessors = [...originalPreprocessors];
+ if (mergedOptions.componentTracking.trackComponents) {
+ mergedPreprocessors.unshift(componentTrackingPreprocessor(mergedOptions.componentTracking));
+ }
return {
...originalConfig,
diff --git a/packages/svelte/src/constants.ts b/packages/svelte/src/constants.ts
deleted file mode 100644
index cb8255040c03..000000000000
--- a/packages/svelte/src/constants.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const UI_SVELTE_INIT = 'ui.svelte.init';
-
-export const UI_SVELTE_UPDATE = 'ui.svelte.update';
-
-export const DEFAULT_COMPONENT_NAME = 'Svelte Component';
diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts
index b50be258bc58..0c21f8d36622 100644
--- a/packages/svelte/src/performance.ts
+++ b/packages/svelte/src/performance.ts
@@ -2,8 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser';
import type { Span } from '@sentry/core';
import { afterUpdate, beforeUpdate, onMount } from 'svelte';
-import { startInactiveSpan } from '@sentry/core';
-import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants';
+import { logger, startInactiveSpan } from '@sentry/core';
import type { TrackComponentOptions } from './types';
const defaultTrackComponentOptions: {
@@ -29,21 +28,27 @@ export function trackComponent(options?: TrackComponentOptions): void {
const customComponentName = mergedOptions.componentName;
- const componentName = `<${customComponentName || DEFAULT_COMPONENT_NAME}>`;
+ const componentName = `<${customComponentName || 'Svelte Component'}>`;
if (mergedOptions.trackInit) {
recordInitSpan(componentName);
}
if (mergedOptions.trackUpdates) {
- recordUpdateSpans(componentName);
+ try {
+ recordUpdateSpans(componentName);
+ } catch {
+ logger.warn(
+ "Cannot track component updates. This is likely because you're using Svelte 5 in Runes mode. Set `trackUpdates: false` in `withSentryConfig` or `trackComponent` to disable this warning.",
+ );
+ }
}
}
function recordInitSpan(componentName: string): void {
const initSpan = startInactiveSpan({
onlyIfParent: true,
- op: UI_SVELTE_INIT,
+ op: 'ui.svelte.init',
name: componentName,
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' },
});
@@ -58,7 +63,7 @@ function recordUpdateSpans(componentName: string): void {
beforeUpdate(() => {
updateSpan = startInactiveSpan({
onlyIfParent: true,
- op: UI_SVELTE_UPDATE,
+ op: 'ui.svelte.update',
name: componentName,
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' },
});
diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts
index 1d21b72ef59d..a1d78195b50f 100644
--- a/packages/svelte/src/sdk.ts
+++ b/packages/svelte/src/sdk.ts
@@ -55,5 +55,6 @@ export function detectAndReportSvelteKit(): void {
* @see https://github.com/sveltejs/kit/issues/307 for more information
*/
export function isSvelteKitApp(): boolean {
+ // eslint-disable-next-line deprecation/deprecation
return getDomElement('div#svelte-announcer') !== null;
}
diff --git a/packages/svelte/test/config.test.ts b/packages/svelte/test/config.test.ts
index a8c84297082a..21f51dc66518 100644
--- a/packages/svelte/test/config.test.ts
+++ b/packages/svelte/test/config.test.ts
@@ -60,7 +60,7 @@ describe('withSentryConfig', () => {
const wrappedConfig = withSentryConfig(originalConfig);
- expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: [sentryPreproc] });
+ expect(wrappedConfig).toEqual({ ...originalConfig });
});
it('handles multiple wraps correctly by only adding our preprocessors once', () => {
diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts
index 21adeee255c3..67a236116444 100644
--- a/packages/svelte/test/performance.test.ts
+++ b/packages/svelte/test/performance.test.ts
@@ -9,7 +9,6 @@ import { getClient, getCurrentScope, getIsolationScope, init, startSpan } from '
import type { TransactionEvent } from '@sentry/core';
-// @ts-expect-error svelte import
import DummyComponent from './components/Dummy.svelte';
const PUBLIC_DSN = 'https://username@domain/123';
@@ -32,7 +31,7 @@ describe('Sentry.trackComponent()', () => {
init({
dsn: PUBLIC_DSN,
- enableTracing: true,
+ tracesSampleRate: 1.0,
beforeSendTransaction,
});
});
@@ -220,7 +219,7 @@ describe('Sentry.trackComponent()', () => {
expect(transaction.spans![1]?.description).toEqual('');
});
- it("doesn't do anything, if there's no ongoing transaction", async () => {
+ it("doesn't do anything, if there's no ongoing parent span", async () => {
render(DummyComponent, {
props: { options: { componentName: 'CustomComponentName' } },
});
@@ -230,7 +229,7 @@ describe('Sentry.trackComponent()', () => {
expect(transactions).toHaveLength(0);
});
- it("doesn't record update spans, if there's no ongoing root span at that time", async () => {
+ it("doesn't record update spans, if there's no ongoing parent span at that time", async () => {
const component = startSpan({ name: 'outer' }, span => {
expect(span).toBeDefined();
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index 7ae146015a74..11a3f36b7fa9 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/sveltekit",
- "version": "8.45.0",
+ "version": "8.54.0",
"description": "Official Sentry SDK for SvelteKit",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit",
@@ -40,20 +40,20 @@
}
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0",
- "@sentry/svelte": "8.45.0",
+ "@sentry/core": "8.54.0",
+ "@sentry/node": "8.54.0",
+ "@sentry/opentelemetry": "8.54.0",
+ "@sentry/svelte": "8.54.0",
"@sentry/vite-plugin": "2.22.6",
"magic-string": "0.30.7",
"magicast": "0.2.8",
"sorcery": "1.0.0"
},
"devDependencies": {
- "@babel/types": "7.20.7",
+ "@babel/types": "^7.26.3",
"@sveltejs/kit": "^2.0.2",
"svelte": "^4.2.8",
- "vite": "^5.4.10"
+ "vite": "^5.4.11"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts
index 95cac7b9318b..a5109c82aa7c 100644
--- a/packages/sveltekit/src/index.types.ts
+++ b/packages/sveltekit/src/index.types.ts
@@ -42,7 +42,6 @@ export declare const contextLinesIntegration: typeof clientSdk.contextLinesInteg
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
-export declare const getClient: typeof clientSdk.getClient;
// eslint-disable-next-line deprecation/deprecation
export declare const getCurrentHub: typeof clientSdk.getCurrentHub;
@@ -50,8 +49,6 @@ export declare function close(timeout?: number | undefined): PromiseLike;
export declare function lastEventId(): string | undefined;
-export declare const continueTrace: typeof clientSdk.continueTrace;
-
// eslint-disable-next-line deprecation/deprecation
export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics;
diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts
index 19a0c2507da5..97f11d9a6d61 100644
--- a/packages/sveltekit/src/server/handle.ts
+++ b/packages/sveltekit/src/server/handle.ts
@@ -2,6 +2,7 @@ import type { Span } from '@sentry/core';
import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ continueTrace,
getActiveSpan,
getCurrentScope,
getDefaultIsolationScope,
@@ -13,7 +14,6 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
-import { continueTrace } from '@sentry/node';
import type { Handle, ResolveOptions } from '@sveltejs/kit';
import { DEBUG_BUILD } from '../common/debug-build';
@@ -40,16 +40,25 @@ export type SentryHandleOptions = {
* Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation
* of `fetch` calls in `load` functions.
*
+ * You can safely set this to `false` if you're using `@sveltejs/kit` version 2.16.0 or newer. This
+ * is only needed for versions older than 2.16.0.
+ *
* @default true
*/
injectFetchProxyScript?: boolean;
/**
- * If this option is set, the `sentryHandle` handler will add a nonce attribute to the script
- * tag it injects into the page. This script is used to enable instrumentation of `fetch` calls
- * in `load` functions.
+ * Warning: Setting this option is **strongly discouraged** and it will be removed in the next major version of the SDK.
+ *
+ * If you set this option, the passed nonce will be added to fetch proxy `