diff --git a/.craft.yml b/.craft.yml
index 44d245311312..8ab0f5ced09c 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -142,27 +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$/
- layerName: SentryNodeServerlessSDK
+ layerName: SentryNodeServerlessSDKv{{{major}}}
compatibleRuntimes:
- name: node
versions:
diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml
index 5eac05a32a2d..fc76b8641bad 100644
--- a/.github/actions/install-playwright/action.yml
+++ b/.github/actions/install-playwright/action.yml
@@ -3,7 +3,7 @@ description: "Installs Playwright dependencies and caches them."
inputs:
browsers:
description: 'What browsers to install.'
- default: 'chromium webkit firefox'
+ default: 'chromium'
cwd:
description: 'The working directory to run Playwright in.'
default: '.'
@@ -17,33 +17,7 @@ runs:
shell: bash
working-directory: ${{ inputs.cwd }}
-
- - name: Restore cached playwright binaries
- uses: actions/cache/restore@v4
- id: playwright-cache
- with:
- path: |
- ~/.cache/ms-playwright
- key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
-
- # We always install all browsers, if uncached
- - name: Install Playwright dependencies (uncached)
- run: npx playwright install chromium webkit firefox --with-deps
- if: steps.playwright-cache.outputs.cache-hit != 'true'
+ - name: Install Playwright dependencies
+ run: npx playwright install chromium
shell: bash
- working-directory: ${{ inputs.cwd }}
-
- - name: Install Playwright system dependencies only (cached)
- run: npx playwright install-deps ${{ inputs.browsers || 'chromium webkit firefox' }}
- if: steps.playwright-cache.outputs.cache-hit == 'true'
- shell: bash
- working-directory: ${{ inputs.cwd }}
-
- # Only store cache on develop branch
- - name: Store cached playwright binaries
- uses: actions/cache/save@v4
- if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
- with:
- path: |
- ~/.cache/ms-playwright
- key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
+ working-directory: ${{ inputs.cwd }}
\ No newline at end of file
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
index dd41d7d50e5e..ed3d87040e3e 100644
--- a/.github/workflows/auto-release.yml
+++ b/.github/workflows/auto-release.yml
@@ -9,7 +9,7 @@ on:
# This workflow tirggers a release when merging a branch with the pattern `prepare-release/VERSION` into master.
jobs:
release:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
name: 'Prepare a new version'
steps:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bf9ba21376bb..8a81d9ea42ea 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:
@@ -60,7 +62,7 @@ env:
jobs:
job_get_metadata:
name: Get Metadata
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
permissions:
pull-requests: read
steps:
@@ -91,9 +93,6 @@ jobs:
filters: |
workflow:
- '.github/**'
- profiling_node:
- - 'packages/profiling-node/**'
- - 'dev-packages/e2e-tests/test-applications/node-profiling/**'
any_code:
- '!**/*.md'
@@ -105,9 +104,8 @@ 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' }}
changed_any_code: ${{ steps.changed.outputs.any_code == 'true' }}
@@ -122,11 +120,11 @@ jobs:
job_build:
name: Build
needs: job_get_metadata
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
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 +169,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
@@ -196,12 +194,11 @@ jobs:
changed_deno: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/deno') }}
changed_bun: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/bun') }}
changed_browser_integration: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry-internal/browser-integration-tests') }}
- # If you are looking for changed_profiling_node, this is defined in job_get_metadata
job_check_branches:
name: Check PR branches
needs: job_get_metadata
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
@@ -217,10 +214,8 @@ jobs:
name: Size Check
needs: [job_get_metadata, job_build]
timeout-minutes: 15
- runs-on: ubuntu-20.04
- if:
- github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_develop == 'true' ||
- needs.job_get_metadata.outputs.is_release == 'true'
+ runs-on: ubuntu-24.04
+ if: false
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -247,7 +242,7 @@ jobs:
# inter-package dependencies resolve cleanly.
needs: [job_get_metadata, job_build]
timeout-minutes: 10
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -270,7 +265,7 @@ jobs:
name: Check file formatting
needs: [job_get_metadata]
timeout-minutes: 10
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -293,7 +288,7 @@ jobs:
name: Circular Dependency Check
needs: [job_get_metadata, job_build]
timeout-minutes: 10
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -312,8 +307,8 @@ jobs:
job_artifacts:
name: Upload Artifacts
- needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node]
- runs-on: ubuntu-20.04
+ needs: [job_get_metadata, job_build]
+ runs-on: ubuntu-24.04
# Build artifacts are only needed for releasing workflow.
if: needs.job_get_metadata.outputs.is_release == 'true'
steps:
@@ -357,7 +352,7 @@ jobs:
name: Browser Unit Tests
needs: [job_get_metadata, job_build]
timeout-minutes: 10
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: Check out base commit (${{ github.event.pull_request.base.sha }})
uses: actions/checkout@v4
@@ -396,9 +391,7 @@ jobs:
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_bun == 'true' || github.event_name != 'pull_request'
timeout-minutes: 10
- runs-on: ubuntu-20.04
- strategy:
- fail-fast: false
+ runs-on: ubuntu-24.04
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -423,9 +416,7 @@ jobs:
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_deno == 'true' || github.event_name != 'pull_request'
timeout-minutes: 10
- runs-on: ubuntu-20.04
- strategy:
- fail-fast: false
+ runs-on: ubuntu-24.04
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -453,7 +444,7 @@ jobs:
name: Node (${{ matrix.node }}) Unit Tests
needs: [job_get_metadata, job_build]
timeout-minutes: 10
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -495,42 +486,11 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
- job_profiling_node_unit_tests:
- name: Node Profiling Unit Tests
- needs: [job_get_metadata, job_build]
- if: |
- needs.job_build.outputs.changed_node == 'true' ||
- needs.job_get_metadata.outputs.changed_profiling_node == 'true' ||
- github.event_name != 'pull_request'
- runs-on: ubuntu-latest
- timeout-minutes: 10
- steps:
- - name: Check out current commit
- uses: actions/checkout@v4
- with:
- ref: ${{ env.HEAD_COMMIT }}
- - uses: actions/setup-node@v4
- with:
- node-version: 20
- - uses: actions/setup-python@v5
- with:
- python-version: '3.11.7'
- - name: Restore caches
- uses: ./.github/actions/restore-cache
- with:
- dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }}
- - name: Build Configure node-gyp
- run: yarn lerna run build:bindings:configure --scope @sentry/profiling-node
- - name: Build Bindings for Current Environment
- run: yarn build --scope @sentry/profiling-node
- - name: Unit Test
- run: yarn lerna run test --scope @sentry/profiling-node
-
job_browser_playwright_tests:
name: Playwright ${{ matrix.bundle }}${{ matrix.project && matrix.project != 'chromium' && format(' {0}', matrix.project) || ''}}${{ matrix.shard && format(' ({0}/{1})', matrix.shard, matrix.shards) || ''}} Tests
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request'
- runs-on: ubuntu-20.04-large-js
+ runs-on: ubuntu-24.04-large-js
timeout-minutes: 25
strategy:
fail-fast: false
@@ -547,12 +507,6 @@ jobs:
project:
- chromium
include:
- # Only check all projects for full bundle
- # We also shard the tests as they take the longest
- - bundle: bundle_tracing_replay_feedback_min
- project: 'webkit'
- - bundle: bundle_tracing_replay_feedback_min
- project: 'firefox'
- bundle: esm
project: chromium
shard: 1
@@ -620,7 +574,7 @@ jobs:
name: PW ${{ matrix.bundle }} Tests
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request'
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
@@ -680,7 +634,7 @@ jobs:
job_check_for_faulty_dts:
name: Check for faulty .d.ts files
needs: [job_get_metadata, job_build]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
@@ -708,12 +662,12 @@ jobs:
Tests
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request'
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
- node: [14, 16, 18, 20, 22]
+ node: [14, 16, '18.20.5', 20, 22]
typescript:
- false
include:
@@ -736,8 +690,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
@@ -751,17 +705,13 @@ jobs:
name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) Tests
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_remix == 'true' || github.event_name != 'pull_request'
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
remix: [1, 2]
- # Remix v2 only supports Node 18+, so run 16 tests separately
- include:
- - node: 16
- remix: 1
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
@@ -796,10 +746,9 @@ jobs:
# - AND if the profiling node bindings were either successful or skipped
if: |
always() &&
- needs.job_build.result == 'success' &&
- (needs.job_compile_bindings_profiling_node.result == 'success' || needs.job_compile_bindings_profiling_node.result == 'skipped')
- needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node]
- runs-on: ubuntu-20.04-large-js
+ needs.job_build.result == 'success'
+ needs: [job_get_metadata, job_build]
+ runs-on: ubuntu-24.04-large-js
timeout-minutes: 15
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
@@ -875,7 +824,7 @@ jobs:
# See: https://github.com/actions/runner/issues/2205
if: always() && needs.job_e2e_prepare.result == 'success' && needs.job_e2e_prepare.outputs.matrix != '{"include":[]}'
needs: [job_get_metadata, job_build, job_e2e_prepare]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 15
env:
# We just use a dummy DSN here, only send to the tunnel anyhow
@@ -896,11 +845,11 @@ jobs:
ref: ${{ env.HEAD_COMMIT }}
- uses: pnpm/action-setup@v4
with:
- version: 9.4.0
+ version: 9.15.0
- name: Set up Node
uses: actions/setup-node@v4
with:
- node-version-file: 'dev-packages/e2e-tests/package.json'
+ node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Bun
if: matrix.test-application == 'node-exports-test-app'
uses: oven-sh/setup-bun@v2
@@ -997,7 +946,7 @@ jobs:
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
github.actor != 'dependabot[bot]'
needs: [job_get_metadata, job_build, job_e2e_prepare]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 15
env:
E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }}
@@ -1019,11 +968,11 @@ jobs:
ref: ${{ env.HEAD_COMMIT }}
- uses: pnpm/action-setup@v4
with:
- version: 9.4.0
+ version: 9.15.0
- name: Set up Node
uses: actions/setup-node@v4
with:
- node-version-file: 'dev-packages/e2e-tests/package.json'
+ node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Restore caches
uses: ./.github/actions/restore-cache
with:
@@ -1096,136 +1045,19 @@ jobs:
directory: dist
workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
- job_profiling_e2e_tests:
- name: E2E ${{ matrix.label || matrix.test-application }} Test
- # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks
- # Dependabot specifically also has access to secrets
- # We need to add the `always()` check here because the previous step has this as well :(
- # See: https://github.com/actions/runner/issues/2205
- if:
- # Only run profiling e2e tests if profiling node bindings have changed
- always() && needs.job_e2e_prepare.result == 'success' &&
- (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
- (
- (needs.job_get_metadata.outputs.changed_profiling_node == 'true') ||
- (needs.job_get_metadata.outputs.is_release == 'true')
- )
- needs: [job_get_metadata, job_build, job_e2e_prepare]
- runs-on: ubuntu-22.04
- timeout-minutes: 15
- env:
- E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }}
- E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
- E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks'
- E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests'
- strategy:
- fail-fast: false
- matrix:
- test-application: ['node-profiling']
- build-command:
- - false
- label:
- - false
- steps:
- - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
- uses: actions/checkout@v4
- with:
- ref: ${{ env.HEAD_COMMIT }}
-
- - uses: pnpm/action-setup@v4
- with:
- version: 9.4.0
-
- - name: Set up Node
- uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Restore caches
- uses: ./.github/actions/restore-cache
- with:
- dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }}
-
- - name: Build Profiling Node
- run: yarn lerna run build:lib --scope @sentry/profiling-node
-
- - name: Extract Profiling Node Prebuilt Binaries
- uses: actions/download-artifact@v4
- with:
- pattern: profiling-node-binaries-${{ github.sha }}-*
- path: ${{ github.workspace }}/packages/profiling-node/lib/
- merge-multiple: true
-
- - name: Restore tarball cache
- uses: actions/cache/restore@v4
- id: restore-tarball-cache
- with:
- path: ${{ github.workspace }}/packages/*/*.tgz
- key: ${{ env.BUILD_CACHE_TARBALL_KEY }}
-
- - name: Build tarballs if not cached
- if: steps.restore-tarball-cache.outputs.cache-hit != 'true'
- run: yarn build:tarball
-
- - name: Install Playwright
- uses: ./.github/actions/install-playwright
- with:
- browsers: chromium
-
- - name: Get node version
- id: versions
- run: |
- echo "echo node=$(jq -r '.volta.node' dev-packages/e2e-tests/package.json)" >> $GITHUB_OUTPUT
-
- - name: Validate Verdaccio
- run: yarn test:validate
- working-directory: dev-packages/e2e-tests
-
- - name: Prepare Verdaccio
- run: yarn test:prepare
- working-directory: dev-packages/e2e-tests
- env:
- E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }}
-
- - name: Setup xvfb and update ubuntu dependencies
- run: |
- sudo apt-get install xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps
- sudo apt-get install build-essential clang libdbus-1-dev libgtk2.0-dev \
- libnotify-dev libgconf2-dev \
- libasound2-dev libcap-dev libcups2-dev libxtst-dev \
- libxss1 libnss3-dev gcc-multilib g++-multilib
-
- - name: Install dependencies
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
- run: yarn install --ignore-engines --frozen-lockfile
-
- - name: Build E2E app
- working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}
- timeout-minutes: 7
- run: yarn ${{ matrix.build-command || 'test:build' }}
-
- - 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
-
job_required_jobs_passed:
name: All required jobs passed or were skipped
needs:
[
job_build,
- job_compile_bindings_profiling_node,
job_browser_unit_tests,
job_bun_unit_tests,
job_deno_unit_tests,
job_node_unit_tests,
- job_profiling_node_unit_tests,
job_node_integration_tests,
job_browser_playwright_tests,
job_browser_loader_tests,
job_remix_integration_tests,
- job_e2e_tests,
- job_profiling_e2e_tests,
job_artifacts,
job_lint,
job_check_format,
@@ -1233,281 +1065,9 @@ jobs:
]
# Always run this, even if a dependent job failed
if: always()
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: Check for failures
if: contains(needs.*.result, 'failure')
run: |
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1
-
- job_compile_bindings_profiling_node:
- name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}
- needs: [job_get_metadata, job_build]
- # Compiling bindings can be very slow (especially on windows), so only run precompile
- # Skip precompile unless we are on a release branch as precompile slows down CI times.
- if: |
- (needs.job_get_metadata.outputs.changed_profiling_node == 'true') ||
- (needs.job_get_metadata.outputs.is_release == 'true')
- runs-on: ${{ matrix.os }}
- container: ${{ matrix.container }}
- timeout-minutes: 30
- strategy:
- fail-fast: false
- matrix:
- include:
- # x64 glibc
- - os: ubuntu-20.04
- node: 16
- binary: linux-x64-glibc-93
- - os: ubuntu-20.04
- node: 18
- binary: linux-x64-glibc-108
- - os: ubuntu-20.04
- node: 20
- binary: linux-x64-glibc-115
- - os: ubuntu-20.04
- node: 22
- binary: linux-x64-glibc-127
-
- # x64 musl
- - os: ubuntu-20.04
- container: node:16-alpine3.16
- binary: linux-x64-musl-93
- node: 16
- - os: ubuntu-20.04
- container: node:18-alpine3.17
- node: 18
- binary: linux-x64-musl-108
- - os: ubuntu-20.04
- container: node:20-alpine3.17
- node: 20
- binary: linux-x64-musl-115
- - os: ubuntu-20.04
- container: node:22-alpine3.18
- node: 22
- binary: linux-x64-musl-127
-
- # arm64 glibc
- - os: ubuntu-20.04
- arch: arm64
- node: 16
- binary: linux-arm64-glibc-93
- - os: ubuntu-20.04
- arch: arm64
- node: 18
- binary: linux-arm64-glibc-108
- - os: ubuntu-20.04
- arch: arm64
- node: 20
- binary: linux-arm64-glibc-115
- - os: ubuntu-20.04
- arch: arm64
- node: 22
- binary: linux-arm64-glibc-127
-
- # arm64 musl
- - os: ubuntu-20.04
- container: node:16-alpine3.16
- arch: arm64
- node: 16
- binary: linux-arm64-musl-93
- - os: ubuntu-20.04
- arch: arm64
- container: node:18-alpine3.17
- node: 18
- binary: linux-arm64-musl-108
- - os: ubuntu-20.04
- arch: arm64
- container: node:20-alpine3.17
- node: 20
- binary: linux-arm64-musl-115
- - os: ubuntu-20.04
- arch: arm64
- container: node:22-alpine3.18
- node: 22
- binary: linux-arm64-musl-127
-
- # macos x64
- - os: macos-13
- node: 16
- arch: x64
- binary: darwin-x64-93
- - os: macos-13
- node: 18
- arch: x64
- binary: darwin-x64-108
- - os: macos-13
- node: 20
- arch: x64
- binary: darwin-x64-115
- - os: macos-13
- node: 22
- arch: x64
- binary: darwin-x64-127
-
- # macos arm64
- - os: macos-13
- arch: arm64
- node: 16
- target_platform: darwin
- binary: darwin-arm64-93
- - os: macos-13
- arch: arm64
- node: 18
- target_platform: darwin
- binary: darwin-arm64-108
- - os: macos-13
- arch: arm64
- node: 20
- target_platform: darwin
- binary: darwin-arm64-115
- - os: macos-13
- arch: arm64
- node: 22
- target_platform: darwin
- binary: darwin-arm64-127
-
- # windows x64
- - os: windows-2022
- node: 16
- arch: x64
- binary: win32-x64-93
- - os: windows-2022
- node: 18
- arch: x64
- binary: win32-x64-108
- - os: windows-2022
- node: 20
- arch: x64
- binary: win32-x64-115
- - os: windows-2022
- node: 22
- arch: x64
- binary: win32-x64-127
-
- steps:
- - name: Setup (alpine)
- if: contains(matrix.container, 'alpine')
- run: |
- apk add --no-cache build-base git g++ make curl python3
- ln -sf python3 /usr/bin/python
-
- - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
- uses: actions/checkout@v4
- with:
- ref: ${{ env.HEAD_COMMIT }}
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node }}
-
- - name: Restore dependency cache
- uses: actions/cache/restore@v4
- id: restore-dependencies
- with:
- path: ${{ env.CACHED_DEPENDENCY_PATHS }}
- key: ${{ needs.job_build.outputs.dependency_cache_key }}
- enableCrossOsArchive: true
-
- - name: Increase yarn network timeout on Windows
- if: contains(matrix.os, 'windows')
- run: yarn config set network-timeout 600000 -g
-
- - name: Install dependencies
- env:
- SKIP_PLAYWRIGHT_BROWSER_INSTALL: "1"
- if: steps.restore-dependencies.outputs.cache-hit != 'true'
- run: yarn install --ignore-engines --frozen-lockfile
-
- - name: Configure safe directory
- run: |
- git config --global --add safe.directory "*"
-
- - name: Setup python
- uses: actions/setup-python@v5
- if: ${{ !contains(matrix.container, 'alpine') }}
- id: python-setup
- with:
- python-version: '3.8.10'
-
- - name: Setup (arm64| ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }})
- if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin'
- run: |
- sudo apt-get update
- sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
-
- - name: Setup Musl
- if: contains(matrix.container, 'alpine')
- run: |
- cd packages/profiling-node
- curl -OL https://musl.cc/aarch64-linux-musl-cross.tgz
- tar -xzvf aarch64-linux-musl-cross.tgz
- $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version
-
- # configure node-gyp
- - name: Configure node-gyp
- if: matrix.arch != 'arm64'
- run: |
- cd packages/profiling-node
- yarn build:bindings:configure
-
- - name: Configure node-gyp (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }})
- if: matrix.arch == 'arm64' && matrix.target_platform != 'darwin'
- run: |
- cd packages/profiling-node
- yarn build:bindings:configure:arm64
-
- - name: Configure node-gyp (arm64, darwin)
- if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin'
- run: |
- cd packages/profiling-node
- yarn build:bindings:configure:arm64
-
- # build bindings
- - name: Build Bindings
- if: matrix.arch != 'arm64'
- run: |
- yarn lerna run build:bindings --scope @sentry/profiling-node
-
- - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }})
- if: matrix.arch == 'arm64' && contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin'
- run: |
- cd packages/profiling-node
- CC=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc \
- CXX=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++ \
- BUILD_ARCH=arm64 \
- yarn build:bindings
-
- - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }})
- if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin'
- run: |
- cd packages/profiling-node
- CC=aarch64-linux-gnu-gcc \
- CXX=aarch64-linux-gnu-g++ \
- BUILD_ARCH=arm64 \
- yarn build:bindings:arm64
-
- - name: Build Bindings (arm64, darwin)
- if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin'
- run: |
- cd packages/profiling-node
- BUILD_PLATFORM=darwin \
- BUILD_ARCH=arm64 \
- yarn build:bindings:arm64
-
- - name: Build Monorepo
- if: steps.restore-build.outputs.cache-hit != 'true'
- run: yarn build --scope @sentry/profiling-node
-
- - name: Test Bindings
- if: matrix.arch != 'arm64'
- run: |
- yarn lerna run test --scope @sentry/profiling-node
-
- - name: Archive Binary
- uses: actions/upload-artifact@v4
- with:
- name: profiling-node-binaries-${{ github.sha }}-${{ matrix.binary }}
- path: ${{ github.workspace }}/packages/profiling-node/lib/sentry_cpu_profiler-${{matrix.binary}}.node
- if-no-files-found: error
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index 24f25fd1ea9a..9e4577a16d32 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -27,7 +27,7 @@ permissions:
jobs:
job_e2e_prepare:
name: Prepare E2E Canary tests
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out current commit
@@ -54,7 +54,7 @@ jobs:
job_e2e_tests:
name: E2E ${{ matrix.label }} Test
needs: [job_e2e_prepare]
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 20
env:
# We just use a dummy DSN here, only send to the tunnel anyhow
@@ -72,24 +72,6 @@ jobs:
- test-application: 'create-react-app'
build-command: 'test:build-canary'
label: 'create-react-app (canary)'
- - test-application: 'nextjs-app-dir'
- build-command: 'test:build-canary'
- label: 'nextjs-app-dir (canary)'
- - test-application: 'nextjs-app-dir'
- build-command: 'test:build-latest'
- label: 'nextjs-app-dir (latest)'
- - test-application: 'nextjs-13'
- build-command: 'test:build-canary'
- label: 'nextjs-13 (canary)'
- - test-application: 'nextjs-13'
- build-command: 'test:build-latest'
- label: 'nextjs-13 (latest)'
- - test-application: 'nextjs-14'
- build-command: 'test:build-canary'
- label: 'nextjs-14 (canary)'
- - test-application: 'nextjs-14'
- build-command: 'test:build-latest'
- label: 'nextjs-14 (latest)'
- test-application: 'nextjs-15'
build-command: 'test:build-canary'
label: 'nextjs-15 (canary)'
@@ -116,13 +98,12 @@ jobs:
ref: ${{ env.HEAD_COMMIT }}
- uses: pnpm/action-setup@v4
with:
- version: 9.4.0
+ version: 9.15.0
- name: Set up Node
uses: actions/setup-node@v4
with:
- node-version-file: 'dev-packages/e2e-tests/package.json'
-
+ node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Restore canary cache
uses: actions/cache/restore@v4
with:
diff --git a/.github/workflows/clear-cache.yml b/.github/workflows/clear-cache.yml
index 5c327553e3b8..274bc3c4cafe 100644
--- a/.github/workflows/clear-cache.yml
+++ b/.github/workflows/clear-cache.yml
@@ -21,7 +21,7 @@ on:
jobs:
clear-caches:
name: Delete all caches
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml
index 8f63b6ca063b..1c239228a459 100644
--- a/.github/workflows/enforce-license-compliance.yml
+++ b/.github/workflows/enforce-license-compliance.yml
@@ -2,13 +2,22 @@ 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:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
steps:
- name: 'Enforce License Compliance'
uses: getsentry/action-enforce-license-compliance@main
diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml
index e9b1e05a2c92..88702202ac21 100644
--- a/.github/workflows/external-contributors.yml
+++ b/.github/workflows/external-contributors.yml
@@ -12,7 +12,7 @@ jobs:
permissions:
pull-requests: write
contents: write
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
if: |
github.event.pull_request.merged == true
&& github.event.pull_request.author_association != 'COLLABORATOR'
diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml
index 5d0d5af5d247..23f813fa7dc9 100644
--- a/.github/workflows/flaky-test-detector.yml
+++ b/.github/workflows/flaky-test-detector.yml
@@ -23,11 +23,11 @@ concurrency:
jobs:
flaky-detector:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
timeout-minutes: 60
name: 'Check tests for flakiness'
# Also skip if PR is from master -> develop
- if: ${{ github.base_ref != 'master' && github.ref != 'refs/heads/master' }}
+ if: false
steps:
- name: Check out current branch
uses: actions/checkout@v4
diff --git a/.github/workflows/gitflow-sync-develop.yml b/.github/workflows/gitflow-sync-develop.yml
index 893dbbbf56fb..0c56069a2d81 100644
--- a/.github/workflows/gitflow-sync-develop.yml
+++ b/.github/workflows/gitflow-sync-develop.yml
@@ -17,7 +17,7 @@ env:
jobs:
main:
name: Create PR master->develop
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
permissions:
pull-requests: write
contents: write
diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml
index 4bbcb29aba21..59782a467c61 100644
--- a/.github/workflows/release-comment-issues.yml
+++ b/.github/workflows/release-comment-issues.yml
@@ -12,7 +12,7 @@ on:
# This workflow is triggered when a release is published
jobs:
release-comment-issues:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
name: 'Notify issues'
steps:
- name: Get version
diff --git a/.github/workflows/release-size-info.yml b/.github/workflows/release-size-info.yml
index 04e51e5ae14e..b669ced346ba 100644
--- a/.github/workflows/release-size-info.yml
+++ b/.github/workflows/release-size-info.yml
@@ -13,7 +13,7 @@ on:
# It fetches the size-limit info from the release branch and adds it to the release
jobs:
release-size-info:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
name: 'Add size-limit info to release'
steps:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2768f18a5bc8..109c7b5b1bda 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,7 @@ on:
default: master
jobs:
release:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-24.04
name: 'Release a new version'
steps:
- name: Get auth token
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..e7612576c528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,220 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 8.55.1
+
+- fix(v8/browser): Ensure that performance.measure spans have a positive duration ([#19959](https://github.com/getsentry/sentry-javascript/pull/19959))
+- fix(v8/core): Filter `gmo` error and Facebook mobile error ([#15447](https://github.com/getsentry/sentry-javascript/pull/15447))
+
+## 8.55.0
+
+### Important Changes
+
+- **chore(ci/v8): Switch lambda layer name to `SentryNodeServerlessSDKv8` ([#15351](https://github.com/getsentry/sentry-javascript/pull/15351))**
+
+The `SentryNodeServerlessSDK` AWS Lambda Layer will stop receiving updates.
+If you intend to stay on `v8` and receive updates use `SentryNodeServerlessSDKv8` instead.
+
+### Other Changes
+
+- feat(flags/v8): add Statsig browser integration ([#15347](https://github.com/getsentry/sentry-javascript/pull/15347))
+- feat(v8/node): Add missing `vercelAIIntegration` export ([#15339](https://github.com/getsentry/sentry-javascript/pull/15339))
+- feat(v8/nuxt): Add `enabled` to disable Sentry module ([#15337](https://github.com/getsentry/sentry-javascript/pull/15337)) (#15381)
+- feat(v8/vue): Support Pinia v3 ([#15384](https://github.com/getsentry/sentry-javascript/pull/15384))
+- fix(astro): Add vue to `registerEsmLoaderHooks` ([#15352](https://github.com/getsentry/sentry-javascript/pull/15352))
+- fix(react/v8): Support lazy-loaded routes and components ([#15281](https://github.com/getsentry/sentry-javascript/pull/15281))
+- fix(v8/nuxt): Detect Azure Function runtime for flushing with timeout ([#15297](https://github.com/getsentry/sentry-javascript/pull/15297))
+- fix(v8/solidstart): Do not copy release-injection map file ([#15304](https://github.com/getsentry/sentry-javascript/pull/15304))
+- fix(v8/svelte): Guard component tracking `beforeUpdate` call ([#15262](https://github.com/getsentry/sentry-javascript/pull/15262))
+
+Work in this release was contributed by @aryanvdesh. Thank you for your contribution!
+
+## 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..bb4569a8f828 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.55.1",
"main": "index.js",
"license": "MIT",
"engines": {
@@ -9,7 +9,7 @@
"private": true,
"scripts": {
"clean": "rimraf -g suites/**/dist loader-suites/**/dist tmp",
- "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && yarn npx playwright install --with-deps || echo 'Skipping browser installation'",
+ "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && yarn npx playwright install || echo 'Skipping browser installation'",
"lint": "eslint . --format stylish",
"fix": "eslint . --format stylish --fix",
"type-check": "tsc",
@@ -41,9 +41,9 @@
},
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/rrweb": "2.31.0",
- "@sentry/browser": "8.45.0",
+ "@sentry/browser": "8.55.1",
"axios": "1.7.7",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^5.5.0",
diff --git a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts
index bf653dfad6b7..bf48eae56fb2 100644
--- a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts
+++ b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts
@@ -6,7 +6,7 @@ import * as glob from 'glob';
/**
* The number of browsers we run the tests in.
*/
-const NUM_BROWSERS = 3;
+const NUM_BROWSERS = 1;
/**
* Assume that each test runs for 2s.
@@ -53,7 +53,7 @@ ${changedPaths.join('\n')}
const cp = childProcess.spawn(
`npx playwright test ${
testPaths.length ? testPaths.join(' ') : './suites'
- } --reporter='line' --repeat-each ${repeatEachCount}`,
+ } --reporter='line' --repeat-each ${repeatEachCount} --project=chromium`,
{ shell: true, cwd },
);
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts
new file mode 100644
index 000000000000..680105d242e5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts
@@ -0,0 +1 @@
+export const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils.
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts
index ed909b19d1fa..5cf63a4914ed 100644
--- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts
@@ -4,7 +4,7 @@ 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.
+import { FLAG_BUFFER_SIZE } from '../../constants';
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts
index e97cb70761ba..d0adafc3d101 100644
--- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts
@@ -4,7 +4,7 @@ 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.
+import { FLAG_BUFFER_SIZE } from '../../constants';
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts
index a3de589677ea..21db2c0b7d85 100644
--- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts
@@ -4,7 +4,7 @@ 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.
+import { FLAG_BUFFER_SIZE } from '../../constants';
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts
index 719782d0b0ab..bae111a5e6f5 100644
--- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts
@@ -4,7 +4,7 @@ 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.
+import { FLAG_BUFFER_SIZE } from '../../constants';
sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts
new file mode 100644
index 000000000000..5b484b91ba22
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts
@@ -0,0 +1,51 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+
+import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
+
+import { FLAG_BUFFER_SIZE } from '../../constants';
+
+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 = (window as any).statsigClient;
+ for (let i = 1; i <= bufferSize; i++) {
+ client.checkGate(`feat${i}`); // values default to false
+ }
+
+ client.setMockGateValue(`feat${bufferSize + 1}`, true);
+ client.checkGate(`feat${bufferSize + 1}`); // eviction
+
+ client.setMockGateValue('feat3', true);
+ client.checkGate('feat3'); // update
+ }, FLAG_BUFFER_SIZE);
+
+ const reqPromise = waitForErrorRequest(page);
+ await page.locator('#error').click();
+ const req = await reqPromise;
+ const event = envelopeRequestParser(req);
+
+ const expectedFlags = [{ flag: 'feat2', result: false }];
+ for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
+ expectedFlags.push({ flag: 'feat3', result: true });
+
+ expect(event.contexts?.flags?.values).toEqual(expectedFlags);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js
new file mode 100644
index 000000000000..61d00587f4c6
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/browser';
+
+class MockStatsigClient {
+ constructor() {
+ this._gateEvaluationListeners = [];
+ this._mockGateValues = {};
+ }
+
+ on(event, listener) {
+ this._gateEvaluationListeners.push(listener);
+ }
+
+ checkGate(name) {
+ const value = this._mockGateValues[name] || false; // unknown features default to false.
+ this._gateEvaluationListeners.forEach(listener => {
+ listener({ gate: { name, value } });
+ });
+ return value;
+ }
+
+ setMockGateValue(name, value) {
+ this._mockGateValues[name] = value;
+ }
+}
+
+window.statsigClient = new MockStatsigClient();
+
+window.Sentry = Sentry;
+window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryStatsigIntegration],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js
new file mode 100644
index 000000000000..e6697408128c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/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/statsig/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html
new file mode 100644
index 000000000000..9330c6c679f4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts
new file mode 100644
index 000000000000..cf41767036b6
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts
@@ -0,0 +1,69 @@
+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?.isForked === true);
+ const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false);
+
+ await page.evaluate(() => {
+ const Sentry = (window as any).Sentry;
+ const errorButton = document.querySelector('#error') as HTMLButtonElement;
+ const client = (window as any).statsigClient;
+
+ client.setMockGateValue('shared', true);
+ client.setMockGateValue('main', true);
+
+ client.checkGate('shared');
+
+ Sentry.withScope((scope: Scope) => {
+ client.setMockGateValue('forked', true);
+ client.setMockGateValue('shared', false); // override the value in the parent scope.
+
+ client.checkGate('forked');
+ client.checkGate('shared');
+ scope.setTag('isForked', true);
+ errorButton.click();
+ });
+
+ client.checkGate('main');
+ 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: 'forked', result: true },
+ { flag: 'shared', result: false },
+ ]);
+
+ expect(mainEvent.contexts?.flags?.values).toEqual([
+ { flag: 'shared', result: true },
+ { flag: 'main', result: true },
+ ]);
+});
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..7e8065dc6172
--- /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';
+
+import { FLAG_BUFFER_SIZE } from '../../constants';
+
+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/public-api/instrumentation/onError/syntax-errors/test.ts b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/syntax-errors/test.ts
index c11d1897e1c2..51ac86e0cf62 100644
--- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/syntax-errors/test.ts
+++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/syntax-errors/test.ts
@@ -27,7 +27,7 @@ sentryTest('should catch syntax errors', async ({ getLocalTestUrl, page, browser
expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'SyntaxError',
- value: "Unexpected token '{'",
+ value: "Failed to execute 'appendChild' on 'Node': Unexpected token '{'",
mechanism: {
type: 'onerror',
handled: false,
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
index 4f1ba066f5e4..cfadb637717e 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
@@ -28,7 +28,7 @@ This is a quite complex test with the goal to ensure correct recording across mu
navigations and page reloads. In particular, we want to check that all breadcrumbs, spans as
well as the correct DOM snapshots and updates are recorded and sent.
*/
-sentryTest(
+sentryTest.skip(
'record page navigations and performance entries across multiple pages',
async ({ getLocalTestUrl, page, browserName }) => {
// We only test this against the NPM package and replay bundles
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium
index 02a3e3f893d6..3ac45a5017fb 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium
@@ -6,7 +6,7 @@
"type": 1,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -26,7 +26,7 @@
"type": 0,
"id": 9,
"x": 41.810001373291016,
- "y": 18.479999542236328
+ "y": 18.5
},
"timestamp": [timestamp]
},
@@ -42,4 +42,4 @@
},
"timestamp": [timestamp]
}
-]
\ No newline at end of file
+]
diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts
index 0dd12d17fe95..88d84481311c 100644
--- a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts
+++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts
@@ -15,7 +15,7 @@ sentryTest('should start a new session on pageload.', async ({ getLocalTestUrl,
expect(session.status).toBe('ok');
});
-sentryTest('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => {
+sentryTest.skip('should start a new session with navigation.', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });
await page.route('**/foo', (route: Route) => route.continue({ url }));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts
index b127a1f674a1..2682c9e0ec81 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts
@@ -100,6 +100,7 @@ sentryTest(
data: {
'browser.script.invoker': 'BUTTON#clickme.onclick',
'browser.script.invoker_type': 'event-listener',
+ 'code.filepath': 'https://example.com/path/to/script.js',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts
index 51447ee84586..84c69cb7d209 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts
@@ -102,6 +102,7 @@ sentryTest(
data: {
'browser.script.invoker': 'BUTTON#clickme.onclick',
'browser.script.invoker_type': 'event-listener',
+ 'code.filepath': 'https://example.com/path/to/script.js',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long-animation-frame',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts
index 2250fa234faf..8af2921f07e0 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation/test.ts
@@ -4,7 +4,7 @@ import type { Event } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
-sentryTest(
+sentryTest.skip(
"doesn't capture long task spans starting before a navigation in the navigation transaction",
async ({ browserName, getLocalTestUrl, page }) => {
// Long tasks only work on chrome
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..93578d852a7d 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
@@ -9,7 +9,7 @@ import {
shouldSkipTracingTest,
} from '../../../utils/helpers';
-sentryTest('updates the DSC when the txn name is updated and high-quality', async ({ getLocalTestUrl, page }) => {
+sentryTest.skip('updates the DSC when the txn name is updated and high-quality', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}
@@ -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/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts
index 1cfee3b670a7..9620fe1548a5 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts
@@ -12,7 +12,7 @@ import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../.
return the `Timing-Allow-Origin` header.
*/
-sentryTest('captures LCP vitals with element details.', async ({ browserName, getLocalTestUrl, page }) => {
+sentryTest.skip('captures LCP vitals with element details.', async ({ browserName, getLocalTestUrl, page }) => {
if (shouldSkipTracingTest() || browserName !== 'chromium') {
sentryTest.skip();
}
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts
index 3f29c0614f14..edeba006b6b6 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts
@@ -59,7 +59,7 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName
expect(fpValue).toBeGreaterThanOrEqual(ttfbValue!);
});
-sentryTest(
+sentryTest.skip(
'captures time origin and navigation activationStart as span attributes',
async ({ getLocalTestUrl, page }) => {
// Only run in chromium to ensure all vitals are present
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..d8a44b36b98f 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.55.1",
"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..8beaa0516d94 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.55.1",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json
index 6452d7752eba..2c8f1f6bd73e 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.55.1",
"license": "MIT",
"private": true,
"scripts": {
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json
index 682c47d30329..bf61770868ab 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json
@@ -29,7 +29,7 @@
"zone.js": "~0.14.3"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@angular-devkit/build-angular": "^17.1.1",
"@angular/cli": "^17.1.1",
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-18/package.json b/dev-packages/e2e-tests/test-applications/angular-18/package.json
index aec1b1d9dac0..1a578b9482f0 100644
--- a/dev-packages/e2e-tests/test-applications/angular-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json
@@ -29,7 +29,7 @@
"zone.js": "~0.14.3"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@angular-devkit/build-angular": "^18.0.0",
"@angular/cli": "^18.0.0",
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/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json
index c8ae32b52378..351486169392 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json
@@ -32,7 +32,7 @@
"@angular-devkit/build-angular": "^19.0.0",
"@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
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/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json
index 1aa316170a64..cd77dab44c6c 100644
--- a/dev-packages/e2e-tests/test-applications/astro-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json
@@ -14,7 +14,7 @@
"dependencies": {
"@astrojs/check": "0.9.2",
"@astrojs/node": "8.3.2",
- "@playwright/test": "^1.46.0",
+ "@playwright/test": "^1.52.0",
"@sentry/astro": "* || latest",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@spotlightjs/astro": "2.1.6",
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json
index 4e02fc855830..122ec6e1bb41 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/internal-helpers": "^0.4.2",
"@astrojs/node": "^9.0.0",
- "@playwright/test": "^1.46.0",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/astro": "^8.42.0",
"astro": "^5.0.3"
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
index 01375fe0c346..ff8bce5ddf66 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
@@ -14,7 +14,7 @@
},
"devDependencies": {
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@playwright/test": "^1.44.1"
+ "@playwright/test": "^1.52.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json
index 67aa6bc247d5..a6bb1634ee8f 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json
@@ -14,7 +14,7 @@
},
"devDependencies": {
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@playwright/test": "^1.41.1"
+ "@playwright/test": "^1.52.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json
index e91c0ee135e5..f93c8d8a5629 100644
--- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json
@@ -22,7 +22,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json
index 5c362ffb97a1..95d7310db46c 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json
@@ -28,7 +28,7 @@
"source-map-support": "^0.5.21"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^2.7.2",
"@sentry/core": "latest || *",
@@ -48,6 +48,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"tsx": "4.7.2",
"typescript": "^5.1.6",
+ "vite": "^5.4.11",
"vite-tsconfig-paths": "^4.2.1"
},
"volta": {
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..d0ff7bbf049f 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
@@ -25,7 +25,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^2.7.2",
"@sentry/core": "latest || *",
@@ -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/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
index 5c362ffb97a1..409951e4ce6f 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
@@ -28,7 +28,7 @@
"source-map-support": "^0.5.21"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^2.7.2",
"@sentry/core": "latest || *",
@@ -48,6 +48,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"tsx": "4.7.2",
"typescript": "^5.1.6",
+ "vite": "^5.4.11",
"vite-tsconfig-paths": "^4.2.1"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json
index 40842474282a..492bceda99fa 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json
@@ -23,7 +23,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^1.19.3",
"@remix-run/eslint-config": "^1.19.3",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json
index 77058ff26783..275038b302b4 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json
@@ -21,7 +21,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "2.7.2",
"@remix-run/eslint-config": "2.7.2",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
index 977408d0945a..b0df6627ed48 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
@@ -21,7 +21,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "2.7.2",
"@remix-run/eslint-config": "2.7.2",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json
index 7e0e587be0d0..9bb3aa504d8f 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json
@@ -23,7 +23,7 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "^1.19.3",
"@remix-run/eslint-config": "^1.19.3",
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/package.json b/dev-packages/e2e-tests/test-applications/default-browser/package.json
index dc31366f2ea8..74f024b8ae92 100644
--- a/dev-packages/e2e-tests/test-applications/default-browser/package.json
+++ b/dev-packages/e2e-tests/test-applications/default-browser/package.json
@@ -28,7 +28,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"webpack": "^5.91.0",
"serve": "14.0.1",
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/package.json b/dev-packages/e2e-tests/test-applications/ember-classic/package.json
index 4c887cda10ea..0cdab08529c4 100644
--- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json
+++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json
@@ -24,7 +24,7 @@
"@ember/optional-features": "~2.0.0",
"@glimmer/component": "~1.1.2",
"@glimmer/tracking": "~1.1.2",
- "@playwright/test": "~1.44.1",
+ "@playwright/test": "~1.52.0",
"@ember/string": "~3.1.1",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/ember": "latest || *",
@@ -82,5 +82,8 @@
},
"volta": {
"extends": "../../package.json"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
index a8a4db191d81..039cae8be831 100644
--- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
+++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
@@ -50,7 +50,7 @@
"loader.js": "^4.7.0",
"tracked-built-ins": "^3.3.0",
"webpack": "^5.91.0",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry/ember": "latest || *",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@tsconfig/ember": "^3.0.6",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
index 20724e8d3b78..e5546113f14d 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
@@ -25,7 +25,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
index 62606e825e33..71cdb6d8135e 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
@@ -27,7 +27,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
index 44dcda348383..be14838489e2 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
@@ -25,7 +25,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
index 6efae6b1c0d5..0ed0dffa07e3 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
@@ -24,7 +24,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
index 6da132e74a4c..8b63aeeb90a3 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
@@ -26,7 +26,7 @@
"fastify": "^4.28.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
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/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
index 2463d24df940..9bb8eda95892 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
@@ -27,7 +27,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
index 0b03e38ccbdb..f6eb5976f6af 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
@@ -23,7 +23,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
index 8f90e1582598..a902bf8212c1 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
@@ -23,7 +23,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
index de03f89fce27..74fd2f441e24 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
@@ -23,7 +23,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/browser-utils": "latest || *",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay": "latest || *",
@@ -38,18 +38,7 @@
"@sentry/vercel-edge": "latest || *"
},
"volta": {
- "extends": "../../package.json"
- },
- "sentryTest": {
- "optionalVariants": [
- {
- "build-command": "test:build-canary",
- "label": "nextjs-13 (canary)"
- },
- {
- "build-command": "test:build-latest",
- "label": "nextjs-13 (latest)"
- }
- ]
+ "extends": "../../package.json",
+ "node": "20.19.5"
}
}
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/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
index d1ef013e6ccc..eccdb533f99d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
@@ -23,7 +23,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
@@ -38,18 +38,7 @@
"@sentry/vercel-edge": "latest || *"
},
"volta": {
- "extends": "../../package.json"
- },
- "sentryTest": {
- "optionalVariants": [
- {
- "build-command": "test:build-canary",
- "label": "nextjs-14 (canary)"
- },
- {
- "build-command": "test:build-latest",
- "label": "nextjs-14 (latest)"
- }
- ]
+ "extends": "../../package.json",
+ "node": "20.19.5"
}
}
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-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index ca92feb9c254..6ffc8d47896e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -24,7 +24,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
@@ -39,7 +39,8 @@
"@sentry/vercel-edge": "latest || *"
},
"volta": {
- "extends": "../../package.json"
+ "extends": "../../package.json",
+ "node": "20.19.5"
},
"sentryTest": {
"optionalVariants": [
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/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
index 4b09aff7f937..bc023f471fa6 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
@@ -25,7 +25,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
@@ -41,7 +41,8 @@
"ts-node": "10.9.1"
},
"volta": {
- "extends": "../../package.json"
+ "extends": "../../package.json",
+ "node": "20.19.5"
},
"sentryTest": {
"variants": [
@@ -49,16 +50,6 @@
"build-command": "test:build-13",
"label": "nextjs-app-dir (next@13)"
}
- ],
- "optionalVariants": [
- {
- "build-command": "test:build-canary",
- "label": "nextjs-app-dir (canary)"
- },
- {
- "build-command": "test:build-latest",
- "label": "nextjs-app-dir (latest)"
- }
]
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json
index 2fd54b440e2e..536d0e667811 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json
@@ -20,7 +20,6 @@
"@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446",
- "geist": "^1.3.0",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -29,7 +28,7 @@
"zod": "^3.23.3"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@types/eslint": "^8.56.10",
"@types/node": "^20.14.10",
@@ -49,6 +48,7 @@
"initVersion": "7.37.0"
},
"volta": {
- "extends": "../../package.json"
+ "extends": "../../package.json",
+ "node": "20.19.5"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx
index e703260be1a3..3d32967aa290 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/src/app/layout.tsx
@@ -1,6 +1,5 @@
import '~/styles/globals.css';
-import { GeistSans } from 'geist/font/sans';
import { type Metadata } from 'next';
import { TRPCReactProvider } from '~/trpc/react';
@@ -13,7 +12,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
-
+
{children}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts
index 80a667d155d8..9739341e9b52 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts
@@ -1,14 +1,6 @@
import { type Config } from 'tailwindcss';
-import { fontFamily } from 'tailwindcss/defaultTheme';
export default {
content: ['./src/**/*.tsx'],
- theme: {
- extend: {
- fontFamily: {
- sans: ['var(--font-geist-sans)', ...fontFamily.sans],
- },
- },
- },
plugins: [],
} satisfies Config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
index 10630c257349..1d88fc1b896c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
@@ -23,7 +23,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry-internal/feedback": "latest || *",
"@sentry-internal/replay-canvas": "latest || *",
@@ -42,7 +42,8 @@
"import-in-the-middle": "1.11.2"
},
"volta": {
- "extends": "../../package.json"
+ "extends": "../../package.json",
+ "node": "20.19.5"
},
"sentryTest": {
"optional": true,
diff --git a/dev-packages/e2e-tests/test-applications/node-connect/package.json b/dev-packages/e2e-tests/test-applications/node-connect/package.json
index 276e8654f8f4..b80714d54313 100644
--- a/dev-packages/e2e-tests/test-applications/node-connect/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-connect/package.json
@@ -20,7 +20,7 @@
"ts-node": "10.9.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
index 5be4d29bbb38..076abea2010c 100644
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
@@ -25,5 +25,8 @@
},
"volta": {
"extends": "../../package.json"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
index 363c1e06636c..660c4220e5a9 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
@@ -14,7 +14,7 @@
"express": "4.20.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
index 6156211e27f8..8e0f62a889d3 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
@@ -14,7 +14,7 @@
"express": "4.20.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
index 03f483307290..6086cb9dc3f5 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
@@ -14,7 +14,7 @@
"express": "4.20.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
index 844ca51fd038..85a0e20aa580 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
@@ -14,7 +14,7 @@
"express": "4.20.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
index 391514a2c1dd..d79f8ec0d34c 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
@@ -22,7 +22,7 @@
"zod": "~3.22.4"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
index 49e98e2c49ad..8f6d1ebdba2f 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
@@ -19,7 +19,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1"
+ "@playwright/test": "^1.52.0"
},
"volta": {
"extends": "../../package.json"
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..d2a05a5b35be 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express/package.json
@@ -15,16 +15,19 @@
"@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"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
+ "resolutions": {
+ "@types/qs": "6.9.17"
+ },
"volta": {
"extends": "../../package.json"
}
diff --git a/dev-packages/e2e-tests/test-applications/node-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-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
index e0a000572a25..37deaa958b11 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
@@ -20,7 +20,7 @@
"ts-node": "10.9.2"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/package.json b/dev-packages/e2e-tests/test-applications/node-fastify/package.json
index b657eddd1de1..4eb952f28cae 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify/package.json
@@ -20,7 +20,7 @@
"ts-node": "10.9.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json
index 2eda8acc7589..4962a63b0662 100644
--- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json
@@ -16,7 +16,7 @@
"@sentry/node": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json
index 0f6ed61216db..8d504b58dcb9 100644
--- a/dev-packages/e2e-tests/test-applications/node-koa/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json
@@ -18,7 +18,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json
index ed286d4c6886..b2d629e7d61b 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json
@@ -25,7 +25,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/package.json
index f6fbc916ad68..85469027c464 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/package.json
@@ -23,7 +23,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
index 30cd21643eb8..b25a4b32d211 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
@@ -21,7 +21,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
index fd2b9bf4aafe..c82942758144 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
@@ -22,7 +22,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
index efe84d86604e..634119e3a68e 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
@@ -25,7 +25,7 @@
"typescript": "4.9.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
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..0776da6a0972
--- /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.52.0",
+ "@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/.npmrc b/dev-packages/e2e-tests/test-applications/node-profiling/.npmrc
index 949fbddc2343..070f80f05092 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling/.npmrc
+++ b/dev-packages/e2e-tests/test-applications/node-profiling/.npmrc
@@ -1,2 +1,2 @@
-# @sentry:registry=http://127.0.0.1:4873
-# @sentry-internal:registry=http://127.0.0.1:4873
+@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-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..4263755f22e4 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",
@@ -13,13 +13,13 @@
},
"dependencies": {
"@electron/rebuild": "^3.7.0",
- "@playwright/test": "^1.48.2",
+ "@playwright/test": "^1.52.0",
"@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/nuxt-3-dynamic-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
index 9379acaf978a..f60139973266 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts
@@ -11,12 +11,6 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
sentry: {
autoInjectServerSentry: 'experimental_dynamic-import',
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
index ac18cebec975..26d9d56f3adf 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
@@ -19,11 +19,14 @@
},
"devDependencies": {
"@nuxt/test-utils": "^3.14.1",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"overrides": {
"nitropack": "~2.9.7",
"ofetch": "^1.4.0"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
index 54bacf4ee358..344bee932277 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
@@ -21,11 +21,12 @@
},
"devDependencies": {
"@nuxt/test-utils": "^3.14.1",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"overrides": {
"nitropack": "2.10.0",
- "ofetch": "1.4.0"
+ "ofetch": "1.4.0",
+ "@vercel/nft": "^0.27.4"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts
index 6cea405151bd..91c0ab62237b 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts
@@ -1,16 +1,12 @@
import { fileURLToPath } from 'node:url';
-import type { ConfigOptions } from '@nuxt/test-utils/playwright';
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-const nuxtConfigOptions: ConfigOptions = {
+const nuxtConfigOptions = {
nuxt: {
rootDir: fileURLToPath(new URL('.', import.meta.url)),
},
};
-/* Make sure to import from '@nuxt/test-utils/playwright' in the tests
- * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */
-
const config = getPlaywrightConfig({
startCommand: `pnpm start:import`,
use: { ...nuxtConfigOptions },
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts
index 66f86755218e..0de0c3dd498a 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from '@nuxt/test-utils/playwright';
+import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
test.describe('client-side errors', async () => {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
index b726636974c9..1f5a6aee4891 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from '@nuxt/test-utils/playwright';
+import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import type { Span } from '@sentry/nuxt';
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
index d5828016d034..fbac0aa482e9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts
@@ -11,12 +11,6 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
sentry: {
autoInjectServerSentry: 'top-level-import',
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
index 9d3dc0066912..95098e6314a7 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
@@ -19,7 +19,10 @@
},
"devDependencies": {
"@nuxt/test-utils": "^3.14.1",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
index 87e046ed39e9..0fcccd560af9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
@@ -11,10 +11,4 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
index 80b76aed58ac..f77960753b62 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
@@ -20,7 +20,10 @@
},
"devDependencies": {
"@nuxt/test-utils": "^3.14.1",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
index da988a9ee003..a7540c06d656 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
@@ -13,10 +13,4 @@ export default defineNuxtConfig({
},
},
},
- nitro: {
- rollupConfig: {
- // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output
- external: [/@sentry\/.*/],
- },
- },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index 0a278f07eedd..38ecefd239eb 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -21,10 +21,13 @@
},
"devDependencies": {
"@nuxt/test-utils": "^3.14.2",
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"overrides": {
"@vercel/nft": "0.27.4"
+ },
+ "sentryTest": {
+ "optional": true
}
}
diff --git a/dev-packages/e2e-tests/test-applications/react-17/package.json b/dev-packages/e2e-tests/test-applications/react-17/package.json
index 9f6762325609..323c30642e0d 100644
--- a/dev-packages/e2e-tests/test-applications/react-17/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-17/package.json
@@ -42,7 +42,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json
index 5de946437a44..8efbeedcc71b 100644
--- a/dev-packages/e2e-tests/test-applications/react-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-19/package.json
@@ -42,7 +42,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1"
},
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..95cd1344d182
--- /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.52.0",
+ "@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..c7ad16eebcf7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx
@@ -0,0 +1,75 @@
+import * as Sentry from '@sentry/react';
+import React, { lazy, Suspense } 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 LazyLoadedUser = lazy(() => import('./pages/LazyLoadedUser'));
+
+const router = sentryCreateBrowserRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/lazy-loaded-user/*',
+ element: (
+ Loading...}>
+
+
+ ),
+ },
+ {
+ 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..12bfb12ec3a9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx
@@ -0,0 +1,26 @@
+// 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
+
+
+ lazy navigate
+
+ >
+ );
+};
+
+export default Index;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx
new file mode 100644
index 000000000000..1410df69124b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedInnerRoute.tsx
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/react';
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+import { Route, Routes } from 'react-router-dom';
+
+const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
+
+const InnerRoute = () => (
+
+ I am a lazy loaded user} />
+
+);
+
+export default InnerRoute;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx
new file mode 100644
index 000000000000..636f99d9c8cb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/LazyLoadedUser.tsx
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/react';
+import * as React from 'react';
+import { Route, Routes } from 'react-router-dom';
+
+const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
+const InnerRoute = React.lazy(() => import('./LazyLoadedInnerRoute'));
+
+const LazyLoadedUser = () => {
+ return (
+
+ Loading...}>
+
+
+ }
+ />
+
+ );
+};
+
+export default LazyLoadedUser;
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..c35d731915d6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts
@@ -0,0 +1,180 @@
+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([]);
+});
+
+test('Captures a lazy pageload transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
+ return event.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/lazy-loaded-user/5/foo');
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent.contexts?.trace).toEqual({
+ 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',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/lazy-loaded-user/:id/:innerId',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(await page.innerText('id=content')).toContain('I am a lazy loaded user');
+
+ expect(transactionEvent.spans).toEqual(
+ expect.arrayContaining([
+ // This one is the outer lazy route
+ expect.objectContaining({
+ op: 'resource.script',
+ origin: 'auto.resource.browser.metrics',
+ }),
+ // This one is the inner lazy route
+ expect.objectContaining({
+ op: 'resource.script',
+ origin: 'auto.resource.browser.metrics',
+ }),
+ ]),
+ );
+});
+
+test('Captures a lazy 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=lazy-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: '/lazy-loaded-user/:id/:innerId',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(await page.innerText('id=content')).toContain('I am a lazy loaded user');
+
+ expect(transactionEvent.spans).toEqual(
+ expect.arrayContaining([
+ // This one is the outer lazy route
+ expect.objectContaining({
+ op: 'resource.script',
+ origin: 'auto.resource.browser.metrics',
+ }),
+ // This one is the inner lazy route
+ expect.objectContaining({
+ op: 'resource.script',
+ origin: 'auto.resource.browser.metrics',
+ }),
+ ]),
+ );
+});
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-hash-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
index e475fb505fc8..b710d56fca99 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
@@ -41,7 +41,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1"
},
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..1b161497af0a
--- /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.52.0",
+ "@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-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
index 0b208b3f5a65..bff16b2bbff1 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
@@ -44,7 +44,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
index ec6d7b05fee3..c429c4ea2acd 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
@@ -44,7 +44,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1",
"npm-run-all2": "^6.2.0"
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-router-6-use-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
index ca78e6af7310..bbacfe956ea2 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
@@ -41,7 +41,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json
index d086c765091c..180895d8583f 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json
@@ -44,7 +44,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"serve": "14.0.1",
"npm-run-all2": "^6.2.0"
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
index 1313fe2eed0e..24f729cd1b5b 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
@@ -11,7 +11,7 @@
"react-router": "^7.0.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"vite": "^6.0.1",
"@vitejs/plugin-react": "^4.3.4",
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
index 836707b3017f..0cd577ac750d 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
@@ -42,7 +42,7 @@
]
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"serve": "14.0.1"
},
"volta": {
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..92ccade57425 100644
--- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json
@@ -14,14 +14,14 @@
},
"license": "MIT",
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"autoprefixer": "^10.4.17",
"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..90aa89c27655 100644
--- a/dev-packages/e2e-tests/test-applications/solid/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid/package.json
@@ -14,14 +14,14 @@
},
"license": "MIT",
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"autoprefixer": "^10.4.17",
"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..864b2a0ddf6e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
@@ -0,0 +1,40 @@
+{
+ "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.52.0",
+ "@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"
+ },
+ "sentryTest": {
+ "optional": true
+ }
+}
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..4f54812b4fa6 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
@@ -15,7 +15,7 @@
"@sentry/solidstart": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -27,11 +27,14 @@
"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"
},
"overrides": {
"@vercel/nft": "0.27.4"
+ },
+ "sentryTest": {
+ "optional": true
}
}
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..94d5ad7fee3e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
@@ -0,0 +1,40 @@
+{
+ "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.52.0",
+ "@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"
+ },
+ "sentryTest": {
+ "optional": true
+ }
+}
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..cea890901ae2 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json
@@ -15,7 +15,7 @@
"@sentry/solidstart": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.13.4",
"@solidjs/start": "^1.0.2",
@@ -27,11 +27,14 @@
"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"
},
"overrides": {
"@vercel/nft": "0.27.4"
+ },
+ "sentryTest": {
+ "optional": true
}
}
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..69e75e5d2c8a 100644
--- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test:prod"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
@@ -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..831a580dacaa 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
@@ -19,17 +19,17 @@
"@spotlightjs/spotlight": "2.0.0-alpha.1"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
- "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/kit": "2.41.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^5.0.0-next.115",
"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-svelte-5/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
index 95855d8f8e81..6a29276f4dc8 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
@@ -316,7 +316,7 @@ test.describe('performance events', () => {
'sentry.origin': 'auto.navigation.sveltekit',
'sentry.op': 'navigation',
'sentry.source': 'route',
- 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.type': 'link',
'sentry.sveltekit.navigation.from': '/',
'sentry.sveltekit.navigation.to': '/redirect2',
'sentry.sample_rate': 1,
@@ -337,7 +337,7 @@ test.describe('performance events', () => {
'sentry.origin': 'auto.ui.sveltekit',
'sentry.sveltekit.navigation.from': '/',
'sentry.sveltekit.navigation.to': '/redirect2',
- 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.type': 'link',
},
});
@@ -353,7 +353,7 @@ test.describe('performance events', () => {
'sentry.origin': 'auto.navigation.sveltekit',
'sentry.op': 'navigation',
'sentry.source': 'route',
- 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.type': 'link',
'sentry.sveltekit.navigation.from': '/',
'sentry.sveltekit.navigation.to': '/users/[id]',
'sentry.sample_rate': 1,
@@ -374,7 +374,7 @@ test.describe('performance events', () => {
'sentry.origin': 'auto.ui.sveltekit',
'sentry.sveltekit.navigation.from': '/',
'sentry.sveltekit.navigation.to': '/users/[id]',
- 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.type': 'link',
},
});
});
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..45c832e4c68d 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
@@ -18,7 +18,7 @@
"@sentry/sveltekit": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
@@ -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..de7069a1b8cd 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
@@ -18,17 +18,17 @@
"@sentry/sveltekit": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^2.0.0",
- "@sveltejs/kit": "^2.5.0",
+ "@sveltejs/kit": "2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"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/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json
index 369e1715adcb..aa53032150cd 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json
@@ -17,7 +17,7 @@
"@sentry/sveltekit": "latest || *"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@sveltejs/adapter-auto": "^2.0.0",
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..625d1be2e6a8 100644
--- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
@@ -24,8 +24,8 @@
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.2.2",
- "vite": "^5.4.10",
- "@playwright/test": "^1.44.1",
+ "vite": "^5.4.11",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils"
},
"volta": {
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..05969e11fa60 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json
@@ -16,12 +16,12 @@
},
"dependencies": {
"@sentry/vue": "latest || *",
- "pinia": "^2.2.3",
+ "pinia": "^3.0.0",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *",
"@tsconfig/node20": "^20.1.2",
@@ -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/src/stores/cart.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/stores/cart.ts
index 7786c7f27cd9..2f406b19b5a6 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/src/stores/cart.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/src/stores/cart.ts
@@ -1,7 +1,6 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
-export const useCartStore = defineStore({
- id: 'cart',
+export const useCartStore = defineStore('cart', {
state: () => ({
rawItems: [] as string[],
}),
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/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
index 2195742a148a..0f10e458d831 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
@@ -8,7 +8,7 @@
"test:assert": "playwright test"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/browser": "latest || *",
"webpack": "^4.47.0",
diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/package.json b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
index 389f817292cd..13eff5a73060 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
@@ -8,7 +8,7 @@
"test:assert": "playwright test"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
+ "@playwright/test": "^1.52.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/browser": "latest || *",
"webpack": "^5.91.0",
diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json
index a4857bd41082..9dd45be50b4e 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.55.1",
"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..9ab54b7289f3 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.55.1",
"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.55.1",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
"@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/amqplib/test.ts b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
index 7a3eb6414237..577fe8c862a4 100644
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
@@ -26,7 +26,8 @@ const EXPECTED_MESSAGE_SPAN_CONSUMER = expect.objectContaining({
status: 'ok',
});
-describe('amqplib auto-instrumentation', () => {
+// eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests
+describe.skip('amqplib auto-instrumentation', () => {
afterAll(async () => {
cleanupChildProcesses();
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
index 92fc857ed4e8..2ad83409022d 100644
--- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
@@ -170,7 +170,8 @@ describe('MongoDB experimental Test', () => {
],
};
- test('CJS - should auto-instrument `mongodb` package.', done => {
+ // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests
+ test.skip('CJS - should auto-instrument `mongodb` package.', done => {
createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
});
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts
index 30eeb62ffe31..c760c8a120ac 100644
--- a/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/mongoose/test.ts
@@ -45,7 +45,8 @@ describe('Mongoose experimental Test', () => {
]),
};
- test('CJS - should auto-instrument `mongoose` package.', done => {
+ // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests
+ test.skip('CJS - should auto-instrument `mongoose` package.', done => {
createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).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..271cf1600a55 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.55.1",
"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..a265613f6399 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.55.1",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json
index 09ad4cf5a55d..dd658d0bcfe5 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.55.1",
"name": "@sentry-internal/test-utils",
"author": "Sentry",
"license": "MIT",
@@ -41,11 +41,11 @@
"clean": "rimraf -g ./node_modules ./build"
},
"peerDependencies": {
- "@playwright/test": "^1.44.1"
+ "@playwright/test": "^1.52.0"
},
"devDependencies": {
- "@playwright/test": "^1.44.1",
- "@sentry/core": "8.45.0"
+ "@playwright/test": "^1.52.0",
+ "@sentry/core": "8.55.1"
},
"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..7f49739d1edf 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.55.1",
"npmClient": "yarn"
}
diff --git a/package.json b/package.json
index e948ae773c72..2d13bebdd72f 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"volta": {
"node": "18.20.3",
"yarn": "1.22.22",
- "pnpm": "9.4.0"
+ "pnpm": "9.15.9"
},
"workspaces": [
"packages/angular",
@@ -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..78022563abc3 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/angular",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -12,7 +12,8 @@
"type": "module",
"module": "build/fesm2015/sentry-angular.mjs",
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"peerDependencies": {
"@angular/common": ">= 14.x <= 19.x",
@@ -21,8 +22,8 @@
"rxjs": "^6.5.5 || ^7.x"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.55.1",
+ "@sentry/core": "8.55.1",
"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..7f013749e77a 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/astro",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -50,20 +50,21 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"peerDependencies": {
"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.55.1",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
"@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..a7daa64fb6ee 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -138,6 +138,8 @@ export {
startSpanManual,
tediousIntegration,
trpcMiddleware,
+ updateSpanName,
+ vercelAIIntegration,
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 {
expect(init({})).not.toBeUndefined();
});
});
+
+ describe('mergeRegisterEsmLoaderHooks', () => {
+ it('merges exclude array when registerEsmLoaderHooks is an object with an exclude array', () => {
+ const options: NodeOptions = {
+ registerEsmLoaderHooks: { exclude: [/test/] },
+ };
+ const result = mergeRegisterEsmLoaderHooks(options);
+ expect(result).toEqual({ exclude: [/test/, /vue/] });
+ });
+
+ it('sets exclude array when registerEsmLoaderHooks is an object without an exclude array', () => {
+ const options: NodeOptions = {
+ registerEsmLoaderHooks: {},
+ };
+ const result = mergeRegisterEsmLoaderHooks(options);
+ expect(result).toEqual({ exclude: [/vue/] });
+ });
+
+ it('returns boolean when registerEsmLoaderHooks is a boolean', () => {
+ const options1: NodeOptions = {
+ registerEsmLoaderHooks: true,
+ };
+ const result1 = mergeRegisterEsmLoaderHooks(options1);
+ expect(result1).toBe(true);
+
+ const options2: NodeOptions = {
+ registerEsmLoaderHooks: false,
+ };
+ const result2 = mergeRegisterEsmLoaderHooks(options2);
+ expect(result2).toBe(false);
+ });
+
+ it('sets exclude array when registerEsmLoaderHooks is undefined', () => {
+ const options: NodeOptions = {};
+ const result = mergeRegisterEsmLoaderHooks(options);
+ expect(result).toEqual({ exclude: [/vue/] });
+ });
+ });
});
diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json
index 856a7dc4f51f..728ec55f8873 100644
--- a/packages/aws-serverless/package.json
+++ b/packages/aws-serverless/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/aws-serverless",
- "version": "8.45.0",
+ "version": "8.55.1",
"description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless",
@@ -61,15 +61,16 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/instrumentation": "^0.56.0",
"@opentelemetry/instrumentation-aws-lambda": "0.49.0",
"@opentelemetry/instrumentation-aws-sdk": "0.48.0",
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
"@types/aws-lambda": "^8.10.62"
},
"devDependencies": {
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 3f167b62a7e3..12160ae3501a 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -121,11 +121,13 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
trpcMiddleware,
+ updateSpanName,
// eslint-disable-next-line deprecation/deprecation
addOpenTelemetryInstrumentation,
zodErrorsIntegration,
profiler,
amqplibIntegration,
+ vercelAIIntegration,
} from '@sentry/node';
export {
diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts
index fc67aaa432ef..a6633fca14f8 100644
--- a/packages/aws-serverless/src/sdk.ts
+++ b/packages/aws-serverless/src/sdk.ts
@@ -220,10 +220,7 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi
* @param context AWS Lambda context that will be used to extract some part of the data
*/
function enhanceScopeWithTransactionData(scope: Scope, context: Context): void {
- scope.addEventProcessor(event => {
- 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..7fac0bfa8f3d 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.55.1",
"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",
@@ -36,10 +36,11 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.55.1"
},
"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..8ba806fdda1c 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';
@@ -419,7 +425,7 @@ export function _addMeasureSpans(
startTime: number,
duration: number,
timeOrigin: number,
-): number {
+): void {
const navEntry = getNavigationEntry(false);
const requestTime = msToSec(navEntry ? navEntry.requestStart : 0);
// Because performance.measure accepts arbitrary timestamps it can produce
@@ -444,13 +450,14 @@ export function _addMeasureSpans(
attributes['sentry.browser.measure_start_time'] = measureStartTimestamp;
}
- startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, {
- name: entry.name as string,
- op: entry.entryType as string,
- attributes,
- });
-
- return measureStartTimestamp;
+ // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process.
+ if (measureStartTimestamp <= measureEndTimestamp) {
+ startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, {
+ name: entry.name as string,
+ op: entry.entryType as string,
+ attributes,
+ });
+ }
}
/** Instrument navigation entries */
@@ -596,6 +603,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..27d489eae140 100644
--- a/packages/browser-utils/test/browser/browserMetrics.test.ts
+++ b/packages/browser-utils/test/browser/browserMetrics.test.ts
@@ -92,6 +92,29 @@ describe('_addMeasureSpans', () => {
}),
);
});
+
+ it('drops measurement spans with negative duration', () => {
+ const spans: Span[] = [];
+
+ getClient()?.on('spanEnd', span => {
+ spans.push(span);
+ });
+
+ const entry = {
+ entryType: 'measure',
+ name: 'measure-1',
+ duration: 10,
+ startTime: 12,
+ } as PerformanceEntry;
+
+ const timeOrigin = 100;
+ const startTime = 23;
+ const duration = -50;
+
+ _addMeasureSpans(span, entry, startTime, duration, timeOrigin);
+
+ expect(spans).toHaveLength(0);
+ });
});
describe('_addResourceSpans', () => {
@@ -131,6 +154,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 256,
decodedBodySize: 256,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, resourceEntryName, 123, 456, 100);
@@ -150,6 +174,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 +194,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 456,
decodedBodySize: 593,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'http/1.1',
});
const timeOrigin = 100;
@@ -195,6 +221,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 +261,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 +283,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 0,
decodedBodySize: 0,
renderBlockingStatus: 'non-blocking',
+ nextHopProtocol: 'h2',
});
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -271,6 +301,8 @@ describe('_addResourceSpans', () => {
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.same_origin']: true,
+ ['network.protocol.name']: 'http',
+ ['network.protocol.version']: '2',
},
}),
);
@@ -288,6 +320,7 @@ describe('_addResourceSpans', () => {
transferSize: 2147483647,
encodedBodySize: 2147483647,
decodedBodySize: 2147483647,
+ nextHopProtocol: 'h3',
});
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -301,6 +334,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 +360,7 @@ describe('_addResourceSpans', () => {
transferSize: null,
encodedBodySize: null,
decodedBodySize: null,
+ nextHopProtocol: 'h3',
} as unknown as PerformanceResourceTiming;
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -338,6 +374,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 +403,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..a7e0c4f5c0a8 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/browser",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,17 +36,18 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"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.55.1",
+ "@sentry-internal/feedback": "8.55.1",
+ "@sentry-internal/replay": "8.55.1",
+ "@sentry-internal/replay-canvas": "8.55.1",
+ "@sentry/core": "8.55.1"
},
"devDependencies": {
- "@sentry-internal/integration-shims": "8.45.0",
+ "@sentry-internal/integration-shims": "8.55.1",
"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..5ee3f150d0ab 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -75,3 +75,5 @@ export {
} from './integrations/featureFlags';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
+export { unleashIntegration } from './integrations/featureFlags/unleash';
+export { statsigIntegration } from './integrations/featureFlags/statsig';
diff --git a/packages/browser/src/integrations/featureFlags/statsig/index.ts b/packages/browser/src/integrations/featureFlags/statsig/index.ts
new file mode 100644
index 000000000000..a208e4c7dc0e
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/statsig/index.ts
@@ -0,0 +1 @@
+export { statsigIntegration } from './integration';
diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts
new file mode 100644
index 000000000000..7870f1250032
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts
@@ -0,0 +1,46 @@
+import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core';
+
+import { defineIntegration } from '@sentry/core';
+import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags';
+import type { FeatureGate, StatsigClient } from './types';
+
+/**
+ * Sentry integration for capturing feature flag evaluations from the Statsig js-client SDK.
+ *
+ * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information.
+ *
+ * @example
+ * ```
+ * import { StatsigClient } from '@statsig/js-client';
+ * import * as Sentry from '@sentry/browser';
+ *
+ * const statsigClient = new StatsigClient();
+ *
+ * Sentry.init({
+ * dsn: '___PUBLIC_DSN___',
+ * integrations: [Sentry.statsigIntegration({featureFlagClient: statsigClient})],
+ * });
+ *
+ * await statsigClient.initializeAsync(); // or statsigClient.initializeSync();
+ *
+ * const result = statsigClient.checkGate('my-feature-gate');
+ * Sentry.captureException(new Error('something went wrong'));
+ * ```
+ */
+export const statsigIntegration = defineIntegration(
+ ({ featureFlagClient: statsigClient }: { featureFlagClient: StatsigClient }) => {
+ return {
+ name: 'Statsig',
+
+ processEvent(event: Event, _hint: EventHint, _client: Client): Event {
+ return copyFlagsFromScopeToEvent(event);
+ },
+
+ setup() {
+ statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => {
+ insertFlagToScope(event.gate.name, event.gate.value);
+ });
+ },
+ };
+ },
+) satisfies IntegrationFn;
diff --git a/packages/browser/src/integrations/featureFlags/statsig/types.ts b/packages/browser/src/integrations/featureFlags/statsig/types.ts
new file mode 100644
index 000000000000..ff0ea820d31f
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/statsig/types.ts
@@ -0,0 +1,15 @@
+export type FeatureGate = {
+ readonly name: string;
+ readonly value: boolean;
+};
+
+type EventNameToEventDataMap = {
+ gate_evaluation: { gate: FeatureGate };
+};
+
+export interface StatsigClient {
+ on(
+ event: keyof EventNameToEventDataMap,
+ callback: (data: EventNameToEventDataMap[keyof EventNameToEventDataMap]) => void,
+ ): void;
+}
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..c180cc101052
--- /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 ? 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..2217a26eb333 100644
--- a/packages/bun/package.json
+++ b/packages/bun/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/bun",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,12 +36,13 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
- "@sentry/opentelemetry": "8.45.0"
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1"
},
"devDependencies": {
"bun-types": "latest"
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 1ba5f2de4786..f55648ef2415 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -141,11 +141,13 @@ export {
spanToTraceHeader,
spanToBaggageHeader,
trpcMiddleware,
+ updateSpanName,
// eslint-disable-next-line deprecation/deprecation
addOpenTelemetryInstrumentation,
zodErrorsIntegration,
profiler,
amqplibIntegration,
+ vercelAIIntegration,
} from '@sentry/node';
export {
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..6fc258952668 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/cloudflare",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,10 +36,11 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.55.1"
},
"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..c4c36f1a3669 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/core",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,12 +36,14 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"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/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts
index 9d9f803a69f5..6234f92e499d 100644
--- a/packages/core/src/integrations/inboundfilters.ts
+++ b/packages/core/src/integrations/inboundfilters.ts
@@ -13,11 +13,13 @@ const DEFAULT_IGNORE_ERRORS = [
/^Javascript error: Script error\.? on line 0$/,
/^ResizeObserver loop completed with undelivered notifications.$/, // The browser logs this when a ResizeObserver handler takes a bit longer. Usually this is not an actual issue though. It indicates slowness.
/^Cannot redefine property: googletag$/, // This is thrown when google tag manager is used in combination with an ad blocker
+ /^Can't find variable: gmo$/, // Error from Google Search App https://issuetracker.google.com/issues/396043331
"undefined is not an object (evaluating 'a.L')", // Random error that happens but not actionable or noticeable to end-users.
'can\'t redefine non-configurable property "solana"', // Probably a browser extension or custom browser (Brave) throwing this error
"vv().getRestrictions is not a function. (In 'vv().getRestrictions(1,a)', 'vv().getRestrictions' is undefined)", // Error thrown by GTM, seemingly not affecting end-users
"Can't find variable: _AutofillCallbackHandler", // Unactionable error in instagram webview https://developers.facebook.com/community/threads/320013549791141/
/^Non-Error promise rejection captured with value: Object Not Found Matching Id:\d+, MethodName:simulateEvent, ParamCount:\d+$/, // unactionable error from CEFSharp, a .NET library that embeds chromium in .NET apps
+ /^Java exception was raised during method invocation$/, // error from Facebook Mobile browser (https://github.com/getsentry/sentry-javascript/issues/15065)
];
/** Options for the InboundFilters integration */
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/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts
index 046ee5a168d7..e06c6bda0da2 100644
--- a/packages/core/test/lib/integrations/inboundfilters.test.ts
+++ b/packages/core/test/lib/integrations/inboundfilters.test.ts
@@ -269,6 +269,12 @@ const GOOGLETAG_EVENT: Event = {
},
};
+const GOOGLE_APP_GMO: Event = {
+ exception: {
+ values: [{ type: 'ReferenceError', value: "Can't find variable: gmo" }],
+ },
+};
+
const CEFSHARP_EVENT: Event = {
exception: {
values: [
@@ -281,6 +287,17 @@ const CEFSHARP_EVENT: Event = {
},
};
+const FB_MOBILE_BROWSER_EVENT: Event = {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Java exception was raised during method invocation',
+ },
+ ],
+ },
+};
+
const MALFORMED_EVENT: Event = {
exception: {
values: [
@@ -397,11 +414,21 @@ describe('InboundFilters', () => {
expect(eventProcessor(GOOGLETAG_EVENT, {})).toBe(null);
});
+ it('uses default filters (Google App "gmo")', () => {
+ const eventProcessor = createInboundFiltersEventProcessor();
+ expect(eventProcessor(GOOGLE_APP_GMO, {})).toBe(null);
+ });
+
it('uses default filters (CEFSharp)', () => {
const eventProcessor = createInboundFiltersEventProcessor();
expect(eventProcessor(CEFSHARP_EVENT, {})).toBe(null);
});
+ it('uses default filters (FB Mobile Browser)', () => {
+ const eventProcessor = createInboundFiltersEventProcessor();
+ expect(eventProcessor(FB_MOBILE_BROWSER_EVENT, {})).toBe(null);
+ });
+
it('filters on last exception when multiple present', () => {
const eventProcessor = createInboundFiltersEventProcessor({
ignoreErrors: ['incorrect type given for parameter `chewToy`'],
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..2f9b1295cb6f 100644
--- a/packages/deno/package.json
+++ b/packages/deno/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/deno",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -18,13 +18,14 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"files": [
"/build"
],
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.55.1"
},
"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..c37dbe3efb5a 100644
--- a/packages/ember/package.json
+++ b/packages/ember/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/ember",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -10,7 +10,8 @@
"ember-addon"
],
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"directories": {
"doc": "doc",
@@ -32,8 +33,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.55.1",
+ "@sentry/core": "8.55.1",
"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..0b966860dea3 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.55.1",
"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",
@@ -19,11 +19,12 @@
],
"main": "src/index.js",
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry-internal/eslint-plugin-sdk": "8.45.0",
- "@sentry-internal/typescript": "8.45.0",
+ "@sentry-internal/eslint-plugin-sdk": "8.55.1",
+ "@sentry-internal/typescript": "8.55.1",
"@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..e8a111d8dd12 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.55.1",
"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",
@@ -19,7 +19,8 @@
],
"main": "src/index.js",
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"scripts": {
"clean": "yarn rimraf sentry-internal-eslint-plugin-sdk-*.tgz",
diff --git a/packages/feedback/package.json b/packages/feedback/package.json
index a5e44856378e..886dfe3740f3 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.55.1",
"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",
@@ -36,10 +36,11 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.55.1"
},
"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..1e1668b2a7f2 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/gatsby",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -42,11 +42,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/react": "8.45.0",
+ "@sentry/core": "8.55.1",
+ "@sentry/react": "8.55.1",
"@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..0ea33fc92f68 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.55.1",
"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",
@@ -45,11 +45,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
"@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..76353d56bdc7 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,
@@ -126,6 +127,7 @@ export {
// eslint-disable-next-line deprecation/deprecation
processThreadBreadcrumbIntegration,
childProcessIntegration,
+ vercelAIIntegration,
} from '@sentry/node';
export {
diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json
index 322922a945a0..9641183517e3 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.55.1",
"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.55.1"
},
"engines": {
"node": ">=14.18"
diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json
index 1bef4cc56c2f..3bc159a3f67e 100644
--- a/packages/nestjs/package.json
+++ b/packages/nestjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nestjs",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -41,11 +41,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0"
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1"
},
"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..217a92e82b95 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nextjs",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -73,18 +73,19 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
"@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.55.1",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1",
+ "@sentry/react": "8.55.1",
+ "@sentry/vercel-edge": "8.55.1",
"@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..454349e86766 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.55.1",
"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.55.1"
},
"devDependencies": {
"rollup": "^4.24.4"
diff --git a/packages/node/package.json b/packages/node/package.json
index 618b9aa89725..939c1bc93f1e 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/node",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -62,43 +62,44 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"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.55.1",
+ "@sentry/opentelemetry": "8.55.1",
"import-in-the-middle": "^1.11.2"
},
"devDependencies": {
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index fa16ac4e6b3d..365124aad030 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -36,6 +36,7 @@ export { dataloaderIntegration } from './integrations/tracing/dataloader';
export { amqplibIntegration } from './integrations/tracing/amqplib';
// eslint-disable-next-line deprecation/deprecation
export { processThreadBreadcrumbIntegration, childProcessIntegration } from './integrations/childProcess';
+export { vercelAIIntegration } from './integrations/tracing/vercelai';
export { SentryContextManager } from './otel/contextManager';
export { generateInstrumentOnce } from './otel/instrument';
@@ -63,9 +64,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 +108,7 @@ export {
getIsolationScope,
getTraceData,
getTraceMetaTags,
+ continueTrace,
withScope,
withIsolationScope,
captureException,
@@ -142,6 +141,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..77237fc191ec 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nuxt",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,20 +36,21 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"peerDependencies": {
"nuxt": ">=3.7.0 || 4.x"
},
"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.55.1",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1",
"@sentry/rollup-plugin": "2.22.7",
"@sentry/vite-plugin": "2.22.6",
- "@sentry/vue": "8.45.0"
+ "@sentry/vue": "8.55.1"
},
"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..c772f6491f74 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.
@@ -88,6 +95,13 @@ type SourceMapsOptions = {
* Build options for the Sentry module. These options are used during build-time by the Sentry SDK.
*/
export type SentryNuxtModuleOptions = {
+ /**
+ * Enable the Sentry Nuxt Module.
+ *
+ * @default true
+ */
+ enabled?: boolean;
+
/**
* Options for the Sentry Vite plugin to customize the source maps upload process.
*
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/module.ts b/packages/nuxt/src/module.ts
index e246430f69d6..de8050cbd158 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -18,6 +18,10 @@ export default defineNuxtModule({
},
defaults: {},
setup(moduleOptionsParam, nuxt) {
+ if ('enabled' in moduleOptionsParam && moduleOptionsParam.enabled === false) {
+ return;
+ }
+
const moduleOptions = {
...moduleOptionsParam,
autoInjectServerSentry: moduleOptionsParam.autoInjectServerSentry,
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..7fd3526f7883 100644
--- a/packages/opentelemetry/package.json
+++ b/packages/opentelemetry/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/opentelemetry",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,23 +36,26 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/core": "8.45.0"
+ "@sentry/core": "8.55.1"
},
"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/README.md b/packages/profiling-node/README.md
index 890022ae2e88..255b720947c8 100644
--- a/packages/profiling-node/README.md
+++ b/packages/profiling-node/README.md
@@ -91,8 +91,6 @@ We currently ship prebuilt binaries for a few of the most common platforms and n
- Linux x64 (glibc)
- Windows x64
-For a more detailed list, see job_compile_bindings_profiling_node job in our build.yml github action workflow.
-
### Bundling
If you are looking to squeeze some extra performance or improve cold start in your application (especially true for
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-108.node
new file mode 100644
index 000000000000..c232be230926
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-115.node
new file mode 100644
index 000000000000..ceb9433ebb73
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-127.node
new file mode 100644
index 000000000000..2de12057c169
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-93.node
new file mode 100644
index 000000000000..1bff41fbb946
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-arm64-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-108.node
new file mode 100644
index 000000000000..b419eb088dd9
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-115.node
new file mode 100644
index 000000000000..02eae2534fef
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-127.node
new file mode 100644
index 000000000000..51126ef4fb19
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-93.node
new file mode 100644
index 000000000000..8fcd7a009b09
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-darwin-x64-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-108.node
new file mode 100644
index 000000000000..18ced4ee1813
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-115.node
new file mode 100644
index 000000000000..18ced4ee1813
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-127.node
new file mode 100644
index 000000000000..1dd96b4d08eb
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-93.node
new file mode 100644
index 000000000000..e2ec51df2a31
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-glibc-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-108.node
new file mode 100644
index 000000000000..454840dda76e
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-115.node
new file mode 100644
index 000000000000..454840dda76e
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-127.node
new file mode 100644
index 000000000000..454840dda76e
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-93.node
new file mode 100644
index 000000000000..ea6675e312e9
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-arm64-musl-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-108.node
new file mode 100755
index 000000000000..6c24ee7d1c07
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-115.node
new file mode 100644
index 000000000000..6c24ee7d1c07
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-127.node
new file mode 100644
index 000000000000..13d38af969c4
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-93.node
new file mode 100644
index 000000000000..76ca9c769a7b
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-glibc-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-108.node
new file mode 100644
index 000000000000..40bc59d813ec
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-115.node
new file mode 100644
index 000000000000..40bc59d813ec
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-127.node
new file mode 100644
index 000000000000..884795a0d37e
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-93.node
new file mode 100644
index 000000000000..67203dde441b
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-linux-x64-musl-93.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-108.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-108.node
new file mode 100644
index 000000000000..a17a1c880035
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-108.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-115.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-115.node
new file mode 100644
index 000000000000..964776dacf01
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-115.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-127.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-127.node
new file mode 100644
index 000000000000..4215e5e6017b
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-127.node differ
diff --git a/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-93.node b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-93.node
new file mode 100644
index 000000000000..a57a0ac221b6
Binary files /dev/null and b/packages/profiling-node/compiled-bindings/sentry_cpu_profiler-win32-x64-93.node differ
diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json
index 19ecb062875e..2f3f7edfc62d 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.55.1",
"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",
@@ -35,7 +36,8 @@
"node": ">=14.18"
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"files": [
"/lib",
@@ -48,7 +50,6 @@
"/scripts/prune-profiler-binaries.js"
],
"scripts": {
- "install": "node scripts/check-build.js",
"clean": "rm -rf build && rm -rf lib",
"lint": "yarn lint:eslint && yarn lint:clang",
"lint:eslint": "eslint . --format stylish",
@@ -56,27 +57,23 @@
"fix": "eslint . --format stylish --fix",
"lint:fix": "yarn fix:eslint && yarn fix:clang",
"lint:fix:clang": "node clang-format.js --fix",
- "build": "yarn build:lib && yarn build:bindings:configure && yarn build:bindings",
+ "build": "yarn build:lib && yarn build:copy-bindings",
"build:lib": "yarn build:types && rollup -c rollup.npm.config.mjs",
- "build:transpile": "yarn build:bindings:configure && yarn build:bindings && yarn build:lib",
+ "build:transpile": "yarn build:copy-bindings && yarn build:lib",
"build:types:downlevel": "yarn downlevel-dts lib/types lib/types-ts3.8 --to ts3.8",
"build:types": "tsc -p tsconfig.types.json && yarn build:types:downlevel",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
- "build:bindings:configure": "node-gyp configure",
- "build:bindings:configure:arm64": "node-gyp configure --arch=arm64 --target_arch=arm64",
- "build:bindings": "node-gyp build && node scripts/copy-target.js",
- "build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.js",
- "build:dev": "yarn clean && yarn build:bindings:configure && yarn build",
+ "build:copy-bindings": "cp -r compiled-bindings/ lib/",
+ "build:dev": "yarn clean && yarn build",
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
"build:watch": "run-p build:transpile:watch build:types:watch",
"build:tarball": "npm pack",
- "test:watch": "cross-env SENTRY_PROFILER_BINARY_DIR=build jest --watch",
- "test:bundle": "node test-binaries.esbuild.js",
- "test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js"
+ "test:watch": "cross-env SENTRY_PROFILER_BINARY_DIR=compiled-bindings jest --watch",
+ "test": "cross-env SENTRY_PROFILER_BINARY_DIR=compiled-bindings jest --config jest.config.js"
},
"dependencies": {
- "@sentry/core": "8.45.0",
- "@sentry/node": "8.45.0",
+ "@sentry/core": "8.55.1",
+ "@sentry/node": "8.55.1",
"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..1a171abb1d84 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/react",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,11 +36,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.55.1",
+ "@sentry/core": "8.55.1",
"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();
+
/**
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions.
*/
@@ -81,10 +84,11 @@ export function createV6CompatibleWrapCreateBrowserRouter<
return createRouterFunction;
}
- // `opts` for createBrowserHistory and createMemoryHistory are different, but also not relevant for us at the moment.
- // `basename` is the only option that is relevant for us, and it is the same for all.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter {
+ return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter {
+ routes.forEach(route => {
+ allRoutes.add(route);
+ });
+
const router = createRouterFunction(routes, opts);
const basename = opts && opts.basename;
@@ -94,7 +98,104 @@ export function createV6CompatibleWrapCreateBrowserRouter<
// This is the earliest convenient time to update the transaction name.
// Callbacks to `router.subscribe` are not called for the initial load.
if (router.state.historyAction === 'POP' && activeRootSpan) {
- updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename);
+ updatePageloadTransaction(
+ activeRootSpan,
+ router.state.location,
+ routes,
+ undefined,
+ basename,
+ Array.from(allRoutes),
+ );
+ }
+
+ router.subscribe((state: RouterState) => {
+ if (state.historyAction === 'PUSH' || state.historyAction === 'POP') {
+ // Wait for the next render if loading an unsettled route
+ if (state.navigation.state !== 'idle') {
+ requestAnimationFrame(() => {
+ handleNavigation({
+ location: state.location,
+ routes,
+ navigationType: state.historyAction,
+ version,
+ basename,
+ allRoutes: Array.from(allRoutes),
+ });
+ });
+ } else {
+ handleNavigation({
+ location: state.location,
+ routes,
+ navigationType: state.historyAction,
+ version,
+ basename,
+ allRoutes: Array.from(allRoutes),
+ });
+ }
+ }
+ });
+
+ return router;
+ };
+}
+
+/**
+ * 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 {
+ routes.forEach(route => {
+ allRoutes.add(route);
+ });
+
+ 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, Array.from(allRoutes));
}
router.subscribe((state: RouterState) => {
@@ -106,6 +207,7 @@ export function createV6CompatibleWrapCreateBrowserRouter<
navigationType: state.historyAction,
version,
basename,
+ allRoutes: Array.from(allRoutes),
});
}
});
@@ -180,8 +282,6 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
return origUseRoutes;
}
- const allRoutes: RouteObject[] = [];
-
const SentryRoutes: React.FC<{
children?: React.ReactNode;
routes: RouteObject[];
@@ -207,10 +307,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 +329,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
routes,
navigationType,
version,
- allRoutes,
+ allRoutes: Array.from(allRoutes),
});
}
}, [navigationType, stableLocationParam]);
@@ -242,7 +353,6 @@ export function handleNavigation(opts: {
allRoutes?: RouteObject[];
}): void {
const { location, routes, navigationType, version, matches, basename, allRoutes } = opts;
-
const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename);
const client = getClient();
@@ -343,14 +453,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);
- allRoutes.push(route);
+ if (route.children && !route.index) {
+ route.children.forEach(child => {
+ const childRoutes = getChildRoutesRecursively(child, allRoutes);
+
+ childRoutes.forEach(r => allRoutes.add(r));
+ });
+ }
+ }
return allRoutes;
}
@@ -428,10 +542,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} />
@@ -474,7 +588,7 @@ function updatePageloadTransaction(
): void {
const branches = Array.isArray(matches)
? matches
- : (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]);
+ : (_matchRoutes(allRoutes || routes, location, basename) as unknown as RouteMatch[]);
if (branches) {
let name,
@@ -490,7 +604,7 @@ function updatePageloadTransaction(
[name, source] = getNormalizedName(routes, location, branches, basename);
}
- getCurrentScope().setTransactionName(name);
+ getCurrentScope().setTransactionName(name || '/');
if (activeRootSpan) {
activeRootSpan.updateName(name);
@@ -513,8 +627,6 @@ export function createV6CompatibleWithSentryReactRouterRouting = (props: P) => {
const isMountRenderPass = React.useRef(true);
@@ -527,10 +639,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 +654,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/types.ts b/packages/react/src/types.ts
index 1a40ec4fce91..b29a2dbd1cad 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -182,10 +182,14 @@ export interface RouterInit {
hydrationData?: HydrationState;
}
+export type NavigationState = {
+ state: 'idle' | 'loading' | 'submitting';
+};
+
export type NavigationStates = {
- Idle: any;
- Loading: any;
- Submitting: any;
+ Idle: NavigationState;
+ Loading: NavigationState;
+ Submitting: NavigationState;
};
export type Navigation = NavigationStates[keyof NavigationStates];
@@ -202,6 +206,7 @@ export declare enum HistoryAction {
export interface RouterState {
historyAction: Action | HistoryAction | any;
location: Location;
+ navigation: Navigation;
}
export interface Router {
state: TState;
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..1fae00af6a9f 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/remix",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -49,16 +49,17 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
"@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.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1",
+ "@sentry/react": "8.55.1",
"glob": "^10.3.4",
"opentelemetry-instrumentation-remix": "0.8.0",
"yargs": "^17.6.0"
@@ -67,8 +68,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",
@@ -103,7 +103,7 @@
"test:integration:ci:common": "run-s test:integration:clean test:integration:prepare test:integration:client:ci test:integration:server",
"test:integration:prepare": "(cd test/integration && yarn install)",
"test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)",
- "test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/ --project='chromium'",
+ "test:integration:client": "yarn playwright test test/integration/test/client/ --project='chromium'",
"test:integration:client:ci": "yarn test:integration:client",
"test:integration:server": "export NODE_OPTIONS='--stack-trace-limit=25' && vitest run",
"test:unit": "jest",
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/remix/test/integration/package.json b/packages/remix/test/integration/package.json
index b00f17b330f9..4e07eeb0817c 100644
--- a/packages/remix/test/integration/package.json
+++ b/packages/remix/test/integration/package.json
@@ -34,6 +34,7 @@
"@sentry-internal/feedback": "file:../../../feedback",
"@vanilla-extract/css": "1.13.0",
"@vanilla-extract/integration": "6.2.4",
+ "minimatch": "^9.0.0",
"@types/mime": "^3.0.0",
"@sentry/remix/glob": "<10.4.3",
"jackspeak": "<3.4.1",
diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json
index 4faeb7db1ef7..b8eff3005d8f 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.55.1",
"description": "Replay canvas integration",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -52,7 +52,8 @@
"yalc:publish": "yalc publish --push --sig"
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"repository": {
"type": "git",
@@ -68,8 +69,8 @@
"@sentry-internal/rrweb": "2.31.0"
},
"dependencies": {
- "@sentry-internal/replay": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry-internal/replay": "8.55.1",
+ "@sentry/core": "8.55.1"
},
"engines": {
"node": ">=14.18"
diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json
index 4267723b8b67..452c357c40fe 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.55.1",
"description": "User replays for Sentry",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -30,7 +30,8 @@
],
"sideEffects": false,
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"scripts": {
"build": "run-p build:transpile build:types build:bundle",
@@ -68,7 +69,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.55.1",
"@sentry-internal/rrweb": "2.31.0",
"@sentry-internal/rrweb-snapshot": "2.31.0",
"fflate": "^0.8.1",
@@ -76,8 +77,8 @@
"jsdom-worker": "^0.2.1"
},
"dependencies": {
- "@sentry-internal/browser-utils": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry-internal/browser-utils": "8.55.1",
+ "@sentry/core": "8.55.1"
},
"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..08e75c940dd6 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.55.1",
"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..f21146aa5c32 100644
--- a/packages/solid/package.json
+++ b/packages/solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solid",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -41,11 +41,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0"
+ "@sentry/browser": "8.55.1",
+ "@sentry/core": "8.55.1"
},
"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..9fed5408d5b9 100644
--- a/packages/solidstart/package.json
+++ b/packages/solidstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solidstart",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -54,7 +54,8 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"peerDependencies": {
"@solidjs/router": "^0.13.4",
@@ -66,10 +67,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.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1",
+ "@sentry/solid": "8.55.1",
"@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..22494710a97c
--- /dev/null
+++ b/packages/solidstart/src/config/addInstrumentation.ts
@@ -0,0 +1,184 @@
+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 =>
+ /^_sentry-release-injection-file-.*\.(js|mjs)$/.test(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..1a71c8dbdd23
--- /dev/null
+++ b/packages/solidstart/test/config/addInstrumentation.test.ts
@@ -0,0 +1,239 @@
+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('does not copy release injection file source map file', async () => {
+ fsExistsSyncMock.mockReturnValue(true);
+ fsReaddirMock.mockResolvedValueOnce(['_sentry-release-injection-file-test.js.map']);
+ fsCopyFileMock.mockResolvedValueOnce(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+
+ await callNitroCloseHook();
+
+ expect(fsCopyFileMock).not.toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/assets/_sentry-release-injection-file-test.js.map',
+ '/path/to/serverDir/assets/_sentry-release-injection-file-test.js.map',
+ );
+ expect(consoleLogSpy).not.toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Successfully created /path/to/serverDir/assets/_sentry-release-injection-file-test.js.map.',
+ );
+ });
+
+ 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..86529954242d 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/svelte",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -36,11 +36,12 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"dependencies": {
- "@sentry/browser": "8.45.0",
- "@sentry/core": "8.45.0",
+ "@sentry/browser": "8.55.1",
+ "@sentry/core": "8.55.1",
"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..f15999896948 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/sveltekit",
- "version": "8.45.0",
+ "version": "8.55.1",
"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",
@@ -28,7 +28,8 @@
}
},
"publishConfig": {
- "access": "public"
+ "access": "public",
+ "tag": "v8"
},
"peerDependencies": {
"@sveltejs/kit": "1.x || 2.x",
@@ -40,20 +41,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.55.1",
+ "@sentry/node": "8.55.1",
+ "@sentry/opentelemetry": "8.55.1",
+ "@sentry/svelte": "8.55.1",
"@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 `