diff --git a/.agents/skills/triage-issue/SKILL.md b/.agents/skills/triage-issue/SKILL.md index c997734d1c99..6505c5b0d9dd 100644 --- a/.agents/skills/triage-issue/SKILL.md +++ b/.agents/skills/triage-issue/SKILL.md @@ -73,11 +73,10 @@ If any of these alternative interpretations apply, capture them in the triage re ### Step 3: Codebase Research -Search for relevant code using Grep/Glob. Find error messages, function names, and stack trace paths in the local repo. +Search for relevant code using Grep/Glob. Find error messages, function names, and stack trace paths in the local repo. Bundler plugin code lives in `packages/bundler-plugins`. Cross-repo searches (only when clearly relevant): -- Bundler issues: `gh api search/code -X GET -f "q=+repo:getsentry/sentry-javascript-bundler-plugins"` - Docs issues: `gh api search/code -X GET -f "q=+repo:getsentry/sentry-docs"` **Shell safety:** Strip shell metacharacters from issue-derived search terms before use in commands. diff --git a/.agents/skills/triage-issue/assets/triage-report.md b/.agents/skills/triage-issue/assets/triage-report.md index 3868d342348d..5b64cbda39b1 100644 --- a/.agents/skills/triage-issue/assets/triage-report.md +++ b/.agents/skills/triage-issue/assets/triage-report.md @@ -29,7 +29,6 @@ ### Cross-Repo Findings -- **bundler-plugins:** - **sentry-docs:** ### Recommended Next Steps diff --git a/.agents/skills/write-tests/SKILL.md b/.agents/skills/write-tests/SKILL.md index af8f0408e8b9..ba876f350f1d 100644 --- a/.agents/skills/write-tests/SKILL.md +++ b/.agents/skills/write-tests/SKILL.md @@ -307,16 +307,31 @@ it.each([ ### Test isolation Tests must never depend on execution order or share mutable state. For this codebase, many tests -need to reset global Sentry state: +need to reset global Sentry state. Use whatever reset the existing tests in your package use (many +packages have a `resetSdk()` or `cleanupOtel()` helper). Otherwise, pick one: ```typescript -beforeEach(() => { - clearGlobalScope(); - getCurrentScope().clear(); - getIsolationScope().clear(); -}); +// Unbind the client. Cheap, and enough for most tests that call `init()`. +getCurrentScope().setClient(undefined); + +// Reset the global, isolation, and current scopes, and the client bound to them. +// `setupOnce` still does not run again, because installed integrations are tracked +// outside the carrier. +getMainCarrier().__SENTRY__ = undefined; + +// Close and unbind the client. Also flushes, so it is slower. +await Sentry.close(); ``` +**Reset the client in every test that calls `init()`.** A second `init()` while a client is bound +prints a "`Sentry.init()` was called more than once" warning. After the next major version, it will +return the bound client instead of a new one, so a test without a reset gets the previous test's +client and options. See `docs/repeated-init.md`. + +To assert on that warning (or any `consoleSandbox` output), stub `originalConsoleMethods.warn` from +`@sentry/core`. `consoleSandbox` calls the stored original method, so a spy on `console.warn` misses +it once the console integration is set up. + ### Grouping 1-2 levels of `describe` is usually enough. Deeper nesting makes tests harder to find and read. @@ -526,6 +541,7 @@ Before you're done, verify each test against these criteria: - [ ] Description reads as a behavior specification (no "should", no "works correctly") - [ ] No dependency on other tests' execution or state - [ ] Mocks and spies are restored (via `beforeEach`) +- [ ] Tests that call `init()` reset the bound client between tests - [ ] Edge cases covered: empty inputs, boundaries, error paths, null/undefined - [ ] Realistic test data (not `"foo"`, `"test"`, `123`) - [ ] No try/catch for error testing — `toThrow` / `rejects.toThrow` only diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 2a17708c2752..f70b4bc51da0 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -76,6 +76,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu - Flag usage of the following APIs: `getCurrentScope()`, `getIsolationScope()`, `getClient()` if they are avoidable. Flag it with severity Low and acknowledge from the start that this is more a "is this necessary" check, rather than a rule violation. - Reason for flagging: Usage of these APIs is problematic for multi-client setups where either there is no "current" client/scope, or the wrong client might be used. Calling these APIs would create a current scope, thereby misleading any future calls to these APIs. - What to do instead: Use an existing reference to the scope or client. For example, this is possible in most `Integration` hooks. +- Flag hard-coded attribute names and span ops (e.g. `'sentry.origin'`, `'sentry.op'`, `'http.request.method'`, `'db.system.name'`) when `@sentry/conventions` exports a constant for them. Use the constant from `@sentry/conventions/attributes` (e.g. `SENTRY_ORIGIN`, `SENTRY_OP`) or `@sentry/conventions/op` instead. Do not flag values that have no constant in `@sentry/conventions`. - Flag unnecessary `span.setAttribute(s)` calls: If data is already available at span start, it must be set via the `attributes` option of `startSpan`, `startSpanManual`, `startInactiveSpan` or `startIdleSpan` calls. This ensures that as much context as possible is available when `tracesSampler` or `ignoreSpans` SDK options are applied. If a `span.setAttribute(s)` call happens at a later time than right after span start and the attribute value can only be computed at that time, do not flag it. ### Code quality @@ -92,6 +93,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu - Flag usage of `expect.objectContaining` and other relaxed assertions, when a test expects something NOT to be included in a payload but there's no respective assertion. - Flag usage of conditionals in one test and recommend splitting up the test for the different paths. - Flag usage of loops testing multiple scenarios in one test and recommend using `(it)|(test).each` instead. +- Apply the `@sentry/conventions` constants rule from above to test files too. Test assertions on span attributes and ops must use the constants, not string literals. - Flag tests that are likely to introduce flakes. In our case this usually means we wait for some telemetry requests sent from an SDK. Patterns to look out for: - Only waiting for a request, after an action is performed. Instead, start waiting, perform action, await request promise. - Race conditions when waiting on multiple requests. Ensure that waiting checks are unique enough and don't depend on a hard order when there's a chance that telemetry can be sent in arbitrary order. diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 2cb41a456e17..5f82ea77210f 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -30,3 +30,17 @@ allow-ghsas: # 0.35.0, which is outside that range. Already present on develop via sharp 0.32.6 # and 0.34.5, both of which are affected by the same advisory. - GHSA-f88m-g3jw-g9cj + # axios, pinned exactly to 1.18.1 by nx (dev-only build tooling, not shipped). + # Can be removed once nx pins axios >= 1.20.0. + - GHSA-542g-h47m-68v8 + - GHSA-3pq3-5fj3-cg6v + - GHSA-x97p-jq2g-jp4f + - GHSA-mghh-pgcx-3jjj + - GHSA-c29m-xwm3-cm6r + - GHSA-r4gj-5m52-g5wh + - GHSA-m8m8-qj5v-23w3 + # brace-expansion, pinned exactly to 5.0.8 by nx (dev-only build tooling, not shipped). + # Can be removed once nx pins brace-expansion >= 5.0.11. + - GHSA-rgw5-rvv9-x895 + - GHSA-6j4f-fj2g-mc7p + - GHSA-qhr7-859c-m2p7 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f680c6c3c630..a37d3618311f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1038,6 +1038,8 @@ jobs: REACT_APP_E2E_TEST_DSN: 'https://username@domain/123' E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' + # The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md + RUNTIME: ${{ matrix.runtime }} strategy: fail-fast: false matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix) }} @@ -1056,7 +1058,7 @@ jobs: - name: Set up Bun if: matrix.test-application == 'node-exports-test-app' || contains(matrix.test-application, 'bun') || - contains(matrix.label, 'bun') + matrix.runtime == 'bun' uses: oven-sh/setup-bun@v2 with: bun-version: '1.3.14' @@ -1067,7 +1069,7 @@ jobs: use-installer: true token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Deno - if: contains(matrix.test-application, 'deno') || contains(matrix.label, 'deno') + if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno' uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} @@ -1177,6 +1179,8 @@ jobs: # Used by test apps that deploy a real Cloudflare Worker, e.g. cloudflare-workers-send-to-sentry CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md + RUNTIME: ${{ matrix.runtime }} strategy: fail-fast: false matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix-optional) }} @@ -1193,8 +1197,15 @@ jobs: uses: actions/setup-node@v7 with: 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' || contains(matrix.test-application, 'bun') || + matrix.runtime == 'bun' + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' - name: Set up Deno - if: matrix.test-application == 'deno' + if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno' uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml deleted file mode 100644 index 78ec8b6586e5..000000000000 --- a/.github/workflows/triage-issue.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Triage Issue - -on: - issues: - types: [opened] - workflow_dispatch: - inputs: - issue_number: - description: 'Issue number (e.g., 1234)' - required: true - type: number - -# Per-issue concurrency to prevent duplicate analysis -concurrency: - group: triage-issue-${{ github.event.issue.number || github.event.inputs.issue_number }} - cancel-in-progress: false - -jobs: - triage-issue: - runs-on: ubuntu-latest - environment: ci-triage - permissions: - contents: read - issues: read - pull-requests: read - id-token: write - # Only run for Bug or Feature issues (automatic mode) - if: | - github.event_name == 'workflow_dispatch' || - contains(github.event.issue.labels.*.name, 'Bug') || - contains(github.event.issue.labels.*.name, 'Feature') - - steps: - - name: Parse issue number - id: parse-issue - env: - EVENT_NAME: ${{ github.event_name }} - EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number }} - run: | - if [ "$EVENT_NAME" = "issues" ]; then - ISSUE_NUM="$EVENT_ISSUE_NUMBER" - else - ISSUE_NUM="$INPUT_ISSUE_NUMBER" - fi - - echo "issue_number=$ISSUE_NUM" >> "$GITHUB_OUTPUT" - echo "Processing issue #$ISSUE_NUM in CI mode" - - - name: Checkout repository - uses: actions/checkout@v7 - with: - ref: develop - - - name: Run Claude triage - id: triage - uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1 - env: - ANTHROPIC_BASE_URL: https://openrouter.ai/api - with: - anthropic_api_key: ${{ secrets.OPENROUTER_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - allowed_non_write_users: '*' - settings: | - { - "env": { - "LINEAR_CLIENT_ID": "${{ secrets.LINEAR_CLIENT_ID }}", - "LINEAR_CLIENT_SECRET": "${{ secrets.LINEAR_CLIENT_SECRET }}" - } - } - prompt: | - /triage-issue ${{ steps.parse-issue.outputs.issue_number }} --ci - IMPORTANT: Do NOT wait for approval. - Do NOT write to `/tmp/` or any other directory. Only write files (e.g. triage_report.md) inside the workspace (repo root). - Do NOT use Bash redirection (> file)—it is blocked. - Do NOT use `python3 -c` or other inline Python in Bash, only the provided scripts are allowed. - Do NOT attempt to delete (`rm`) temporary files you create. - claude_args: | - --model claude-opus-4-8 - --max-turns 50 --allowedTools "Write,Bash(gh api *),Bash(gh pr list *),Bash(npm info *),Bash(npm ls *),Bash(python3 .claude/skills/triage-issue/scripts/post_linear_comment.py *),Bash(python3 .claude/skills/triage-issue/scripts/parse_gh_issues.py *),Bash(python3 .claude/skills/triage-issue/scripts/detect_prompt_injection.py *),Bash(python3 .claude/skills/triage-issue/scripts/write_job_summary.py *)" - - - name: Post triage job summary - if: always() - run: | - EXEC_FILE="${{ steps.triage.outputs.execution_file }}" - if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then - EXEC_FILE="${RUNNER_TEMP}/claude-execution-output.json" - fi - if [ ! -f "$EXEC_FILE" ]; then - EXEC_FILE="${GITHUB_WORKSPACE}/../../_temp/claude-execution-output.json" - fi - if [ -f "$EXEC_FILE" ]; then - python3 .claude/skills/triage-issue/scripts/write_job_summary.py "$EXEC_FILE" >> "$GITHUB_STEP_SUMMARY" - else - echo "## Claude Triage Run" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No execution output file found. Run may have been skipped or failed before writing output." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/AGENTS.md b/AGENTS.md index 7947db750390..c1ac68c704ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,9 @@ Uses **Git Flow** (see `docs/gitflow.md`). Runtime packages (`node`, `cloudflare`, ...) re-export from `@sentry/server-utils` rather than defining their own. +- `Sentry.init()` follows one rule for repeated calls: the first call wins. See + `docs/repeated-init.md` before you add or change an `init()`. + ## Linting & Formatting - This project uses **Oxlint** and **Oxfmt** — NOT ESLint or Prettier diff --git a/CHANGELOG.md b/CHANGELOG.md index 9452bacdf056..8ca19496299f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,96 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 11.5.0 + +### Important Changes + +- **feat(core): Warn on repeated `Sentry.init()` and unbind the client on `close()` ([#24962](https://github.com/getsentry/sentry-javascript/pull/24962))** + + `Sentry.init()` now warns when it runs while a client is still active. For now, the new client still replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` once, or call `await Sentry.close()` before you call it again. `Sentry.close()` now unbinds the client it closes, so after `close()`, `getClient()` returns `undefined`, `isInitialized()` returns `false`, and a later `init()` sets up a new client. In `@sentry/cloudflare`, later requests no longer reuse the isolate's cached client once it is closed or closing. On the server, `@sentry/nextjs` and `@sentry/remix` now return the active client from a repeated `init()` call, not `undefined`. + +### Other Changes + +- chore(deps): Bump rrweb to 2.44.1 ([#25100](https://github.com/getsentry/sentry-javascript/pull/25100)) +- chore(deps): Bump web-vitals to 6.2.3 ([#25062](https://github.com/getsentry/sentry-javascript/pull/25062)) +- feat(bun): keep server spans open until streaming responses finish ([#25025](https://github.com/getsentry/sentry-javascript/pull/25025)) +- feat(langchain): Derive gen_ai.conversation.id from the invoke config ([#24831](https://github.com/getsentry/sentry-javascript/pull/24831)) +- feat(nextjs): Add `code.file.path` to `use cache` fill spans ([#24988](https://github.com/getsentry/sentry-javascript/pull/24988)) +- feat(react-router): Export createSentryServerInstrumentation from the Cloudflare entry ([#25042](https://github.com/getsentry/sentry-javascript/pull/25042)) +- feat(remix): Parameterize Remix 3 navigation spans ([#25045](https://github.com/getsentry/sentry-javascript/pull/25045)) +- feat(remix): Parameterize Remix 3 page load spans ([#25053](https://github.com/getsentry/sentry-javascript/pull/25053)) +- feat(remix): Report the matched route on Remix 3 HTML responses ([#25052](https://github.com/getsentry/sentry-javascript/pull/25052)) +- fix(ai): Normalize token usage and preserve cache breakdowns ([#25074](https://github.com/getsentry/sentry-javascript/pull/25074)) +- fix(browser-utils): Handle NS_ERROR_NOT_INITIALIZED from setTimeout in Firefox ([#25024](https://github.com/getsentry/sentry-javascript/pull/25024)) +- fix(bun): Add conversationIdIntegration to the default integrations ([#25003](https://github.com/getsentry/sentry-javascript/pull/25003)) +- fix(cloudflare): Detect TypeSafe Jev calls in Workers AI by model ID ([#25033](https://github.com/getsentry/sentry-javascript/pull/25033)) +- fix(cloudflare): Keep an installed OpenTelemetry async context strategy ([#24995](https://github.com/getsentry/sentry-javascript/pull/24995)) +- fix(feedback): Correct overlapping screenshot annotations and drag dimming ([#25040](https://github.com/getsentry/sentry-javascript/pull/25040)) +- fix(nextjs): Don't capture forbidden() and unauthorized() as errors ([#25088](https://github.com/getsentry/sentry-javascript/pull/25088)) +- fix(nextjs): Include basePath in request url of Pages Router errors ([#24985](https://github.com/getsentry/sentry-javascript/pull/24985)) +- fix(nextjs): Skip `init` inside a request of `@sentry/cloudflare` ([#24996](https://github.com/getsentry/sentry-javascript/pull/24996)) +- fix(node): Flush telemetry before Vercel functions are suspended ([#24910](https://github.com/getsentry/sentry-javascript/pull/24910)) +- fix(nuxt): Do not inject debug IDs into the SSR build ([#25080](https://github.com/getsentry/sentry-javascript/pull/25080)) +- fix(react-router): Pass the project when creating a release ([#25089](https://github.com/getsentry/sentry-javascript/pull/25089)) +- fix(remix): Pass the project when creating a release ([#25079](https://github.com/getsentry/sentry-javascript/pull/25079)) +- fix(server-utils): Derive the Vercel AI conversation id from the OpenAI conversation option ([#24979](https://github.com/getsentry/sentry-javascript/pull/24979)) +- fix(server-utils): Instrument the Vercel AI experimental_decide channel event ([#25068](https://github.com/getsentry/sentry-javascript/pull/25068)) +- fix(server-utils): Map Flue messages and token usage to the gen_ai conventions ([#24992](https://github.com/getsentry/sentry-javascript/pull/24992)) +- fix(server-utils): Record Vercel AI `experimental_telemetry.metadata` on gen_ai spans ([#24721](https://github.com/getsentry/sentry-javascript/pull/24721)) +- fix(sveltekit): Export consoleLoggingIntegration from worker entry ([#25061](https://github.com/getsentry/sentry-javascript/pull/25061)) +- fix(tanstackstart-react): Fix server route parametrization gaps ([#25071](https://github.com/getsentry/sentry-javascript/pull/25071)) +- ref(core): Deprecate SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ([#25039](https://github.com/getsentry/sentry-javascript/pull/25039)) +- ref(core): Remove `beforeSendSpan` null return warning ([#24129](https://github.com/getsentry/sentry-javascript/pull/24129)) + +
+ Internal Changes + +- chore: Add external contributor to CHANGELOG.md ([#25111](https://github.com/getsentry/sentry-javascript/pull/25111)) +- chore(bugbot): Flag hard-coded attribute names that have a conventions constant ([#25070](https://github.com/getsentry/sentry-javascript/pull/25070)) +- chore(deps-dev): bump nx from 22.7.7 to 22.7.10 ([#25075](https://github.com/getsentry/sentry-javascript/pull/25075)) +- chore(deps): Bump `@sentry/conventions` to 0.26.0 ([#25073](https://github.com/getsentry/sentry-javascript/pull/25073)) +- chore(github): Remove triage-issue workflow ([#25098](https://github.com/getsentry/sentry-javascript/pull/25098)) +- chore(skills): Drop bundler-plugins cross-repo search from triage-issue skill ([#25099](https://github.com/getsentry/sentry-javascript/pull/25099)) +- feat(deps): bump compression from 1.8.1 to 1.8.2 ([#25109](https://github.com/getsentry/sentry-javascript/pull/25109)) +- feat(deps): Bump js-yaml from 3.15.1 to 3.15.2 ([#24236](https://github.com/getsentry/sentry-javascript/pull/24236)) +- feat(deps): bump proxy-addr from 2.0.7 to 2.0.8 ([#25077](https://github.com/getsentry/sentry-javascript/pull/25077)) +- feat(deps): bump shell-quote from 1.10.0 to 1.12.0 ([#25108](https://github.com/getsentry/sentry-javascript/pull/25108)) +- feat(deps): bump source-map-js from 1.2.1 to 1.2.2 ([#25076](https://github.com/getsentry/sentry-javascript/pull/25076)) +- feat(deps): Bump svgo from 4.0.2 to 4.1.0 ([#24220](https://github.com/getsentry/sentry-javascript/pull/24220)) +- fix(deps): Bump seroval to 1.6.8 to fix two Dependabot alerts ([#25104](https://github.com/getsentry/sentry-javascript/pull/25104)) +- ref: Reuse `isObjectLike` for object guards ([#25058](https://github.com/getsentry/sentry-javascript/pull/25058)) +- ref(browser): Use SENTRY_ORIGIN convention constant ([#25037](https://github.com/getsentry/sentry-javascript/pull/25037)) +- ref(core): Use SENTRY_OP convention constant ([#24982](https://github.com/getsentry/sentry-javascript/pull/24982)) +- ref(core): Use SENTRY_ORIGIN convention constant in shared tracing ([#24983](https://github.com/getsentry/sentry-javascript/pull/24983)) +- ref(meta-frameworks): Use SENTRY_ORIGIN convention constant ([#25038](https://github.com/getsentry/sentry-javascript/pull/25038)) +- ref(nextjs): Move the server span hooks into `serverSpanHooks.ts` ([#24997](https://github.com/getsentry/sentry-javascript/pull/24997)) +- ref(server): Use SENTRY_ORIGIN convention constant ([#25036](https://github.com/getsentry/sentry-javascript/pull/25036)) +- test(aws-serverless): Port GraphQL test to span streaming ([#25056](https://github.com/getsentry/sentry-javascript/pull/25056)) +- test(browser): port general browser integration suites to default streaming ([#24876](https://github.com/getsentry/sentry-javascript/pull/24876)) +- test(bun): Port request-body tests to span streaming ([#25021](https://github.com/getsentry/sentry-javascript/pull/25021)) +- test(cloudflare): Add Workers AI tracing tests for Clef ([#25027](https://github.com/getsentry/sentry-javascript/pull/25027)) +- test(e2e): Add Bun, Deno and Cloudflare variants to react-router-8-framework ([#24598](https://github.com/getsentry/sentry-javascript/pull/24598)) +- test(e2e): Add eve test for Jev evaluate calls ([#25034](https://github.com/getsentry/sentry-javascript/pull/25034)) +- test(e2e): Bump bundler plugins to 5.4.1 in e2e test apps ([#25107](https://github.com/getsentry/sentry-javascript/pull/25107)) +- test(e2e): Cover isolation, trace propagation and logs on all react-router-8-framework runtimes ([#25041](https://github.com/getsentry/sentry-javascript/pull/25041)) +- test(e2e): Move react-router-8-cloudflare MySQL tests into react-router-8-framework ([#24599](https://github.com/getsentry/sentry-javascript/pull/24599)) +- test(e2e): Port Astro 5 Cloudflare to span streaming ([#25016](https://github.com/getsentry/sentry-javascript/pull/25016)) +- test(e2e): Raise mock Sentry server chunk size to fit whole bundles ([#25097](https://github.com/getsentry/sentry-javascript/pull/25097)) +- test(ember): Pin embroider packages away from broken releases ([#25066](https://github.com/getsentry/sentry-javascript/pull/25066)) +- test(nextjs): Expect use cache route handler file path on Turbopack ([#25117](https://github.com/getsentry/sentry-javascript/pull/25117)) +- test(nextjs): Pin nextjs-16-cf-workers latest variant to Next.js 16.3 ([#25118](https://github.com/getsentry/sentry-javascript/pull/25118)) +- test(node): Port Apollo GraphQL integration tests to span streaming ([#25030](https://github.com/getsentry/sentry-javascript/pull/25030)) +- test(node): port AWS integration tests to span streaming ([#25018](https://github.com/getsentry/sentry-javascript/pull/25018)) +- test(node): port filesystem integration tests to span streaming ([#25019](https://github.com/getsentry/sentry-javascript/pull/25019)) +- test(node): Port GraphQL tracing-channel tests to span streaming ([#25055](https://github.com/getsentry/sentry-javascript/pull/25055)) +- test(node): port rejection, feature flag, and client-report tests to span streaming ([#25020](https://github.com/getsentry/sentry-javascript/pull/25020)) +- test(node/bun): drop static pins from non-tracing integration suites ([#25017](https://github.com/getsentry/sentry-javascript/pull/25017)) +- test(nuxt): Upgrade to Nuxt 4.6 ; Test imports from `nuxt/server` ([#25093](https://github.com/getsentry/sentry-javascript/pull/25093)) + +
+ +Work in this release was contributed by @minwookshin. Thank you for your contribution! + ## 11.4.0 - feat(sveltekit): Support stable SvelteKit 3 ([#25009](https://github.com/getsentry/sentry-javascript/pull/25009)) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 5f53fe3567d1..d805439f943a 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -59,11 +59,11 @@ "@babel/core": "^7.29.6", "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.63.0", - "@sentry/rrweb": "2.43.2", + "@sentry/rrweb": "2.44.1", "@sentry/browser": "11.4.0", "@sentry/replay": "11.4.0", "@sentry/opentelemetry": "11.4.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@supabase/supabase-js": "2.109.0", "axios": "1.20.0", "babel-loader": "^10.1.1", diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js index 0ca6f3d1f855..783f188b3ba2 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', enhanceFetchErrorMessages: false, }); diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js index 9de3c3f1bf62..535d3397fb60 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', enhanceFetchErrorMessages: 'report-only', }); diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js index be51e911f499..740fb69558ed 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.feedback = feedback; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [feedback], }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js index d918418d502d..e6da8b5973bb 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ feedbackIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js index ec99f339e2c1..c8d5dbcdb232 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', replaysOnErrorSampleRate: 1.0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js index 81b53be75df9..27e5495f66a8 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ feedbackIntegration({ tags: { from: 'integration init' }, styleNonce: 'foo1234', scriptNonce: 'foo1234' }), diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js index 727d203aadd4..19dbc202c7e1 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js @@ -8,7 +8,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts index ecbb7c3790ec..a9ef9812ff41 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; import { sentryTest } from '../../../utils/fixtures'; import { envelopeRequestParser, getEnvelopeType, shouldSkipFeedbackTest, shouldSkipTracingTest, - waitForTransactionRequest, } from '../../../utils/helpers'; sentryTest( @@ -21,8 +21,8 @@ sentryTest( responseHeaders: { 'Document-Policy': 'js-profiling' }, }); - // Wait for the pageload transaction to be sent (idle span ended) - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + // Wait for the idle pageload span to end. + const pageloadSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const feedbackRequestPromise = page.waitForResponse(res => { const req = res.request(); @@ -40,7 +40,7 @@ sentryTest( await page.goto(url); // Wait for the idle page load span to finish - const pageLoadEvent = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; // Submit feedback after idle span ended — no active span await page.getByText('Report a Bug').waitFor({ state: 'visible' }); @@ -55,11 +55,10 @@ sentryTest( expect(feedbackEvent.contexts?.trace?.trace_id).toMatch(/\w{32}/); expect(feedbackEvent.contexts?.trace?.span_id).toMatch(/\w{16}/); - // contexts.trace.data must include thread.id to identify which thread is associated with the transaction - expect(pageLoadEvent.contexts?.trace?.data?.['thread.id']).toBe('0'); - expect(pageLoadEvent.contexts?.trace?.data?.['thread.name']).toBe('main'); + expect(pageloadSpan.attributes['thread.id']).toEqual({ type: 'string', value: '0' }); + expect(pageloadSpan.attributes['thread.name']).toEqual({ type: 'string', value: 'main' }); - const profilerId = pageLoadEvent.contexts?.profile?.profiler_id; + const profilerId = pageloadSpan.attributes['sentry.profiler_id']?.value; expect(profilerId).toMatch(/^[a-f\d]{32}$/); expect(feedbackEvent.contexts?.profile?.profiler_id).toBe(profilerId); }, diff --git a/dev-packages/browser-integration-tests/suites/feedback/logger/init.js b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js index 46e56c1330e3..3251bd6c7a4c 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.feedback = feedback; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', debug: true, integrations: [feedback], diff --git a/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js new file mode 100644 index 000000000000..94453fb8e47a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/browser'; +import { feedbackIntegration } from '@sentry/browser'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [feedbackIntegration({ colorScheme: 'light' })], +}); diff --git a/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts new file mode 100644 index 000000000000..e0d177283073 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts @@ -0,0 +1,81 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipFeedbackTest } from '../../../utils/helpers'; + +async function drawBox(page: Page, from: { x: number; y: number }, to: { x: number; y: number }): Promise { + await page.mouse.move(from.x, from.y); + await page.mouse.down(); + await page.mouse.move(to.x, to.y, { steps: 3 }); + await page.mouse.up(); +} + +sentryTest.beforeEach(async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipFeedbackTest()); + sentryTest.skip(browserName !== 'chromium', 'The canvas screen-capture fixture requires Chromium.'); + + await page.addInitScript(() => { + // The fixture runs over HTTP, but screenshot capture is stubbed below. + Object.defineProperty(window, 'isSecureContext', { value: true }); + Object.defineProperty(navigator, 'mediaDevices', { + value: { + getDisplayMedia: async () => { + const canvas = document.createElement('canvas'); + canvas.width = 1600; + canvas.height = 1000; + const ctx = canvas.getContext('2d')!; + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + return canvas.captureStream(); + }, + }, + configurable: true, + }); + }); + + await page.goto(await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true })); + await page.getByRole('button', { name: 'Report a Bug', exact: true }).click(); + await page.getByRole('button', { name: 'Add a screenshot', exact: true }).click(); + await expect(page.locator('#foreground')).toBeVisible(); + await expect.poll(() => page.locator('#background').evaluate((canvas: HTMLCanvasElement) => canvas.width)).toBe(1600); +}); + +sentryTest('starts a highlight at the cursor inside an existing box', async ({ page }) => { + const canvas = page.locator('#foreground'); + const bounds = (await canvas.boundingBox())!; + const point = (x: number, y: number) => ({ x: bounds.x + x, y: bounds.y + y }); + await drawBox(page, point(80, 60), point(280, 240)); + await expect(page.locator('.editor__rect')).toHaveCount(1); + const start = point(160, 140); + const end = point(240, 200); + + await drawBox(page, start, end); + + await expect(page.locator('.editor__rect')).toHaveCount(2); + const box = (await page.locator('.editor__rect').nth(1).boundingBox())!; + expect(box.x).toBeCloseTo(start.x, 0); + expect(box.y).toBeCloseTo(start.y, 0); + expect(box.width).toBeCloseTo(end.x - start.x, 0); + expect(box.height).toBeCloseTo(end.y - start.y, 0); +}); + +sentryTest('keeps background dimming unchanged while dragging another box', async ({ page }) => { + const canvas = page.locator('#foreground'); + const bounds = (await canvas.boundingBox())!; + const backgroundPixel = () => + canvas.evaluate((element: HTMLCanvasElement) => + Array.from(element.getContext('2d')!.getImageData(1400, 800, 1, 1).data), + ); + await drawBox(page, { x: bounds.x + 80, y: bounds.y + 60 }, { x: bounds.x + 240, y: bounds.y + 180 }); + await expect(page.locator('.editor__rect')).toHaveCount(1); + await expect.poll(backgroundPixel).toEqual([0, 0, 0, 64]); + + await page.mouse.move(bounds.x + 280, bounds.y + 60); + await page.mouse.down(); + await page.mouse.move(bounds.x + 360, bounds.y + 180, { steps: 3 }); + + expect(await backgroundPixel()).toEqual([0, 0, 0, 64]); + await page.mouse.up(); + await expect(page.locator('.editor__rect')).toHaveCount(2); + await expect.poll(backgroundPixel).toEqual([0, 0, 0, 64]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js index 50c6a0433fd3..82fa0942064b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration(), Sentry.consoleIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js index 19eb351a12a9..cdcb68f2b48f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js @@ -4,7 +4,6 @@ import { contextLinesIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [contextLinesIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js index b5214b18f262..eac01ad8ff64 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js @@ -12,7 +12,6 @@ const myClickListener = () => { btn.addEventListener('click', myClickListener); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js index d8f8833ee972..ecf0d4331b0d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js @@ -12,7 +12,6 @@ const myClickListener = () => { btn.addEventListener('click', myClickListener); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserApiErrorsIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js index 0cc89d7a3328..d3f555f38933 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js @@ -4,7 +4,6 @@ import { captureConsoleIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], attachStacktrace: true, diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js index 47b7cfd820ae..d265bdde9ed3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js @@ -4,7 +4,6 @@ import { captureConsoleIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], attachStacktrace: false, diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js deleted file mode 100644 index c69a872adc77..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts deleted file mode 100644 index 0d3484ef41bd..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; - -sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - - expect(pageloadSpan!.attributes['culture.locale']).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) }); -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js index 476df71792bd..a408b17f97f8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js @@ -2,19 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -const integrations = Sentry.getDefaultIntegrations({}).filter( - defaultIntegration => defaultIntegration.name === 'CultureContext', -); - -const client = new Sentry.BrowserClient({ +Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - transport: Sentry.makeFetchTransport, - stackParser: Sentry.defaultStackParser, - integrations: integrations, + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, }); - -const scope = new Sentry.Scope(); -scope.setClient(client); -client.init(); - -window._sentryScope = scope; diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js index 7881f177f9ae..564808cf73f0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js @@ -1 +1 @@ -window._sentryScope.captureException(new Error('test error')); +window.Sentry.captureException(new Error('test error')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts index a3078b315a92..d3c92273d124 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts @@ -1,12 +1,14 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const errorRequestPromise = waitForErrorRequest(page); + await page.goto(url); + const eventData = envelopeRequestParser(await errorRequestPromise); expect(eventData.exception?.values).toHaveLength(1); @@ -16,3 +18,21 @@ sentryTest('cultureContextIntegration captures locale, timezone, and calendar', calendar: expect.any(String), }); }); + +sentryTest( + 'cultureContextIntegration adds locale, timezone, and calendar to spans', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); + + const pageloadSpan = await spanPromise; + + expect(pageloadSpan.attributes['culture.locale']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js index 9bea0bc1b630..894f46aaa102 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; // window.sentryFeatureFlagsIntegration = Sentry.featureFlagsIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [Sentry.featureFlagsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js index f3f8089dffda..fa6a67ec3711 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index ee132346d51c..1ff0935b65f3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js index f81b5b380a1b..e7831a1c2c0b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js @@ -31,7 +31,6 @@ window.Sentry = Sentry; window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryGrowthBookIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js index f3e059103533..d755d7a1d972 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts index b93047ed01d3..df64f1c51bec 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest( "GrowthBook onSpan: flags are added to active span's attributes on span end", @@ -23,12 +18,8 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { @@ -44,22 +35,20 @@ sentryTest( return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = [] as Array<[string, unknown]>; + const expectedInnerSpanFlags = [] as Array<[string, unknown]>; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }, ); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js index 5289952f89b5..810539a8c07c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryLDIntegration = Sentry.launchDarklyIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryLDIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js index a16f1729de83..9e4b802f28f3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryLDIntegration = Sentry.launchDarklyIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index f6f243262f49..3b9eabb860f2 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const ldClient = (window as any).initializeLD(); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js index ca57b1550df0..971e08755fe6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryOpenFeatureIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js index 0fad111b5d95..b2b48519b8a9 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryOpenFeatureIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js index eaebe2c27258..4fc1cace150c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index ff03be5a972b..8dffd14502b7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = (window as any).initialize(); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js index 1ffa2a176dcd..61d00587f4c6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); Sentry.init({ - traceLifecycle: 'static', 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/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js index 15446bd6e931..22f74d2ebd7c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index 34a760a02e61..d78ebe56de88 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = (window as any).statsigClient; @@ -49,22 +39,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); 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 index 0599b97ef85f..1e0303b9c356 100644 --- 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 @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', 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/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js index cdfc8753fd55..ddc74b6427b4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js @@ -44,7 +44,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', 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/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js index f75906993803..93993d8f6188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -49,7 +49,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index cf92c32f2a8f..12e31d3ac568 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = new (window as any).UnleashClient(); @@ -47,22 +37,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts index 98bdc15e6d4e..7337b2e089a6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; // Duplicate from subject.js const query = `query Test{ @@ -13,7 +13,7 @@ const query = `query Test{ sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -33,34 +33,34 @@ sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTe }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/foo (query Test)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - type: 'fetch', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/foo', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.document': query, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/foo' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.document': { type: 'string', value: query }, }), }); }); sentryTest('should update breadcrumbs for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -80,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL fetch requests', async ({ getL }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js index c7bd45083a80..ef8d5fa541e4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js @@ -5,7 +5,6 @@ import { graphqlClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts index 85bca8b11546..c2b617049695 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest('should update spans for GraphQL persisted query fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -27,28 +27,31 @@ sentryTest('should update spans for GraphQL persisted query fetch requests', asy }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/graphql (persisted GetUser)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - type: 'fetch', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/graphql', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', - 'graphql.persisted_query.version': 1, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/graphql' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.persisted_query.hash.sha256': { + type: 'string', + value: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + 'graphql.persisted_query.version': { type: 'integer', value: 1 }, }), }); }); @@ -57,7 +60,7 @@ sentryTest( 'should update breadcrumbs for GraphQL persisted query fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -79,7 +82,15 @@ sentryTest( }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts index 7e48cf913934..db89e9bf7f57 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest('should update spans for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -27,35 +27,38 @@ sentryTest('should update spans for GraphQL persisted query XHR requests', async }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/graphql (persisted GetUser)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: { - type: 'xhr', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/graphql', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', - 'graphql.persisted_query.version': 1, - }, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'xhr' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/graphql' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.persisted_query.hash.sha256': { + type: 'string', + value: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + 'graphql.persisted_query.version': { type: 'integer', value: 1 }, + }), }); }); sentryTest('should update breadcrumbs for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -77,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL persisted query XHR requests', }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts index 722f6487c072..b46292284e82 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; // Duplicate from subject.js const query = `query Test{ @@ -13,7 +13,7 @@ const query = `query Test{ sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -33,34 +33,34 @@ sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTest }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/foo (query Test)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: { - type: 'xhr', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/foo', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.document': query, - }, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'xhr' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/foo' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.document': { type: 'string', value: query }, + }), }); }); sentryTest('should update breadcrumbs for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -80,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL XHR requests', async ({ getLoc }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js deleted file mode 100644 index c69a872adc77..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js deleted file mode 100644 index f1547634fb45..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js +++ /dev/null @@ -1,5 +0,0 @@ -Sentry.startSpan({ name: 'parent-span', op: 'test' }, () => { - Sentry.startSpan({ name: 'child-span', op: 'test-child' }, () => { - // noop - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts deleted file mode 100644 index d37127a7897a..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; - -sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url, { referer: 'https://sentry.io/' }); - - const spans = await spansPromise; - - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - - expect(pageloadSpan!.attributes[URL_FULL]).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({ - type: 'string', - value: expect.any(String), - }); - expect(pageloadSpan!.attributes['http.request.header.referer']).toEqual({ - type: 'array', - value: ['https://sentry.io/'], - }); -}); - -sentryTest( - 'httpContextIntegration only attaches the user agent to non-segment spans', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => s.name === 'child-span')); - - await page.goto(url, { referer: 'https://sentry.io/' }); - - const spans = await spansPromise; - - const childSpan = spans.find(s => s.name === 'child-span'); - - expect(childSpan!.is_segment).toBe(false); - expect(childSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({ type: 'string', value: expect.any(String) }); - // The document URL and referer only belong on the segment span. - expect(childSpan!.attributes[URL_FULL]).toBeUndefined(); - expect(childSpan!.attributes['http.request.header.referer']).toBeUndefined(); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js index 984534454ac7..a408b17f97f8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js @@ -2,19 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -const integrations = Sentry.getDefaultIntegrations({}).filter( - defaultIntegration => defaultIntegration.name === 'HttpContext', -); - -const client = new Sentry.BrowserClient({ +Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - transport: Sentry.makeFetchTransport, - stackParser: Sentry.defaultStackParser, - integrations: integrations, + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, }); - -const scope = new Sentry.Scope(); -scope.setClient(client); -client.init(); - -window._sentryScope = scope; diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js index 62ce205e2ffc..8abda6d4b6f7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js @@ -1 +1,7 @@ -window._sentryScope.captureException(new Error('client init')); +Sentry.startSpan({ name: 'parent-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'child-span', op: 'test-child' }, () => { + // noop + }); +}); + +window.Sentry.captureException(new Error('test error')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts index dd8a870a3bad..db8a384510a4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts @@ -1,17 +1,18 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('httpContextIntegration captures user-agent and referrer', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const errorEventPromise = getFirstSentryEnvelopeRequest(page); + const errorEventPromise = waitForErrorRequest(page); // Simulate document.referrer being set to test full functionality of the integration await page.goto(url, { referer: 'https://sentry.io/' }); - const errorEvent = await errorEventPromise; + const errorEvent = envelopeRequestParser(await errorEventPromise); expect(errorEvent.exception?.values).toHaveLength(1); @@ -23,3 +24,44 @@ sentryTest('httpContextIntegration captures user-agent and referrer', async ({ g url: expect.any(String), }); }); + +sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url, { referer: 'https://sentry.io/' }); + + const pageloadSpan = await spanPromise; + + expect(pageloadSpan.attributes[URL_FULL]).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes[USER_AGENT_ORIGINAL]).toEqual({ + type: 'string', + value: expect.any(String), + }); + expect(pageloadSpan.attributes['http.request.header.referer']).toEqual({ + type: 'array', + value: ['https://sentry.io/'], + }); +}); + +sentryTest( + 'httpContextIntegration only attaches the user agent to non-segment spans', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.name === 'child-span'); + + await page.goto(url, { referer: 'https://sentry.io/' }); + + const childSpan = await spanPromise; + + expect(childSpan.is_segment).toBe(false); + expect(childSpan.attributes[USER_AGENT_ORIGINAL]).toEqual({ type: 'string', value: expect.any(String) }); + // The document URL and referer only belong on the segment span. + expect(childSpan.attributes[URL_FULL]).toBeUndefined(); + expect(childSpan.attributes['http.request.header.referer']).toBeUndefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js index c51de9a85535..85b9d1a0fb2e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js @@ -8,7 +8,6 @@ window.Sentry = { }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js index dedd00bbcec5..d0ff6bbb92c9 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js index 27950d9cf079..5991109fce66 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js index 98bcbc6c0d00..eb75d30bf760 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js @@ -7,7 +7,6 @@ window.Sentry = { }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js index ba3766db49f5..d06b9b7b1dfd 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js @@ -6,7 +6,6 @@ import * as Sentry from '@sentry/browser'; window._testSentry = { ...Sentry }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js index b64706e66c9a..d5b05443ca9e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser'; import { moduleMetadataIntegration } from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [moduleMetadataIntegration()], beforeSend(event) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js index a72466f73a64..3c89709b3dfb 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js @@ -6,7 +6,6 @@ import { moduleMetadataIntegration } from '@sentry/browser'; import { rewriteFramesIntegration } from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ moduleMetadataIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index d7f5949f6033..631eb37b3eb1 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index 5d066ae72ff1..1f3ece2217e5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -1,12 +1,8 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; async function mockSupabaseAuthRoutesSuccess(page: Page) { await page.route(/\/auth\/v1\/token\?grant_type=password/, route => { @@ -84,82 +80,96 @@ if (bundle.startsWith('bundle')) { sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseAuthRoutesSuccess(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db')); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const signInSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signInWithPassword'); + const signOutSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signOut'); + await page.goto(url); - expect(supabaseSpans).toHaveLength(2); - expect(supabaseSpans![0]).toMatchObject({ - description: 'auth signInWithPassword', - op: 'db', - parent_span_id: eventData.contexts?.trace?.span_id, + const [rootSpan, signInSpan, signOutSpan] = await Promise.all([ + rootSpanPromise, + signInSpanPromise, + signOutSpanPromise, + ]); + + expect(signInSpan).toMatchObject({ + name: 'auth.signInWithPassword', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signInWithPassword', - 'db.system.name': 'postgresql', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signInWithPassword' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); - expect(supabaseSpans![1]).toMatchObject({ - description: 'auth signOut', - op: 'db', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(signOutSpan).toMatchObject({ + name: 'auth.signOut', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signOut', - 'db.system.name': 'postgresql', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signOut' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); }); sentryTest('should capture Supabase authentication errors', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseAuthRoutesFailure(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - - const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db')); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const signInSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signInWithPassword'); + const errorRequestPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'Invalid email or password', + ); + await page.goto(url); + + const [rootSpan, signInSpan, errorRequest] = await Promise.all([ + rootSpanPromise, + signInSpanPromise, + errorRequestPromise, + ]); + const errorEvent = envelopeRequestParser(errorRequest); expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password'); - expect(supabaseSpans).toHaveLength(2); - expect(supabaseSpans![0]).toMatchObject({ - description: 'auth signInWithPassword', - op: 'db', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + expect(signInSpan).toMatchObject({ + name: 'auth.signInWithPassword', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - status: 'internal_error', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signInWithPassword', - 'db.system.name': 'postgresql', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signInWithPassword' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index 1e93dfea4606..0f600426009f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 132a473e35f1..c392118f3e3d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -1,12 +1,7 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; async function mockSupabaseRoute(page: Page) { await page.route('**/rest/v1/todos**', route => { @@ -30,14 +25,16 @@ if (bundle.startsWith('bundle')) { sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData.breadcrumbs).toBeDefined(); expect(eventData.breadcrumbs).toContainEqual({ @@ -53,34 +50,32 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - - expect(events).toHaveLength(2); - - events.forEach(event => { - expect( - event.breadcrumbs?.some(breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.')), - ).toBe(true); - }); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const event = envelopeRequestParser(await eventPromise); + const supabaseBreadcrumbs = event.breadcrumbs?.filter(breadcrumb => breadcrumb.type === 'supabase'); + expect(supabaseBreadcrumbs?.map(breadcrumb => breadcrumb.category)).toEqual(['db.insert', 'db.select']); }); sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const eventData = envelopeRequestParser(await eventPromise); const supabaseBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js index c5b7ded472bd..9b8269790d41 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js @@ -25,7 +25,6 @@ _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.as ); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ thirdPartyErrorFilterIntegration({ behaviour: 'apply-tag-if-contains-third-party-frames', filterKeys: ['my-app'] }), diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js index 069ac69900b5..16e92edb9230 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js @@ -4,7 +4,6 @@ import { viewHierarchyIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [viewHierarchyIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js index 2b6db24fef69..100b16a2d408 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser'; // Initialize Sentry with webWorker integration Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/ipv6/init.js b/dev-packages/browser-integration-tests/suites/ipv6/init.js index 2016ec4e2498..de8412c65a86 100644 --- a/dev-packages/browser-integration-tests/suites/ipv6/init.js +++ b/dev-packages/browser-integration-tests/suites/ipv6/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@[2001:db8::1]/1337', sendClientReports: false, defaultIntegrations: false, diff --git a/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js index 640d3dc01e8c..5d5cb2799a4e 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; window.chrome = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', skipBrowserExtensionCheck: true, }); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js index c49c01424c29..7494359a0a8a 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js @@ -6,6 +6,5 @@ window.Sentry = Sentry; window.browser = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js index 3c7486e35a1d..6cb3b49ceb53 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js @@ -6,6 +6,5 @@ window.Sentry = Sentry; window.chrome = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js index 46a9f9358fc7..c21a085e735e 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js @@ -15,6 +15,5 @@ window.__SENTRY__ = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js index 14b0455ede2e..0bb1961ce1d9 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js @@ -15,6 +15,5 @@ window.__SENTRY__ = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js b/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js index a649c5b5e29a..3796a084234a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js index 2fd49d572deb..099425cd3f2f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { Sentry.captureFeedback({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js index e52afed53f2b..b1cc371e5ec6 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { Sentry.captureFeedback({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js index fb8609f66043..99ffd9f0ab31 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', attachStacktrace: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js index fb8609f66043..99ffd9f0ab31 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', attachStacktrace: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js index bd0421b32761..7c200c542c56 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js index 0a4b1d7a5f94..baedf4f10a29 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, dataCollection: { userInfo: false }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts index 7d048c9e9b48..9d3209772cf5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts @@ -1,18 +1,17 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; -sentryTest('sets user.ip_address to "auto" on transactions by default', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('infers user IP and user agent on streamed spans by default', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - expect(transaction.sdk?.settings?.infer_ip).toBe('auto'); + const envelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && span.name === 'woot'), + ); + await page.goto(url); + + const envelope = await envelopePromise; + expect(envelope[1][0][1].ingest_settings).toEqual({ infer_ip: 'auto', infer_user_agent: 'auto' }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js index 687dd524d917..530fceb8ec7b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js index 596d13f7af7b..b3b47b24b57e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/debug/init.js b/dev-packages/browser-integration-tests/suites/public-api/debug/init.js index b8a138c0fdbf..573e4fdcb621 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/debug/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/debug/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js b/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js index 78d16f24cc20..c16b31fd1c85 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js @@ -5,7 +5,6 @@ window.Sentry = Sentry; window._errorCount = 0; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', denyUrls: ['foo.js'], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js index 052a5b96651e..9bb07ba0cc11 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts index c18d75464223..5faa0f4d6964 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts @@ -1,6 +1,14 @@ +import type { SerializedStreamedSpan, StreamedSpanEnvelope } from '@sentry/core'; import { expect } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { + envelopeRequestParser, + getEnvelopeType, + properFullEnvelopeParser, + shouldSkipTracingTest, + waitForErrorRequest, +} from '../../../utils/helpers'; sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ getLocalTestUrl, page }) => { const bundle = process.env.PW_BUNDLE; @@ -9,7 +17,16 @@ sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ ge sentryTest.skip(); } - const pageloadRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); + const spans: SerializedStreamedSpan[] = []; + page.on('request', request => { + if (!request.url().startsWith('https://dsn.ingest.sentry.io/')) { + return; + } + if (getEnvelopeType(request) === 'span') { + spans.push(...properFullEnvelopeParser(request)[1][0][1].items); + } + }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); // mock sdk connectivity url to avoid making actual request to sentry.io page.route('**/api/4509632503087104/envelope/**/*', route => { @@ -30,17 +47,21 @@ sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ ge const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const pageLoadEvent = envelopeRequestParser(await pageloadRequestPromise); + await pageloadSpanPromise; // undefined is expected and means the request was successful expect(await diagnoseMessagePromise).toEqual('SDK connectivity: undefined'); // the request to sentry.io should not be traced, hence no http.client span should be sent. - const httpClientSpans = pageLoadEvent.spans?.filter(s => s.op === 'http.client'); + await page.evaluate(() => (window as any).Sentry.flush()); + const httpClientSpans = spans.filter(span => getSpanOp(span) === 'http.client'); expect(httpClientSpans).toHaveLength(0); // no fetch breadcrumb should be sent (only breadcrumb for the console log) - expect(pageLoadEvent.breadcrumbs).toEqual([ + const eventPromise = waitForErrorRequest(page, event => event.message === 'connectivity checked'); + await page.evaluate(() => (window as any).Sentry.captureMessage('connectivity checked')); + const event = envelopeRequestParser(await eventPromise); + expect(event.breadcrumbs).toEqual([ expect.objectContaining({ category: 'console', message: 'SDK connectivity: undefined', diff --git a/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js b/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js index b3e95f5cd7f9..e66214c5c0bf 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js @@ -5,7 +5,6 @@ window.Sentry = Sentry; window._errorCount = 0; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', ignoreErrors: ['ignoreErrorTest'], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/init.js b/dev-packages/browser-integration-tests/suites/public-api/init/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/init/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/init/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js index cebc5f71b52b..2b5313b11a6d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: '0', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js index 8e180dcfc1ee..15e8680cc075 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js @@ -8,6 +8,5 @@ window.originalBuiltIns = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js index 33923ae30df1..0223ee8568b4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // We assert on the debug message, so we need to enable debug mode debug: true, diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js index 6e9fbee0b83c..7b5ee5690589 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; // consoleLoggingIntegration should not actually work, but still not error out Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, integrations: [Sentry.consoleLoggingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js index f8f1e800c1c7..5f00ab4126c4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.consoleLoggingIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js index a0e95abc054b..c4b3013773c3 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js index f3c84a9f36f5..5590fbb90547 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js index 8a9f2f73c857..73c6e63ed335 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js index a2d04de8cfb6..e184d8a0116e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js b/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js b/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js b/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js b/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js b/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js b/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index 326c05a787f4..824220e5133f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -21,7 +21,7 @@ sentryTest( const attributes = transaction.contexts?.trace?.data; expect(attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts index 289a5716746f..5cc3bd34f817 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; @@ -19,6 +14,8 @@ import { SENTRY_STATUS_MESSAGE, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; sentryTest( @@ -77,11 +74,11 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -126,7 +123,7 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -171,7 +168,7 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -240,11 +237,11 @@ sentryTest( type: 'string', value: expect.any(String), }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js b/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js index b1fa399a0611..2c19109f6fd9 100644 --- a/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js b/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js index a31b02589f2b..7b73a029761d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js index a023909c786f..dfc142feaa09 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js index d83f950346bd..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js index 172f476c24d7..f9dccbffb530 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js index 20035a47839b..7704e9934cbf 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js index 8c59786159ec..7bea93df6756 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js index 566e6650d4c5..d933111eb57e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js index 12e46e733956..f04edea57625 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js index ccab05b4662e..f8ffa20ceb8f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index e8ca0b243c08..f71ddfdaaa9b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -9,7 +9,6 @@ window.Replay = replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js index 53f2e9191b09..0e1297ae8710 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js index 6e4e9a4e5cb5..ab1e26f4490a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js index 74d9998abeb2..973a0ae3efdc 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js index a919d5aed823..2cfec5213d3f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js index ed91c960bdec..6411031d12b5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), window.Replay], tracePropagationTargets: [/.*/], diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index e63c45e42293..f3eec3a11fb2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -2,12 +2,8 @@ import { expect } from '@playwright/test'; import type * as Sentry from '@sentry/browser'; import type { EventEnvelopeHeaders } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForErrorRequest, - waitForTransactionRequest, -} from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../utils/spanUtils'; import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers'; type TestWindow = Window & { @@ -16,7 +12,7 @@ type TestWindow = Window & { }; sentryTest( - 'should add replay_id to dsc of transactions when in session mode', + 'should add replay_id to dsc of streamed spans when in session mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -26,7 +22,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); // Wait for this to be available await page.waitForFunction('!!window.Replay'); @@ -40,15 +38,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -67,7 +59,7 @@ sentryTest( ); sentryTest( - 'should not add replay_id to dsc of transactions when in buffer mode', + 'should not add replay_id to dsc of streamed spans when in buffer mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -77,7 +69,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(() => { (window as unknown as TestWindow).Replay.startBuffering(); @@ -88,15 +82,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -114,7 +102,7 @@ sentryTest( ); sentryTest( - 'should add replay_id to dsc of transactions when switching from buffer to session mode', + 'should add replay_id to dsc of streamed spans when switching from buffer to session mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -124,7 +112,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(() => { (window as unknown as TestWindow).Replay.startBuffering(); @@ -140,15 +130,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -168,7 +152,7 @@ sentryTest( ); sentryTest( - 'should not add replay_id to dsc of transactions if replay is not enabled', + 'should not add replay_id to dsc of streamed spans if replay is not enabled', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -178,20 +162,16 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(async () => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); @@ -222,16 +202,20 @@ sentryTest('should add replay_id to error DSC while replay is active', async ({ const error1Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #1'); const error2Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #2'); - // We want to wait for the transaction to be done, to ensure we have a consistent test - const transactionReq = hasTracing ? waitForTransactionRequest(page) : Promise.resolve(); + // We want to wait for the pageload span to end, to ensure we have a consistent test + const spanEnvelopePromise = hasTracing + ? waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ) + : Promise.resolve(); // Wait for this to be available await page.waitForFunction('!!window.Replay'); - // We have to start replay before we finish the transaction, otherwise the DSC will not be frozen with the Replay ID + // We have to start replay before we finish the pageload span, otherwise the DSC will not be frozen with the Replay ID await page.evaluate('window.Replay.start();'); await waitForReplayRunning(page); - await transactionReq; + await spanEnvelopePromise; await page.evaluate('window._triggerError(1)'); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js index da36e5671e30..83e514016c1b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js index b0e3b3b7f091..c24b8e1daca8 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js index 0a70bee557e1..5e71c58fc0aa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 29e87a8198af..7ab704b50d6a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index e31b3b638884..a765fc48c67b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/init.js index d83f950346bd..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js index 332aae5c8a0f..5a97a87c2571 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js index 0c788f53c321..82bc5a045870 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js index 332aae5c8a0f..5a97a87c2571 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js index 0c788f53c321..82bc5a045870 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js index 837a81466609..c973a578baae 100644 --- a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js index e961e3ceb30c..269b531b57eb 100644 --- a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/init.js b/dev-packages/browser-integration-tests/suites/replay/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index 69cc3b55cfb8..d09fe6530e9c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/logger/init.js b/dev-packages/browser-integration-tests/suites/replay/logger/init.js index f675e1af8681..195be16ddad3 100644 --- a/dev-packages/browser-integration-tests/suites/replay/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/logger/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js index 27092017f938..7bab07447b3a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js index 909da70f67a8..2a3dae73541d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js index 980127dd5ee9..58873b85f9ec 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js index 4e5b196e47de..f5a01ece0a37 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js index 2e24918c3980..16000b4c4412 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js index c20873c35d32..96f5c063e13d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index 5a234f35eecd..c20c8b36a4c2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js index 5248651ac1c3..9200b5771ec6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js @@ -10,7 +10,6 @@ window.Replay = new Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js index a705f7431907..77fc89c70a25 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/requests/init.js b/dev-packages/browser-integration-tests/suites/replay/requests/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/requests/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/requests/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js index ce6e4b19eb22..6405faaf741b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index 1ff49d804e31..ee55b5cca2c4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js index df437ea858a1..3af8409da672 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 2bf99b713783..214340ba12c5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index 852014c9f109..b21921ae5726 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js index 8f7316b1d38f..77b6bf6b67e4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js index 708bb8533402..5083bd48c745 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js index 4acf8b57936f..6f2a54514433 100644 --- a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js index 7a2460d4059c..4d4476012c0d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js index 752b0a131101..cc0094a0366b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/sessions/init.js b/dev-packages/browser-integration-tests/suites/sessions/init.js index 91040d71877d..af2df91a7ceb 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js index 0ad971be30a2..2be1d55b5782 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js index 91040d71877d..af2df91a7ceb 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js b/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js index f5c6bdef5e5f..7bba2ce96cac 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', sampleRate: 0, diff --git a/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/user/init.js b/dev-packages/browser-integration-tests/suites/sessions/user/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/user/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/user/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/init.js b/dev-packages/browser-integration-tests/suites/stacktraces/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/init.js +++ b/dev-packages/browser-integration-tests/suites/stacktraces/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts index 6f7116bd33a7..f885fc4c3bde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -45,8 +44,8 @@ sentryTest( value: 'https://sentry-test-site.example/path/to/script.js', }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); @@ -94,8 +93,8 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); 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 17db0254533e..937662e117a4 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 @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -44,8 +44,8 @@ sentryTest( 'browser.script.source_char_position': 0, 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); @@ -98,8 +98,8 @@ sentryTest( 'browser.script.invoker': 'BUTTON#clickme.onclick', 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts index d97eba7c9a0c..94ece2b74e6f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -47,8 +46,8 @@ sentryTest( value: 'https://sentry-test-site.example/path/to/script.js', }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); @@ -96,8 +95,8 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_ORIGIN]: { type: 'string', value: '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 4e14a8add77d..b571a7d6a306 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 @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -46,8 +46,8 @@ sentryTest( 'browser.script.source_char_position': 0, 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); @@ -100,8 +100,8 @@ sentryTest( 'browser.script.invoker': 'BUTTON#clickme.onclick', 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts index 4791b010015b..829710ce5011 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; @@ -44,17 +40,17 @@ sentryTest( expect(navigationRequest.transaction).toEqual('/sub-page'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(pageloadRequest.request).toEqual({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts index 8b512db6a4f9..caa2b6afb328 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts @@ -1,12 +1,14 @@ import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; sentryTest( 'creates a pageload and navigation root spans each with multiple navigation.redirect childspans', @@ -31,10 +33,10 @@ sentryTest( expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts index b0d8a061fec8..fc126da536f3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts @@ -1,12 +1,14 @@ import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; sentryTest('creates a pageload root span with navigation.redirect childspan', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -24,10 +26,10 @@ sentryTest('creates a pageload root span with navigation.redirect childspan', as expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index 5d298cb1e9cd..62fcd88c337f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_TRACE_LIFECYCLE, @@ -13,6 +8,8 @@ import { USER_AGENT_ORIGINAL, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -124,11 +121,11 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, @@ -224,7 +221,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: '/sub-page', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, @@ -236,7 +233,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: 'url', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, @@ -253,7 +250,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: '/sub-page-2', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, @@ -265,7 +262,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: 'url', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index 30a457268d45..ab8a3931e132 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -1,11 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -41,17 +37,17 @@ sentryTest('should create a navigation transaction on page navigation', async ({ expect(navigationRequest.transaction).toEqual('/index.html'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(pageloadRequest.request).toEqual({ @@ -119,10 +115,10 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa expect(navigationRequest.transaction).toEqual('/sub-page'); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest.request).toEqual({ @@ -139,10 +135,10 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa expect(navigationRequest2.transaction).toEqual('/sub-page-2'); expect(navigationRequest2.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest2.request).toEqual({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index 1fdaaadce2bd..717011ffb9c3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { BROWSER_NAVIGATION_TYPE, SENTRY_SEGMENT_NAME_SOURCE, @@ -18,6 +13,8 @@ import { USER_AGENT_ORIGINAL, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -136,11 +133,11 @@ sentryTest( type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'pageload', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser', }, 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 401b30e2a5fb..0776668fb9e8 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,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -27,10 +23,10 @@ sentryTest( expect(eventData.transaction).toBe('new name'); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }); expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); 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 index f6c3282e30c8..ab1b1ed20c46 100644 --- 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 @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -25,10 +21,10 @@ sentryTest( expect(traceContextData).toBeDefined(); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }); expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index bd02b4799199..8e7c3a4b2480 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -26,10 +22,10 @@ sentryTest('creates a pageload transaction with url as source', async ({ getLoca expect(startTimestamp).toBeCloseTo(timeOrigin, 1); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts index 3d3d4e69069c..3c4a37bc9de3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts @@ -1,10 +1,11 @@ -import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { expect } from '@playwright/test'; import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -25,10 +26,10 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts index 9983809f9947..8ea2ea1ca842 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -25,10 +21,10 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, 'sentry.idle_span_finish_reason': { type: 'string', value: 'finalTimeout' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts index e71f12c00993..db6f6737ccd6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -25,10 +21,10 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, 'sentry.idle_span_finish_reason': { type: 'string', value: 'cancelled' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts index 9c8c0b48fb69..bca0d6052b95 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -27,10 +23,10 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'reportPageLoaded', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts index 90deb74d0f2b..ae6ffdec21b3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -27,10 +23,10 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'finalTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts index 6b0bf8ceb8b8..9e947592a576 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -27,10 +23,10 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 2a6123aa2aa9..0870974ba825 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_ID, @@ -12,6 +12,8 @@ import { URL_PATH, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -76,11 +78,11 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'ui.action.click', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.browser.interactions', }, @@ -133,7 +135,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'stream', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'ui.interaction.click', }, @@ -141,7 +143,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: expect.any(String), }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.browser.interactions', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 12e3861db87f..276234385fae 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { type Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -95,8 +96,8 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.img', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', 'http.request.same_origin': false, @@ -148,8 +149,8 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.link', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', 'http.request.same_origin': 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 index 226c60d5859b..d24b26bdffc7 100644 --- a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js @@ -4,7 +4,6 @@ import { makeMultiplexedTransport } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: makeMultiplexedTransport(Sentry.makeFetchTransport, ({ getEvent }) => { const event = getEvent('event'); diff --git a/dev-packages/browser-integration-tests/suites/transport/offline/init.js b/dev-packages/browser-integration-tests/suites/transport/offline/init.js index 809d0c646ed1..e102d6a8a5a5 100644 --- a/dev-packages/browser-integration-tests/suites/transport/offline/init.js +++ b/dev-packages/browser-integration-tests/suites/transport/offline/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: Sentry.makeBrowserOfflineTransport(), }); diff --git a/dev-packages/browser-integration-tests/suites/wasm/init.js b/dev-packages/browser-integration-tests/suites/wasm/init.js index e4cffb1b682f..9b9aad7b911f 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration()], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js b/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js index 602bdd6b9a16..2a0766dfa03f 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration()], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js b/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js index 85c1fde92c15..912a4aafd728 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js @@ -25,7 +25,6 @@ _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.as window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ wasmIntegration({ applicationKey: 'wasm-test-app' }), diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js index e02171c7ae8c..39007ede4bc0 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration({ applicationKey: 'wasm-worker-app' })], }); diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 20124be5a264..d27790a79a0f 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -2,7 +2,7 @@ // fails on Bun is skipped with `test.skipIf` on `RUNTIME` in the Node suite, not listed here. // Node-only features: ANR and native thread watchdogs, child processes, the AWS Lambda Node runtime, -// and `node:sqlite`, which `flue` needs. +// `node:sqlite`, which `flue` needs, and the Vercel keep-alive, which needs `http.server.response.finish`. const NODE_ONLY = [ 'suites/anr/test.ts', 'suites/aws-serverless/**', @@ -10,6 +10,7 @@ const NODE_ONLY = [ 'suites/child-process/test.ts', 'suites/thread-blocked-native/test.ts', 'suites/tracing/flue/test.ts', + 'suites/vercel/keep-alive/test.ts', ]; // Bun does not publish `http.server.request.start`, so `@sentry/node` creates no `http.server` @@ -258,10 +259,8 @@ const BUN_BUILD_NOT_TRIAGED = [ 'suites/tracing/mongoose-v9/test.ts', 'suites/tracing/mongoose/test.ts', 'suites/tracing/mysql/test.ts', - 'suites/tracing/openai/test.ts', 'suites/tracing/prisma-orm-v8/test.ts', 'suites/tracing/together-ai/test.ts', - 'suites/tracing/vercelai/test.ts', 'suites/tracing/vercelai/v6_v7/test.ts', ]; diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts index 6fcc7d11fac5..e5acad073040 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts index 47b9e22fafcd..5f481fe5838f 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts index 7f4ee4d4dcf1..5fad87f86e5f 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts index dbd9ecfd5dd2..fab2418ca66f 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts @@ -7,8 +7,6 @@ const mode = process.env.BODY_MODE; Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, - // Request bodies are only attached to transaction events, so this suite needs the static trace lifecycle. - traceLifecycle: 'static', ...(mode === 'explicit-small' && { dataCollection: { httpBodies: [] }, integrations: integrations => [ diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts index 0134022d2b29..f81b2b99b619 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts @@ -9,13 +9,22 @@ test('captures incoming request bodies by default', async () => { const runner = createRunner(__dirname, 'index.ts') .withMockSentryServer() .expect({ - transaction: transaction => { - expect(transaction.request).toMatchObject({ - method: 'POST', - url: expect.stringContaining('/default'), - query_string: 'source=test', - headers: expect.objectContaining({ 'content-type': 'text/plain' }), - data: 'captured-by-default', + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.method']).toEqual({ value: 'POST', type: 'string' }); + expect(span?.attributes['url.full']).toEqual({ + value: expect.stringContaining('/default'), + type: 'string', + }); + expect(span?.attributes['url.query']).toEqual({ value: 'source=test', type: 'string' }); + expect(span?.attributes['http.request.header.content-type']).toEqual({ + value: ['text/plain'], + type: 'array', + }); + expect(span?.attributes['http.request.body.data']).toEqual({ + value: 'captured-by-default', + type: 'string', }); }, }) @@ -34,8 +43,10 @@ test('an explicit small size overrides disabled body collection', async () => { .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-small' }) .expect({ - transaction: transaction => { - expect(transaction.request?.data).toBe(`${'a'.repeat(997)}...`); + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.body.data']?.value).toBe(`${'a'.repeat(997)}...`); }, }) .start(); @@ -54,9 +65,11 @@ test('an explicit none overrides enabled body collection', async () => { .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-none' }) .expect({ - transaction: transaction => { - expect(transaction.request?.method).toBe('POST'); - expect(transaction.request?.data).toBeUndefined(); + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.method']?.value).toBe('POST'); + expect(span?.attributes['http.request.body.data']).toBeUndefined(); }, }) .start(); diff --git a/dev-packages/bun-integration-tests/suites/streaming/index.ts b/dev-packages/bun-integration-tests/suites/streaming/index.ts new file mode 100644 index 000000000000..aeb5dc2e6460 --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/streaming/index.ts @@ -0,0 +1,33 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/bun'; + +Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1 }); + +const server = Bun.serve({ + port: 0, + fetch(request) { + const path = new URL(request.url).pathname; + if (path === '/error') { + throw new Error('handler failed'); + } + const span = Sentry.getActiveSpan(); + const body = new ReadableStream({ + async start(controller) { + controller.enqueue(new TextEncoder().encode('data: first\n\n')); + await Bun.sleep(50); + if (path === '/stream-error') { + controller.error(new Error('stream failed')); + return; + } + controller.enqueue(new TextEncoder().encode(`data: recording=${span?.isRecording()}\n\n`)); + span?.setAttribute('test.stream.completed', true); + controller.close(); + }, + }); + return new Response(body, { status: 201, headers: { 'content-type': 'text/event-stream' } }); + }, + error() { + return new Response('failed', { status: 500 }); + }, +}); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/streaming/test.ts b/dev-packages/bun-integration-tests/suites/streaming/test.ts new file mode 100644 index 000000000000..25a0f138a04b --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/streaming/test.ts @@ -0,0 +1,83 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; + +afterAll(cleanupChildProcesses); + +test('records streaming work after the response headers have been returned', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const root = container.items.find(item => item.is_segment); + expect(root).toEqual( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'url.path': { value: '/events', type: 'string' }, + 'http.response.status_code': { value: 201, type: 'integer' }, + 'test.stream.completed': { value: true, type: 'boolean' }, + }), + }), + ); + }, + }) + .start(); + expect(await runner.makeRequest('get', '/events')).toBe('data: first\n\ndata: recording=true\n\n'); + await runner.completed(); +}); + +test('records a thrown handler as an error span', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const root = container.items.find(item => item.is_segment); + expect(root).toEqual(expect.objectContaining({ status: 'error' })); + }, + }) + .start(); + await runner.makeRequest('get', '/error', { expectError: true }); + await runner.completed(); +}); + +test('captures a response stream failure with its request context and error span', async () => { + let spanTraceId: string | undefined; + let eventTraceId: string | undefined; + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const roots = container.items.filter(item => item.is_segment); + expect(roots).toHaveLength(1); + const root = roots[0]!; + expect(root.status).toBe('error'); + expect(root.attributes['url.path']?.value).toBe('/stream-error'); + expect(root.attributes['http.response.status_code']?.value).toBe(201); + spanTraceId = root.trace_id; + }, + }) + .expect({ + event: event => { + expect(event.exception?.values).toHaveLength(1); + const exception = event.exception!.values![0]!; + expect(exception.type).toBe('Error'); + expect(exception.value).toBe('stream failed'); + expect(exception.mechanism).toEqual({ type: 'auto.http.bun.serve', handled: false }); + expect(event.request?.method).toBe('GET'); + expect(new URL(event.request!.url!).pathname).toBe('/stream-error'); + eventTraceId = event.contexts?.trace?.trace_id; + }, + }) + .start(); + + await expect.poll(() => runner.getPort(), { timeout: 30_000 }).toBeTypeOf('number'); + const response = await fetch(`http://localhost:${runner.getPort()}/stream-error`); + expect(response.status).toBe(201); + // Bun versions differ in whether a failed stream rejects or closes the HTTP response body. + await Promise.allSettled([response.text()]); + await runner.completed(); + expect(spanTraceId).toMatch(/^[a-f\d]{32}$/); + expect(eventTraceId).toBe(spanTraceId); +}); diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 781566e141d4..bdae3c275a3b 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -33,7 +33,7 @@ "@cloudflare/workers-types": "^4.20260426.0", "@cloudflare/workers-types-v5": "npm:@cloudflare/workers-types@5.20260710.1", "@sentry-internal/test-utils": "11.4.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "eslint-plugin-regexp": "^3.1.0", "prisma": "6.15.0", "vite": "^8.3.1", diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts index f5a787bbc91e..0fa5d410368b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts @@ -1,9 +1,4 @@ -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SDK_NAME, @@ -14,6 +9,8 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; @@ -80,7 +77,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, @@ -89,7 +86,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, }, name: 'test-child-span', @@ -108,7 +105,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, @@ -145,7 +142,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, @@ -167,13 +164,13 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SENTRY_OP]: { type: 'string', value: 'test' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, }, name: 'test-span', @@ -197,10 +194,10 @@ it('sends a streamed span envelope with correct spans for a manually started spa value: expect.arrayContaining(['SpanStreaming']), }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.server' }, + [SENTRY_OP]: { type: 'string', value: 'http.server' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'route' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts index 2cd081b06b51..9c88edd7b0a3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest'; import type { Envelope, SerializedStreamedSpan } from '@sentry/core'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; import { SENTRY_SDK_NAME, SENTRY_SDK_VERSION, @@ -8,6 +8,8 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRACE_LIFECYCLE, SENTRY_ENVIRONMENT, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -35,8 +37,8 @@ function commonAttributes(segmentSpan: SerializedStreamedSpan): SerializedStream [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan.span_id }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' }, + [SENTRY_OP]: { type: 'string', value: 'db.query' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' }, 'db.system.name': { type: 'string', value: 'cloudflare-d1' }, }; } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts index e9feaa6215f4..1f8e514601a8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts @@ -1,5 +1,5 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -23,7 +23,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/exec' }); const sqlSpans = spans.filter( - span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value === 'auto.db.cloudflare.durable_object.sql', + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.cloudflare.durable_object.sql', ); expect(sqlSpans).toHaveLength(3); @@ -32,8 +32,8 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal expect.objectContaining({ name: 'CREATE TABLE users', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object.sql' }, + [SENTRY_OP]: { type: 'string', value: 'db.query' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object.sql' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'exec' }, 'db.query.text': { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts index 1051890b8f94..ddbfc897054e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; import { expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -29,8 +29,8 @@ it('instruments sync KV operations on Durable Object storage', async ({ signal } expect.objectContaining({ name: 'durable_object_storage_kv_put', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'put' }, }), diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts index 6cb7888217b0..f894df8a361d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts @@ -1,11 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -24,8 +20,8 @@ it('Scheduled handler creates a segment span with correct attributes', async ({ is_segment: true, status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'function' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.scheduled' }, + [SENTRY_OP]: { type: 'string', value: 'function' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.scheduled' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, 'code.function.name': { type: 'string', value: 'scheduled' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts index 42d2d895fea0..5f793c2c2e54 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -42,6 +42,15 @@ export default Sentry.withSentry( return new Response(JSON.stringify(result)); } + if (url.pathname === '/evaluate-clef') { + const result = await ai.run('@cf/cloudflare/clef', { + model: 'clef', + state: 'Checkout has been failing for every customer for the last hour.', + questions: { urgent: { type: 'noul', instructions: 'Is this support request urgent?' } }, + }); + return new Response(JSON.stringify(result)); + } + if (url.pathname === '/stream') { const stream = (await ai.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'What is the capital of France?' }], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts index e0a26d02bf86..0d7e599cca52 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts @@ -34,6 +34,15 @@ export class MockAi { }; } + // Unwrapped, as in the documented Clef output schema, so this covers the path without `{ state, result }`. + if (model === '@cf/cloudflare/clef') { + return { + model: 'clef', + answers: { urgent: { type: 'noul', noul: 0.98 } }, + usage: { input_tokens: 412, output_tokens: 1 }, + }; + } + if (inputs?.stream === true) { return createSseStream([ '{"response":"The capital "}', diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts index 6ed31514dfe4..600062d6eb0a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -17,6 +17,9 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRACE_LIFECYCLE, SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { SDK_VERSION } from '@sentry/core'; import { expect, it } from 'vitest'; @@ -45,8 +48,8 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => { status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.chat', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, @@ -64,7 +67,7 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => { type: 'string', value: 'The capital of France is Paris.', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -99,8 +102,8 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) = status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.chat', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, @@ -118,7 +121,7 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) = type: 'string', value: 'The capital of France is Paris.', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -150,8 +153,8 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.evaluate', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.evaluate', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'evaluate', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: 'typesafe/jev', type: 'string' }, @@ -164,7 +167,7 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si type: 'string', value: '[{"type":"evaluation","answers":{"is_urgent":{"type":"noul","noul":0.97}}}]', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -180,6 +183,52 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si await runner.completed(); }); +it('traces a Clef evaluation like a TypeSafe Jev evaluation', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const spans = getSpansFromEnvelope(envelope); + const segmentSpan = spans.find(span => span.is_segment); + + const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.')); + expect(genAiSpans).toHaveLength(1); + + expect(genAiSpans[0]).toEqual( + expect.objectContaining({ + name: 'evaluate @cf/cloudflare/clef', + status: 'ok', + is_segment: false, + attributes: { + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.evaluate', type: 'string' }, + [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, + [GEN_AI_OPERATION_NAME]: { value: 'evaluate', type: 'string' }, + [GEN_AI_REQUEST_MODEL]: { value: '@cf/cloudflare/clef', type: 'string' }, + [GEN_AI_RESPONSE_MODEL]: { value: 'clef', type: 'string' }, + [GEN_AI_USAGE_INPUT_TOKENS]: { value: 412, type: 'integer' }, + [GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 1, type: 'integer' }, + [GEN_AI_USAGE_TOTAL_TOKENS]: { value: 413, type: 'integer' }, + // collect only output messages + [GEN_AI_OUTPUT_MESSAGES]: { + type: 'string', + value: '[{"type":"evaluation","answers":{"urgent":{"type":"noul","noul":0.98}}}]', + }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.cloudflare', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, + }, + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/evaluate-clef'); + await runner.completed(); +}); + // The Workers AI integration deliberately does not call `captureException` itself. // When a `run` call fails, the error must bubble up out of the fetch handler and be // reported by the top-level Cloudflare instrumentation instead — so it shows up in diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts index f54038aaa714..86bcab0fb7ce 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; it('Workflow steps create segment spans with correct attributes', async ({ signal }) => { @@ -28,8 +24,8 @@ it('Workflow steps create segment spans with correct attributes', async ({ signa is_segment: true, status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'function' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.workflow' }, + [SENTRY_OP]: { type: 'string', value: 'function' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.workflow' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, 'code.function.name': { type: 'string', value: stepName }, diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index cb2964332492..f4fd271d5c74 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -92,6 +92,8 @@ For example, if you have the following variants in your test app's `package.json If you run `yarn test:run nextjs-pages-dir --variant 13`, it will match against the very first matching variant, which is `nextjs-pages-dir (next@13)`. If you need to target the second variant in the example, you need to be more specific and use `--variant 13-canary`. +A variant runs its `build-command` and `assert-command`. Without them, it runs the app's `test:build` and `test:assert` scripts, like the app's default job. + ## How they work We build our packages, pack them into tarballs (`yarn build:tarball`), and create symlinks in the `packed/` directory @@ -134,6 +136,59 @@ Apps like `cloudflare-workers-send-to-sentry` deploy the built app as a real Wor - Set `"sentryTest": { "optional": true }` in `package.json`, because only the optional CI job has the Cloudflare secrets. +## Runtime variants (Bun, Deno, Cloudflare) + +To test a framework on a runtime other than Node.js, add an `optionalVariants` entry to the existing test app instead of +creating a new app. When the variant is green, move it to `variants`, so that it is required. `react-router-8-framework` +is the reference setup. + +- **`runtime`**: set `"runtime": "bun"`, `"deno"` or `"cloudflare"` on the variant. For such a variant, CI installs Bun + or Deno, and the runner and CI set the `RUNTIME` env var for the build and the assert command. Without `runtime`, + `RUNTIME` is empty or not set, which means `node`. So a runtime variant can use the default `test:build` and + `test:assert` scripts, with no `test:assert:` script, and a new variant needs no change to + `.github/workflows/build.yml`. +- **`RUNTIME` env var**: read it with `getRuntime()` from `@sentry-internal/test-utils`, which throws on any value other + than `node`, `bun`, `deno` or `cloudflare`. Tests branch on it where the runtimes are expected to differ (for example + `platform` or `sdk.name`). +- **SDK per runtime**: each runtime inits its own SDK, the way a user of that runtime would: Node the framework SDK + (`instrument.mjs`), Bun `@sentry/bun` (`instrument.bun.mjs`), Deno `@sentry/deno` (`instrument.deno.mjs`) and + Cloudflare `@sentry/cloudflare`. The framework SDK only provides the framework wrappers on the other runtimes, so + values that come from its `init()` (`sdk.name`, default integrations) are Node-only. +- **Start commands**: `playwright.config.mjs` selects the start command with `getRuntime()`. Bun and Deno use the same build + as Node and only change the start command, for example + `bun --bun --preload ./instrument.bun.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js` and + `deno run -A --preload=@sentry/deno/import --preload=./instrument.deno.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js`. + The app's `deno.json` maps `@sentry/deno/import` to `node_modules`, because the e2e dependencies are `file:` tarballs + that `npm:@sentry/deno/import` does not resolve. +- **Cloudflare**: the app has the Cloudflare dependencies installed all the time. The Cloudflare build has its own + `vite.cloudflare.config.ts` with `@cloudflare/vite-plugin` and `sentryCloudflareVitePlugin` from + `@sentry/cloudflare/vite`, which the runner uses in place of `vite.config.ts` (see runtime-specific files). List the + Sentry plugins after all other plugins: they set no `enforce`, so their position decides when their hooks run, and a + sourcemap upload has to see the final output. Node, Bun and Deno share `vite.config.ts`. The Worker entry lives in a + separate file (for example `workers/app.ts`) and exports a plain handler: the Sentry plugin wraps it with + `withSentry` and reads the init options from `instrument.server.ts` next to the entry. The start command runs + `wrangler dev` on the build output. Code at module scope must not do I/O (for example open a database connection), + because workerd does not allow it. +- **Runtime-specific files**: a file that has the runtime as a part of its name replaces the existing file without that + part, for example `app/entry.server.cloudflare.tsx` replaces `app/entry.server.tsx` and `vite.cloudflare.config.ts` + replaces `vite.config.ts`. For a variant with `runtime`, the runner copies these files over the others in the + temporary copy of the app, before the build. This needs no framework or bundler config. A build in the app folder + itself does not get these files, so run a variant with `yarn test:run --variant `. +- **Deno version**: a variant can pin the Deno version, for example `"deno-version": "v2.9.0"`. +- **Bun**: under `bun run` the SDK cannot inject diagnostics channels into packages that stay outside the build (for + example Express behind `react-router-serve`), so those produce no spans on Bun. Where a test depends on them, branch + on `RUNTIME` and say why in a comment. + +```json +"sentryTest": { + "optionalVariants": [ + { "runtime": "bun", "label": "my-app (bun)" }, + { "runtime": "deno", "label": "my-app (deno)" }, + { "runtime": "cloudflare", "label": "my-app (cloudflare)" } + ] +} +``` + ## Troubleshooting ### Common Issues diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts index 24c3629d8dce..649fe01fed3f 100644 --- a/dev-packages/e2e-tests/ciCopyToTemp.ts +++ b/dev-packages/e2e-tests/ciCopyToTemp.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { copyToTemp } from './lib/copyToTemp'; +import { applyRuntimeFiles } from './lib/runtimeFiles'; async function run(): Promise { const originalPath = process.argv[2]; @@ -13,6 +14,10 @@ async function run(): Promise { console.log(`Copying ${originalPath} to ${tmpDirPath}...`); await copyToTemp(originalPath, tmpDirPath); + + if (process.env.RUNTIME) { + applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); + } } run().catch(error => { diff --git a/dev-packages/e2e-tests/lib/runtimeFiles.ts b/dev-packages/e2e-tests/lib/runtimeFiles.ts new file mode 100644 index 000000000000..a29c0bc890d3 --- /dev/null +++ b/dev-packages/e2e-tests/lib/runtimeFiles.ts @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +import { copyFileSync, existsSync, readdirSync } from 'fs'; +import { join } from 'path'; + +/** + * Copies each file in `dir` that has `runtime` as a part of its name over the existing file without that part, for + * example `app/entry.server.cloudflare.tsx` over `app/entry.server.tsx`. + */ +export function applyRuntimeFiles(dir: string, runtime: string): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) { + continue; + } + + const path = join(dir, entry.name); + if (entry.isDirectory()) { + applyRuntimeFiles(path, runtime); + continue; + } + + const segments = entry.name.split('.'); + const index = segments.indexOf(runtime); + if (index <= 0 || index === segments.length - 1) { + continue; + } + + const target = join(dir, segments.filter((_, i) => i !== index).join('.')); + if (existsSync(target)) { + copyFileSync(path, target); + console.log(`Copied ${path} to ${target} for ${runtime}`); + } + } +} diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 317aa42a3a64..f3a7f115925e 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -6,6 +6,7 @@ import { sync as globSync } from 'glob'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; import { copyToTemp } from './lib/copyToTemp'; +import { applyRuntimeFiles } from './lib/runtimeFiles'; import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; import { addPnpmOverrides } from './lib/pnpmOverrides'; @@ -14,6 +15,8 @@ interface SentryTestVariant { 'assert-command'?: string; label?: string; skip?: boolean; + /** Runtime of the variant (`bun`, `deno` or `cloudflare`). Sets `RUNTIME` and selects the runtime-specific files. */ + runtime?: string; } interface PackageJson { @@ -89,6 +92,7 @@ async function getVariantBuildCommand( testLabel: string; matchedVariantLabel?: string; skip?: boolean; + runtime?: string; }> { try { const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); @@ -108,6 +112,7 @@ async function getVariantBuildCommand( testLabel: matchingVariant.label || testAppPath, matchedVariantLabel: matchingVariant.label, skip: matchingVariant.skip, + runtime: matchingVariant.runtime, }; } @@ -232,16 +237,16 @@ async function run(): Promise { // at all - breaks the build with `unable to open database file`. Give each app its own. const sentryConfigDir = join(tmpDirPath, '.tmp_sentry_home'); await mkdir(sentryConfigDir, { recursive: true }); - const appEnv = { ...env, SENTRY_CONFIG_DIR: sentryConfigDir }; const cwd = tmpDirPath; // Resolve variant if needed - const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel + const { buildCommand, assertCommand, testLabel, matchedVariantLabel, runtime } = variantLabel ? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath) : { buildCommand: 'pnpm test:build', assertCommand: 'pnpm test:assert', testLabel: testAppPath, + runtime: undefined, }; // Print which variant we're using if found @@ -249,6 +254,12 @@ async function run(): Promise { console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`); } + if (runtime) { + applyRuntimeFiles(tmpDirPath, runtime); + } + + const appEnv = { ...env, SENTRY_CONFIG_DIR: sentryConfigDir, ...(runtime ? { RUNTIME: runtime } : {}) }; + console.log(`Building ${testLabel} in ${tmpDirPath}...`); await asyncExec(`volta run ${buildCommand}`, { env: appEnv, cwd }); 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 e3d141736b9d..04bef8b8f52d 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -36,6 +36,7 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/jasmine": "~5.1.0", 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 2abfce92b545..b78206f5566c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -1,7 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -// Cannot use @sentry/angular here due to build stuff -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; // The `angular-19 (streamed)` variant builds the app with `traceLifecycle: 'stream'`, which emits // spans instead of transactions. See `streamed-performance.test.ts` for that variant. @@ -257,7 +256,7 @@ test.describe('TraceDirective', () => { const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); const traceDirectiveSpans = navigationTxn.spans?.filter( - span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + span => span?.data && span?.data[SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', ); expect(traceDirectiveSpans).toHaveLength(2); @@ -265,8 +264,8 @@ test.describe('TraceDirective', () => { expect.arrayContaining([ expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_OP]: 'ui.mount', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'sample-component', }, description: '', // custom component name passed to trace directive @@ -277,8 +276,8 @@ test.describe('TraceDirective', () => { }), expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_OP]: 'ui.mount', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'app-sample-component', }, description: '', // fallback selector name @@ -304,15 +303,15 @@ test.describe('TraceClass Decorator', () => { const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); const classDecoratorSpan = navigationTxn.spans?.find( - span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + span => span?.data && span?.data[SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', ); expect(classDecoratorSpan).toBeDefined(); expect(classDecoratorSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + [SENTRY_OP]: 'ui.mount', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', 'ui.component_name': 'ComponentTrackingComponent', }, description: '', @@ -344,8 +343,8 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_OP]: 'function', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngOnInit', }, description: '', @@ -375,8 +374,8 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_OP]: 'function', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngAfterViewInit', }, description: '', diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js index e4cdc576c585..2b79ec0ed337 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/astro'; Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js index e4cdc576c585..afdc1e1f784e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js @@ -1,9 +1,2 @@ -import * as Sentry from '@sentry/astro'; - -Sentry.init({ - traceLifecycle: 'static', - dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, - environment: 'qa', - tracesSampleRate: 1.0, - tunnel: 'http://localhost:3031/', // proxy server -}); +// The Worker wrapper initializes Sentry from Wrangler bindings. +// Keep this file to prevent Astro from injecting its default Node SDK initialization. diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts index 2a964941217a..e5541b27321b 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts @@ -1,31 +1,29 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('Astro actions', () => { - test('captures transaction for action call', async ({ page }) => { - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /action-test'; + test('captures span for action call', async ({ page }) => { + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /action-test'; }); await page.goto('/action-test'); - const transactionEvent = await transactionEventPromise; + const span = await spanPromise; - expect(transactionEvent).toMatchObject({ - transaction: 'GET /action-test', + expect(span).toMatchObject({ + name: 'GET /action-test', }); - const traceId = transactionEvent.contexts?.trace?.trace_id; + const traceId = span.trace_id; expect(traceId).toMatch(/[a-f0-9]{32}/); }); - test('action submission creates a transaction', async ({ page }) => { + test('action submission creates a span', async ({ page }) => { await page.goto('/action-test'); - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return ( - transactionEvent.transaction?.includes('action-test') && transactionEvent.transaction !== 'GET /action-test' - ); + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'POST /_actions/[...path]'; }); await page.getByText('Submit Action').click(); @@ -36,8 +34,8 @@ test.describe('Astro actions', () => { const resultText = await page.locator('#result').textContent(); expect(resultText).toContain('success'); - const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toBeDefined(); - expect(transactionEvent.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/); + const span = await spanPromise; + expect(span).toBeDefined(); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts index df23d740e830..4e43b4599384 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('server-side errors', () => { test('captures SSR error', async ({ page }) => { @@ -7,8 +7,8 @@ test.describe('server-side errors', () => { return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; }); - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /ssr-error'; + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /ssr-error'; }); // This page returns an error status code, so we need to catch the navigation error @@ -17,14 +17,14 @@ test.describe('server-side errors', () => { }); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const span = await spanPromise; - expect(transactionEvent).toMatchObject({ - transaction: 'GET /ssr-error', + expect(span).toMatchObject({ + name: 'GET /ssr-error', }); - const traceId = transactionEvent.contexts?.trace?.trace_id; - const spanId = transactionEvent.contexts?.trace?.span_id; + const traceId = span.trace_id; + const spanId = span.span_id; expect(traceId).toMatch(/[a-f0-9]{32}/); expect(spanId).toMatch(/[a-f0-9]{16}/); @@ -73,30 +73,30 @@ test.describe('server-side errors', () => { const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; }); - const transactionEventApiPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error/api'; + const apiSpanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /endpoint-error/api'; }); - const transactionEventEndpointPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error'; + const endpointSpanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /endpoint-error'; }); await page.goto('/endpoint-error'); await page.getByText('Get Data').click(); const errorEvent = await errorEventPromise; - const transactionEventApi = await transactionEventApiPromise; - const transactionEventEndpoint = await transactionEventEndpointPromise; + const apiSpan = await apiSpanPromise; + const endpointSpan = await endpointSpanPromise; - expect(transactionEventEndpoint).toMatchObject({ - transaction: 'GET /endpoint-error', + expect(endpointSpan).toMatchObject({ + name: 'GET /endpoint-error', }); - const traceId = transactionEventEndpoint.contexts?.trace?.trace_id; + const traceId = endpointSpan.trace_id; expect(traceId).toMatch(/[a-f0-9]{32}/); - expect(transactionEventApi).toMatchObject({ - transaction: 'GET /endpoint-error/api', + expect(apiSpan).toMatchObject({ + name: 'GET /endpoint-error/api', }); expect(errorEvent).toMatchObject({ diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc index 8ca6b10483d9..0b7b36047973 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc @@ -9,7 +9,6 @@ "SENTRY_ENVIRONMENT": "qa", "SENTRY_TRACES_SAMPLE_RATE": "1.0", "SENTRY_TUNNEL": "http://localhost:3031/", - "SENTRY_TRACE_LIFECYCLE": "static", }, "assets": { "binding": "ASSETS", diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json index 0a51fa94443b..0aa7157512c5 100644 --- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz", - "@sentry/vite-plugin": "^5.3.0" + "@sentry/vite-plugin": "^5.4.1" }, "volta": { "node": "20.19.2", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts index 87ae6c9cff41..990859d99f7e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts @@ -38,6 +38,14 @@ export default { }); return Response.json({ traceId: spanContext?.traceId }); } + case '/test-workers-ai-clef': { + await env.AI.run('@cf/cloudflare/clef', { + model: 'clef', + state: 'Checkout has been failing for every customer for the last hour.', + questions: { urgent: { type: 'noul', instructions: 'Is this support request urgent?' } }, + }); + return Response.json({ traceId: spanContext?.traceId }); + } case '/test-span': return Response.json({ spanId: spanContext?.spanId, traceId: spanContext?.traceId }); case '/test-workflow-sleep': { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index b391f2f8244b..f5ce99ab2a0f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -114,3 +114,24 @@ test('Sends a Workers AI gen_ai.evaluate span for a TypeSafe Jev call to Sentry' 'gen_ai.output.messages': expect.stringContaining('"is_urgent"'), }); }); + +test('Sends a Workers AI gen_ai.evaluate span for a Clef call to Sentry', async () => { + const { traceId }: { traceId: string } = JSON.parse(await fetchFromWorker(`${workerUrl}/test-workers-ai-clef`, 200)); + + console.log(`Polling for gen_ai.evaluate span: sentry trace view ${traceTarget(traceId)}`); + + let spanId: string | undefined; + await expect + .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.evaluate')?.event_id), EVENT_POLLING_OPTIONS) + .toBeDefined(); + + await expect + .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) + .toMatchObject({ + 'gen_ai.operation.name': 'evaluate', + 'gen_ai.request.model': '@cf/cloudflare/clef', + 'gen_ai.response.model': expect.stringContaining('clef'), + 'gen_ai.input.messages': expect.stringContaining('Checkout has been failing'), + 'gen_ai.output.messages': expect.stringContaining('"urgent"'), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index 660ab6ae4628..8a24a5a34fbc 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.35.0", "vitest": "^4.1.11", - "@sentry/rollup-plugin": "^5.3.0" + "@sentry/rollup-plugin": "^5.4.1" }, "volta": { "extends": "../../package.json" 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 f5cfb2795c43..87da5f5f5571 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json @@ -81,6 +81,11 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/macros@1.21.2": "1.21.1" + } + }, "volta": { "extends": "../../package.json" } 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 0653d743b450..67d7d462d9b2 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json @@ -66,6 +66,11 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/macros@1.21.2": "1.21.1" + } + }, "volta": { "extends": "../../package.json" } diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json index 7acab343c96c..d1230979cc65 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json @@ -42,6 +42,14 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/vite@1.7.14": "1.7.13", + "@embroider/macros@1.21.2": "1.21.1", + "@embroider/core@4.6.8": "4.6.7", + "@embroider/compat@4.1.26": "4.1.25" + } + }, "volta": { "extends": "../../package.json" }, diff --git a/dev-packages/e2e-tests/test-applications/ember-vite/package.json b/dev-packages/e2e-tests/test-applications/ember-vite/package.json index 7106f8aa2a00..c1bcfd114fa5 100644 --- a/dev-packages/e2e-tests/test-applications/ember-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-vite/package.json @@ -67,6 +67,14 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/vite@1.7.14": "1.7.13", + "@embroider/macros@1.21.2": "1.21.1", + "@embroider/core@4.6.8": "4.6.7", + "@embroider/compat@4.1.26": "4.1.25" + } + }, "volta": { "extends": "../../package.json" } diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json index ab3b68ce1f5d..2c0d72b3b1d6 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json @@ -40,10 +40,12 @@ "label": "hono-4-legacy (node)" }, { + "runtime": "bun", "assert-command": "RUNTIME=bun pnpm test:assert", "label": "hono-4-legacy (bun)" }, { + "runtime": "deno", "assert-command": "RUNTIME=deno pnpm test:assert", "label": "hono-4-legacy (deno)" } diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json index a3fd78b9ec67..99b2e1ce111e 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -41,10 +41,12 @@ "sentryTest": { "variants": [ { + "runtime": "bun", "assert-command": "pnpm test:assert:bun", "label": "hono-4 (bun)" }, { + "runtime": "deno", "assert-command": "pnpm test:assert:deno", "label": "hono-4 (deno)" }, diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index 9c90734a57c9..7687007647b3 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -16,7 +16,7 @@ "dependencies": { "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "@sentry/vite-plugin": "^5.3.0", + "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2025.5.0", "@shopify/remix-oxygen": "^3.0.0", "graphql": "^16.10.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts new file mode 100644 index 000000000000..bc0bcf0986e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts @@ -0,0 +1,5 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(_req: NextApiRequest, _res: NextApiResponse) { + throw new Error('Pages Router API route error with basePath'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx new file mode 100644 index 000000000000..a410f412abf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return

This page should never render

; +} + +export async function getServerSideProps() { + throw new Error('Pages Router getServerSideProps error with basePath'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts new file mode 100644 index 000000000000..189f19003a5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +test('Includes the basePath in the request url of getServerSideProps errors and spans', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-15-basepath', errorEvent => { + return errorEvent.exception?.values?.[0]?.value === 'Pages Router getServerSideProps error with basePath'; + }); + const segmentSpanPromise = waitForStreamedSpan('nextjs-15-basepath', span => { + return !!span.is_segment && span.name === 'GET /pages-router/gssp-error'; + }); + + await request.get('/my-app/pages-router/gssp-error?q=1'); + + const errorEvent = await errorEventPromise; + const segmentSpan = await segmentSpanPromise; + + expect(errorEvent.request).toMatchObject({ + url: 'http://localhost:3030/my-app/pages-router/gssp-error?q=1', + query_string: 'q=1', + }); + expect(segmentSpan.attributes).toMatchObject({ + 'url.full': { value: 'http://localhost:3030/my-app/pages-router/gssp-error?q=1', type: 'string' }, + }); +}); + +test('Includes the basePath in the request url of Pages Router API route errors and spans', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-15-basepath', errorEvent => { + return errorEvent.exception?.values?.[0]?.value === 'Pages Router API route error with basePath'; + }); + const segmentSpanPromise = waitForStreamedSpan('nextjs-15-basepath', span => { + return !!span.is_segment && span.name === 'GET /api/pages-router-api-error'; + }); + + await request.get('/my-app/api/pages-router-api-error'); + + const errorEvent = await errorEventPromise; + const segmentSpan = await segmentSpanPromise; + + expect(errorEvent.request?.url).toBe('http://localhost:3030/my-app/api/pages-router-api-error'); + expect(segmentSpan.attributes).toMatchObject({ + 'url.full': { value: 'http://localhost:3030/my-app/api/pages-router-api-error', type: 'string' }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 899d3d4e6c87..309cb063a0a5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -20,7 +20,7 @@ "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", "test:assert": "pnpm test:prod && pnpm test:dev", - "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + "test:assert-webpack": "TEST_BUNDLER=webpack pnpm test:prod && pnpm test:dev-webpack" }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index 9c6245a06f5c..ad7b3670c42a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -35,6 +35,8 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn const putSpans = (missTx.spans ?? []).filter(span => span.op === 'cache.put'); expect(new Set(putSpans.map(span => span.description)).size).toBe(1); + expect(putSpans[0]!.data?.['code.file.path']).toBe('app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx'); + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan?.description).toBe(putSpans[0]!.description); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts index dadb754e0a9b..2a5a6c44c9dc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts @@ -57,6 +57,8 @@ test('Should create cache spans around `use cache` functions', async ({ request }), }); + expect(putSpan!.data?.['code.file.path']).toBe('app/api/use-cache/route.ts'); + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get'); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan).toMatchObject({ @@ -97,7 +99,12 @@ test('Should create cache spans for `use cache` inside a rendered page', async ( await request.get(`/use-cache-page?id=${id}`); const hitTx = await hitTxPromise; - expect(missTx.spans?.some(span => span.op === 'cache.put')).toBe(true); + // The source file on a fill span marks which cached function produced the entry. + const missPutSpans = missTx.spans?.filter(span => span.op === 'cache.put') ?? []; + expect(missPutSpans.length).toBeGreaterThan(0); + for (const putSpan of missPutSpans) { + expect(putSpan.data?.['code.file.path']).toBe('app/use-cache-page/page.tsx'); + } // A render can read more than one cache entry, so look at every hit instead of the first `cache.get`. const hitGetSpans = hitTx.spans?.filter(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) ?? []; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json index 6de585c71e76..2bd1b3f77857 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json @@ -13,7 +13,7 @@ "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm cf:build", "test:build-canary": "pnpm install && pnpm add next@canary && pnpm cf:build", - "test:build-latest": "pnpm install && pnpm add next@latest && pnpm cf:build", + "test:build-latest": "pnpm install && pnpm add next@16.3 && pnpm cf:build", "test:assert": "pnpm test:prod" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json index 08d3348c689f..d6b1d3873afd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json @@ -20,7 +20,7 @@ "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", "test:assert": "pnpm test:prod && pnpm test:dev", - "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + "test:assert-webpack": "TEST_BUNDLER=webpack pnpm test:prod && pnpm test:dev-webpack" }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index abd984c429dc..828e867ae3d7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -36,6 +36,8 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn const putSpan = findCacheSpan(missSpans, 'cache.put'); expect(putSpan).toBeDefined(); + expect(putSpan!.attributes['code.file.path']?.value).toBe('app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx'); + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); @@ -114,6 +116,14 @@ test('links two cached levels to different origin traces after the layout expire const fillPutSpans = fillSpans.filter(span => getSpanOp(span) === 'cache.put'); expect(new Set(fillPutSpans.map(span => JSON.stringify(span.attributes['cache.key']?.value))).size).toBe(2); + // One fill per file: the layout (multipart key) and the cached component (JSON key). + expect(new Set(fillPutSpans.map(span => span.attributes['code.file.path']?.value))).toEqual( + new Set([ + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + 'app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx', + ]), + ); + // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. await new Promise(resolve => setTimeout(resolve, 3_000)); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts index 2704de1a11f6..cca7ad2a9e63 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts @@ -30,4 +30,32 @@ test('uses low-cardinality names for `use cache` spans', async ({ request }) => expect(putSpan).toBeDefined(); expect(putSpan!.name).toBe('cache.put'); expect(putSpan!.attributes['cache.key']?.value).toEqual(cacheKeyDigest); + + expect(putSpan!.attributes['code.file.path']?.value).toBe('app/api/use-cache/route.ts'); +}); + +test('sets the source file of the cached component on `cache.put` spans', async ({ request }) => { + const id = crypto.randomUUID(); + + const spansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-sibling-components' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.put').length >= 2 + ); + }); + + await request.get(`/cached-sibling-components?id=${id}`); + const spans = await spansPromise; + + // Both sibling entries come from the same page file. + const putSpans = spans.filter(span => getSpanOp(span) === 'cache.put'); + expect(putSpans.length).toBeGreaterThanOrEqual(2); + for (const putSpan of putSpans) { + expect(putSpan.attributes['code.file.path']?.value).toBe('app/cached-sibling-components/page.tsx'); + } + + // The source file marks the producer of an entry. Reads do not carry it. + const getSpan = findCacheSpan(spans, 'cache.get'); + expect(getSpan).toBeDefined(); + expect(getSpan!.attributes['code.file.path']).toBeUndefined(); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx new file mode 100644 index 000000000000..d59c333380fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx @@ -0,0 +1,7 @@ +import { forbidden } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function ForbiddenPage() { + forbidden(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts new file mode 100644 index 000000000000..349e85337344 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts @@ -0,0 +1,7 @@ +import { forbidden } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + forbidden(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts new file mode 100644 index 000000000000..4b56478b45b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts @@ -0,0 +1,7 @@ +import { unauthorized } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + unauthorized(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx new file mode 100644 index 000000000000..326d27592ccc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/nextjs'; +import { headers } from 'next/headers'; +import { forbidden, unauthorized } from 'next/navigation'; + +export default function AuthInterruptsServerActionPage() { + async function forbiddenServerAction() { + 'use server'; + return await Sentry.withServerActionInstrumentation('forbiddenServerAction', { headers: await headers() }, () => { + forbidden(); + }); + } + + async function unauthorizedServerAction() { + 'use server'; + return await Sentry.withServerActionInstrumentation( + 'unauthorizedServerAction', + { headers: await headers() }, + () => { + unauthorized(); + }, + ); + } + + return ( + <> +
+ +
+
+ +
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx new file mode 100644 index 000000000000..8929e86d1613 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx @@ -0,0 +1,7 @@ +import { unauthorized } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function UnauthorizedPage() { + unauthorized(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts index 8f6c5edfbcca..9278da016887 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts @@ -6,6 +6,7 @@ process.env.VERCEL = '1'; const nextConfig: NextConfig = { experimental: { + authInterrupts: true, sri: { algorithm: 'sha256', }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts new file mode 100644 index 000000000000..a24f6e2c7108 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpansUntilSegment, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +const cases = [ + { kind: 'forbidden', label: 'Forbidden', status: 403, statusMessage: 'permission_denied' }, + { kind: 'unauthorized', label: 'Unauthorized', status: 401, statusMessage: 'unauthenticated' }, +] as const; + +function waitForAuthInterruptError(status: number): Promise { + return waitForError('nextjs-16', errorEvent => { + return JSON.stringify(errorEvent.exception ?? {}).includes(`NEXT_HTTP_ERROR_FALLBACK;${status}`); + }); +} + +// The error would be sent in the same flush as the spans, so a short grace period is enough. +async function expectNoError(errorPromise: Promise): Promise { + const result = await Promise.race([ + errorPromise.then(() => 'error'), + new Promise(resolve => setTimeout(() => resolve('timeout'), 2000)), + ]); + expect(result).toBe('timeout'); +} + +for (const { kind, label, status, statusMessage } of cases) { + test(`Does not capture ${kind}() in a server component and marks the render span`, async ({ page }) => { + const errorPromise = waitForAuthInterruptError(status); + const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', `GET /auth-interrupts/${kind}`); + + const response = await page.goto(`/auth-interrupts/${kind}`); + expect(response?.status()).toBe(status); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + expect(segmentSpan.status).toBe('error'); + expect(segmentSpan.attributes['http.response.status_code']?.value).toBe(status); + + // Server components are only wrapped by our webpack loader, so only webpack builds set a status on the render span. + if (!segmentSpan.attributes['turbopack']) { + expect(spans).toContainEqual( + expect.objectContaining({ + name: `render route (app) /auth-interrupts/${kind}`, + status: 'error', + attributes: expect.objectContaining({ + 'sentry.status.message': { value: statusMessage, type: 'string' }, + }), + }), + ); + } + + await expectNoError(errorPromise); + }); + + test(`Does not capture ${kind}() in a server action and marks the action span`, async ({ page }) => { + const errorPromise = waitForAuthInterruptError(status); + const spanPromise = waitForStreamedSpan('nextjs-16', span => { + return span.name === `${kind}ServerAction` && span.is_segment; + }); + + await page.goto('/auth-interrupts/server-action'); + await page.getByText(`Run ${label} Action`).click(); + + const span = await spanPromise; + expect(span.status).toBe('error'); + expect(span.attributes['sentry.status.message']?.value).toBe(statusMessage); + + await expectNoError(errorPromise); + }); + + test(`Does not capture ${kind}() in a route handler and sets the response status`, async ({ request }) => { + const errorPromise = waitForAuthInterruptError(status); + const spanPromise = waitForStreamedSpan('nextjs-16', span => { + return span.name === `GET /auth-interrupts/route-handler/${kind}` && span.is_segment; + }); + + const response = await request.get(`/auth-interrupts/route-handler/${kind}`); + expect(response.status()).toBe(status); + + const span = await spanPromise; + expect(span.status).toBe('error'); + expect(span.attributes['sentry.status.message']?.value).toBe(statusMessage); + expect(span.attributes['http.response.status_code']?.value).toBe(status); + + await expectNoError(errorPromise); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md b/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md index e67cae081e40..ecdf9ca459ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md +++ b/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md @@ -4,5 +4,6 @@ You are a concise assistant used by an automated end-to-end test. for that place and answer in one short sentence using its result. - When the user asks to count items, call the `count_items` tool with the item names. - When the user asks you to trigger a failure, call the `fail_now` tool. +- When the user asks to classify a ticket, call the `classify_ticket` tool with the ticket text. Do not ask follow-up questions. diff --git a/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts b/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts new file mode 100644 index 000000000000..9dd65a11fd90 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts @@ -0,0 +1,43 @@ +import { experimental_evaluate } from 'ai'; +import { Experimental_EvaluationMockModelV4 } from 'ai/test'; +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +// Runs a TypeSafe Jev evaluation so the e2e test can assert its `gen_ai.evaluate` span. The model is +// mocked because the e2e environment has no TypeSafe credentials. +const jev = new Experimental_EvaluationMockModelV4({ + provider: 'gateway', + modelId: 'typesafe-ai/jev', + doEvaluate: async () => ({ + answers: { + wantsRefund: { type: 'boolean', probability: 0.99 }, + department: { + type: 'choice', + choice: 'billing', + probabilities: { billing: 0.64, technical: 0.36 }, + }, + }, + usage: { inputTokens: 275, outputTokens: 20 }, + warnings: [], + }), +}); + +export default defineTool({ + description: 'Classify a support ticket. Call this when asked to classify a ticket.', + inputSchema: z.object({ ticket: z.string().min(1) }), + async execute({ ticket }) { + const { answers } = await experimental_evaluate({ + model: jev, + state: ticket, + questions: { + wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' }, + department: { + type: 'choice', + instructions: 'Which team should handle this?', + criteria: { billing: 'Charges and refunds', technical: 'Bugs and outages' }, + }, + }, + }); + return answers; + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts index 9099824bd7be..04ad1e5151f4 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts @@ -134,3 +134,34 @@ test('captures errors thrown inside an eve tool', async ({ baseURL }) => { transaction: expect.stringMatching(EVE_AGENT_PATH), }); }); + +test('captures a gen_ai.evaluate span for a Jev call inside an eve tool', async ({ baseURL }) => { + const traceSpansPromise = collectStreamedSpans(APP, spansOfTrace => + ['gen_ai.execute_tool', 'gen_ai.evaluate'].every(op => spansOfTrace.some(span => getSpanOp(span) === op)), + ); + + const sessionId = await runAgentTurn( + baseURL!, + 'Classify this ticket: I cannot log in, and I also want a refund for last month.', + ); + + const traceSpans = await traceSpansPromise; + + const executeTool = traceSpans.find(span => getSpanOp(span) === 'gen_ai.execute_tool'); + const evaluate = traceSpans.find(span => getSpanOp(span) === 'gen_ai.evaluate'); + + expect(executeTool?.attributes?.['gen_ai.tool.name']?.value).toBe('classify_ticket'); + + expect(evaluate?.name).toBe('evaluate typesafe-ai/jev'); + expect(evaluate?.status).toBe('ok'); + expect(evaluate?.attributes?.['sentry.origin']?.value).toBe('auto.vercelai.channel'); + expect(evaluate?.attributes?.['gen_ai.operation.name']?.value).toBe('evaluate'); + expect(evaluate?.attributes?.['gen_ai.request.model']?.value).toBe('typesafe-ai/jev'); + expect(evaluate?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(275); + expect(evaluate?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(20); + expect(evaluate?.attributes?.['gen_ai.input.messages']?.value).toContain('refund for last month'); + expect(evaluate?.attributes?.['gen_ai.conversation.id']?.value).toBe(sessionId); + + expect(evaluate?.trace_id).toBe(executeTool?.trace_id); + expect(evaluate?.parent_span_id).toBe(executeTool?.span_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts index 14d64149cefc..49b9839d98ba 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts @@ -2,6 +2,9 @@ import * as assert from 'assert/strict'; import * as fs from 'fs'; import * as path from 'path'; import { + findDebugIdsWithConflictingSourcemaps, + findFilesWithForeignSourcemaps, + findFilesWithMultipleDebugIds, findInjectedDebugIds, findSourceMapFiles, findSourceMappingUrlComments, @@ -9,6 +12,7 @@ import { getAssembleRequests, getChunkUploadPosts, getDebugIdPairs, + getSourcemapSources, getSourcemaps, loadMockServerResults, } from '@sentry-internal/test-utils'; @@ -173,6 +177,36 @@ for (const outputDir of isStaticBuild ? [CLIENT_OUTPUT] : [CLIENT_OUTPUT, SERVER assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`); console.log(` ${outputDir}: ${injectedDebugIds.length} injected debug ID(s), all uploaded`); + + // Nitro bundles the SSR build again, so its debug IDs can end up next to Nitro's own. + // Reference issue: https://github.com/getsentry/sentry-javascript/issues/18519 + const filesWithMultipleDebugIds = findFilesWithMultipleDebugIds({ outputDir }); + assert.deepEqual(filesWithMultipleDebugIds, [], `Expected at most one debug ID per file in ${outputDir}`); + + // A server file can borrow the debug ID of its SSR build chunk. Then the upload check above passes on + // Vite's map even if Nitro uploaded nothing. + const filesWithForeignSourcemaps = findFilesWithForeignSourcemaps({ outputDir }, sourcemaps); + assert.deepEqual( + filesWithForeignSourcemaps, + [], + `Expected the source map uploaded for each file in ${outputDir} to be generated for that file`, + ); +} + +// If both builds upload a map under a borrowed debug ID, Sentry can resolve with the wrong one. +const conflictingDebugIds = findDebugIdsWithConflictingSourcemaps(sourcemaps); +assert.deepEqual(conflictingDebugIds, [], 'Expected every uploaded debug ID to resolve to exactly one source map'); + +if (!isStaticBuild) { + // Nitro bundles Vite's SSR output, so server frames reach `.vue` files only through the maps of that build. + const serverDebugIds = new Set(findInjectedDebugIds({ outputDir: SERVER_OUTPUT })); + const serverSources = sourcemaps + .filter(map => map.debugId && serverDebugIds.has(map.debugId)) + .flatMap(map => getSourcemapSources(map.sourcemap)); + assert.ok( + serverSources.some(source => source.endsWith('app/pages/index.vue')), + `Expected the server source maps to resolve to app/pages/index.vue, got: ${serverSources.join(', ')}`, + ); } console.log(''); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json index e77ada5ee0d9..d1ac621c81c2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts index d8ad57bca377..93eace9154e4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Cache Instrumentation', () => { const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; @@ -24,9 +24,7 @@ test.describe('Cache Instrumentation', () => { }; // Test that we have cache operations from cachedFunction and cachedEventHandler - const allCacheSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allCacheSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allCacheSpans?.length).toBeGreaterThan(0); // Test getItem spans for cachedFunction - should have both cache miss and cache hit @@ -42,8 +40,8 @@ test.describe('Cache Instrumentation', () => { ); if (cacheMissSpan) { expect(cacheMissSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: false, 'db.operation.name': 'getItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), @@ -59,8 +57,8 @@ test.describe('Cache Instrumentation', () => { ); if (cacheHitSpan) { expect(cacheHitSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), @@ -78,8 +76,8 @@ test.describe('Cache Instrumentation', () => { ); if (cacheSetSpan) { expect(cacheSetSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.put', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'setItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), }); @@ -125,18 +123,16 @@ test.describe('Cache Instrumentation', () => { const transaction1 = await transactionPromise; // Get all cache-related spans - const allCacheSpans = transaction1.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allCacheSpans = transaction1.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); // We should have cache operations expect(allCacheSpans?.length).toBeGreaterThan(0); // Get all getItem operations - const allGetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.get'); + const allGetItemSpans = allCacheSpans?.filter(span => span.data?.[SENTRY_OP] === 'cache.get'); // Get all setItem operations - const allSetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.put'); + const allSetItemSpans = allCacheSpans?.filter(span => span.data?.[SENTRY_OP] === 'cache.put'); // We should have both get and set operations expect(allGetItemSpans?.length).toBeGreaterThan(0); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts index ee1950050482..e6ef2dfc364e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -28,8 +28,8 @@ test.describe('Storage Instrumentation - Aliases', () => { const setSpan = setSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(setSpan).toBeDefined(); expect(setSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.put', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), 'db.operation.name': 'setItem', 'db.collection.name': 'test-storage', @@ -43,8 +43,8 @@ test.describe('Storage Instrumentation - Aliases', () => { const getSpan = getSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(getSpan).toBeDefined(); expect(getSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', @@ -59,8 +59,8 @@ test.describe('Storage Instrumentation - Aliases', () => { const hasSpan = hasSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(hasSpan).toBeDefined(); expect(hasSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'hasItem', @@ -75,8 +75,8 @@ test.describe('Storage Instrumentation - Aliases', () => { const delSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp1')); expect(delSpan).toBeDefined(); expect(delSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.remove', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -87,8 +87,8 @@ test.describe('Storage Instrumentation - Aliases', () => { const removeSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp2')); expect(removeSpan).toBeDefined(); expect(removeSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.remove', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -97,9 +97,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(removeSpan?.description).toBe(prefixKey('alias:temp2')); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allStorageSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allStorageSpans?.length).toBeGreaterThan(0); allStorageSpans?.forEach(span => { expect(span.status).toBe('ok'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts index b4138a8eb6e8..5dd3e7daa680 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -28,8 +28,8 @@ test.describe('Storage Instrumentation', () => { const setItemSpan = setItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(setItemSpan).toBeDefined(); expect(setItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.put', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), 'db.operation.name': 'setItem', 'db.collection.name': 'test-storage', @@ -45,8 +45,8 @@ test.describe('Storage Instrumentation', () => { ); expect(setItemRawSpan).toBeDefined(); expect(setItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.put', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), 'db.operation.name': 'setItemRaw', 'db.collection.name': 'test-storage', @@ -59,8 +59,8 @@ test.describe('Storage Instrumentation', () => { const hasItemSpan = hasItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(hasItemSpan).toBeDefined(); expect(hasItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'hasItem', @@ -74,8 +74,8 @@ test.describe('Storage Instrumentation', () => { const getItemSpan = getItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(getItemSpan).toBeDefined(); expect(getItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', @@ -92,8 +92,8 @@ test.describe('Storage Instrumentation', () => { ); expect(getItemRawSpan).toBeDefined(); expect(getItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItemRaw', @@ -105,8 +105,8 @@ test.describe('Storage Instrumentation', () => { const getKeysSpans = findSpansByMethod('getKeys'); expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); expect(getKeysSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.get', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'getKeys', 'db.collection.name': 'test-storage', 'db.system.name': 'memory', @@ -120,8 +120,8 @@ test.describe('Storage Instrumentation', () => { ); expect(removeItemSpan).toBeDefined(); expect(removeItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.remove', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -132,17 +132,15 @@ test.describe('Storage Instrumentation', () => { const clearSpans = findSpansByMethod('clear'); expect(clearSpans.length).toBeGreaterThanOrEqual(1); expect(clearSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_OP]: 'cache.remove', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'clear', 'db.collection.name': 'test-storage', 'db.system.name': 'memory', }); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allStorageSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allStorageSpans?.length).toBeGreaterThan(0); allStorageSpans?.forEach(span => { expect(span.status).toBe('ok'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts index 2bc3c805f172..58f65a87ec26 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('nuxt-4-static', transactionEvent => { @@ -14,8 +14,8 @@ test('sends a server action transaction on pageload', async ({ page }) => { expect(transaction.contexts.trace).toEqual( expect.objectContaining({ data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', + [SENTRY_OP]: 'http.server', + [SENTRY_ORIGIN]: 'auto.http.http_server', }), }), ); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue index 3547773a1af9..2768eb370f94 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue @@ -7,17 +7,19 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue index 019404aaf460..fde203b1575c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue @@ -3,9 +3,10 @@ import { useRoute, useFetch } from '#imports'; const route = useRoute(); const param = route.params.param; +const apiPrefix = route.query.apiPrefix ?? '/api'; const fetchError = async () => { - await useFetch(`/api/param-error/${param}`); + await useFetch(`${apiPrefix}/param-error/${param}`); }; const fetchData = async () => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue index 41daf0460b05..39d629b25ad6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue @@ -3,8 +3,9 @@ import { useFetch, useRoute } from '#imports'; const route = useRoute(); const userId = route.params.userId as string; +const apiPrefix = route.query.apiPrefix ?? '/api'; -const { data } = await useFetch(`/api/user/${userId}`, { +const { data } = await useFetch(`${apiPrefix}/user/${userId}`, { server: false, // Don't fetch during SSR, only client-side }); 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 ac1ea1939999..5f84fb085b6d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -25,7 +25,7 @@ "ioredis": "5.10.1", "mongoose": "^9.10.2", "mysql": "^2.18.1", - "nuxt": "^4.1.2" + "nuxt": "^4.6.0" }, "devDependencies": { "@playwright/test": "~1.63.0", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts new file mode 100644 index 000000000000..ab0b283df34b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts @@ -0,0 +1,9 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + return { + message: 'Server middleware test endpoint', + path: event.url.pathname, + method: event.req.method, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts new file mode 100644 index 000000000000..8f8f4c4ae4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts @@ -0,0 +1,7 @@ +import { defineEventHandler } from 'nuxt/server'; + +// `$fetch` is a Nitro global, not a `nuxt/server` export, so the portable copy uses `fetch`. +export default defineEventHandler(async () => { + const response = await fetch('https://example.com'); + return response.text(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts new file mode 100644 index 000000000000..6ad843e18b9f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts new file mode 100644 index 000000000000..106f0ea9110c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts new file mode 100644 index 000000000000..7b70b0af0586 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts @@ -0,0 +1,16 @@ +import { defineEventHandler } from 'nuxt/server'; + +// Mimics ky's and got's `HTTPError`: it shares its `name` with h3's error class, but keeps the +// status on `response` instead of on the error itself. +class ThirdPartyHTTPError extends Error { + public readonly response = { status: 404 }; + + public constructor(message: string) { + super(message); + this.name = 'HTTPError'; + } +} + +export default defineEventHandler(() => { + throw new ThirdPartyHTTPError('Nuxt 4 third-party HTTPError'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts new file mode 100644 index 000000000000..6fff4f8f91b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from 'nuxt/server'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts new file mode 100644 index 000000000000..58d68a494905 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + event.res.headers.set('x-nuxt-server-first-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts new file mode 100644 index 000000000000..62b19ad9c1ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts @@ -0,0 +1,9 @@ +import { defineEventHandler, getQuery } from 'nuxt/server'; + +export default defineEventHandler(event => { + if (getQuery(event).throwNuxtServerError === 'true') { + throw new Error('nuxt/server auth middleware error'); + } + + event.res.headers.set('x-nuxt-server-auth-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts index ea9c78b2d60e..418c09f16c57 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts @@ -1,94 +1,97 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; - -test.describe('server-side errors', async () => { - test('captures api fetch error (fetched on click)', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Server API Error', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/server-error'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', - }); - - expect(exception1.type).toEqual('Error'); - expect(exception1.value).toEqual('Nuxt 4 Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, - }); - }); - - test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; +import { IMPORT_SURFACES } from './importSurfaces'; + +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test.describe(`server-side errors (${name})`, () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Server API Error', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/server-error`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('Error'); + expect(exception1.value).toEqual('Nuxt 4 Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - await page.goto(`/test-param/1234`); - await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/param-error/1234'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Param Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; + }); + + await page.goto(`/test-param/1234?apiPrefix=${apiPrefix}`); + await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/param-error/1234`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Param Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('Error'); + expect(exception1.value).toEqual('Nuxt 4 Param Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - expect(exception1.type).toEqual('Error'); - expect(exception1.value).toEqual('Nuxt 4 Param Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, + // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the + // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike + // is covered by the unit tests. + test('captures a thrown third-party `HTTPError`', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError'); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/third-party-http-error`); + expect(error.exception.values).toContainEqual( + expect.objectContaining({ + value: 'Nuxt 4 third-party HTTPError', + mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }), + }), + ); }); }); - - // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the - // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike - // is covered by the unit tests. - test('captures a thrown third-party `HTTPError`', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError'); - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/third-party-http-error'); - expect(error.exception.values).toContainEqual( - expect.objectContaining({ - value: 'Nuxt 4 third-party HTTPError', - mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }), - }), - ); - }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts new file mode 100644 index 000000000000..d0d4fd9a22d3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts @@ -0,0 +1,6 @@ +// The server test routes under `apiPrefix` are copies of each other that differ only in where they import from. +// Reference: https://github.com/nuxt/nuxt/pull/36275 +export const IMPORT_SURFACES = [ + { name: '#imports', apiPrefix: '/api' }, + { name: 'nuxt/server', apiPrefix: '/api/nuxt-server' }, +] as const; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts index fe44b684fe14..cc1dd5b14d2d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -1,139 +1,145 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpansUntilSegment, getSpanOp, waitForError } from '@sentry-internal/test-utils'; - -async function collectRequestSpans() { - const spans = await collectStreamedSpansUntilSegment( - 'nuxt-4', - span => span.attributes['url.path']?.value === '/api/middleware-test', - ); - const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/middleware-test'); +import { IMPORT_SURFACES } from './importSurfaces'; + +// Nitro runs every file in `server/middleware/` on every request, so each surface tells its spans apart by +// middleware name. `nuxt/server` has no object syntax (`onRequest`, `onBeforeResponse`) and no `eventHandler` +// alias, so only the simple middleware has a copy. +const MIDDLEWARE = { + '#imports': { + names: ['01.first', '02.second', '03.auth', '04.hooks', '05.array-hooks'], + simpleNames: ['01.first', '02.second', '03.auth'], + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) + spanCount: 11, + authName: '03.auth', + authErrorQuery: 'throwError=true', + authErrorMessage: 'Auth middleware error', + }, + 'nuxt/server': { + names: ['06.nuxt-server-first', '07.nuxt-server-auth'], + simpleNames: ['06.nuxt-server-first', '07.nuxt-server-auth'], + spanCount: 2, + authName: '07.nuxt-server-auth', + authErrorQuery: 'throwNuxtServerError=true', + authErrorMessage: 'nuxt/server auth middleware error', + }, +} as const; + +async function collectRequestSpans(path = '/api/middleware-test') { + const spans = await collectStreamedSpansUntilSegment('nuxt-4', span => span.attributes['url.path']?.value === path); + const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); return spans.filter(span => span.trace_id === rootSpan?.trace_id); } -test.describe('Server Middleware Instrumentation', () => { - test('should create separate spans for each server middleware', async ({ request }) => { - const spansPromise = collectRequestSpans(); +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + const middleware = MIDDLEWARE[name]; + const path = `${apiPrefix}/middleware-test`; + const isSurfaceMiddlewareSpan = (span: { attributes: Record }) => + (middleware.names as readonly unknown[]).includes(span.attributes['nuxt.middleware.name']?.value); - // Make request to the API endpoint that will trigger all server middleware - const response = await request.get('/api/middleware-test'); - expect(response.status()).toBe(200); + test.describe(`Server Middleware Instrumentation (${name})`, () => { + test('should create separate spans for each server middleware', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const responseData = await response.json(); - expect(responseData.message).toBe('Server middleware test endpoint'); + const response = await request.get(path); + expect(response.status()).toBe(200); - const spans = await spansPromise; + const responseData = await response.json(); + expect(responseData.message).toBe('Server middleware test endpoint'); - // Verify that we have spans for each middleware - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + const spans = await spansPromise; - // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) - expect(middlewareSpans).toHaveLength(11); - - // Check for specific middleware spans - const findSpanByName = (name: string) => - middlewareSpans.find(span => span.attributes['nuxt.middleware.name']?.value === name); - - const firstMiddlewareSpan = findSpanByName('01.first'); - const secondMiddlewareSpan = findSpanByName('02.second'); - const authMiddlewareSpan = findSpanByName('03.auth'); - const hooksOnRequestSpan = findSpanByName('04.hooks'); - const arrayHooksHandlerSpan = findSpanByName('05.array-hooks'); - - expect(firstMiddlewareSpan).toBeDefined(); - expect(secondMiddlewareSpan).toBeDefined(); - expect(authMiddlewareSpan).toBeDefined(); - expect(hooksOnRequestSpan).toBeDefined(); - expect(arrayHooksHandlerSpan).toBeDefined(); - - // Verify each span has the correct attributes - [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { - expect(span).toEqual( - expect.objectContaining({ - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'middleware' }, - 'sentry.origin': { type: 'string', value: 'auto.middleware.nuxt' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'http.route': { type: 'string', value: '/api/middleware-test' }, - }), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }), - ); - }); + const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && isSurfaceMiddlewareSpan(span)); - // Verify spans have different span IDs (each middleware gets its own span) - const spanIds = middlewareSpans.map(span => span.span_id); - const uniqueSpanIds = new Set(spanIds); - // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) - expect(uniqueSpanIds.size).toBe(11); + expect(middlewareSpans).toHaveLength(middleware.spanCount); - // Verify spans share the same trace ID - const traceIds = middlewareSpans.map(span => span.trace_id); - const uniqueTraceIds = new Set(traceIds); - expect(uniqueTraceIds.size).toBe(1); - }); + const findSpanByName = (middlewareName: string) => + middlewareSpans.find(span => span.attributes['nuxt.middleware.name']?.value === middlewareName); - test('middleware spans should have proper parent-child relationship', async ({ request }) => { - const spansPromise = collectRequestSpans(); + middleware.names.forEach(middlewareName => { + expect(findSpanByName(middlewareName)).toBeDefined(); + }); - await request.get('/api/middleware-test'); - const spans = await spansPromise; + middleware.simpleNames.forEach(middlewareName => { + expect(findSpanByName(middlewareName)).toEqual( + expect.objectContaining({ + is_segment: false, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.nuxt' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'http.route': { type: 'string', value: path }, + }), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }), + ); + }); - const segmentSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/middleware-test', - ); - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + // Verify spans have different span IDs (each middleware gets its own span) + const uniqueSpanIds = new Set(middlewareSpans.map(span => span.span_id)); + expect(uniqueSpanIds.size).toBe(middleware.spanCount); - // All middleware spans should be children of the request's segment span - middlewareSpans.forEach(span => { - expect(span.parent_span_id).toBe(segmentSpan?.span_id); + const uniqueTraceIds = new Set(middlewareSpans.map(span => span.trace_id)); + expect(uniqueTraceIds.size).toBe(1); }); - }); - test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { - const spansPromise = collectRequestSpans(); + test('middleware spans should have proper parent-child relationship', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const errorEventPromise = waitForError('nuxt-4', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Auth middleware error'; + await request.get(path); + const spans = await spansPromise; + + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); + const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && isSurfaceMiddlewareSpan(span)); + + expect(middlewareSpans).toHaveLength(middleware.spanCount); + middlewareSpans.forEach(span => { + expect(span.parent_span_id).toBe(segmentSpan?.span_id); + }); }); - // Make request with query param to trigger error in auth middleware - const response = await request.get('/api/middleware-test?throwError=true'); + test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - // The request should fail due to the middleware error - expect(response.status()).toBe(500); + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === middleware.authErrorMessage; + }); - const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); + const response = await request.get(`${path}?${middleware.authErrorQuery}`); - // Find the auth middleware span - const authMiddlewareSpan = spans.find( - span => getSpanOp(span) === 'middleware' && span.attributes['nuxt.middleware.name']?.value === '03.auth', - ); + expect(response.status()).toBe(500); - expect(authMiddlewareSpan).toBeDefined(); + const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - // Verify the span has error status - expect(authMiddlewareSpan?.status).toBe('error'); + const authMiddlewareSpan = spans.find( + span => + getSpanOp(span) === 'middleware' && span.attributes['nuxt.middleware.name']?.value === middleware.authName, + ); + + expect(authMiddlewareSpan).toBeDefined(); + expect(authMiddlewareSpan?.status).toBe('error'); - // Verify the error event is associated with the correct request - expect(errorEvent.transaction).toContain('GET /api/middleware-test'); + expect(errorEvent.transaction).toContain(`GET ${path}`); - // Verify the error has the correct mechanism - expect(errorEvent.exception?.values?.[0]).toEqual( - expect.objectContaining({ - value: 'Auth middleware error', - type: 'Error', - mechanism: expect.objectContaining({ - handled: false, - type: 'auto.middleware.nuxt', + expect(errorEvent.exception?.values?.[0]).toEqual( + expect.objectContaining({ + value: middleware.authErrorMessage, + type: 'Error', + mechanism: expect.objectContaining({ + handled: false, + type: 'auto.middleware.nuxt', + }), }), - }), - ); + ); + }); }); +}); +// `nuxt/server` has no object syntax for middleware hooks, so these only run on the classic surface. +test.describe('Server Middleware Instrumentation (#imports hooks)', () => { test('should create spans for onRequest and onBeforeResponse hooks', async ({ request }) => { const spansPromise = collectRequestSpans(); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts index 16097e9d1dd2..4ef6d7c039e0 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; test('sends a server root span on pageload', async ({ page }) => { const serverSpanPromise = waitForStreamedSpan('nuxt-4', span => { @@ -38,34 +39,36 @@ test('does not send spans for build asset folder "_nuxt"', async ({ page }) => { expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); }); -test('captures server API calls made with Nitro $fetch', async ({ page }) => { - // The server-side http.client span can flush before its segment. Accumulate until both arrived. - const spansPromise = collectStreamedSpans('nuxt-4', spans => { - return ( - spans.some(span => span.is_segment && span.attributes['url.path']?.value === '/api/nitro-fetch') && - spans.some( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes('example.com'), - ) +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`captures server API calls made with Nitro $fetch (${name})`, async ({ page }) => { + // The server-side http.client span can flush before its segment. Accumulate until both arrived. + const spansPromise = collectStreamedSpans('nuxt-4', spans => { + return ( + spans.some(span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`) && + spans.some( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', + ) + ); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); + + const spans = await spansPromise; + + const serverSegmentSpan = spans.find( + span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`, + ); + const httpClientSpan = spans.find( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', ); - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); - - const spans = await spansPromise; - - const serverSegmentSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/nitro-fetch', - ); - const httpClientSpan = spans.find( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes('example.com'), - ); - expect(serverSegmentSpan).toBeDefined(); - expect(getSpanOp(serverSegmentSpan!)).toBe('http.server'); + expect(serverSegmentSpan).toBeDefined(); + expect(getSpanOp(serverSegmentSpan!)).toBe('http.server'); - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan?.name).toBe('GET example.com'); - expect(httpClientSpan?.trace_id).toBe(serverSegmentSpan?.trace_id); - expect(httpClientSpan?.parent_span_id).toBe(serverSegmentSpan?.span_id); + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan?.name).toBe('GET example.com'); + expect(httpClientSpan?.trace_id).toBe(serverSegmentSpan?.trace_id); + expect(httpClientSpan?.parent_span_id).toBe(serverSegmentSpan?.span_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index 70419d18389e..7ee91e3eeb9c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -1,9 +1,9 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; test.describe('distributed tracing', () => { const PARAM = 's0me-param'; - const API_PATH = `/api/user/${PARAM}`; test('capture a distributed pageload trace', async ({ page }) => { const clientSpanPromise = waitForStreamedSpan('nuxt-4', span => { @@ -62,87 +62,93 @@ test.describe('distributed tracing', () => { expect(serverSpan.trace_id).toBe(metaTraceId); }); - test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { - // The `http.client` span ends after the pageload segment, so it can be flushed in a later - // envelope. Accumulate until both spans have arrived. - const clientSpansPromise = collectStreamedSpans('nuxt-4', spans => { - return ( - spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && - spans.some( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ) + IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`capture a distributed trace from a client-side API request with parametrized routes (${name})`, async ({ + page, + }) => { + const API_PATH = `${apiPrefix}/user/${PARAM}`; + + // The `http.client` span ends after the pageload segment, so it can be flushed in a later + // envelope. Accumulate until both spans have arrived. + const clientSpansPromise = collectStreamedSpans('nuxt-4', spans => { + return ( + spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && + spans.some( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), + ) + ); + }); + const ssrSpanPromise = waitForStreamedSpan('nuxt-4', span => { + return span.is_segment && span.name.includes('GET /test-param/user'); + }); + const serverReqSpanPromise = waitForStreamedSpan('nuxt-4', span => { + return span.is_segment && span.name.includes(`GET ${apiPrefix}/user/`); + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}?apiPrefix=${apiPrefix}`); + + const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ + clientSpansPromise, + ssrSpanPromise, + serverReqSpanPromise, + ]); + + const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); + const httpClientSpan = clientSpans.find( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), ); - }); - const ssrSpanPromise = waitForStreamedSpan('nuxt-4', span => { - return span.is_segment && span.name.includes('GET /test-param/user'); - }); - const serverReqSpanPromise = waitForStreamedSpan('nuxt-4', span => { - return span.is_segment && span.name.includes('GET /api/user/'); - }); - // Navigate to the page which will trigger an API call from the client-side - await page.goto(`/test-param/user/${PARAM}`); - - const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ - clientSpansPromise, - ssrSpanPromise, - serverReqSpanPromise, - ]); - - const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); - const httpClientSpan = clientSpans.find( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ); - - expect(pageloadSpan).toMatchObject({ - name: '/test-param/user/:userId()', - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'pageload' }, - 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); - - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan).toMatchObject({ - // A relative fetch has no domain of its own, so it resolves against the page origin. - name: 'GET localhost', - parent_span_id: pageloadSpan?.span_id, // pageload span is parent - attributes: expect.objectContaining({ - type: { type: 'string', value: 'fetch' }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, - 'url.domain': { type: 'string', value: 'localhost' }, - }), + expect(pageloadSpan).toMatchObject({ + name: '/test-param/user/:userId()', + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), + }); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toMatchObject({ + // A relative fetch has no domain of its own, so it resolves against the page origin. + name: 'GET localhost', + parent_span_id: pageloadSpan?.span_id, // pageload span is parent + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, + 'url.domain': { type: 'string', value: 'localhost' }, + }), + }); + + expect(ssrSpan).toMatchObject({ + name: 'GET /test-param/user/:userId()', // parametrized route + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), + }); + + expect(serverReqSpan).toMatchObject({ + name: `GET ${apiPrefix}/user/:userId`, // parametrized route + is_segment: true, + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + }), + }); + + // `collectStreamedSpans` already guarantees the pageload and http.client spans share a trace, + // so only the independently awaited server spans need the check. + expect(pageloadSpan?.trace_id).toBeDefined(); + expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); + expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); - - expect(ssrSpan).toMatchObject({ - name: 'GET /test-param/user/:userId()', // parametrized route - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); - - expect(serverReqSpan).toMatchObject({ - name: 'GET /api/user/:userId', // parametrized route - is_segment: true, - parent_span_id: httpClientSpan?.span_id, // http.client span is parent - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - }), - }); - - // `collectStreamedSpans` already guarantees the pageload and http.client spans share a trace, - // so only the independently awaited server spans need the check. - expect(pageloadSpan?.trace_id).toBeDefined(); - expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); - expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore deleted file mode 100644 index e637ae342782..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# 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 - -# react router -.react-router - -# cloudflare -.wrangler diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx deleted file mode 100644 index 0d928d68760a..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sentry from '@sentry/react-router'; -import { startTransition, StrictMode } from 'react'; -import { hydrateRoot } from 'react-dom/client'; -import { HydratedRouter } from 'react-router/dom'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: 'https://username@domain/123', - tunnel: `http://localhost:3031/`, // proxy server - integrations: [Sentry.reactRouterTracingIntegration()], - tracesSampleRate: 1.0, - tracePropagationTargets: [/^\//], -}); - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx deleted file mode 100644 index c09b53b99d46..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts deleted file mode 100644 index 744ffc9485de..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { index, prefix, route, type RouteConfig } from '@react-router/dev/routes'; - -export default [ - index('routes/home.tsx'), - ...prefix('performance', [route('db-mysql', 'routes/performance/db-mysql.tsx')]), -] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx deleted file mode 100644 index 7e59685f3987..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Link } from 'react-router'; - -export default function Home() { - return ( -
-

react-router-8-cloudflare

- db-mysql -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml deleted file mode 100644 index c03828e36b43..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - db: - image: mysql:8.0 - restart: always - container_name: e2e-tests-react-router-8-cloudflare-mysql - # The `mysql` 2.x driver doesn't speak MySQL 8's default - # `caching_sha2_password` auth, so force the legacy plugin. - command: ['--default-authentication-plugin=mysql_native_password'] - ports: - - '3306:3306' - environment: - MYSQL_ROOT_PASSWORD: docker - healthcheck: - test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker'] - interval: 2s - timeout: 3s - retries: 30 - start_period: 10s diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs deleted file mode 100644 index b0695e3731f7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default async function globalSetup() { - // `--wait` blocks until the healthcheck passes, so the first request can connect. - execSync('docker compose up -d --wait', { - cwd: __dirname, - stdio: 'inherit', - }); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs deleted file mode 100644 index 2742279431ad..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default async function globalTeardown() { - execSync('docker compose down --volumes', { - cwd: __dirname, - stdio: 'inherit', - }); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json deleted file mode 100644 index 094e2e4ebfbe..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "react-router-8-cloudflare", - "version": "0.1.0", - "type": "module", - "private": true, - "dependencies": { - "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "isbot": "^5.1.17", - "mysql": "^2.18.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router": "^8" - }, - "devDependencies": { - "@cloudflare/vite-plugin": "^1.35.0", - "@cloudflare/workers-types": "^4.20260504.0", - "@playwright/test": "~1.63.0", - "@react-router/dev": "^8", - "@sentry-internal/test-utils": "link:../../../test-utils", - "@types/mysql": "^2.15.26", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "typescript": "^5.9.0", - "vite": "7.3.5", - "wrangler": "^4.120.0" - }, - "scripts": { - "build": "react-router build", - "dev": "react-router dev", - "preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030", - "proxy": "node start-event-proxy.mjs", - "typecheck": "react-router typegen && tsc", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm typecheck && TEST_ENV=production playwright test" - }, - "volta": { - "node": "24.15.0", - "extends": "../../package.json" - }, - "sentryTest": { - "optional": true - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs deleted file mode 100644 index 579222812424..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig( - { - startCommand: 'pnpm preview', - port: 3030, - }, - { - globalSetup: './global-setup.mjs', - globalTeardown: './global-teardown.mjs', - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico deleted file mode 100644 index 5dbdfcddcb14..000000000000 Binary files a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico and /dev/null differ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts deleted file mode 100644 index 51e8967770b3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Config } from '@react-router/dev/config'; - -export default { - ssr: true, -} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs deleted file mode 100644 index d78c58fa1714..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'react-router-8-cloudflare', -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts deleted file mode 100644 index 1dfa920d1625..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; - -// Under span streaming the mysql span name is the query summary, so both queries here are named -// `SELECT`. `db.query.text` is what tells them apart. -test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'db'), - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); - expect(firstQuery).toBeDefined(); - expect(firstQuery!.name).toBe('SELECT'); - expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); - expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); - expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); - expect(firstQuery!.attributes['server.port']?.value).toBe(3306); - expect(firstQuery!.attributes['db.user']?.value).toBe('root'); -}); - -test('a nested query lands on the same segment (async context restored)', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - - // These are scoped to the http.server segment's trace, so both queries landing here is what proves - // the nested one kept the async context. - const queryTexts = spans - .filter(span => getSpanOp(span) === 'db') - .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT ? + ? AS solution'); - expect(queryTexts).toContain('SELECT NOW()'); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json deleted file mode 100644 index 573bcd3ad911..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - // workers-types rather than node: the server runs in workerd. - "types": ["@cloudflare/workers-types", "vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", - - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true - }, - "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"] -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts deleted file mode 100644 index cd07d7a3fe03..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { cloudflare } from '@cloudflare/vite-plugin'; -import { reactRouter } from '@react-router/dev/vite'; -import { sentryReactRouter } from '@sentry/react-router/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig(async config => ({ - plugins: [ - cloudflare({ viteEnvironment: { name: 'ssr' } }), - reactRouter(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...((await sentryReactRouter({ sourcemaps: { disable: true } }, config)) as any[]), - ], -})); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts deleted file mode 100644 index d9a6c5451d72..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Sentry from '@sentry/cloudflare'; -import { createRequestHandler } from 'react-router'; - -const requestHandler = createRequestHandler(() => import('virtual:react-router/server-build'), import.meta.env.MODE); - -interface Env { - E2E_TEST_DSN: string; -} - -// `withSentry` is what reads the build-time orchestrion marker; without it the injected -// `diagnostics_channel` publishers would fire with nobody subscribed. -export default Sentry.withSentry( - (env: Env) => ({ - dsn: env.E2E_TEST_DSN, - tunnel: 'http://localhost:3031/', - tracesSampleRate: 1.0, - environment: 'qa', // dynamic sampling bias to keep transactions - }), - { - // No load context: React Router 8 takes a `RouterContextProvider`, not v7's `{ cloudflare }` - // object, and nothing here reads bindings from a loader. - async fetch(request) { - return requestHandler(request); - }, - } satisfies ExportedHandler, -); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx similarity index 78% rename from dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx rename to dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx index df56b2a3a8c0..31a4fec981f6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx @@ -1,9 +1,9 @@ +import { captureException } from '@sentry/cloudflare'; import * as Sentry from '@sentry/react-router/cloudflare'; import { isbot } from 'isbot'; import { renderToReadableStream } from 'react-dom/server'; import { type EntryContext, type HandleErrorFunction, ServerRouter } from 'react-router'; -// workerd has no `renderToPipeableStream`, so this renders to a web stream instead. async function handleRequest( request: Request, responseStatusCode: number, @@ -12,8 +12,10 @@ async function handleRequest( ): Promise { let shellRendered = false; const userAgent = request.headers.get('user-agent'); + const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16)))); - const body = await renderToReadableStream(, { + const body = await renderToReadableStream(, { + nonce, signal: request.signal, onError(error: unknown) { responseStatusCode = 500; @@ -26,7 +28,6 @@ async function handleRequest( }); shellRendered = true; - // Bots need complete markup rather than a streamed shell. if (userAgent && isbot(userAgent)) { await body.allReady; } @@ -40,12 +41,12 @@ async function handleRequest( } export const handleError: HandleErrorFunction = (error, { request }) => { - // React Router aborts interrupted requests, don't report those. if (!request.signal.aborted) { - Sentry.captureException(error); - // eslint-disable-next-line no-console + captureException(error, { mechanism: { type: 'react-router', handled: false } }); console.error(error); } }; export default Sentry.wrapSentryHandleRequest(handleRequest); + +export const instrumentations = [Sentry.createSentryServerInstrumentation()]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx index 01682288ab71..9f9a70db740a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx @@ -18,3 +18,5 @@ const handleRequest = Sentry.createSentryHandleRequest({ export default handleRequest; export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true }); + +export const instrumentations = [Sentry.createSentryServerInstrumentation()]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts index 3e3839295b1d..4da99dc5b86c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts @@ -3,6 +3,9 @@ import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes export default [ index('routes/home.tsx'), route('__sentry-flush', 'routes/sentry-flush.tsx'), + route('api/headers', 'routes/api/headers.ts'), + route('isolation/:id', 'routes/isolation.ts'), + route('logs', 'routes/logs.ts'), ...prefix('errors', [ route('client', 'routes/errors/client.tsx'), route('client/:client-param', 'routes/errors/client-param.tsx'), @@ -20,5 +23,7 @@ export default [ route('server-action', 'routes/performance/server-action.tsx'), route('with-middleware', 'routes/performance/with-middleware.tsx'), route('redis', 'routes/performance/redis.tsx'), + route('db-mysql', 'routes/performance/db-mysql.tsx'), + route('outgoing-fetch', 'routes/performance/outgoing-fetch.ts'), ]), ] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts new file mode 100644 index 000000000000..900a5f946174 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts @@ -0,0 +1,8 @@ +import type { Route } from './+types/headers'; + +export function loader({ request }: Route.LoaderArgs) { + return Response.json({ + 'sentry-trace': request.headers.get('sentry-trace'), + baggage: request.headers.get('baggage'), + }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts new file mode 100644 index 000000000000..1a0783990068 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/react-router'; +import type { Route } from './+types/isolation'; + +const waitingRequests = new Map void>(); + +// Holds the first of two requests until the second one has set its tag, so both requests use their scopes at the +// same time. +function waitForPartner(id: string, partner: string): Promise { + const releasePartner = waitingRequests.get(partner); + if (releasePartner) { + waitingRequests.delete(partner); + releasePartner(); + return Promise.resolve(); + } + return new Promise(resolve => waitingRequests.set(id, resolve)); +} + +export async function loader({ params, request }: Route.LoaderArgs) { + Sentry.setTag('isolation-id', params.id); + + const partner = new URL(request.url).searchParams.get('partner'); + if (partner) { + await waitForPartner(params.id, partner); + } + + Sentry.captureMessage(`isolation ${params.id}`); + return Response.json({ id: params.id }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts new file mode 100644 index 000000000000..d218fd4c649c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/react-router'; + +export function loader() { + Sentry.logger.info('react-router server log', { route: 'logs' }); + return Response.json({ logged: true }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx similarity index 87% rename from dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx rename to dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx index ea2ac1fcea61..55d73a44004c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx @@ -1,8 +1,8 @@ import mysql from 'mysql'; import type { Route } from './+types/db-mysql'; -// These queries produce `db` spans from the build-time orchestrion transform alone — workerd can't -// monkey-patch requires, so there's no OTel hook involved. +// On Cloudflare these `db` spans come from the build-time orchestrion transform alone, because workerd +// can't monkey-patch requires. export async function loader(): Promise<{ status: string }> { // Connect inside the loader: workerd forbids I/O in global scope. const connection = mysql.createConnection({ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts new file mode 100644 index 000000000000..0f4535bb4943 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts @@ -0,0 +1,6 @@ +import type { Route } from './+types/outgoing-fetch'; + +export async function loader({ request }: Route.LoaderArgs) { + const response = await fetch(new URL('/api/headers', request.url)); + return Response.json(await response.json()); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx index cba8275fcf63..cf3eb5dbd4be 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx @@ -1,9 +1,11 @@ import Redis from 'ioredis'; import type { Route } from './+types/redis'; -const redis = new Redis(); +// workerd does not allow a socket connect at module scope, so the client is made in the loader. +let redis: Redis | undefined; export async function loader() { + redis ??= new Redis(); const key = 'cache:greeting'; await redis.set(key, 'hello from react-router'); const value = await redis.get(key); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx index c86f78e17164..0c8f45eee61d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx @@ -11,11 +11,11 @@ async function getUser() { }; } -const authMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => { - Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { +const authMiddleware: Route.MiddlewareFunction = async ({ context }, next) => { + return Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { const user: User = await getUser(); context.set(userContext, user); - await next(); + return next(); }); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json new file mode 100644 index 000000000000..46456a69617a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@sentry/deno/import": "./node_modules/@sentry/deno/build/import.mjs" + }, + "nodeModulesDir": "manual" +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml index 27367c5ae12a..dfb0c2de26c0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml @@ -10,3 +10,20 @@ services: interval: 1s timeout: 3s retries: 30 + mysql: + image: mysql:8.0 + restart: always + container_name: e2e-tests-react-router-8-mysql + # The `mysql` 2.x driver doesn't speak MySQL 8's default + # `caching_sha2_password` auth, so force the legacy plugin. + command: ['--default-authentication-plugin=mysql_native_password'] + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: docker + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 10s diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs index 55af1feaa14c..a7ab404749f5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs @@ -13,7 +13,7 @@ export default async function globalSetup() { // part of the same project - but the container name is fixed, so the daemon // still refuses to create a new one. Force-remove any stale leftover first. try { - execSync('docker rm -f e2e-tests-react-router-8-redis', { stdio: 'ignore' }); + execSync('docker rm -f e2e-tests-react-router-8-redis e2e-tests-react-router-8-mysql', { stdio: 'ignore' }); } catch { // no stale container to remove } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs new file mode 100644 index 000000000000..c2796e9f5278 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.init({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs new file mode 100644 index 000000000000..2dda332a88b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/deno'; + +Sentry.init({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index d4d993061d6f..4cf3a981b6be 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -9,11 +9,17 @@ "react-router": "^8.0.0", "@react-router/node": "^8.0.0", "@react-router/serve": "^8.0.0", + "@sentry/bun": "file:../../packed/sentry-bun-packed.tgz", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "^5.11.1", - "isbot": "^5.1.43" + "isbot": "^5.1.43", + "mysql": "^2.18.1" }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.35.0", + "@types/mysql": "^2.15.26", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/node": "^22", @@ -21,13 +27,17 @@ "@playwright/test": "~1.63.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.6.3", - "vite": "^7.3.2" + "vite": "^7.3.2", + "wrangler": "^4.120.0" }, "scripts": { "build": "react-router build", "test:build-latest": "pnpm install && pnpm add react-router@latest && pnpm add @react-router/node@latest && pnpm add @react-router/serve@latest && pnpm build", "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev", "start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js", + "start:bun": "NODE_ENV=production bun --bun --preload ./instrument.bun.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js", + "start:deno": "NODE_ENV=production deno run -A --preload=@sentry/deno/import --preload=./instrument.deno.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js", + "start:cloudflare": "wrangler dev --config ./build/server/wrangler.json --port 3030", "proxy": "node start-event-proxy.mjs", "typecheck": "react-router typegen && tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", @@ -63,6 +73,19 @@ { "build-command": "pnpm test:build-latest", "label": "react-router-8-framework (latest)" + }, + { + "runtime": "bun", + "label": "react-router-8-framework (bun)" + }, + { + "runtime": "deno", + "deno-version": "v2.9.0", + "label": "react-router-8-framework (deno)" + }, + { + "runtime": "cloudflare", + "label": "react-router-8-framework (cloudflare)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs index 70e62c2b9e3b..6e93d6fc020e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs @@ -1,9 +1,16 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +import { getPlaywrightConfig, getRuntime } from '@sentry-internal/test-utils'; import { fileURLToPath } from 'url'; +const startCommands = { + node: 'PORT=3030 pnpm start', + bun: 'PORT=3030 pnpm start:bun', + deno: 'PORT=3030 pnpm start:deno', + cloudflare: 'pnpm start:cloudflare', +}; + const config = getPlaywrightConfig( { - startCommand: `PORT=3030 pnpm start`, + startCommand: startCommands[getRuntime()], port: 3030, }, // Boot Redis before the tests run, outside the webServer startup-timeout window. diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts index f6971b89941e..68883a5e51d4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts @@ -1 +1,5 @@ +import { getRuntime } from '@sentry-internal/test-utils'; + +export const RUNTIME = getRuntime(); + export const APP_NAME = 'react-router-8-framework'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts index acb11603fedc..35eb7a8854de 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('server-side errors', () => { test('captures error thrown in server loader', async ({ page }) => { @@ -21,26 +21,26 @@ test.describe('server-side errors', () => { value: errorMessage, mechanism: { handled: false, - type: 'react-router', + type: 'react_router.loader', }, }, ], }, - // todo: should be 'GET /errors/server-loader' - transaction: 'GET /{*splat}', + transaction: 'GET /errors/server-loader', request: { url: expect.stringContaining('errors/server-loader'), headers: expect.any(Object), }, level: 'error', - platform: 'node', + // Only Node inits `@sentry/react-router`. Bun, Deno and Cloudflare init their runtime's own SDK. + platform: RUNTIME === 'cloudflare' || RUNTIME === 'deno' ? 'javascript' : 'node', environment: 'qa', sdk: { integrations: expect.any(Array), - name: 'sentry.javascript.react-router', + name: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, version: expect.any(String), }, - tags: { runtime: 'node' }, + ...(RUNTIME === 'node' ? { tags: { runtime: 'node' } } : {}), contexts: { trace: { span_id: expect.any(String), @@ -69,26 +69,25 @@ test.describe('server-side errors', () => { value: errorMessage, mechanism: { handled: false, - type: 'react-router', + type: 'react_router.action', }, }, ], }, - // todo: should be 'POST /errors/server-action' - transaction: 'POST /{*splat}', + transaction: 'POST /errors/server-action', request: { url: expect.stringContaining('errors/server-action'), headers: expect.any(Object), }, level: 'error', - platform: 'node', + platform: RUNTIME === 'cloudflare' || RUNTIME === 'deno' ? 'javascript' : 'node', environment: 'qa', sdk: { integrations: expect.any(Array), - name: 'sentry.javascript.react-router', + name: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, version: expect.any(String), }, - tags: { runtime: 'node' }, + ...(RUNTIME === 'node' ? { tags: { runtime: 'node' } } : {}), contexts: { trace: { span_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts new file mode 100644 index 000000000000..dc2c775a080b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test('keeps the scope data of concurrent requests apart', async ({ request }) => { + const eventAPromise = waitForError(APP_NAME, event => event.message === 'isolation a'); + const eventBPromise = waitForError(APP_NAME, event => event.message === 'isolation b'); + + await Promise.all([request.get('/isolation/a?partner=b'), request.get('/isolation/b?partner=a')]); + + expect((await eventAPromise).tags).toMatchObject({ 'isolation-id': 'a' }); + expect((await eventBPromise).tags).toMatchObject({ 'isolation-id': 'b' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts new file mode 100644 index 000000000000..59566b6e208f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test('sends a log from a server loader', async ({ request }) => { + const logEnvelopeItemPromise = waitForEnvelopeItem(APP_NAME, ([header, body]) => { + return ( + header.type === 'log' && + (body as { items: { body: string }[] }).items.some(log => log.body === 'react-router server log') + ); + }); + + await request.get('/logs'); + + const [, body] = await logEnvelopeItemPromise; + const log = (body as { items: { body: string }[] }).items.find(item => item.body === 'react-router server log'); + + expect(log).toMatchObject({ + level: 'info', + attributes: { route: { value: 'logs', type: 'string' } }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts new file mode 100644 index 000000000000..5cf9d9f0bcbd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test('continues the trace of an incoming request', async ({ request }) => { + const traceId = '12312012123120121231201212312012'; + const parentSpanId = '1121201211212012'; + + const spanPromise = waitForStreamedSpan(APP_NAME, span => span.is_segment && span.trace_id === traceId); + + await request.get('/performance/server-loader', { + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: `sentry-trace_id=${traceId},sentry-environment=qa,sentry-public_key=username,sentry-sample_rate=1,sentry-sampled=true`, + }, + }); + + const span = await spanPromise; + + expect(span.name).toBe('GET /performance/server-loader'); + expect(getSpanOp(span)).toBe('http.server'); + expect(span.parent_span_id).toBe(parentSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts index a1f44a541709..417c665f7314 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,10 +1,15 @@ import { expect, test } from '@playwright/test'; import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('low-quality span filter', () => { test('does not send a server span for /__manifest? requests', async ({ page }) => { + test.skip( + RUNTIME !== 'node', + "The filter is a default integration of `@sentry/react-router`'s `init()`, which only Node uses; the other runtimes init their own SDK", + ); + const streamedSpans: SerializedStreamedSpan[] = []; const navigationPromise = waitForStreamedSpan(APP_NAME, span => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts new file mode 100644 index 000000000000..20377d10c47d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +// Under span streaming the mysql span name is the query summary, so both queries here are named +// `SELECT`. `db.query.text` is what tells them apart. +test.describe('server - mysql db spans', () => { + test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'db'), + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); + expect(firstQuery).toBeDefined(); + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); + }); + + test('a nested query lands on the same segment (async context restored)', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + + // These are scoped to the http.server segment's trace, so both queries landing here is what proves + // the nested one kept the async context. + const queryTexts = spans + .filter(span => getSpanOp(span) === 'db') + .map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts index e203c87b9664..419bc727d874 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts @@ -78,7 +78,7 @@ test.describe('client - navigation performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); // pageload + await page.goto(`/performance/`); // pageload await pageloadSpanPromise; await page.getByRole('link', { name: 'Search Only Navigate' }).click(); // navigation with search-only object to @@ -88,7 +88,6 @@ test.describe('client - navigation performance', () => { 'sentry.op': { value: 'navigation', type: 'string' }, 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, 'url.template': { value: '/performance', type: 'string' }, - // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), @@ -143,7 +142,7 @@ test.describe('client - navigation performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); + await page.goto(`/performance/`); await pageloadSpanPromise; const forwardNavPromise = waitForStreamedSpan(APP_NAME, span => { @@ -166,7 +165,6 @@ test.describe('client - navigation performance', () => { 'sentry.op': { value: 'navigation', type: 'string' }, 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, 'url.template': { value: '/performance', type: 'string' }, - // react-router-serve 301-redirects the bare index route to a trailing slash 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts new file mode 100644 index 000000000000..dd8edc2d9da4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test('propagates the trace to an outgoing fetch', async ({ request }) => { + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; + }); + + const response = await request.get('/performance/outgoing-fetch'); + const headers = (await response.json()) as { 'sentry-trace': string | null; baggage: string | null }; + + expect(headers['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/); + const [traceId, fetchSpanId] = headers['sentry-trace']!.split('-'); + expect(headers.baggage).toContain(`sentry-trace_id=${traceId}`); + + // The header names the span of the outgoing fetch, which belongs to the trace of the incoming request. + await expect.poll(() => streamedSpans.find(span => span.span_id === fetchSpanId)).toBeDefined(); + const fetchSpan = streamedSpans.find(span => span.span_id === fetchSpanId)!; + expect(getSpanOp(fetchSpan)).toBe('http.client'); + expect(fetchSpan.trace_id).toBe(traceId); + + await expect + .poll(() => streamedSpans.find(span => span.is_segment && span.trace_id === traceId && !span.parent_span_id)) + .toBeDefined(); + const serverSpan = streamedSpans.find(span => span.is_segment && span.trace_id === traceId && !span.parent_span_id)!; + expect(getSpanOp(serverSpan)).toBe('http.server'); + expect(serverSpan.name).toBe('GET /performance/outgoing-fetch'); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts index 15ece1778f40..e7dfc682e436 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts @@ -8,7 +8,7 @@ test.describe('client - pageload performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); + await page.goto(`/performance/`); const span = await spanPromise; @@ -30,7 +30,6 @@ test.describe('client - pageload performance', () => { 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.template': { value: '/performance', type: 'string' }, - // react-router-serve 301-redirects the bare index route to a trailing slash 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts index 432b7c60f5f0..c3f594ee2061 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('server - performance', () => { test('should send server span on pageload', async ({ page }) => { @@ -23,10 +23,13 @@ test.describe('server - performance', () => { expect(getSpanOp(span)).toBe('http.server'); expect(span.attributes).toMatchObject({ 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.instrumentation_api', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.name': { + value: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, + type: 'string', + }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.full': { value: expect.stringContaining('/performance'), type: 'string' }, @@ -53,10 +56,13 @@ test.describe('server - performance', () => { expect(getSpanOp(span)).toBe('http.server'); expect(span.attributes).toMatchObject({ 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.instrumentation_api', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.name': { + value: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, + type: 'string', + }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.full': { value: expect.stringContaining('/performance/with/some-param'), type: 'string' }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts index 9ac6ccb807b1..b2991b7f0dc0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('Trace propagation', () => { test('should inject metatags in ssr pageload', async ({ page }) => { @@ -51,7 +51,12 @@ test.describe('Trace propagation', () => { const requestHandlerSpan = streamedSpans.find(span => span.span_id === handlerSpanId); expect(requestHandlerSpan).toBeDefined(); - expect(getSpanOp(requestHandlerSpan!)).toBe('handler'); + // On Node and Deno the Express request handler span is active while the page renders. Without an Express + // layer (Cloudflare, and Bun, where Express is not instrumented under `bun run`) the tag names the + // http.server segment itself. + expect(getSpanOp(requestHandlerSpan!)).toBe( + RUNTIME === 'cloudflare' || RUNTIME === 'bun' ? 'http.server' : 'handler', + ); expect(requestHandlerSpan!.trace_id).toBe(traceId); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts new file mode 100644 index 000000000000..8a905ba1e382 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts @@ -0,0 +1,19 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { reactRouter } from '@react-router/dev/vite'; +import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; +import { sentryReactRouter } from '@sentry/react-router/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig(async config => ({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' } }), + reactRouter(), + sentryCloudflareVitePlugin(), + ...((await sentryReactRouter( + // Both Sentry plugins inject the orchestrion snippet, and injecting it twice fails the build. + { sourcemaps: { disable: true }, buildTimeInstrumentation: false }, + config, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + )) as any[]), + ], +})); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts new file mode 100644 index 000000000000..e14713f82cde --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts @@ -0,0 +1,9 @@ +import { createRequestHandler } from 'react-router'; + +const requestHandler = createRequestHandler(() => import('virtual:react-router/server-build'), import.meta.env.MODE); + +export default { + async fetch(request: Request) { + return requestHandler(request); + }, +}; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts new file mode 100644 index 000000000000..be998ae29a13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts @@ -0,0 +1,8 @@ +import { defineCloudflareOptions } from '@sentry/cloudflare'; + +export default defineCloudflareOptions({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc similarity index 72% rename from dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc rename to dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc index 32521671d64e..e9cb6bee7cd7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc @@ -1,13 +1,10 @@ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "react-router-8-cloudflare", + "name": "react-router-8-framework", "compatibility_date": "2026-06-29", "compatibility_flags": ["nodejs_compat"], "main": "./workers/app.ts", "assets": { "directory": "./build/client", }, - "observability": { - "enabled": true, - }, } diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index bc9c31b5c10c..5e27e3c1046e 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -19,7 +19,7 @@ "@remix-run/cloudflare-pages": "^2.17.4", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", - "@sentry/vite-plugin": "^5.3.0", + "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "2.0.10", "graphql": "^16.6.0", diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx index 69be89b5d961..95b3ac655fec 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx @@ -5,18 +5,25 @@ import { ImportMap } from 'remix/component/server'; import { assets, entry } from '../assets.ts'; import { routes } from '../routes.ts'; +// Every document loads the browser entry, so the SDK runs on a direct load of any route. +function Head(handle: Handle<{ title: string }>) { + return () => ( + + + {handle.props.title} + + {entry.preloads.map(href => ( + + ))} + + + ); +} + function HomePage(handle: Handle>) { return () => ( - - - Sentry Remix 3 - - {entry.preloads.map(href => ( - - ))} - - +

Sentry Remix 3

{/* No `data-rmx-document`, so the runtime intercepts this through the Navigation API. */} @@ -37,12 +44,12 @@ function HomePage(handle: Handle>) { function UserPage(handle: Handle<{ id?: string }>) { return () => ( - - - User - +

User {handle.props.id}

+ ); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts index 8a3876dc890b..bbeee228783a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts @@ -31,7 +31,7 @@ document.addEventListener('click', event => { if (id === 'component-error') { void app.frames.top.replace(createElement(Boom, {})); } - if (id === 'throw-error') { + if (id === 'throw-error' || id === 'throw-on-user') { throwError(); } }); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts index 9428d8b53565..70f77fb7627f 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts @@ -9,10 +9,22 @@ test('sends a pageload span', async ({ page }) => { await page.goto('/'); const span = await spanPromise; - // Ordinary document load, so this span comes from the upstream integration and takes the low - // cardinality streaming name. - expect(span.name).toBe('Pageload'); + // Started by the upstream integration, then named after the route the server reported. + expect(span.name).toBe('/'); expect(span.attributes?.['sentry.origin']?.value).toBe('auto.pageload.browser'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); +}); + +test('parameterizes a page load on a route with params', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => getSpanOp(span) === 'pageload' && span.is_segment === true); + + await page.goto('/users/12345'); + + const span = await spanPromise; + expect(span.name).toBe('/users/:id'); + expect(span.name).not.toContain('12345'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); + expect(span.attributes?.['url.template']?.value).toBe('/users/:id'); }); test('sends a navigation span for a link the runtime intercepts', async ({ page }) => { @@ -31,7 +43,51 @@ test('sends a navigation span for a link the runtime intercepts', async ({ page const span = await spanPromise; expect(span.is_segment).toBe(true); - expect(span.name).toBe('Navigation'); + // The pattern, not the path: an id in the name would make every navigation its own transaction. + expect(span.name).toBe('/users/:id'); + expect(span.name).not.toContain('12345'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); +}); + +test('names a navigation to a route it loaded before right at the start', async ({ page }) => { + await page.goto('/'); + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + await page.goBack(); + await expect(page.locator('#to-user')).toBeVisible(); + + // Named through the route provider before any response, so the span never carried the fallback name. + // Matched on the destination path: the back navigation is a Remix navigation span too, and it can + // be sent when this click starts. + const spanPromise = waitForStreamedSpan( + APP_NAME, + span => + getSpanOp(span) === 'navigation' && + span.attributes?.['sentry.origin']?.value === 'auto.navigation.remix_v3' && + span.attributes?.['url.path']?.value === '/users/12345', + ); + + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + + const span = await spanPromise; + expect(span.name).toBe('/users/:id'); + expect(span.attributes?.['url.template']?.value).toBe('/users/:id'); +}); + +test('groups an error after a first visit navigation by the route, not the path', async ({ page }) => { + await page.goto('/'); + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + + const errorPromise = waitForError(APP_NAME, event => event.exception?.values?.[0]?.value === 'Remix 3 client error'); + + await page.locator('#throw-on-user').click(); + + const error = await errorPromise; + // The span started under the fallback name and was renamed later; the scope's transaction name + // has to follow, otherwise the error would carry `/users/12345`. + expect(error.transaction).toBe('/users/:id'); }); test('captures a component render error the runtime never rethrows', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json index ebc880b29186..70257bef6fcc 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts index 7d15bbe3eba8..814908bf80e4 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/solidstart'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('solidstart-static', transactionEvent => { @@ -16,8 +16,8 @@ test('sends a server action transaction on pageload', async ({ page }) => { expect.objectContaining({ description: 'getPrefecture', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_OP]: 'function', + [SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, }), @@ -41,8 +41,8 @@ test('sends a server action transaction on client navigation', async ({ page }) expect.objectContaining({ description: 'getPrefecture', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_OP]: 'function', + [SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, }), diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/client.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/client.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/early-side-effect.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/early-side-effect.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/globals.d.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/globals.d.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/instrument.client.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/instrument.client.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/router.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/router.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/__root.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/__root.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-ioredis.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-ioredis.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-mysql.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-mysql.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.error.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.error.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.flush.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.flush.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.test-middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.test-middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.user.$id.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.user.$id.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/crash-before-hydration.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/crash-before-hydration.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/custom-monitor.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/custom-monitor.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/index.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/index.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/param.$id.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/param.$id.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/ssr-error.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/ssr-error.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-middleware.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-middleware.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-serverFn.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-serverFn.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.$userId.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.$userId.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/server.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/start.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/start.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts index f13413b3077d..c92d813212b7 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts @@ -36,7 +36,7 @@ test('Sends a server function transaction with auto-instrumentation', async ({ p 'code.function.name': 'testLog', 'http.request.method': 'GET', 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + 'tanstackstart.function.filename': 'app/routes/test-serverFn.tsx', }, status: 'ok', }), @@ -77,7 +77,7 @@ test('Sends a server function transaction for a nested server function only if i 'code.function.name': 'testNestedLog', 'http.request.method': 'GET', 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + 'tanstackstart.function.filename': 'app/routes/test-serverFn.tsx', }, status: 'ok', }), diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json index 5dcdb1fa6f4a..eb48d2313365 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json @@ -21,6 +21,6 @@ "noFallthroughCasesInSwitch": true, "strictNullChecks": true }, - "include": ["src"], + "include": ["app"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts index 2385d8aa5e93..0eef43b166ae 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts @@ -45,7 +45,8 @@ export default defineConfig({ }, plugins: [ tsConfigPaths(), - tanstackStart(), + // Non-default `srcDirectory` moves the generated route tree, which route parametrization has to find + tanstackStart({ srcDirectory: 'app' }), nitro(), // react's vite plugin must come after start's vite plugin viteReact(), diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts new file mode 100644 index 000000000000..08c510a2c3a6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/files/$')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(JSON.stringify({ path: params._splat }), { + headers: { 'Content-Type': 'application/json' }, + }); + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts new file mode 100644 index 000000000000..1885acdad7d5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/sitemap-{$page}.xml')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(``, { + headers: { 'Content-Type': 'application/xml' }, + }); + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx new file mode 100644 index 000000000000..749ca7f6d79c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/artists/')({ + component: ArtistsPage, +}); + +function ArtistsPage() { + return

Artists

; +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts index a1e63e1d9ec2..775624bffc0b 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts @@ -72,3 +72,50 @@ test('should parametrize API route span names', async ({ baseURL }) => { expect(serverSpan.name).toBe('GET /api/user/$id'); expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); }); + +test('should parametrize server span names for index routes', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/artists'; + }); + + await page.goto('/artists'); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /artists/'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); + +test('should parametrize server span names for params with a prefix and suffix', async ({ baseURL }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/api/sitemap-') + ); + }); + + await fetch(`${baseURL}/api/sitemap-3.xml`); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /api/sitemap-{$page}.xml'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); + +test('should parametrize server span names for splat routes', async ({ baseURL }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/api/files/') + ); + }); + + await fetch(`${baseURL}/api/files/a/b/c.txt`); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /api/files/$'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index ad564a41bc75..ec46ca451aac 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -108,7 +108,7 @@ }, "devDependencies": { "@opentelemetry/instrumentation-http": "0.220.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "11.4.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", diff --git a/dev-packages/node-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-integration-tests/suites/anr/app-path.mjs index de06e125c084..8fcb638aeaae 100644 --- a/dev-packages/node-integration-tests/suites/anr/app-path.mjs +++ b/dev-packages/node-integration-tests/suites/anr/app-path.mjs @@ -12,7 +12,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs index 157a32cc4c94..71a0f21b18f5 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic-session.js b/dev-packages/node-integration-tests/suites/anr/basic-session.js index 8684deca9f02..70a814e0312c 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-session.js +++ b/dev-packages/node-integration-tests/suites/anr/basic-session.js @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index e1e6adc31912..bb0ba6d54d3d 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 4217c5d7c1a5..967a1ae7b132 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/forked.js b/dev-packages/node-integration-tests/suites/anr/forked.js index ca5f5f50c4a2..080ae1ed029d 100644 --- a/dev-packages/node-integration-tests/suites/anr/forked.js +++ b/dev-packages/node-integration-tests/suites/anr/forked.js @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs index 6d8576a14a94..4b3b4c2787e7 100644 --- a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs +++ b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-integration-tests/suites/anr/isolated.mjs index 03883a0925ac..95452f723b93 100644 --- a/dev-packages/node-integration-tests/suites/anr/isolated.mjs +++ b/dev-packages/node-integration-tests/suites/anr/isolated.mjs @@ -5,7 +5,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js index bf37172abcc4..2536c48553e7 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit.js b/dev-packages/node-integration-tests/suites/anr/should-exit.js index 7ecc3c140526..85ad4c508e17 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js index e7359f552664..d1b2bc8d962c 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -7,7 +7,6 @@ setTimeout(() => { const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs deleted file mode 100644 index 624a10e9523b..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs +++ /dev/null @@ -1,183 +0,0 @@ -import * as Sentry from '@sentry/aws-serverless'; -import { createHash } from 'crypto'; -import { DynamoDBClient, PutItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb'; -import { KinesisClient, PutRecordCommand } from '@aws-sdk/client-kinesis'; -import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; -import { S3 } from '@aws-sdk/client-s3'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; -import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn'; -import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; -import { ReceiveMessageCommand, SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; -// The Kinesis client defaults to an HTTP/2 request handler, which `nock` cannot intercept. -// Force the HTTP/1 handler so the request is mocked instead of hitting real AWS. -import { NodeHttpHandler } from '@smithy/node-http-handler'; -import nock from 'nock'; - -nock.disableNetConnect(); - -const region = 'us-east-1'; -const credentials = { accessKeyId: 'aws-test-key', secretAccessKey: 'aws-test-secret' }; - -async function s3() { - const s3Client = new S3({ region, credentials, maxAttempts: 1 }); - const host = `https://ot-demo-test.s3.${region}.amazonaws.com`; - - nock(host).put('/aws-ot-s3-test-object.txt?x-id=PutObject').reply(200, 'test'); - await s3Client.putObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - - nock(host).get('/aws-ot-s3-test-object.txt?x-id=GetObject').reply(200, 'contents'); - const getResult = await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - await getResult.Body?.transformToString(); - - nock(host) - .get('/missing-object.txt?x-id=GetObject') - .reply( - 404, - 'NoSuchKeyThe specified key does not exist.', - { 'content-type': 'application/xml' }, - ); - try { - await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'missing-object.txt' }); - } catch { - // expected - } -} - -async function dynamodb() { - const client = new DynamoDBClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({}), { 'content-type': 'application/x-amz-json-1.0' }); - await client.send(new PutItemCommand({ TableName: 'my-table', Item: { id: { S: 'some-id' } } })); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ Items: [{ id: { S: 'some-id' } }], Count: 1, ScannedCount: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send( - new QueryCommand({ - TableName: 'my-table', - KeyConditionExpression: 'id = :id', - ExpressionAttributeValues: { ':id': { S: 'some-id' } }, - }), - ); -} - -async function sqs() { - const client = new SQSClient({ region, credentials, maxAttempts: 1 }); - const queueUrl = `https://sqs.${region}.amazonaws.com/123456789012/my-queue`; - const messageBody = 'Hello from Sentry'; - const md5 = createHash('md5').update(messageBody).digest('hex'); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ MessageId: 'message-id-1', MD5OfMessageBody: md5 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: messageBody })); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - JSON.stringify({ - Messages: [{ MessageId: 'message-id-2', Body: messageBody, MD5OfBody: md5, ReceiptHandle: 'handle' }], - }), - { 'content-type': 'application/x-amz-json-1.0' }, - ); - await client.send(new ReceiveMessageCommand({ QueueUrl: queueUrl })); -} - -async function sns() { - const client = new SNSClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-1request-id-1', - { 'content-type': 'text/xml' }, - ); - await client.send(new PublishCommand({ TopicArn: 'arn:aws:sns:us-east-1:123456789012:my-topic', Message: 'Hello' })); - - // Publish to a platform endpoint, whose ARN ends in a per-device id. - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-2request-id-2', - { 'content-type': 'text/xml' }, - ); - await client.send( - new PublishCommand({ - TargetArn: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - Message: 'Hello', - }), - ); -} - -async function lambda() { - const client = new LambdaClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://lambda.${region}.amazonaws.com`) - .post('/2015-03-31/functions/my-function/invocations') - .reply(200, JSON.stringify({ result: 'ok' }), { - 'content-type': 'application/json', - 'x-amzn-requestid': 'request-id-1', - }); - await client.send(new InvokeCommand({ FunctionName: 'my-function' })); -} - -async function kinesis() { - const client = new KinesisClient({ region, credentials, maxAttempts: 1, requestHandler: new NodeHttpHandler() }); - - nock(`https://kinesis.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ SequenceNumber: '1', ShardId: 'shardId-000000000000' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send( - new PutRecordCommand({ StreamName: 'my-stream', Data: Buffer.from('data'), PartitionKey: 'partition-key' }), - ); -} - -async function secretsmanager() { - const client = new SecretsManagerClient({ region, credentials, maxAttempts: 1 }); - const secretArn = 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc'; - - nock(`https://secretsmanager.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ ARN: secretArn, Name: 'my-secret', SecretString: 'secret-value' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send(new GetSecretValueCommand({ SecretId: secretArn })); -} - -async function stepfunctions() { - const client = new SFNClient({ region, credentials, maxAttempts: 1 }); - const stateMachineArn = 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine'; - - nock(`https://states.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ executionArn: `${stateMachineArn}:execution-1`, startDate: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new StartExecutionCommand({ stateMachineArn, input: '{}' })); -} - -async function run() { - await Sentry.startSpan({ name: 'Test Service Span' }, async () => { - await s3(); - await dynamodb(); - await sqs(); - await sns(); - await lambda(); - await kinesis(); - await secretsmanager(); - await stepfunctions(); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts deleted file mode 100644 index b4974f86ef2e..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; -import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; - -// See the non-streamed `aws-integration` suite: only the origin differs between the OTel and -// orchestrion diagnostics-channel runs. -const ORIGIN = 'auto.aws.aws_sdk'; - -// The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The -// patch target differs between aws-sdk versions, so we run the exact same assertions against both: -// - the current aws-sdk (default, resolved from the workspace) which routes through `@smithy/core` >= 3.24.0 -// - aws-sdk 3.1041.0 pinned together with the pre-`@smithy/core` stack (`@smithy/middleware-stack`) -const LEGACY_AWS_SDK_DEPENDENCIES = { - '@aws-sdk/client-dynamodb': '3.1041.0', - '@aws-sdk/client-kinesis': '3.1041.0', - '@aws-sdk/client-lambda': '3.1041.0', - '@aws-sdk/client-s3': '3.1041.0', - '@aws-sdk/client-secrets-manager': '3.1041.0', - '@aws-sdk/client-sfn': '3.1041.0', - '@aws-sdk/client-sns': '3.1041.0', - '@aws-sdk/client-sqs': '3.1041.0', - // Pin the smithy layer to the pre-`@smithy/core` versions, otherwise the 3.1041.0 clients still - // resolve `@smithy/smithy-client` >= 4.13 (which routes through `@smithy/core` >= 3.24.0). - '@smithy/smithy-client': '4.12.13', - '@smithy/core': '3.23.17', - '@smithy/middleware-stack': '4.2.14', - '@smithy/node-http-handler': '4.7.8', -}; - -/** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. - */ -function assertAwsServiceSpans(spanCcontainer: SerializedStreamedSpanContainer): void { - const spans = spanCcontainer.items; - - const expectSpan = ( - label: string, - expected: Record, - find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, - ): void => { - const matches = spans.filter(item => item.name === expected.name); - const span = find ? matches.find(find) : matches[0]; - expect(span, label).toMatchObject(expected); - }; - - const segmentSpan = spans.find(item => item.is_segment); - - expect(segmentSpan?.name).toBe('Test Service Span'); - - // S3 - PutObject (success) - expectSpan('S3.PutObject', { - name: 'S3.PutObject', - status: 'ok', - attributes: expect.objectContaining({ - 'sentry.origin': { value: ORIGIN, type: 'string' }, - 'sentry.op': { value: 'rpc', type: 'string' }, - 'rpc.system': { value: 'aws-api', type: 'string' }, - 'rpc.method': { value: 'PutObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'cloud.region': { value: 'us-east-1', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - 'sentry.kind': { value: 'client', type: 'string' }, - }), - }); - - // S3 - GetObject (success) - expectSpan( - 'S3.GetObject (success)', - { - name: 'S3.GetObject', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - }), - }, - // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. - item => item.attributes['http.response.status_code']?.value === 200, - ); - - // S3 - GetObject (errored, missing key) - expectSpan( - 'S3.GetObject (error)', - { - name: 'S3.GetObject', - status: 'error', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - }), - }, - item => item.attributes['http.response.status_code']?.value === 404, - ); - - // DynamoDB - PutItem - expectSpan('DynamoDB.PutItem', { - name: 'DynamoDB.PutItem', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutItem', type: 'string' }, - 'rpc.service': { value: 'DynamoDB', type: 'string' }, - 'db.system.name': { value: 'dynamodb', type: 'string' }, - 'db.namespace': { value: 'my-table', type: 'string' }, - 'db.operation.name': { value: 'PutItem', type: 'string' }, - 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, - }), - }); - - // DynamoDB - Query - expectSpan('DynamoDB.Query', { - name: 'DynamoDB.Query', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Query', type: 'string' }, - 'db.operation.name': { value: 'Query', type: 'string' }, - 'aws.dynamodb.count': { value: 1, type: 'integer' }, - 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, - }), - }); - - // SQS - SendMessage (producer) - expectSpan('SQS SendMessage', { - name: 'send my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'rpc.method': { value: 'SendMessage', type: 'string' }, - 'rpc.service': { value: 'SQS', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.destination.name': { value: 'my-queue', type: 'string' }, - 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, - 'messaging.message.id': { value: 'message-id-1', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // SQS - ReceiveMessage (consumer) - expectSpan('SQS ReceiveMessage', { - name: 'receive my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.receive', type: 'string' }, - 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.operation.type': { value: 'receive', type: 'string' }, - 'messaging.batch.message_count': { value: 1, type: 'integer' }, - 'sentry.kind': { value: 'consumer', type: 'string' }, - }), - }); - - // SNS - Publish (producer) - expectSpan('SNS Publish', { - name: 'send my-topic', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'messaging.system': { value: 'aws.sns', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'my-topic', type: 'string' }, - 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // The ARN suffix is a per-device id, so the streamed name drops the destination. - expectSpan('SNS Publish (platform endpoint)', { - name: 'send', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, - 'messaging.destination.name': { - value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - type: 'string', - }, - }), - }); - - // Lambda - Invoke - expectSpan('Lambda Invoke', { - name: 'my-function Invoke', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Invoke', type: 'string' }, - 'rpc.service': { value: 'Lambda', type: 'string' }, - 'faas.invoked_name': { value: 'my-function', type: 'string' }, - 'faas.invoked_provider': { value: 'aws', type: 'string' }, - 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, - }), - }); - - // Kinesis - PutRecord - expectSpan('Kinesis.PutRecord', { - name: 'Kinesis.PutRecord', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutRecord', type: 'string' }, - 'rpc.service': { value: 'Kinesis', type: 'string' }, - 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, - }), - }); - - // SecretsManager - GetSecretValue - expectSpan('SecretsManager.GetSecretValue', { - name: 'SecretsManager.GetSecretValue', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetSecretValue', type: 'string' }, - 'rpc.service': { value: 'SecretsManager', type: 'string' }, - 'aws.secretsmanager.secret.arn': { - value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', - type: 'string', - }, - }), - }); - - // StepFunctions - StartExecution - expectSpan('StepFunctions.StartExecution', { - name: 'SFN.StartExecution', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'StartExecution', type: 'string' }, - 'rpc.service': { value: 'SFN', type: 'string' }, - 'aws.step_functions.state_machine.arn': { - value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', - type: 'string', - }, - }), - }); -} - -describe('awsIntegration (streamed)', () => { - afterAll(() => { - cleanupChildProcesses(); - }); - - describe.each([ - { label: 'latest', additionalDependencies: undefined }, - { label: 'v3.1041.0 (@smithy/middleware-stack)', additionalDependencies: LEGACY_AWS_SDK_DEPENDENCIES }, - ])('aws-sdk $label', ({ additionalDependencies }) => { - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ span: assertAwsServiceSpans }).start().completed(); - }); - }, - { additionalDependencies }, - ); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs index d6a9efde7bae..fe1c5c47983f 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/aws-serverless'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index cac71f20ad6e..e12bf1ecaff2 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -1,4 +1,4 @@ -import type { TransactionEvent } from '@sentry/core'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -29,189 +29,216 @@ const LEGACY_AWS_SDK_DEPENDENCIES = { }; /** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. + * Asserts the collected spans include one span per instrumented aws-sdk service. Each service is checked + * with its own `expect` so a failure points at the specific service rather than the whole trace. */ -function assertAwsServiceSpans(transaction: TransactionEvent): void { - const spans = transaction.spans ?? []; - - const expectSpan = (label: string, expected: Record): void => { - expect(spans, `expected an aws-sdk span for "${label}"`).toContainEqual(expect.objectContaining(expected)); +function assertAwsServiceSpans(spans: SerializedStreamedSpanContainer['items']): void { + const expectSpan = ( + label: string, + expected: Record, + find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, + ): void => { + const matches = spans.filter(item => item.name === expected.name); + const span = find ? matches.find(find) : matches[0]; + expect(span, label).toMatchObject(expected); }; - expect(transaction.transaction).toBe('Test Transaction'); + const segmentSpan = spans.find(item => item.is_segment); + + expect(segmentSpan?.name).toBe('Test Transaction'); // S3 - PutObject (success) expectSpan('S3.PutObject', { - description: 'S3.PutObject', - op: 'rpc', - origin: ORIGIN, + name: 'S3.PutObject', status: 'ok', - data: expect.objectContaining({ - 'sentry.origin': ORIGIN, - 'sentry.op': 'rpc', - 'rpc.system': 'aws-api', - 'rpc.method': 'PutObject', - 'rpc.service': 'S3', - 'cloud.region': 'us-east-1', - 'aws.s3.bucket': 'ot-demo-test', - 'sentry.kind': 'client', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'rpc', type: 'string' }, + 'rpc.system': { value: 'aws-api', type: 'string' }, + 'rpc.method': { value: 'PutObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'cloud.region': { value: 'us-east-1', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + 'sentry.kind': { value: 'client', type: 'string' }, }), }); // S3 - GetObject (success) - expectSpan('S3.GetObject (success)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'ok', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3', 'aws.s3.bucket': 'ot-demo-test' }), - }); + expectSpan( + 'S3.GetObject (success)', + { + name: 'S3.GetObject', + status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + }), + }, + // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. + item => item.attributes['http.response.status_code']?.value === 200, + ); // S3 - GetObject (errored, missing key) - expectSpan('S3.GetObject (error)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'internal_error', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3' }), - }); + expectSpan( + 'S3.GetObject (error)', + { + name: 'S3.GetObject', + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + }), + }, + item => item.attributes['http.response.status_code']?.value === 404, + ); // DynamoDB - PutItem expectSpan('DynamoDB.PutItem', { - description: 'DynamoDB.PutItem', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'sentry.op': 'db', - 'rpc.method': 'PutItem', - 'rpc.service': 'DynamoDB', - 'db.system.name': 'dynamodb', - 'db.namespace': 'my-table', - 'db.operation.name': 'PutItem', - 'aws.dynamodb.table_names': ['my-table'], + name: 'DynamoDB.PutItem', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutItem', type: 'string' }, + 'rpc.service': { value: 'DynamoDB', type: 'string' }, + 'db.system.name': { value: 'dynamodb', type: 'string' }, + 'db.namespace': { value: 'my-table', type: 'string' }, + 'db.operation.name': { value: 'PutItem', type: 'string' }, + 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, }), }); // DynamoDB - Query expectSpan('DynamoDB.Query', { - description: 'DynamoDB.Query', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Query', - 'db.operation.name': 'Query', - 'aws.dynamodb.count': 1, - 'aws.dynamodb.scanned_count': 1, + name: 'DynamoDB.Query', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Query', type: 'string' }, + 'db.operation.name': { value: 'Query', type: 'string' }, + 'aws.dynamodb.count': { value: 1, type: 'integer' }, + 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, }), }); // SQS - SendMessage (producer) expectSpan('SQS SendMessage', { - description: 'my-queue send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'SendMessage', - 'rpc.service': 'SQS', - 'messaging.system': 'aws_sqs', - 'messaging.destination.name': 'my-queue', - 'url.full': 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', - 'messaging.message.id': 'message-id-1', - 'sentry.kind': 'producer', + name: 'send my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'rpc.method': { value: 'SendMessage', type: 'string' }, + 'rpc.service': { value: 'SQS', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.destination.name': { value: 'my-queue', type: 'string' }, + 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, + 'messaging.message.id': { value: 'message-id-1', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); // SQS - ReceiveMessage (consumer) expectSpan('SQS ReceiveMessage', { - description: 'my-queue receive', - op: 'queue.receive', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'ReceiveMessage', - 'messaging.system': 'aws_sqs', - 'messaging.operation.type': 'receive', - 'messaging.batch.message_count': 1, - 'sentry.kind': 'consumer', + name: 'receive my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.receive', type: 'string' }, + 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.operation.type': { value: 'receive', type: 'string' }, + 'messaging.batch.message_count': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'consumer', type: 'string' }, }), }); // SNS - Publish (producer) expectSpan('SNS Publish', { - description: 'my-topic send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.system': 'aws.sns', - 'messaging.destination': 'my-topic', - 'aws.sns.topic.arn': 'arn:aws:sns:us-east-1:123456789012:my-topic', - 'sentry.kind': 'producer', + name: 'send my-topic', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'messaging.system': { value: 'aws.sns', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'my-topic', type: 'string' }, + 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); - // Without span streaming the name keeps the raw ARN suffix, including the per-device id. + // The ARN suffix is a per-device id, so the streamed name drops the destination. expectSpan('SNS Publish (platform endpoint)', { - description: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3 send', - op: 'queue.publish', - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.destination': 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - 'messaging.destination.name': - 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + name: 'send', + attributes: expect.objectContaining({ + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, + 'messaging.destination.name': { + value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + type: 'string', + }, }), }); // Lambda - Invoke expectSpan('Lambda Invoke', { - description: 'my-function Invoke', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Invoke', - 'rpc.service': 'Lambda', - 'faas.invoked_name': 'my-function', - 'faas.invoked_provider': 'aws', - 'faas.invocation_id': 'request-id-1', + name: 'my-function Invoke', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Invoke', type: 'string' }, + 'rpc.service': { value: 'Lambda', type: 'string' }, + 'faas.invoked_name': { value: 'my-function', type: 'string' }, + 'faas.invoked_provider': { value: 'aws', type: 'string' }, + 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, }), }); // Kinesis - PutRecord expectSpan('Kinesis.PutRecord', { - description: 'Kinesis.PutRecord', - op: 'rpc', - origin: ORIGIN, + name: 'Kinesis.PutRecord', status: 'ok', - data: expect.objectContaining({ - 'rpc.method': 'PutRecord', - 'rpc.service': 'Kinesis', - 'aws.kinesis.stream.name': 'my-stream', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutRecord', type: 'string' }, + 'rpc.service': { value: 'Kinesis', type: 'string' }, + 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, }), }); // SecretsManager - GetSecretValue expectSpan('SecretsManager.GetSecretValue', { - description: 'SecretsManager.GetSecretValue', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'GetSecretValue', - 'rpc.service': 'SecretsManager', - 'aws.secretsmanager.secret.arn': 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + name: 'SecretsManager.GetSecretValue', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetSecretValue', type: 'string' }, + 'rpc.service': { value: 'SecretsManager', type: 'string' }, + 'aws.secretsmanager.secret.arn': { + value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + type: 'string', + }, }), }); // StepFunctions - StartExecution expectSpan('StepFunctions.StartExecution', { - description: 'SFN.StartExecution', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'StartExecution', - 'rpc.service': 'SFN', - 'aws.step_functions.state_machine.arn': 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + name: 'SFN.StartExecution', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'StartExecution', type: 'string' }, + 'rpc.service': { value: 'SFN', type: 'string' }, + 'aws.step_functions.state_machine.arn': { + value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + type: 'string', + }, }), }); } @@ -231,7 +258,18 @@ describe('awsIntegration', () => { 'instrument.mjs', (createTestRunner, test) => { test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ transaction: assertAwsServiceSpans }).start().completed(); + const spans: SerializedStreamedSpanContainer['items'] = []; + await createTestRunner() + .ignore('event') + .unordered() + .expect({ + span: container => { + spans.push(...container.items); + assertAwsServiceSpans(spans); + }, + }) + .start() + .completed(); }); }, { additionalDependencies }, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js index a37f51672bba..4023421921b5 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/aws-serverless'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, integrations: [Sentry.graphqlIntegration({ useOperationNameForRootSpan: true })], diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts index 1138a4b4c6ca..723e6a9e3d95 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts @@ -1,17 +1,6 @@ import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -const EXPECTED_TRANSCATION = { - transaction: 'Test Transaction (query GetHello)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query GetHello', - origin: 'auto.graphql.diagnostic_channel', - status: 'ok', - }), - ]), -}; - describe('graphqlIntegration', () => { afterAll(() => { cleanupChildProcesses(); @@ -20,8 +9,31 @@ describe('graphqlIntegration', () => { test('should use GraphQL operation name for root span if useOperationNameForRootSpan is set', async () => { await createRunner(__dirname, 'scenario.js') .ignore('event') - .expect({ transaction: { transaction: 'Test Server Start' } }) - .expect({ transaction: EXPECTED_TRANSCATION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + const executeSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'execute'); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'GetHello', + type: 'string', + }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs index 31f242feae18..792d86199ecb 100644 --- a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -7,7 +7,6 @@ import { Worker } from 'worker_threads'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', integrations: [Sentry.childProcessIntegration(), Sentry.workerThreadsIntegration()], diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.js b/dev-packages/node-integration-tests/suites/child-process/fork.js index 2c9dcf0a3ae4..064a21d57657 100644 --- a/dev-packages/node-integration-tests/suites/child-process/fork.js +++ b/dev-packages/node-integration-tests/suites/child-process/fork.js @@ -4,7 +4,6 @@ const path = require('path'); const { fork } = require('child_process'); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-integration-tests/suites/child-process/fork.mjs index 19d612141570..f28b7f313ec7 100644 --- a/dev-packages/node-integration-tests/suites/child-process/fork.mjs +++ b/dev-packages/node-integration-tests/suites/child-process/fork.mjs @@ -6,7 +6,6 @@ import * as path from 'path'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.js b/dev-packages/node-integration-tests/suites/child-process/worker.js index 56c574bb325e..5468f7f45a9d 100644 --- a/dev-packages/node-integration-tests/suites/child-process/worker.js +++ b/dev-packages/node-integration-tests/suites/child-process/worker.js @@ -4,7 +4,6 @@ const path = require('path'); const { Worker } = require('worker_threads'); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-integration-tests/suites/child-process/worker.mjs index 5a8252e45554..d0a749d464e3 100644 --- a/dev-packages/node-integration-tests/suites/child-process/worker.mjs +++ b/dev-packages/node-integration-tests/suites/child-process/worker.mjs @@ -6,7 +6,6 @@ import { Worker } from 'worker_threads'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts index 2b90554e068b..4c6f464b62f9 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, beforeSend(event) { diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts index bade2d046447..f92604e5971d 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts index 56a4eef4831b..70af7e321108 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts index 9d5524afe3ab..8f838322f5ec 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts index e511fc97e8a9..965985ea78d0 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts @@ -1,4 +1,4 @@ -import { afterAll, test } from 'vitest'; +import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { @@ -25,9 +25,11 @@ test('records a client report and no error event when tracesSampler throws', asy test('sends the span when tracesSampler throws but tracesSampleRate is 1', async () => { await createRunner(__dirname, 'scenario-fallback.ts') + .unordered() .expect({ - transaction: { - transaction: 'sampled via tracesSampleRate fallback', + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('sampled via tracesSampleRate fallback'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts index 9975b9feeb0b..72a46dcc5635 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, clientReportFlushInterval: 5000, diff --git a/dev-packages/node-integration-tests/suites/consola/instrument.mjs b/dev-packages/node-integration-tests/suites/consola/instrument.mjs index 4ce4f3c0ffa9..cededb10ce20 100644 --- a/dev-packages/node-integration-tests/suites/consola/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/consola/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs index 0418e09c174f..9ffde125d498 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, 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 index d202b4d2e057..18df082e3b2d 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts index f1e1ba0c3b8c..b3966b050d89 100644 --- a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { CronJob } from 'cron'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', }); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts index bac06675d452..fb69a5de2482 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as cron from 'node-cron'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts index 04aea6c84cae..b64b2d814b67 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as cron from 'node-cron'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts index e905b9be4d6c..2bb67cc0047c 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as schedule from 'node-schedule'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs index d45016438d03..a131c93e86fc 100644 --- a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs +++ b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', integrations: [Sentry.modulesIntegration()], diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js index 1bcb3b642b5a..0a2f192d9bc7 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs index a42337b2a47c..e7309895b992 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs @@ -3,7 +3,6 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry import express from 'express'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts index 2f78fabdebe9..2c07e46b40ed 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts index 4a417a3c3959..b62c330ce81e 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN, type SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; @@ -8,24 +8,31 @@ afterAll(() => { test('Flags captured on span attributes with max limit', async () => { // Based on scenario.ts. - const expectedFlags: Record = {}; + const expectedFlags: Record = {}; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + expectedFlags[`flag.evaluation.feat${i}`] = { type: 'boolean', value: i === 3 }; } + const spans: SerializedStreamedSpanContainer['items'] = []; await createRunner(__dirname, 'scenario.ts') + .unordered() .expect({ - transaction: { - spans: [ - expect.objectContaining({ - description: 'test-span', - data: expect.objectContaining({}), - }), - expect.objectContaining({ - description: 'test-nested-span', - data: expect.objectContaining(expectedFlags), - }), - ], + span: container => { + spans.push(...container.items); + const children = spans.filter(span => !span.is_segment); + expect(children).toHaveLength(2); + expect(children).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'test-span', + attributes: expect.objectContaining({}), + }), + expect.objectContaining({ + name: 'test-nested-span', + attributes: expect.objectContaining(expectedFlags), + }), + ]), + ); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts index 7f9da04fdbd8..b25f36f00951 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts @@ -44,7 +44,6 @@ class GrowthBookWrapper { const gb = new GrowthBookWrapper(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts index 3f0011b4f617..8f5d4fdbdf9b 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts @@ -7,26 +7,26 @@ afterAll(() => { test('GrowthBook flags are added to active span attributes on span end', async () => { await createRunner(__dirname, 'scenario.ts') + .unordered() .expect({ - transaction: { - contexts: { - trace: { - data: { - 'flag.evaluation.feat1': true, - 'flag.evaluation.feat2': false, - 'flag.evaluation.bool-feat': true, - // string-feat should NOT be here since it's not boolean - }, - op: 'function', - origin: 'manual', - status: 'ok', - span_id: expect.stringMatching(/[a-f\d]{16}/), - trace_id: expect.stringMatching(/[a-f\d]{32}/), + span: container => { + expect(container.items).toHaveLength(1); + const span = container.items.find(item => item.is_segment); + expect(span).toMatchObject({ + name: 'test-span', + status: 'ok', + span_id: expect.stringMatching(/[a-f\d]{16}/), + trace_id: expect.stringMatching(/[a-f\d]{32}/), + attributes: { + 'flag.evaluation.feat1': { type: 'boolean', value: true }, + 'flag.evaluation.feat2': { type: 'boolean', value: false }, + 'flag.evaluation.bool-feat': { type: 'boolean', value: true }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'manual' }, }, - }, - spans: [], - transaction: 'test-span', - type: 'transaction', + }); + // string-feat should NOT be here since it's not boolean + expect(span?.attributes['flag.evaluation.string-feat']).toBeUndefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs index 7043bf763c1b..eff54f5b1ba7 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs index 018477190488..05beff418e87 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs index bc31198a2137..f17a1e3bd449 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts index a49710d6e560..e4d4a1474e05 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts @@ -1,7 +1,36 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../utils/runner'; +type FileSpan = Pick; + +function expectFileSpans( + segmentName: string, + expected: FileSpan[], +): (container: SerializedStreamedSpanContainer) => void { + return container => { + const spans = container.items; + const segment = spans.find(span => span.is_segment && span.name === segmentName); + expect(segment).toBeDefined(); + + const fileSpans = spans.filter( + span => span.attributes[SENTRY_OP]?.value === 'file' && span.trace_id === segment!.trace_id, + ); + expect( + fileSpans.map(({ name, status, attributes }) => ({ + name, + status, + attributes: Object.fromEntries( + Object.entries(attributes).filter( + ([key]) => key.endsWith('_argument') || key === 'error.type' || key === SENTRY_OP || key === SENTRY_ORIGIN, + ), + ), + })), + ).toEqual(expect.arrayContaining(expected)); + }; +} + describe('fs instrumentation', () => { afterAll(() => { cleanupChildProcesses(); @@ -16,22 +45,21 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take target argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - data: { - 'error.type': 'ENOENT', - path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + attributes: { + 'error.type': { type: 'string', value: 'ENOENT' }, + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -43,41 +71,38 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take one path', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile', [ + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promises.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -89,44 +114,44 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take src and dest arguments', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /copyFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /copyFile', [ + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + dest_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.copy') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promises.txt') }, + dest_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), }, - }), - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promisify.txt') }, + dest_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), }, - }), - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -138,44 +163,44 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take existing path and new path arguments', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /link', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/fixtures/some-file.txt'), - new_path_argument: expect.stringMatching('/fixtures/some-file.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/some-file-promises.txt'), - new_path_argument: expect.stringMatching('/some-file-promises.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /link', [ + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + new_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.link') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt') }, + new_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt.link') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - new_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + new_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -187,41 +212,35 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take prefix argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /mkdtemp', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /mkdtemp', [ + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -233,44 +252,50 @@ describe('fs instrumentation', () => { test('should create spans for fs symlink operations that take target argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /symlink', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/some-file-promisify.txt'), - path_argument: expect.stringMatching('/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /symlink', [ + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt') }, + path_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt.symlink') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), + }, + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, - }), - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -282,31 +307,29 @@ describe('fs instrumentation', () => { test('should create spans for fs.exists callback and promisified versions', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /exists', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.exists', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /exists', [ + { + name: 'fs.exists', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.exists', + status: 'ok', + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.exists', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -328,22 +351,21 @@ describe('fs instrumentation', () => { test('records file path but not error messages when only `recordFilePaths` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - // `path_argument` is recorded, but `error.type` is NOT, since `recordErrorMessagesAsSpanAttributes` is off - data: { - path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + // `path_argument` is recorded, but `error.type` is NOT, since `recordErrorMessagesAsSpanAttributes` is off + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -365,22 +387,18 @@ describe('fs instrumentation', () => { test('records error messages but not file paths when only `recordErrorMessagesAsSpanAttributes` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - // `error.type` is recorded, but `path_argument` is NOT, since `recordFilePaths` is off - data: { - 'error.type': 'ENOENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + // `error.type` is recorded, but `path_argument` is NOT, since `recordFilePaths` is off + attributes: { + 'error.type': { type: 'string', value: 'ENOENT' }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -392,21 +410,17 @@ describe('fs instrumentation', () => { test('does not record file paths on successful operations when only `recordErrorMessagesAsSpanAttributes` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - // Neither `path_argument` nor `error.type` are recorded - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /readFile', [ + { + name: 'fs.readFile', + status: 'ok', + // Neither `path_argument` nor `error.type` are recorded + attributes: { + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs index 7d9a68a1c677..16c05ae32ed0 100644 --- a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/modules/instrument.mjs b/dev-packages/node-integration-tests/suites/modules/instrument.mjs index 432e6ae5475b..c8cdc9cac0a2 100644 --- a/dev-packages/node-integration-tests/suites/modules/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/modules/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js index c57515443a5e..bf5ec659e060 100644 --- a/dev-packages/node-integration-tests/suites/modules/server.js +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts index f6a4e0a3162c..94a7161fe00c 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts index f56c4cd5fd2e..e838df6e9408 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts index b968e8c7a5c2..1e174e5c29cc 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs index 5ea7061a8ffb..42c938885229 100644 --- a/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs +++ b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/pino/instrument.mjs b/dev-packages/node-integration-tests/suites/pino/instrument.mjs index b3975e128c04..009af91971f3 100644 --- a/dev-packages/node-integration-tests/suites/pino/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/pino/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/pino/test.ts b/dev-packages/node-integration-tests/suites/pino/test.ts index b5ef17cca2a2..d961b09c3200 100644 --- a/dev-packages/node-integration-tests/suites/pino/test.ts +++ b/dev-packages/node-integration-tests/suites/pino/test.ts @@ -11,7 +11,7 @@ describe('Pino integration', () => { await createRunner() .withMockSentryServer() .ignore('event') - .ignore('transaction') + .ignore('span') .expect({ log: log => { const traceId1 = log.items?.[0]?.trace_id; @@ -26,7 +26,7 @@ describe('Pino integration', () => { test('captures event and logs', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { @@ -110,7 +110,7 @@ describe('Pino integration', () => { test('captures with Pino integrated channel', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { @@ -193,7 +193,7 @@ describe('Pino integration', () => { test('captures logs when autoInstrument is false and logger is tracked', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ log: { items: [ @@ -246,7 +246,7 @@ describe('Pino integration', () => { test('captures structured logs with msg field', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ log: { items: [ @@ -308,7 +308,7 @@ describe('Pino integration', () => { test('attaches log message and fields to captured error events', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .ignore('log') .expect({ event: { @@ -356,7 +356,7 @@ describe('Pino integration', () => { test('captures logs with custom messageKey and errorKey', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs index 5fde6f9da29c..b63c0ecc4419 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs @@ -15,5 +15,5 @@ export async function resolve(specifier, context, nextResolve) { (async () => { const Sentry = await import('@sentry/node'); - Sentry.init({ traceLifecycle: 'static' }); + Sentry.init(); })(); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js index 822a315c08c8..8ea8dd93fb4d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs index f95449b27718..c0a26a6aa709 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js index 93d6b1ad15ca..e9695c1a4aae 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, dataCollection: { stackFrameVariables: false }, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js index 0574f218a884..48e6e36b83a3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, dataCollection: { stackFrameVariables: { deny: ['secretVar'] } }, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs index 1f998cbb4ddb..0b39efcfd8db 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js index 3487f3ae7007..b9027b749181 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js index 6e400cce7eba..c8da0acc0d4e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js @@ -12,7 +12,6 @@ function in_app_function() { } Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, includeLocalVariables: true, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js index 76fc88a8a905..119bbad71ccf 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js @@ -7,7 +7,6 @@ const externalFunctionFile = require.resolve('./node_modules/out-of-app-function const { out_of_app_function } = require(externalFunctionFile); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, includeLocalVariables: true, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js index 163520bb2f08..744cb747c70c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js index dd585e2846f3..8b64c80cbc5f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js index bcb24329cd20..2857fc0e3ce9 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js index fc863f1eaeec..efb21fab10db 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js index d0aca741ba23..656a5b19220b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js index 28362c14f05d..758f9e26cc5b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js @@ -1,7 +1,6 @@ const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js index fa2faf46d3cf..59af71f30c60 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUncaughtExceptionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js index de44f866ef44..5eb71328de0e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUncaughtExceptionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js index c1c65df8cb84..9a8c69daaf35 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js index 75555e4a5952..a2b13b91bce6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, debug: false, diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs index 31831d03ff78..9263fe27bce1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, debug: false, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts index 6c4d5a448287..e49220c4d79d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts index c05db95cf7ad..2a28a7d19bb1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts index 5a30820da2cc..5d166004c87d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts index f9e2eea18d4d..53f994b1686e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts index 29da30447751..081af449fd81 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts index 749f52de5a63..f74a8eb68035 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts index d225b20d9019..ed1a4bfb1c83 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts index 2eadd73a01d9..3d2375572830 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts index 73556ca773aa..4761a4afc1d3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts index d67046d55134..8ae145b43b71 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts index 405691623768..64b13cfd1db5 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts new file mode 100644 index 000000000000..db5aaf531c0c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +async function run(): Promise { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + }); + + // The root scope still holds the closed client after this. + await Sentry.withIsolationScope(() => Sentry.close()); + + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, + }); + + Sentry.captureMessage('after close in a child scope and init'); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts new file mode 100644 index 000000000000..7edc88abd2ba --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +async function run(): Promise { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + }); + + await Sentry.close(); + + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, + }); + + Sentry.captureMessage('after close and init'); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts new file mode 100644 index 000000000000..1c6b4c4074a6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, +}); + +Sentry.captureMessage('after repeated init'); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts new file mode 100644 index 000000000000..df98963ce645 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts @@ -0,0 +1,38 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +const REPEATED_INIT_WARNING = '`Sentry.init()` was called more than once'; + +test('warns on a repeated init and sends events with the new client', async () => { + const runner = createRunner(__dirname, 'scenario-repeated.ts') + .expect({ event: { message: 'after repeated init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).toContain(REPEATED_INIT_WARNING); +}); + +test('does not warn on init after close and sends events with the new client', async () => { + const runner = createRunner(__dirname, 'scenario-close.ts') + .expect({ event: { message: 'after close and init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).not.toContain(REPEATED_INIT_WARNING); +}); + +test('does not warn on init after close in a child scope', async () => { + const runner = createRunner(__dirname, 'scenario-close-in-scope.ts') + .expect({ event: { message: 'after close in a child scope and init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).not.toContain(REPEATED_INIT_WARNING); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts index eac14291c9eb..74a43eb5356a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts index bee87c236470..39817fc39b7d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts index b9d68b4f3d54..1f6d60f91cac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts index 80ee934d2820..a985f2a0fce3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js index 5005a1bc3e2c..f85ebfada002 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUnhandledRejectionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js index 84676901b052..d59cd557a4cb 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // Use default mode: 'warn' - integration is active but should ignore AI_NoOutputGeneratedError }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js index 5db0588888ec..cad5ce1c8fe5 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'none' })], }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js index 1eeaf1e71cab..841668b4b4ac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })], }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js index a9d3648132b8..45a84bbf5594 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js index 2be232cfa92e..79ea78de095f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts index 053a73a4b790..a26e19de58d3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts index 9ed5ee4a447a..24b3093a554c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts index af022a693202..72d83d70ec72 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts index 214ab0a52855..edff30f114ca 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts index 0cc15ce97173..199f3dffe5aa 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts @@ -1,4 +1,4 @@ -import type { Event } from '@sentry/node'; +import type { Event, SerializedStreamedSpanContainer } from '@sentry/node'; import * as childProcess from 'child_process'; import * as path from 'path'; import { afterAll, describe, expect, test } from 'vitest'; @@ -142,15 +142,16 @@ test rejection`); .completed(); }); - // Bun: the error event arrives before the transaction, or has a different span id. - test.skipIf(RUNTIME === 'bun')('handles unhandled rejection in spans', async () => { - let transactionEvent: Event | undefined; + test('handles unhandled rejection in spans', async () => { + let segment: SerializedStreamedSpanContainer['items'][number] | undefined; let errorEvent: Event | undefined; await createRunner(__dirname, 'scenario-with-span.ts') + .unordered() .expect({ - transaction: transaction => { - transactionEvent = transaction; + span: container => { + segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('test-span'); }, }) .expect({ @@ -161,23 +162,24 @@ test rejection`); .start() .completed(); - expect(transactionEvent).toBeDefined(); + expect(segment).toBeDefined(); expect(errorEvent).toBeDefined(); - expect(transactionEvent!.transaction).toBe('test-span'); - - expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); - expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); }); + // Bun: the rejection event's span ID differs from the already-ended span's ID. test.skipIf(RUNTIME === 'bun')('handles unhandled rejection in spans that are ended early', async () => { - let transactionEvent: Event | undefined; + let segment: SerializedStreamedSpanContainer['items'][number] | undefined; let errorEvent: Event | undefined; await createRunner(__dirname, 'scenario-with-span-ended.ts') + .unordered() .expect({ - transaction: transaction => { - transactionEvent = transaction; + span: container => { + segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('test-span'); }, }) .expect({ @@ -188,13 +190,11 @@ test rejection`); .start() .completed(); - expect(transactionEvent).toBeDefined(); + expect(segment).toBeDefined(); expect(errorEvent).toBeDefined(); - expect(transactionEvent!.transaction).toBe('test-span'); - - expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); - expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); }); test('should not warn when AI_NoOutputGeneratedError or AbortError is rejected (default ignore)', () => diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts index c85e7f29ac5b..1922e8c69ef0 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts index 70c92c779b98..1b22d2d8e8ac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts index 3d99cc5285d2..1fb504808b0f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts index 88ebfce95011..d063729bcc93 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts index 41d536441a13..6bbe78c88aa7 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts index 0efaf4267b84..4f66d74f7e7a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts index 2bb45f6cb085..2644bb47ea3c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts index 88e378a90019..f356949c6327 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts index 0e4ad0f07289..4e0bc30c0c2b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts index 0e4ad0f07289..4e0bc30c0c2b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts index 8c6c3ba27de9..9e617168b539 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts index c9c548ace4a1..4c2eb2221bf6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 2eb452d43386..87a40fa69b3f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -1,9 +1,4 @@ -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SEGMENT_ID, @@ -14,6 +9,8 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { expect, test } from 'vitest'; import { createRunner } from '../../../../utils/runner'; @@ -60,7 +57,7 @@ test('sends a streamed span envelope with correct spans for a manually started s attributes: { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, 'sentry.is_localhost': { type: 'boolean', value: false }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, @@ -70,7 +67,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, name: 'test-child-span', is_segment: false, @@ -94,7 +91,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, links: [ { @@ -131,7 +128,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, name: 'test-manual-span', is_segment: false, @@ -146,7 +143,7 @@ test('sends a streamed span envelope with correct spans for a manually started s const expectedAttributes: Record = { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, 'sentry.is_localhost': { type: 'boolean', value: false }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SDK_NAME]: { type: 'string', value: EXPECTED_SDK_NAME }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, @@ -158,7 +155,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'custom' }, 'process.runtime.engine.name': { type: 'string', value: 'v8' }, 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, diff --git a/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts index ff3d57c57872..42a1da41a195 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts index ef1d4b13c778..93f425ff6a89 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index b422aca7d289..8ab29a183bd2 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful and crashed sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index 8a3a91e6997a..9c9bd740af31 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful, crashed and erroneous sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index 79ebc5bf5566..173cfffee68c 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/instrument.mjs b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs index c8a21c59d4b2..e727f2c5068a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs b/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs index 080e8a707c82..4bb7c6e1b4ee 100644 --- a/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs +++ b/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { readFileSync } from 'fs'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/system-error/basic.mjs b/dev-packages/node-integration-tests/suites/system-error/basic.mjs index 080e8a707c82..4bb7c6e1b4ee 100644 --- a/dev-packages/node-integration-tests/suites/system-error/basic.mjs +++ b/dev-packages/node-integration-tests/suites/system-error/basic.mjs @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { readFileSync } from 'fs'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs index 062a385c00b6..c561b221d95f 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs @@ -13,7 +13,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration({ appRootPath: __dirname })], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs index c90b4f630245..15a1f496de61 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 15000); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: process.env.SENTRY_DSN, release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs index bada88535eb4..32135d2246f2 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration({ maxEventsPerHour: 2 })], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js index 1c17a01b5da7..30740bbd031b 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js @@ -9,7 +9,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs index 98a51621443d..49f30db2e59e 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 12000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs index ed0335e7f675..55eecb5c23ec 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs index 4503cc90104b..ee66bf82f8bf 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { eventLoopBlockIntegration } from '@sentry/node-native'; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: process.env.SENTRY_DSN, release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js index 467277f16d30..71622bdbe083 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js @@ -3,7 +3,6 @@ const { eventLoopBlockIntegration } = require('@sentry/node-native'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js index 426e4df15e02..cda4c0e10d3a 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js @@ -3,7 +3,6 @@ const { eventLoopBlockIntegration } = require('@sentry/node-native'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs similarity index 69% rename from dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs rename to dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs index bfdb2468109c..29122b580fae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs @@ -1,11 +1,9 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; -// `graphqlIntegration()` is the diagnostics-channel implementation by default; configure it with -// `ignoreResolveSpans: false` so resolver spans are captured. Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', + traceLifecycle: 'static', release: '1.0', tracesSampleRate: 1.0, integrations: [Sentry.graphqlIntegration({ ignoreResolveSpans: false })], diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts new file mode 100644 index 000000000000..752bb091892c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts @@ -0,0 +1,69 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('GraphQL/Apollo Tests > static naming', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-query.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('keeps operation and resolver names in transaction spans', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('test span name (query GetHello)'); + const spans = event.spans?.filter(span => span.op === 'graphql'); + expect(spans?.map(span => span.description).sort()).toEqual([ + 'graphql.parse', + 'graphql.resolve hello', + 'graphql.validate', + 'query GetHello', + ]); + }, + }) + .start() + .completed(); + }); + }, + ); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-multiple-operations.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('sorts operation names in the transaction name', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ transaction: { transaction: 'test span name (query GetHello, query GetWorld)' } }) + .start() + .completed(); + }); + }, + ); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-multiple-operations-many.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('truncates the transaction name after five operations', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ + transaction: { + transaction: + 'test span name (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', + }, + }) + .start() + .completed(); + }); + }, + ); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs index c69cfdcd4622..aed7c6310653 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts index 6021abe5aabe..b3603e18873e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts @@ -1,105 +1,56 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - describe('GraphQL/Apollo Tests > resolve spans', () => { afterAll(() => { cleanupChildProcesses(); }); - // With `ignoreResolveSpans: false`, the instrumentation emits a span for the execute step as well as - // for `parse`, `validate` and each (non-trivial) field resolver. - const EXPECTED_TRANSACTION = { - // `useOperationNameForRootSpan` defaults to true, so the root span name gets the operation appended. - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.operation.type': 'query', - 'graphql.processing.type': 'execute', - 'graphql.document': '{hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - }), - }), - expect.objectContaining({ - description: 'graphql.parse', - data: expect.objectContaining({ 'graphql.processing.type': 'parse' }), - }), - expect.objectContaining({ - description: 'graphql.validate', - data: expect.objectContaining({ 'graphql.processing.type': 'validate' }), - }), - expect.objectContaining({ - description: 'graphql.resolve hello', - data: expect.objectContaining({ - 'graphql.processing.type': 'resolve', - 'graphql.field.name': 'hello', - 'graphql.field.path': 'hello', - 'graphql.field.type': 'String', - 'graphql.parent.name': 'Query', - }), - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { test('emits parse, validate and resolve spans when ignoreResolveSpans is false', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - }); + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); - // Same behavior on the diagnostics-channel path: passing the channel integration explicitly (see - // instrument-dc.mjs) must carry `ignoreResolveSpans: false` through — the explicit instance wins over - // the swapped-in default — and emit resolve spans with the orchestrion origin. - const EXPECTED_ORCHESTRION_TRANSACTION = { - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.operation.type': 'query', - 'graphql.processing.type': 'execute', - 'graphql.document': '{hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - }), - }), - expect.objectContaining({ - description: 'graphql.parse', - data: expect.objectContaining({ 'graphql.processing.type': 'parse' }), - }), - expect.objectContaining({ - description: 'graphql.validate', - data: expect.objectContaining({ 'graphql.processing.type': 'validate' }), - }), - expect.objectContaining({ - description: 'graphql.resolve hello', - data: expect.objectContaining({ - 'graphql.processing.type': 'resolve', - 'graphql.field.name': 'hello', - 'graphql.field.path': 'hello', - 'graphql.field.type': 'String', - 'graphql.parent.name': 'Query', - }), - }), - ]), - }; - - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument-dc.mjs', (createTestRunner, test) => { - test('emits resolve spans via diagnostics-channel injection when configured explicitly', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_ORCHESTRION_TRANSACTION }) + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'execute'); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + const parseSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'parse'); + expect(parseSpan?.name).toBe('GraphQL parse'); + expect(parseSpan?.attributes['graphql.processing.type']).toEqual({ value: 'parse', type: 'string' }); + const validateSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'validate', + ); + expect(validateSpan?.name).toBe('GraphQL validate'); + expect(validateSpan?.attributes['graphql.processing.type']).toEqual({ value: 'validate', type: 'string' }); + const helloResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'hello', + ); + expect(helloResolverSpan?.name).toBe('GraphQL resolve'); + expect(helloResolverSpan?.attributes['graphql.processing.type']).toEqual({ + value: 'resolve', + type: 'string', + }); + expect(helloResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.type']).toEqual({ value: 'String', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts index 4b60ccf01f3a..40193bff052a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts @@ -1,47 +1,12 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - -const ORIGIN = 'auto.graphql.diagnostic_channel'; - -function graphqlExecuteSpan(opts: { - description: string; - operationType: string; - operationName?: string; - document: unknown; - status?: string; -}): ReturnType { - const { description, operationType, operationName, document, status = 'ok' } = opts; - return expect.objectContaining({ - description, - status, - origin: ORIGIN, - data: expect.objectContaining({ - 'graphql.operation.type': operationType, - ...(operationName ? { 'graphql.operation.name': operationName } : {}), - 'graphql.document': document, - 'sentry.origin': ORIGIN, - }), - }); -} - describe('GraphQL/Apollo Tests', () => { afterAll(() => { cleanupChildProcesses(); }); describe('query', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ description: 'query', operationType: 'query', document: '{hello}' }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-query.mjs', @@ -49,9 +14,30 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should instrument GraphQL queries used from Apollo Server.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -61,18 +47,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('mutation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation Mutation)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ - description: 'mutation Mutation', - operationType: 'mutation', - operationName: 'Mutation', - document: 'mutation Mutation($email: String) {\n login(email: $email)\n}', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-mutation.mjs', @@ -80,9 +54,45 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should instrument GraphQL mutations used from Apollo Server.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation Mutation', + type: 'string', + }); + const mutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Mutation', + ); + expect(mutationSpan?.name).toBe('GraphQL mutation'); + expect(mutationSpan?.status).toBe('ok'); + expect(mutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'Mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Mutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(mutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -92,18 +102,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('redaction', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation)', - spans: expect.arrayContaining([ - // The inline email literal must be redacted to `"*"`, so the raw value never reaches the span. - graphqlExecuteSpan({ - description: 'mutation', - operationType: 'mutation', - document: expect.stringContaining('login(email: "*")'), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-redaction.mjs', @@ -111,9 +109,36 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('redacts inline literal values from the graphql document.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'mutation', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL mutation'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ + value: expect.stringContaining('login(email: "*")'), + type: 'string', + }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -123,19 +148,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('error', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation Mutation)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ - description: 'mutation Mutation', - operationType: 'mutation', - operationName: 'Mutation', - document: 'mutation Mutation($email: String) {\n login(email: $email)\n}', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-error.mjs', @@ -143,9 +155,49 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should handle GraphQL errors.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation Mutation', + type: 'string', + }); + const mutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Mutation', + ); + expect(mutationSpan?.name).toBe('GraphQL mutation'); + expect(mutationSpan?.status).toBe('error'); + expect(mutationSpan?.attributes['sentry.status.message']).toEqual({ + value: 'internal_error', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'Mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Mutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(mutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs index 35d159556ae1..4f8eddd456c8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts index d414cabfa3d4..9ef795290bae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts @@ -1,41 +1,48 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { afterAll(() => { cleanupChildProcesses(); }); describe('single query operation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query GetHello)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetHello', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetHello {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetHello', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single query operation', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + const getHelloSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetHello', + ); + expect(getHelloSpan?.name).toBe('GraphQL query'); + expect(getHelloSpan?.status).toBe('ok'); + expect(getHelloSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetHello', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetHello {hello}', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); @@ -43,32 +50,51 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('single mutation operation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (mutation TestMutation)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'TestMutation', - 'graphql.operation.type': 'mutation', - 'graphql.document': `mutation TestMutation($email: String) { - login(email: $email) -}`, - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'mutation TestMutation', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-mutation.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single mutation operation', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation TestMutation', + type: 'string', + }); + const testMutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'TestMutation', + ); + expect(testMutationSpan?.name).toBe('GraphQL mutation'); + expect(testMutationSpan?.status).toBe('ok'); + expect(testMutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'TestMutation', + type: 'string', + }); + expect(testMutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(testMutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation TestMutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(testMutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(testMutationSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(testMutationSpan?.attributes['graphql.processing.type']).toEqual({ + value: 'execute', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -76,29 +102,33 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('query without name', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.type': 'query', - 'graphql.document': 'query {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-no-operation-name.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single query operation without name', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: 'query {hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); @@ -106,68 +136,64 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('multiple operations', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query GetHello, query GetWorld)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetHello', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetHello {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetHello', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetWorld', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetWorld {world}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetWorld', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-multiple-operations.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with multiple query operations', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query GetWorld', 'query GetHello'], + type: 'array', + }); + const getHelloSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetHello', + ); + expect(getHelloSpan?.name).toBe('GraphQL query'); + expect(getHelloSpan?.status).toBe('ok'); + expect(getHelloSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetHello', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetHello {hello}', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + const getWorldSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetWorld', + ); + expect(getWorldSpan?.name).toBe('GraphQL query'); + expect(getWorldSpan?.status).toBe('ok'); + expect(getWorldSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetWorld', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetWorld {world}', + type: 'string', + }); + expect(getWorldSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getWorldSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); }); }); - - describe('many operations', () => { - const EXPECTED_TRANSACTION = { - transaction: - 'test span name (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', - }; - - createEsmAndCjsTests( - __dirname, - 'scenario-multiple-operations-many.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('useOperationNameForRootSpan works with more than 5 query operations', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - }, - ); - }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts index 4d67fee0a0d2..83f987a0cfdf 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts @@ -1,4 +1,4 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; @@ -17,7 +17,7 @@ Sentry.startSpan( attributes: { 'http.request.method': 'GET', 'http.route': '/route', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }, }, diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts index 69b76f248555..4c46fe4bcd9e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts @@ -1,4 +1,4 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; @@ -17,7 +17,7 @@ Sentry.startSpan( attributes: { 'http.request.method': 'GET', 'http.route': '/route', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs similarity index 68% rename from dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs rename to dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs index abfa58bbff9f..1c507ba84b9c 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs @@ -1,10 +1,10 @@ -import * as Sentry from '@sentry/aws-serverless'; +import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, + dataCollection: { genAI: { inputs: true, outputs: true } }, transport: loggingTransport, - traceLifecycle: 'stream', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts index 98ad59dcc4df..f4cf35579ae4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts @@ -2,8 +2,13 @@ import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, GEN_AI_COST_TOTAL_TOKENS, + GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_SYSTEM_INSTRUCTIONS, + GEN_AI_TOOL_CALL_ARGUMENTS, + GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_NAME, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, @@ -87,9 +92,10 @@ conditionalTest({ min: 22 })('Flue integration', () => { expect(chat.attributes[GEN_AI_COST_TOTAL_TOKENS]?.value).toEqual(expect.any(Number)); } + // pi-ai's `toolUse` is reported as the conventions' `tool_call`. expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ '["stop"]', - '["toolUse"]', + '["tool_call"]', ]); const tool = tools[0]!; @@ -111,4 +117,62 @@ conditionalTest({ min: 22 })('Flue integration', () => { }, FLUE_DEPENDENCIES, ); + + createEsmAndCjsTests( + __dirname, + 'scenario.mjs', + 'instrument-with-pii.mjs', + (createRunner, test, mode) => { + if (mode === 'cjs') { + return; + } + + test('records the messages of the turns in the gen_ai conventions shape', async () => { + await createRunner() + .expect({ + span: container => { + const chats = container.items + .filter(span => span.name === 'chat faux-model') + .sort((a, b) => a.start_timestamp - b.start_timestamp); + expect(chats).toHaveLength(2); + + // Flue sends pi-ai's shape (`toolResult`, `toolCall`); Sentry renders the conventions. + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ + { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, + ]); + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_OUTPUT_MESSAGES]?.value))).toEqual([ + { + role: 'assistant', + parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }], + finish_reason: 'tool_call', + }, + ]); + + const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); + expect(answerInput.map((message: { role: string }) => message.role)).toEqual([ + 'user', + 'assistant', + 'tool', + ]); + expect(answerInput[2].parts[0]).toMatchObject({ + type: 'tool_call_response', + id: 'call_1', + name: 'get_weather', + }); + expect(answerInput[2].parts[0].result).toContain('sunny in Berlin'); + expect(chats[1]!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toContain('You are a helpful assistant.'); + + // The tool span records the result the model receives, not Flue's internal wrapper. + const tool = container.items.find(span => span.name === 'execute_tool get_weather')!; + expect(tool.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).not.toContain('details'); + }, + }) + .start() + .completed(); + }); + }, + FLUE_DEPENDENCIES, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs index ca0a97c22c53..908715027e04 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs index c69cfdcd4622..aed7c6310653 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts index da29892f852a..911971abf5f9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts @@ -10,47 +10,85 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > resolve spans', () cleanupChildProcesses(); }); - const expectedResolveSpan = (path: string, fieldName: string, parentName: string) => - expect.objectContaining({ - description: `graphql.resolve ${path}`, - op: 'graphql', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.field.name': fieldName, - 'graphql.field.path': path, - 'graphql.parent.name': parentName, - }), - }); - - const EXPECTED_TRANSACTION = { - // Root span renamed with the (sorted) operation names. useOperationNameForRootSpan defaults to true. - transaction: 'Test Transaction (query, query GetUser)', - spans: expect.arrayContaining([ - expect.objectContaining({ description: 'query', op: 'graphql' }), - expect.objectContaining({ description: 'query GetUser', op: 'graphql' }), - expectedResolveSpan('hello', 'hello', 'Query'), - expectedResolveSpan('user', 'user', 'Query'), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('emits resolver spans when ignoreResolveSpans is false', async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner() + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + const children = container.items.filter(span => !span.is_segment); + + expect(segment?.name).toBe('Test Transaction'); + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query', 'query GetUser'], + type: 'array', + }); + const executeSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name'] === undefined, + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{ hello }', type: 'string' }); + const getUserSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); + expect(getUserSpan?.name).toBe('GraphQL query'); + expect(getUserSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getUserSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetUser', type: 'string' }); + const helloResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'hello', + ); + expect(helloResolverSpan?.name).toBe('GraphQL resolve'); + expect(helloResolverSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(helloResolverSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(helloResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + const userResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'user', + ); + expect(userResolverSpan?.name).toBe('GraphQL resolve'); + expect(userResolverSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(userResolverSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(userResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'user', type: 'string' }); + expect(userResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'user', type: 'string' }); + expect(userResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + }, + }) + .start() + .completed(); }); test('skips the default property resolver (trivial resolve) by default', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + // `user.name` uses graphql's default property resolver, so no span is emitted for it. - expect(spans.find(span => span.description === 'graphql.resolve user.name')).toBeUndefined(); + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user.name')).toBeUndefined(); // ...but the user-defined resolvers do produce spans. - expect(spans.find(span => span.description === 'graphql.resolve user')).toBeDefined(); + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user')).toBeDefined(); }, }) .start() @@ -70,9 +108,12 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > resolve spans', () test('emits a span for the trivial default resolver when ignoreTrivialResolveSpans is false', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - expect(spans.find(span => span.description === 'graphql.resolve user.name')).toBeDefined(); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user.name')).toBeDefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts index 3347c606e4fb..e1ea0083877a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts @@ -2,64 +2,84 @@ import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// graphql >= 17 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes to -// those channels (`subscribeGraphqlDiagnosticChannels`) instead of the vendored OTel patcher. This -// suite pins `^17` and asserts the diagnostics-channel path: graphql semconv attributes, redacted -// document text, span relationships, and that the legacy OTel path does NOT also fire (no double -// instrumentation). graphql 17 requires Node >= 22, so this suite is skipped on older Node. +// GraphQL 17 requires Node >= 22, so this suite is skipped on older Node. conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { afterAll(() => { cleanupChildProcesses(); }); - const expectedExecuteSpan = (description: string, extraData: Record = {}) => - expect.objectContaining({ - description, - op: 'graphql', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining(extraData), - }); - - const EXPECTED_TRANSACTION = { - // Root span renamed with the (sorted) operation names. useOperationNameForRootSpan defaults to true. - transaction: 'Test Transaction (mutation Login, query, query Boom, query GetUser)', - spans: expect.arrayContaining([ - expect.objectContaining({ description: 'graphql.parse', op: 'graphql' }), - expect.objectContaining({ description: 'graphql.validate', op: 'graphql' }), - // anonymous query -> span named after the operation type only - expectedExecuteSpan('query', { 'graphql.operation.type': 'query' }), - expectedExecuteSpan('query GetUser', { - 'graphql.operation.type': 'query', - 'graphql.operation.name': 'GetUser', - // the inline `42` literal is redacted out of the document - 'graphql.document': 'query GetUser { user(id: *) { name } }', - }), - expectedExecuteSpan('mutation Login', { - 'graphql.operation.type': 'mutation', - // the inline email literal must be redacted to `"*"`, so the raw value can never leak - 'graphql.document': 'mutation Login { login(email: "*") }', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('subscribes to graphql >= 17 diagnostics channels with graphql semconv attributes', async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); - - test('does not double-instrument: the vendored OTel graphql patcher does not fire on 17', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - // The vendored OTel path (origin `auto.graphql.graphql`) must be inactive on 17+. - expect(spans.find(span => span.origin === 'auto.graphql.graphql')).toBeUndefined(); - // ...while the diagnostics-channel path is active. - expect(spans.find(span => span.origin === 'auto.graphql.diagnostic_channel')).toBeDefined(); + span: container => { + const segment = container.items.find(span => span.is_segment); + const children = container.items.filter(span => !span.is_segment); + + expect(segment?.name).toBe('Test Transaction'); + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query', 'query GetUser', 'mutation Login', 'query Boom'], + type: 'array', + }); + const parseSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'parse'); + expect(parseSpan?.name).toBe('GraphQL parse'); + expect(parseSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + const validateSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'validate', + ); + expect(validateSpan?.name).toBe('GraphQL validate'); + expect(validateSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + const executeSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name'] === undefined, + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{ hello }', type: 'string' }); + const getUserSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); + expect(getUserSpan?.name).toBe('GraphQL query'); + expect(getUserSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getUserSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getUserSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getUserSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetUser', type: 'string' }); + expect(getUserSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetUser { user(id: *) { name } }', + type: 'string', + }); + const loginSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Login', + ); + expect(loginSpan?.name).toBe('GraphQL mutation'); + expect(loginSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(loginSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(loginSpan?.attributes['graphql.operation.name']).toEqual({ value: 'Login', type: 'string' }); + expect(loginSpan?.attributes['graphql.operation.type']).toEqual({ value: 'mutation', type: 'string' }); + expect(loginSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Login { login(email: "*") }', + type: 'string', + }); }, }) .start() @@ -69,14 +89,22 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('never leaks raw inline literal values into graphql.document', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - for (const span of spans) { - const document = span.data?.['graphql.document']; - if (typeof document === 'string') { - expect(document).not.toContain('secret@example.com'); - } - } + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const loginSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Login', + ); + expect(loginSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Login { login(email: "*") }', + type: 'string', + }); + const documents = spans.map(span => span.attributes['graphql.document']?.value); + expect(documents.join('\n')).not.toContain('secret@example.com'); }, }) .start() @@ -86,25 +114,39 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('flags the execute span as errored when a resolver throws', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const boomSpan = spans.find(span => span.description === 'query Boom'); - expect(boomSpan).toBeDefined(); - expect(boomSpan?.status).toBe('internal_error'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const boomSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Boom', + ); + expect(boomSpan?.status).toBe('error'); + expect(boomSpan?.attributes['sentry.status.message']?.value).toBe('internal_error'); }, }) .start() .completed(); }); - test('parents the execute span to the surrounding transaction', async () => { + test('parents the execute span to the surrounding segment', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const executeSpan = spans.find(span => span.description === 'query GetUser'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const executeSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); expect(executeSpan).toBeDefined(); - expect(executeSpan?.parent_span_id).toBe(event.contexts?.trace?.span_id); + expect(executeSpan?.parent_span_id).toBe(segment?.span_id); }, }) .start() @@ -124,11 +166,14 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('flags the validate span as errored for an invalid document', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const validateSpan = spans.find(span => span.description === 'graphql.validate'); - expect(validateSpan).toBeDefined(); - expect(validateSpan?.status).toBe('invalid_argument'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const validateSpan = spans.find(span => span.attributes['graphql.processing.type']?.value === 'validate'); + expect(validateSpan?.status).toBe('error'); + expect(validateSpan?.attributes['sentry.status.message']?.value).toBe('invalid_argument'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs index 43403fda0caf..62affdd7a041 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts index 081a437aeab7..1739309f1e1b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts @@ -1,4 +1,4 @@ -import { afterAll } from 'vitest'; +import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -13,9 +13,19 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('renames the root span with a single operation', async () => { + test('records a single operation without renaming the root span', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction (query GetHello)' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -28,9 +38,19 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor 'scenario-multiple.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('accumulates multiple operations on the root span name (sorted)', async () => { + test('accumulates multiple operations without renaming the root span', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction (query GetHello, query GetWorld)' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query GetWorld', 'query GetHello'], + type: 'array', + }); + }, + }) .start() .completed(); }); @@ -45,7 +65,14 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor (createTestRunner, test) => { test('keeps the original root span name when useOperationNameForRootSpan is false', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toBeUndefined(); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts index 5797f38d2d1b..ab5271d177ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -15,6 +14,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -37,8 +38,8 @@ describe('Groq integration', () => { expect(chatSpan!.name).toBe('chat llama-3.3-70b-versatile'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('llama-3.3-70b-versatile'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -70,7 +71,7 @@ describe('Groq integration', () => { expect(embeddingsSpan).toBeDefined(); expect(embeddingsSpan!.name).toBe('embeddings nomic-embed-text-v1_5'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); expect(embeddingsSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT]).toBeUndefined(); @@ -88,7 +89,7 @@ describe('Groq integration', () => { 'embeddings nomic-embed-text-v1_5', ]); for (const span of rawSpans) { - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(span.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(span.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); expect(span.attributes[GEN_AI_USAGE_TOTAL_TOKENS]).toBeUndefined(); expect(span.attributes[GEN_AI_RESPONSE_TEXT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts index c1ac1f7fc4a6..fefa45353102 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; describe('filtering child spans with ignoreSpans (streaming)', () => { afterAll(() => { @@ -28,9 +28,7 @@ describe('filtering child spans with ignoreSpans (streaming)', () => { // Would be 7 if we didn't ignore the 'middleware - expressInit' and 'custom-to-drop' spans expect(container.items).toHaveLength(8); const getSpan = (name: string, op: string) => - container.items.find( - item => item.name === name && item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === op, - ); + container.items.find(item => item.name === name && item.attributes[SENTRY_OP]?.value === op); const queryMiddlewareSpan = getSpan('query', 'middleware'); const corsMiddlewareSpan = getSpan('corsMiddleware', 'middleware'); const requestHandlerSpan = getSpan('/test/express', 'handler'); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs new file mode 100644 index 000000000000..8fcb48226773 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs @@ -0,0 +1,85 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import { RunnableLambda, RunnableSequence } from '@langchain/core/runnables'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_conversation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'The weather is sunny.' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + temperature: 0, + maxTokens: 50, + apiKey: 'mock-api-key', + clientOptions: { baseURL: baseUrl }, + }); + + const formatStep = RunnableLambda.from(input => `Tell me about: ${input.topic}`).withConfig({ + runName: 'format_prompt', + }); + + const chain = RunnableSequence.from([formatStep, model]).withConfig({ + runName: 'weather_chain', + }); + + const handler = Sentry.createLangChainCallbackHandler(); + + // 1. `configurable.thread_id` is copied into run metadata by LangChain and inherited by every + // child run, so the chain, the lambda step and the chat call all carry it. + await chain.invoke( + { topic: 'weather' }, + { + callbacks: [handler], + configurable: { thread_id: 'thread_from_config' }, + }, + ); + + // 2. `RunnableWithMessageHistory`-style `sessionId` is picked up too. + await chain.invoke( + { topic: 'weather' }, + { + callbacks: [handler], + configurable: { sessionId: 'session_from_config' }, + }, + ); + + // 3. An id set on the scope outranks the one from the config. + Sentry.setConversationId('conversation_from_scope'); + await chain.invoke({ topic: 'weather' }, { callbacks: [handler], configurable: { thread_id: 'thread_ignored' } }); + Sentry.setConversationId(null); + + // 4. No id anywhere leaves the attribute unset. + await chain.invoke({ topic: 'weather' }, { callbacks: [handler] }); + }); + + await Sentry.flush(2000); + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index 1d74840579bc..290cf38bcba6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect } from 'vitest'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, @@ -416,6 +417,32 @@ describe('LangChain integration', () => { }); }); + createEsmAndCjsTests(__dirname, 'scenario-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { + test('derives gen_ai.conversation.id from the invoke config and lets a scope id win', async () => { + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + // Four invocations, each producing the outer chain span, the lambda step span and the chat span. + expect(container.items).toHaveLength(12); + const idsByName = (name: string): (string | undefined)[] => + container.items + .filter(span => span.name === name) + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value as string | undefined); + + // The id reaches the chain span, the child lambda span and the child chat span alike. + const expected = ['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]; + expect(idsByName('chain weather_chain')).toEqual(expected); + expect(idsByName('chain format_prompt')).toEqual(expected); + expect(idsByName('chat claude-3-5-sonnet-20241022')).toEqual(expected); + }, + }) + .start() + .completed(); + }); + }); + createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { test('leads chain span names with the operation when span streaming is enabled', async () => { await createRunner() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs new file mode 100644 index 000000000000..6d8f246a9d00 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs @@ -0,0 +1,62 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import { RunnableLambda, RunnableSequence } from '@langchain/core/runnables'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_conversation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'The weather is sunny.' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + temperature: 0, + maxTokens: 50, + apiKey: 'mock-api-key', + clientOptions: { baseURL: baseUrl }, + }); + + const formatStep = RunnableLambda.from(input => `Tell me about: ${input.topic}`); + const chain = RunnableSequence.from([formatStep, model]); + + // `@langchain/core` >= 1.1.40 no longer copies `configurable` into run metadata for our handler, + // so the integration copies the conversation id keys itself. + await chain.invoke({ topic: 'weather' }, { configurable: { thread_id: 'thread_from_config' } }); + await chain.invoke({ topic: 'weather' }, { configurable: { sessionId: 'session_from_config' } }); + + Sentry.setConversationId('conversation_from_scope'); + await chain.invoke({ topic: 'weather' }, { configurable: { thread_id: 'thread_ignored' } }); + Sentry.setConversationId(null); + + await chain.invoke({ topic: 'weather' }); + }); + + await Sentry.flush(2000); + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index c451bfe7bd2d..669ac68afa34 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect } from 'vitest'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -209,6 +210,37 @@ describe('LangChain integration (v1)', () => { }, ); + createEsmAndCjsTests( + __dirname, + 'scenario-conversation-id.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('derives gen_ai.conversation.id from the invoke config and lets a scope id win', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const ids = container.items + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value); + expect(ids).toEqual(['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + langchain: '^1.0.0', + '@langchain/core': '^1.0.0', + '@langchain/anthropic': '^1.0.0', + }, + }, + ); + createEsmTests( __dirname, 'scenario-openai-before-langchain.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs index f499d9eff5f5..ad73f1133f41 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs @@ -110,9 +110,10 @@ async function run() { name: 'math_assistant', }); - await agent.invoke({ - messages: [new HumanMessage('Calculate (3 + 5) * 4')], - }); + await agent.invoke( + { messages: [new HumanMessage('Calculate (3 + 5) * 4')] }, + { configurable: { thread_id: 'math_thread' } }, + ); }); await Sentry.flush(2000); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 3f85267b154e..3ec4961fb10f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_AGENT_NAME, @@ -15,6 +14,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getStringAttributeValue } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -274,8 +275,8 @@ describe('LangGraph integration', () => { status: 'ok', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), [GEN_AI_PIPELINE_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), }), @@ -284,7 +285,7 @@ describe('LangGraph integration', () => { expect(spans).toContainEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), }), }), @@ -314,7 +315,7 @@ describe('LangGraph integration', () => { expect.objectContaining({ status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'math_assistant' }), }), @@ -325,7 +326,7 @@ describe('LangGraph integration', () => { name: 'execute_tool add', status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'add' }), }), @@ -336,15 +337,20 @@ describe('LangGraph integration', () => { name: 'execute_tool multiply', status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'multiply' }), }), }), ); - expect( - spans.filter(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat'), - ).toHaveLength(3); + expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')).toHaveLength(3); + + // `thread_id` reaches the agent span and every chat and tool span under it. + const genAiSpans = spans.filter(span => String(span.attributes[SENTRY_OP]?.value).startsWith('gen_ai.')); + expect(genAiSpans).toHaveLength(6); + for (const span of genAiSpans) { + expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe('math_thread'); + } }, }) .start() @@ -362,9 +368,7 @@ describe('LangGraph integration', () => { }) .expect({ span: container => { - const chatSpans = container.items.filter( - s => s.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat', - ); + const chatSpans = container.items.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); expect(chatSpans).toHaveLength(1); expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts index 49fe09771da1..1b1e91310beb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_AGENT_NAME, GEN_AI_EMBEDDINGS_INPUT, @@ -20,6 +19,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; @@ -47,8 +48,8 @@ describe('Mistral integration', () => { expect(chatSpan!.name).toBe('chat mistral-small-latest'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-small-latest'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -140,8 +141,8 @@ describe('Mistral integration', () => { expect(embeddingsSpan!.name).toBe('embeddings mistral-embed'); expect(embeddingsSpan!.status).toBe('ok'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-embed'); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); @@ -183,8 +184,8 @@ describe('Mistral integration', () => { expect(agentSpan!.name).toBe('invoke_agent'); expect(agentSpan!.status).toBe('ok'); expect(agentSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); - expect(agentSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.invoke_agent'); - expect(agentSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(agentSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.invoke_agent'); + expect(agentSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(agentSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(agentSpan!.attributes[GEN_AI_AGENT_NAME]?.value).toBe('ag-mock-123'); expect(agentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(10); @@ -262,7 +263,7 @@ describe('Mistral integration', () => { expect(chatSpan).toBeDefined(); expect(chatSpan!.name).toBe('chat mistral-small-latest'); expect(chatSpan!.status).toBe('ok'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('["Hello from the manual client!"]'); @@ -297,7 +298,7 @@ describe('Mistral integration', () => { expect(parseSpans[0]!.name).toBe('chat mistral-small-latest'); expect(parseSpans[0]!.status).toBe('ok'); expect(parseSpans[0]!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(parseSpans[0]!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(parseSpans[0]!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(parseSpans[0]!.attributes[GEN_AI_REQUEST_STREAM]?.value).toBe(false); expect(parseSpans[0]!.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBe(12); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts index 2188dd49f356..e0db6de0be75 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -3,8 +3,7 @@ import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests, createEsmAndCjsTests } from '../../../utils/runner'; import { startMysqlTestServer } from './mysql-test-server'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; describe('mysql auto instrumentation', () => { // A minimal in-process MySQL server (on a random free port) so the client's @@ -176,9 +175,7 @@ describe('mysql auto instrumentation', () => { const segmentSpan = container.items.find(item => item.is_segment); expect(segmentSpan?.name).toBe('Test Transaction'); - const dbSpans = container.items.filter( - spanItem => spanItem.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db', - ); + const dbSpans = container.items.filter(spanItem => spanItem.attributes[SENTRY_OP]?.value === 'db'); expect(dbSpans.length).toBe(2); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index 89bf6c7b0544..5dfd13c21f7f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -15,6 +14,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -92,11 +93,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -142,11 +143,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -202,11 +203,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -255,11 +256,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -328,11 +329,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -392,11 +393,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -463,11 +464,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -527,11 +528,11 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index a27e3308179a..2da282ebabb4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { @@ -18,6 +17,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -50,11 +51,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -107,11 +108,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -157,11 +158,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -184,11 +185,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -247,11 +248,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -319,11 +320,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -340,11 +341,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -393,11 +394,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -466,11 +467,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -535,11 +536,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -593,11 +594,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -624,11 +625,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -699,11 +700,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -783,11 +784,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -804,11 +805,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -866,11 +867,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1019,11 +1020,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1064,11 +1065,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1093,11 +1094,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1140,11 +1141,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1187,11 +1188,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1238,11 +1239,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1271,11 +1272,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1312,11 +1313,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1360,8 +1361,8 @@ describe('OpenAI integration', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_OP]: 'gen_ai.chat', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1402,8 +1403,8 @@ describe('OpenAI integration', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_OP]: 'gen_ai.chat', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1446,11 +1447,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(conversationCreateSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationCreateSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1475,11 +1476,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(conversationResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationResponseSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1498,12 +1499,12 @@ describe('OpenAI integration', () => { const unlinkedResponseSpan = container.items.find( span => - span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat' && + span.attributes[SENTRY_OP]?.value === 'gen_ai.chat' && span.attributes[GEN_AI_CONVERSATION_ID] === undefined, ); expect(unlinkedResponseSpan).toBeDefined(); expect(unlinkedResponseSpan!.status).toBe('ok'); - expect(unlinkedResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(unlinkedResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1518,11 +1519,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(previousResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(previousResponseSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1549,11 +1550,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(rawConversationSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawConversationSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(rawConversationSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawConversationSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1749,11 +1750,11 @@ describe('OpenAI integration', () => { expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ type: 'string', value: 'chat' }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1779,11 +1780,11 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1809,11 +1810,11 @@ describe('OpenAI integration', () => { expect(parsedResponsesSpan!.name).toBe('chat gpt-4o'); expect(parsedResponsesSpan!.status).toBe('ok'); expect(parsedResponsesSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ type: 'string', value: 'chat' }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index 091867486e4e..49d5cb48bd9d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -16,6 +15,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -50,11 +51,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -105,11 +106,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -155,11 +156,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -182,11 +183,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -245,11 +246,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -317,11 +318,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -359,11 +360,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -426,11 +427,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -484,11 +485,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -515,11 +516,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -590,11 +591,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -674,11 +675,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -784,11 +785,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -829,11 +830,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -858,11 +859,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -924,11 +925,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -975,11 +976,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1008,11 +1009,11 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1075,8 +1076,8 @@ describe('OpenAI integration (V6)', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_OP]: 'gen_ai.chat', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1128,8 +1129,8 @@ describe('OpenAI integration (V6)', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_OP]: 'gen_ai.chat', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts index 6d7eb552c219..fbea79d4f34c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { GEN_AI_OPERATION_NAME, @@ -11,6 +10,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { conditionalTest } from '../../../../utils/index'; @@ -44,11 +45,11 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { expect(chatCompletionSpan).toBeDefined(); expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -96,7 +97,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { const errorSpan = container.items.find(span => span.name === 'chat error-model' && span.status !== 'ok'); expect(errorSpan).toBeDefined(); - expect(errorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -135,11 +136,11 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', ); expect(singleEmbeddingSpan).toBeDefined(); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -184,11 +185,11 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { expect(chatCompletionSpan).toBeDefined(); expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts index 3a2cea4a4c74..bec779c70b02 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts @@ -1,6 +1,5 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { conditionalTest, EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -157,7 +156,7 @@ const CREATE_NATIVE_USER_TABLE_STATEMENT = 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))'; function getDbSpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpanContainer['items'] { - return container.items.filter(item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db'); + return container.items.filter(item => item.attributes[SENTRY_OP]?.value === 'db'); } describeWithDockerCompose('postgres auto instrumentation (streamed)', { workingDirectory: [__dirname] }, () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 50cd473957b8..d8b8b728a2af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -1,5 +1,5 @@ -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, type SerializedStreamedSpanContainer } from '@sentry/core'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { type SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -136,9 +136,7 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d const segmentSpan = container.items.find(item => item.is_segment); expect(segmentSpan?.name).toBe('Test Span'); - const dbSpans = container.items.filter( - item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === redisSpanOp, - ); + const dbSpans = container.items.filter(item => item.attributes[SENTRY_OP]?.value === redisSpanOp); expect(dbSpans).toEqual([ expectedDbSpan({ operation: 'set', statement: 'set test-key [1 other arguments]' }), diff --git a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts index 2e7c3b94dc7b..1e912a358fc0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -15,6 +14,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -37,8 +38,8 @@ describe('Together integration', () => { expect(chatSpan!.name).toBe('chat meta-llama/Llama-3.3-70B-Instruct-Turbo'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('meta-llama/Llama-3.3-70B-Instruct-Turbo'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -70,7 +71,7 @@ describe('Together integration', () => { expect(embeddingsSpan).toBeDefined(); expect(embeddingsSpan!.name).toBe('embeddings togethercomputer/m2-bert-80M-8k-retrieval'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); expect(embeddingsSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT]).toBeUndefined(); @@ -88,7 +89,7 @@ describe('Together integration', () => { 'embeddings togethercomputer/m2-bert-80M-8k-retrieval', ]); for (const span of rawSpans) { - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(span.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(span.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); expect(span.attributes[GEN_AI_USAGE_TOTAL_TOKENS]).toBeUndefined(); expect(span.attributes[GEN_AI_RESPONSE_TEXT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index e4276d73a0dd..50385f0e0c40 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -14,6 +13,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -50,8 +51,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Second span - generate_content for simple generateText @@ -65,8 +66,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Third span - invoke_agent for explicit telemetry generateText @@ -79,8 +80,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fourth span - tool call invoke_agent @@ -93,8 +94,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fifth span - tool call generate_content @@ -107,8 +108,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Sixth span - execute_tool @@ -120,8 +121,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), @@ -144,8 +145,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Second span - generate_content with input/output messages @@ -163,8 +164,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Third span - explicit telemetry invoke_agent with messages @@ -181,8 +182,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fourth span - tool call invoke_agent with messages @@ -199,8 +200,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fifth span - tool call generate_content with tool definitions @@ -216,8 +217,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Sixth span - execute_tool with description and input/output @@ -231,8 +232,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), @@ -245,8 +246,8 @@ describe('Vercel AI integration (streaming v4)', () => { status: 'error', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), expect.objectContaining({ @@ -258,8 +259,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), expect.objectContaining({ @@ -269,8 +270,8 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs new file mode 100644 index 000000000000..303a577f8ef0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs @@ -0,0 +1,82 @@ +import * as Sentry from '@sentry/node'; +import { embed, embedMany, generateText, streamText } from 'ai'; +import { MockEmbeddingModelV3, MockLanguageModelV3, simulateReadableStream } from 'ai/test'; + +const usage = { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 20, noCache: 20, cached: 0 }, + totalTokens: { total: 30, noCache: 30, cached: 0 }, +}; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + await generateText({ + experimental_telemetry: { + isEnabled: true, + functionId: 'support-chat', + metadata: { requestId: 'req_generate', tenantId: 'acme', attempt: 2 }, + }, + model: new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage, + content: [{ type: 'text', text: 'Hello!' }], + warnings: [], + }), + }), + prompt: 'Hi', + }); + + const result = streamText({ + experimental_telemetry: { + isEnabled: true, + functionId: 'support-stream', + metadata: { requestId: 'req_stream', tenantId: 'acme' }, + }, + model: new MockLanguageModelV3({ + doStream: async () => ({ + stream: simulateReadableStream({ + chunks: [ + { type: 'stream-start', warnings: [] }, + { type: 'text-start', id: '0' }, + { type: 'text-delta', id: '0', delta: 'Hello!' }, + { type: 'text-end', id: '0' }, + { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage }, + ], + }), + }), + }), + prompt: 'Hi', + }); + for await (const _part of result.fullStream) { + void _part; + } + + await embed({ + experimental_telemetry: { isEnabled: true, metadata: { requestId: 'req_embed', tenantId: 'acme' } }, + model: new MockEmbeddingModelV3({ + doEmbed: async () => ({ embeddings: [[0.1, 0.2, 0.3]], usage: { tokens: 10 } }), + }), + value: 'Embed me', + }); + + await embedMany({ + experimental_telemetry: { isEnabled: true, metadata: { requestId: 'req_embed_many', tenantId: 'acme' } }, + model: new MockEmbeddingModelV3({ + maxEmbeddingsPerCall: 5, + doEmbed: async () => ({ + embeddings: [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + ], + usage: { tokens: 20 }, + }), + }), + values: ['First', 'Second'], + }); + }); + + await Sentry.flush(2000); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts index 939ba08e9746..68c88b131783 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -14,6 +13,8 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -47,8 +48,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Second span - generate_content for simple generateText @@ -62,8 +63,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Third span - invoke_agent for explicit telemetry generateText @@ -76,8 +77,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fourth span - tool call invoke_agent @@ -90,8 +91,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fifth span - tool call generate_content @@ -104,8 +105,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Sixth span - execute_tool @@ -117,8 +118,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), @@ -141,8 +142,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Second span - generate_content with input/output messages @@ -160,8 +161,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Third span - explicit telemetry invoke_agent with messages @@ -178,8 +179,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fourth span - tool call invoke_agent with messages (V6: no text part, only tool_call) @@ -196,8 +197,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fifth span - tool call generate_content with tool definitions @@ -213,8 +214,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(origin), }), }), // Sixth span - execute_tool with description and input/output @@ -228,8 +229,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), @@ -241,8 +242,8 @@ describe('Vercel AI integration (streaming, v6)', () => { name: 'invoke_agent', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_ORIGIN]: attr(origin), }), }), expect.objectContaining({ @@ -254,8 +255,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_ORIGIN]: attr(origin), }), }), expect.objectContaining({ @@ -265,8 +266,8 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), @@ -304,6 +305,49 @@ describe('Vercel AI integration (streaming, v6)', () => { }, ); + function spanWithMetadata(name: string, op: string, requestId: string, extra: Record = {}) { + return expect.objectContaining({ + name, + attributes: expect.objectContaining({ + 'vercel.ai.telemetry.metadata.requestId': attr(requestId), + 'vercel.ai.telemetry.metadata.tenantId': attr('acme'), + ...extra, + [SENTRY_OP]: attr(op), + }), + }); + } + + const EXPECTED_SPANS_TELEMETRY_METADATA = { + items: expect.arrayContaining([ + spanWithMetadata('invoke_agent support-chat', 'gen_ai.invoke_agent', 'req_generate', { + 'vercel.ai.telemetry.metadata.attempt': { type: 'string', value: '2' }, + }), + spanWithMetadata('generate_content mock-model-id', 'gen_ai.generate_content', 'req_generate', { + 'vercel.ai.telemetry.metadata.attempt': { type: 'string', value: '2' }, + }), + spanWithMetadata('invoke_agent support-stream', 'gen_ai.invoke_agent', 'req_stream'), + spanWithMetadata('generate_content mock-model-id', 'gen_ai.generate_content', 'req_stream'), + spanWithMetadata('embeddings mock-model-id', 'gen_ai.embeddings', 'req_embed'), + spanWithMetadata('embeddings mock-model-id', 'gen_ai.embeddings', 'req_embed_many'), + ]), + }; + + createEsmAndCjsTests( + __dirname, + 'scenario-telemetry-metadata.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('records experimental_telemetry.metadata as span attributes', async () => { + await createRunner().expect({ span: EXPECTED_SPANS_TELEMETRY_METADATA }).start().completed(); + }); + }, + { + additionalDependencies: { + ai: '^6.0.0', + }, + }, + ); + createEsmAndCjsTests( __dirname, 'scenario-error-in-tool.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 75945701e92f..e6ec70fa35ff 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -492,7 +492,7 @@ describe('Vercel AI integration (v4)', () => { }); createEsmAndCjsTests(__dirname, 'scenario-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { - test('does not overwrite conversation id set via Sentry.setConversationId with responseId from provider metadata', async () => { + test('keeps the conversation id set via Sentry.setConversationId and ignores the provider responseId', async () => { await createRunner() .expect({ transaction: { transaction: 'main' } }) .expect({ diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs new file mode 100644 index 000000000000..fd724db00276 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs @@ -0,0 +1,75 @@ +import * as Sentry from '@sentry/node'; +import { generateText, tool } from 'ai'; +import { MockLanguageModelV3 } from 'ai/test'; +import { z } from 'zod'; + +const usage = { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 5, noCache: 5, cached: 0 }, + totalTokens: { total: 15, noCache: 15, cached: 0 }, +}; + +const textModel = new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage, + content: [{ type: 'text', text: 'Hello!' }], + warnings: [], + // A per-response id: present on every turn, never the conversation id. + providerMetadata: { openai: { responseId: 'resp_turn' } }, + }), +}); + +const toolCallModel = new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'tool-calls', raw: 'tool_calls' }, + usage, + content: [{ type: 'tool-call', toolCallId: 'tc-1', toolName: 'echo', input: JSON.stringify({ text: 'hi' }) }], + warnings: [], + }), +}); + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + // A turn of an OpenAI Conversations API conversation. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'First turn', + providerOptions: { openai: { conversation: 'conv_abc123' } }, + }); + + // The Azure Responses API carries the same option under the `azure` key; this turn also runs a tool. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: toolCallModel, + prompt: 'Second turn', + providerOptions: { azure: { conversation: 'conv_azure' } }, + tools: { + echo: tool({ + inputSchema: z.object({ text: z.string() }), + execute: async ({ text }) => text, + }), + }, + }); + + // Chaining on the previous response names a response, not a thread, so no conversation id. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'Chained turn', + providerOptions: { openai: { previousResponseId: 'resp_turn' } }, + }); + + // An id set through the SDK API wins over the provider option. Last, since it stays on the scope. + Sentry.setConversationId('conv-from-api'); + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'API turn', + providerOptions: { openai: { conversation: 'conv_ignored' } }, + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index f8a26a410833..805a65313a66 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -767,7 +767,7 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe 'scenario-provider-metadata.mjs', 'instrument.mjs', (createRunner, test) => { - test('derives provider-metadata token breakdown, conversation id and system instructions', async () => { + test('derives provider-metadata token breakdown and system instructions', async () => { await createRunner() .expect({ transaction: { transaction: 'main' } }) .expect({ @@ -781,12 +781,13 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe )!; expect(generateContent).toBeDefined(); - // Cache/reasoning token breakdown and conversation id are derived from the model's - // `providerMetadata` — by the OTel processor on v6 and by the channel subscriber on v7, - // both via the shared `getProviderMetadataAttributes` helper, so the shape is identical. + // Cache/reasoning token breakdown is derived from the model's `providerMetadata` by the + // channel subscriber, which v6 reaches through the orchestrion adapter, so the shape is the + // same on both versions. expect(generateContent.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]?.value).toBe(5); expect(generateContent.attributes[GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]?.value).toBe(7); - expect(generateContent.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe('resp_abc123'); + // The per-response `responseId` is not a conversation id and must not be recorded as one. + expect(generateContent.attributes[GEN_AI_CONVERSATION_ID]).toBeUndefined(); const invokeAgent = container.items.find( span => span.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', @@ -813,6 +814,66 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-openai-conversation.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('derives gen_ai.conversation.id from the OpenAI `conversation` provider option', async () => { + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + const genAiSpans = container.items.filter(s => + String(s.attributes['sentry.op']?.value ?? '').startsWith('gen_ai.'), + ); + const conversationIdOf = (span: (typeof genAiSpans)[number]) => + span.attributes[GEN_AI_CONVERSATION_ID]?.value; + const invokeAgentSpans = genAiSpans.filter( + s => s.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', + ); + expect(invokeAgentSpans).toHaveLength(4); + const [firstTurn, secondTurn, chainedTurn, apiTurn] = invokeAgentSpans.sort( + (a, b) => a.start_timestamp - b.start_timestamp, + ); + + // `providerOptions.openai.conversation` is the Conversations API id: the same on every turn. + expect(conversationIdOf(firstTurn!)).toBe('conv_abc123'); + // The Azure Responses API uses the `azure` key for the same option. + expect(conversationIdOf(secondTurn!)).toBe('conv_azure'); + // `previousResponseId` names a response rather than a thread, and the response's own + // `responseId` is recorded as `gen_ai.response.id` only. + expect(conversationIdOf(chainedTurn!)).toBeUndefined(); + // `Sentry.setConversationId()` beats the provider option. + expect(conversationIdOf(apiTurn!)).toBe('conv-from-api'); + + // Model-call and tool spans carry their operation's id, even though their start events + // do not carry `providerOptions`. + const modelCallSpans = genAiSpans.filter( + s => s.attributes['sentry.op']?.value === 'gen_ai.generate_content', + ); + expect(modelCallSpans.map(conversationIdOf).sort()).toEqual([ + 'conv-from-api', + 'conv_abc123', + 'conv_azure', + undefined, + ]); + const toolSpan = genAiSpans.find(s => s.attributes['sentry.op']?.value === 'gen_ai.execute_tool')!; + expect(toolSpan).toBeDefined(); + expect(conversationIdOf(toolSpan)).toBe('conv_azure'); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + ai: vercelAiVersion, + }, + }, + ); + createEsmTests( __dirname, 'scenario-cache-tokens.mjs', diff --git a/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts b/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts new file mode 100644 index 000000000000..15b47d11e9b6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts @@ -0,0 +1,124 @@ +// Models a Vercel Node.js function: the platform exposes a per-request context with `waitUntil`, +// stops accepting `waitUntil` work once the response has closed, and freezes the instance right after the +// registered work settles. Nothing else (unref'd timers, SIGTERM) runs after the freeze. +import { AsyncLocalStorage } from 'node:async_hooks'; +import * as http from 'node:http'; +import * as net from 'node:net'; +import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +interface RequestStore { + waitUntil: (task: Promise) => void; + closed: boolean; +} + +const als = new AsyncLocalStorage(); +const registered: Promise[] = []; +let late = 0; + +(globalThis as Record)[Symbol.for('@vercel/request-context')] = { + get: () => als.getStore(), +}; + +function loggingTransport(_options: BaseTransportOptions): Transport { + return { + send(envelope: Envelope): Promise { + // eslint-disable-next-line no-console + console.log(JSON.stringify(envelope)); + return Promise.resolve({ statusCode: 200 }); + }, + flush(): PromiseLike { + return Promise.resolve(true); + }, + }; +} + +// Cases: `default`, `no-tracing` (no `tracesSampleRate`), `head` (HEAD requests never get a server span), +// `slow` (the handler responds late) and `late-root` (the root span ends after the response closes). +const testCase = process.env.TEST_CASE || 'default'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: testCase === 'no-tracing' ? undefined : 1, + enableLogs: true, + traceLifecycle: process.env.TRACE_LIFECYCLE === 'stream' ? 'stream' : 'static', + transport: loggingTransport, + integrations: testCase === 'late-root' ? [Sentry.httpIntegration({ spans: false })] : [], +}); + +const server = http.createServer(async (_req, res) => { + if (testCase === 'slow') { + await new Promise(resolve => setTimeout(resolve, 300)); + } + + const respond = (): void => { + Sentry.logger.info('keep alive log'); + Sentry.metrics.count('keep_alive_metric', 1); + if (testCase === 'no-tracing') { + Sentry.captureException(new Error('keep alive error')); + } + // Without a server span (HEAD), a manual span would become a root span itself and mask the case. + if (testCase !== 'head') { + Sentry.startSpan({ name: 'keep-alive-child' }, () => undefined); + } + res.writeHead(200); + res.end('ok'); + }; + + if (testCase === 'late-root') { + // Some frameworks end the root span a moment after the response has closed. + Sentry.startSpanManual({ name: 'GET /', op: 'http.server' }, span => { + res.once('close', () => setTimeout(() => span.end(), 20)); + respond(); + }); + } else { + respond(); + } +}); + +// As on Vercel, `http.server.request.start` fires before the request context exists. The context is active +// for the handler and the response it writes. +const originalEmit = server.emit; +server.emit = function (this: http.Server, event: string, ...args: unknown[]): boolean { + if (event !== 'request') { + return originalEmit.call(this, event, ...args); + } + + const store: RequestStore = { + closed: false, + waitUntil: task => { + if (store.closed) { + late++; + } else { + registered.push(task); + } + }, + }; + // Registered before the original emit, so it runs before Sentry's own 'close' listeners. + (args[1] as http.ServerResponse).once('close', () => { + store.closed = true; + }); + return als.run(store, () => originalEmit.call(this, event, ...args)); +} as typeof server.emit; + +server.listen(0, async () => { + const { port } = server.address() as net.AddressInfo; + + // A raw socket keeps the request free of trace propagation headers, which would otherwise carry a + // sampling decision into the server span. + await new Promise(resolve => { + const socket = net.connect(port, 'localhost', () => { + socket.write( + `${testCase === 'head' ? 'HEAD' : 'GET'} / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`, + ); + }); + socket.resume(); + socket.on('close', () => resolve()); + }); + + // The freeze: only what was registered in time keeps the instance alive. + await Promise.all(registered); + // eslint-disable-next-line no-console + console.log(`WAITUNTIL registered=${registered.length} late=${late}`); + process.exit(0); +}); diff --git a/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts b/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts new file mode 100644 index 000000000000..e104ddbfd051 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts @@ -0,0 +1,110 @@ +import { afterAll, describe, expect, test, vi } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe.each(['stream', 'static'] as const)('keeps the instance alive for telemetry (%s)', lifecycle => { + const logExpectation = { + log: { + items: [expect.objectContaining({ body: 'keep alive log' })], + }, + }; + + const rootSpanExpectation = + lifecycle === 'stream' + ? { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ name: 'keep-alive-child', is_segment: false }), + expect.objectContaining({ is_segment: true }), + ]), + }, + } + : { + transaction: { + contexts: { trace: { op: 'http.server' } }, + spans: [expect.objectContaining({ description: 'keep-alive-child' })], + }, + }; + + const metricExpectation = { + trace_metric: { + items: [expect.objectContaining({ name: 'keep_alive_metric', value: 1 })], + }, + }; + + test('sends the root span, log and metric before the instance freezes', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends the root span of a request that responds late', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'slow' }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends a root span that ends after the response closes', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'late-root' }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends errors and logs when tracing is disabled', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'no-tracing' }) + .unordered() + .expect({ + event: { + exception: { values: [expect.objectContaining({ value: 'keep alive error' })] }, + }, + }) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends logs for requests that get no server span', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'head' }) + .unordered() + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts b/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts index 94466eb0986b..21aa0deb1a1e 100644 --- a/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts +++ b/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts @@ -22,7 +22,6 @@ function bufferedLoggingTransport(_options: BaseTransportOptions): Transport { } Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: bufferedLoggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/winston/instrument.mjs b/dev-packages/node-integration-tests/suites/winston/instrument.mjs index 7ac52e645e47..f21bdc650450 100644 --- a/dev-packages/node-integration-tests/suites/winston/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/winston/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/test-utils/src/build-output.ts b/dev-packages/test-utils/src/build-output.ts index a3c2445e9f8a..f42e5dd1bdb0 100644 --- a/dev-packages/test-utils/src/build-output.ts +++ b/dev-packages/test-utils/src/build-output.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import type { SourcemapEntry } from './sourcemap-upload-utils'; export interface OutputScanOptions { /** Directory holding the emitted bundles, e.g. `/.output/public`. */ @@ -18,6 +19,12 @@ const SOURCE_MAPPING_URL_PATTERN = /[#@]\s*sourceMappingURL\s*=\s*(?!data:)(\S+) /** The `sentry-dbid-` identifier the bundler plugin injects alongside `_sentryDebugIds`. */ const INJECTED_DEBUG_ID_PATTERN = /sentry-dbid-([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})/gi; +/** Distinct lowercased debug IDs in `file`, in order of appearance. */ +function readDebugIds(file: string): string[] { + const matches = fs.readFileSync(file, 'utf8').matchAll(INJECTED_DEBUG_ID_PATTERN); + return [...new Set([...matches].map(match => (match[1] as string).toLowerCase()))]; +} + function* walkFiles(outputDir: string, extensions?: string[]): Generator { if (!fs.existsSync(outputDir)) { throw new Error(`[build-output] Output directory does not exist: ${outputDir}`); @@ -147,12 +154,59 @@ export function findInjectedDebugIds({ outputDir, extensions = JS_EXTENSIONS }: const debugIds = new Set(); for (const file of walkFiles(outputDir, extensions)) { - const contents = fs.readFileSync(file, 'utf8'); + readDebugIds(file).forEach(debugId => debugIds.add(debugId)); + } + + return [...debugIds]; +} + +/** + * Returns ` → ` for each file with more than one debug ID. + * The uploader reads the first ID, but the SDK can report any of them. + */ +export function findFilesWithMultipleDebugIds({ + outputDir, + buildDir = process.cwd(), + extensions = JS_EXTENSIONS, +}: OutputScanOptions): string[] { + const offenders: string[] = []; + + for (const file of walkFiles(outputDir, extensions)) { + const debugIds = readDebugIds(file); - for (const match of contents.matchAll(INJECTED_DEBUG_ID_PATTERN)) { - debugIds.add((match[1] as string).toLowerCase()); + if (debugIds.length > 1) { + offenders.push(`${path.relative(buildDir, file)} → ${debugIds.join(', ')}`); } } - return [...debugIds]; + return offenders; +} + +/** + * Returns ` → ` for each file where no map uploaded under its first debug ID names it in `file`. + * Skips files without a debug ID or without an uploaded map that has `file`. + */ +export function findFilesWithForeignSourcemaps( + { outputDir, buildDir = process.cwd(), extensions = JS_EXTENSIONS }: OutputScanOptions, + uploadedSourcemaps: SourcemapEntry[], +): string[] { + const offenders: string[] = []; + + for (const file of walkFiles(outputDir, extensions)) { + const [debugId] = readDebugIds(file); + if (!debugId) { + continue; + } + + const mapFiles = uploadedSourcemaps + .filter(entry => entry.debugId === debugId) + .map(entry => entry.sourcemap.file) + .filter(mapFile => mapFile !== undefined); + + if (mapFiles.length > 0 && !mapFiles.includes(path.basename(file))) { + offenders.push(`${path.relative(buildDir, file)} → ${mapFiles.join(', ')}`); + } + } + + return offenders; } diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 215ec05693cb..3da52584017a 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -23,6 +23,8 @@ export { findSourceMapFiles, findSourceMappingUrlComments, findInjectedDebugIds, + findFilesWithMultipleDebugIds, + findFilesWithForeignSourcemaps, bundleReferencesModule, } from './build-output'; export type { OutputScanOptions } from './build-output'; @@ -32,6 +34,8 @@ export type { InstrumentationFixture } from './bundler-instrumentation'; export { hidePage } from './page'; export { getPlaywrightConfig } from './playwright-config'; +export { getRuntime } from './runtime'; +export type { Runtime } from './runtime'; export { createBasicSentryServer, createTestServer } from './server'; export { startMockSentryServer } from './mock-sentry-server'; diff --git a/dev-packages/test-utils/src/mock-sentry-server.ts b/dev-packages/test-utils/src/mock-sentry-server.ts index 17390e74c30d..0e59dc0ac4fa 100644 --- a/dev-packages/test-utils/src/mock-sentry-server.ts +++ b/dev-packages/test-utils/src/mock-sentry-server.ts @@ -171,7 +171,9 @@ function sendResponse( res.end( JSON.stringify({ url: `http://localhost:${port}/api/0/organizations/${org}/chunk-upload/`, - chunkSize: 8388608, + // Each chunk is unzipped on its own, so a bundle split across chunks loses its manifest. + // Keep chunks as large as a request allows so that every bundle fits in one chunk. + chunkSize: 33554432, chunksPerRequest: 64, maxFileSize: 2147483648, maxRequestSize: 33554432, diff --git a/dev-packages/test-utils/src/runtime.ts b/dev-packages/test-utils/src/runtime.ts new file mode 100644 index 000000000000..236948a7fc17 --- /dev/null +++ b/dev-packages/test-utils/src/runtime.ts @@ -0,0 +1,17 @@ +export type Runtime = 'node' | 'bun' | 'deno' | 'cloudflare'; + +const RUNTIMES: readonly Runtime[] = ['node', 'bun', 'deno', 'cloudflare']; + +/** + * Returns the server runtime a test app runs on, read from the `RUNTIME` env var that runtime variants + * set in their assert script. Defaults to `node`. + */ +export function getRuntime(): Runtime { + const runtime = process.env.RUNTIME || 'node'; + + if (!RUNTIMES.includes(runtime as Runtime)) { + throw new Error(`Unknown RUNTIME "${runtime}", expected one of: ${RUNTIMES.join(', ')}`); + } + + return runtime as Runtime; +} diff --git a/dev-packages/test-utils/src/sourcemap-upload-utils.ts b/dev-packages/test-utils/src/sourcemap-upload-utils.ts index 88ee298824ac..559169f05bfc 100644 --- a/dev-packages/test-utils/src/sourcemap-upload-utils.ts +++ b/dev-packages/test-utils/src/sourcemap-upload-utils.ts @@ -58,6 +58,7 @@ export interface ArtifactBundleData { export interface ParsedSourcemap { [key: string]: unknown; version?: number; + file?: string; sources?: string[]; /** Absent when the generator drops all sources (Rollup's `sourcemapExcludeSources`), null per dropped entry. */ sourcesContent?: (string | null)[]; @@ -68,6 +69,8 @@ export interface ParsedSourcemap { export interface SourcemapEntry { url: string; bundleDir: string; + /** Lowercased `debug-id` manifest header. */ + debugId?: string; sourcemap: ParsedSourcemap; } @@ -168,6 +171,28 @@ export function getDebugIdPairs(bundles: ArtifactBundleData[]): DebugIdPair[] { return pairs; } +/** + * Returns ` → ` for each debug ID uploaded with maps that differ in `sources` or `mappings`. + * Byte-identical chunks share a debug ID, so the `file` field is not compared. + */ +export function findDebugIdsWithConflictingSourcemaps(sourcemaps: SourcemapEntry[]): string[] { + const mapsByDebugId = new Map>(); + + for (const { url, debugId, sourcemap } of sourcemaps) { + if (!debugId) continue; + + const mappingKey = JSON.stringify([getSourcemapSources(sourcemap), sourcemap.mappings]); + + const maps = mapsByDebugId.get(debugId) ?? new Map(); + maps.set(mappingKey, sourcemap.file ?? url); + mapsByDebugId.set(debugId, maps); + } + + return [...mapsByDebugId] + .filter(([, maps]) => maps.size > 1) + .map(([debugId, maps]) => `${debugId} → ${[...maps.values()].join(', ')}`); +} + /** * Read and parse all sourcemap files from artifact bundles. */ @@ -195,7 +220,8 @@ export function getSourcemaps(bundles: ArtifactBundleData[]): SourcemapEntry[] { continue; } - sourcemaps.push({ url: entry.url, bundleDir, sourcemap }); + const debugId = getManifestHeader(entry.headers, 'debug-id')?.toLowerCase(); + sourcemaps.push({ url: entry.url, bundleDir, debugId, sourcemap }); } } diff --git a/docs/repeated-init.md b/docs/repeated-init.md new file mode 100644 index 000000000000..a8f6445f0ffc --- /dev/null +++ b/docs/repeated-init.md @@ -0,0 +1,78 @@ +# Repeated `Sentry.init()` Calls + +This document records how `Sentry.init()` should act when an app calls it +more than once. Use it when you add or change an `init()` in any SDK. + +## The rule + +> One active client per `init()` target. The first `init()` wins. A later +> `init()` changes nothing, returns the active client, and warns. To +> reconfigure, call `close()` first. + +"Active" means a client is bound to the current scope and is not closed. +`Sentry.close()` closes the client and unbinds it, so a later `init()` sets +up a new client. Other scopes can still hold the closed client (for example, +when `close()` runs inside a request, or when code calls `client.close()` +directly), so a check for "already initialized" must not use `getClient()` +alone. + +A repeated `init()` is not supported. Until the next major version, most +SDKs still replace the client (see below). Do not depend on that. + +## Why + +When `init()` replaces a client, nothing closes the old one: + +- Buffered logs, metrics, spans, client reports, and flush timers stay on + the old client. +- `setupOnce` runs only for the first client, because the list of + installed integrations is global. The result mixes settings from both + calls. +- `initialScope` merges into the current scope on each call. + +"First wins" never gives a user half of one config and half of another. + +## Current behavior + +| Entry point | Repeated call | +| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `initAndBind` (browser and its wrappers, Deno), Node `_init`, Vercel Edge | Warns, then replaces the client. Returns the new client. | +| Next.js server, Remix server, Hono Node | Keeps the first client and returns it. Logs in debug mode only. | +| Hono Bun and Deno | Keeps the first client and returns it. Warns with its own text. | +| Nuxt server | Keeps the first client and returns it. Logs that a `--import` preload is no longer needed. | +| Cloudflare (default) | Keeps the first client of the isolate and returns it, unless that client is closed or closing. `cacheClient: false` makes a new client on each call, with no warning. | +| Next.js edge | Warns, then replaces the client. Returns `void`. | +| Next.js server and edge in a request of `withSentry` (Cloudflare) | Creates no client, because the client of `withSentry` handles the request. Returns `undefined`. | + +The shared warning lives in `warnIfClientIsActive()` in +`packages/core/src/sdk.ts`. Core exports it as +`_INTERNAL_warnIfClientIsActive` for SDKs that build their client without +`initAndBind`. + +A wrapper that expects a repeated call, such as a server bundle and a +`--import` preload that both run the config, keeps its own guard and +returns early. The shared warning then does not show. Guards use +`getActiveClient()` (exported as `_INTERNAL_getActiveClient`), which +returns the bound client only if it is not closed. + +## TODO(v12): Plan for the next major version + +1. Move the "first wins" guard into `initAndBind`, Node's `_init`, and + Vercel Edge's `init`. Remove the guards in each wrapper. +2. Switch browser to "first wins". For two apps on one page, point users + to separate clients that are not bound with `init()` (see #24883). +3. Make Next.js edge return the client. + +## Tests + +A test that calls `init()` again without a reset now prints the warning. +Reset between tests with one of these: + +- `getCurrentScope().setClient(undefined)`, or a helper that clears the + carrier (`getMainCarrier().__SENTRY__ = undefined`). +- `await Sentry.close()`. This also flushes, so it is slower. + +The warning goes through `consoleSandbox`, which calls the method stored +in `originalConsoleMethods`. To assert on it, replace +`originalConsoleMethods.warn` with a mock. A spy on `console.warn` misses +it once the console integration is set up. diff --git a/package.json b/package.json index f755d4692b92..4eb2f0af3612 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "madge": "8.0.0", "nodemon": "^3.1.10", "npm-run-all2": "^6.2.0", - "nx": "22.7.7", + "nx": "22.7.10", "oxfmt": "^0.70.0", "oxlint": "^1.75.0", "oxlint-tsgolint": "7", diff --git a/packages/angular/package.json b/packages/angular/package.json index bcccb1ea0a53..d90a603e09fc 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -23,7 +23,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 553da897f9e2..3a50bf232339 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -15,7 +15,6 @@ import { getClient, getCurrentScope, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startBrowserTracingNavigationSpan, startInactiveSpan, @@ -30,6 +29,7 @@ import { URL_TEMPLATE, SENTRY_DESCRIPTION, UI_COMPONENT_NAME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, ROUTER } from '@sentry/conventions/op'; import type { Integration, Span } from '@sentry/core'; @@ -90,7 +90,7 @@ export function _updateSpanAttributesForParametrizedUrl(route: string, url: stri const absoluteUrl = getAbsoluteUrl(url); span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${attributes[SENTRY_OP]}.angular`, + [SENTRY_ORIGIN]: `auto.${attributes[SENTRY_OP]}.angular`, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_FULL]: filterCollectedUrl(absoluteUrl), [URL_PATH]: parseStringToURLObject(absoluteUrl)?.pathname, @@ -133,7 +133,7 @@ export class TraceService implements OnDestroy { // is only known on `ResolveEnd`, which updates the span name then. name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : strippedUrl, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.angular', + [SENTRY_ORIGIN]: 'auto.navigation.angular', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, @@ -157,7 +157,7 @@ export class TraceService implements OnDestroy { name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : `${navigationEvent.url}`, attributes: { [SENTRY_OP]: ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular', + [SENTRY_ORIGIN]: 'auto.ui.angular', [URL_FULL]: strippedUrl, ...(navigationEvent.navigationTrigger && { navigationTrigger: navigationEvent.navigationTrigger, @@ -324,7 +324,7 @@ export class TraceDirective implements OnInit, AfterViewInit { name: hasSpanStreaming ? componentName : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', [UI_COMPONENT_NAME]: componentName, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -382,7 +382,7 @@ export function TraceClass(options?: TraceClassOptions): ClassDecorator { name: hasSpanStreaming ? componentName || UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -440,7 +440,7 @@ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { startTime: now, attributes: { [SENTRY_OP]: FUNCTION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', // override description inference by Relay to preserve the original (transaction-based) description. // sentry-conventions can't map the special case with the angle brackets. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), diff --git a/packages/angular/test/sdk.test.ts b/packages/angular/test/sdk.test.ts index 25beb721a448..4d6821e5642c 100644 --- a/packages/angular/test/sdk.test.ts +++ b/packages/angular/test/sdk.test.ts @@ -1,8 +1,12 @@ import * as SentryBrowser from '@sentry/browser'; -import { vi } from 'vitest'; +import { afterEach, vi } from 'vitest'; import { getDefaultIntegrations, init } from '../src/sdk'; describe('init', () => { + afterEach(() => { + SentryBrowser.getCurrentScope().setClient(undefined); + }); + it('sets the Angular version (if available) in the global scope', () => { const setContextSpy = vi.spyOn(SentryBrowser, 'setContext'); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 13acbc9d8622..769882727288 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,10 +1,16 @@ import { ElementRef } from '@angular/core'; import type { ActivatedRouteSnapshot } from '@angular/router'; -import { getMainCarrier, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; -import { describe, it } from 'vitest'; +import { getCurrentScope, getMainCarrier, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; +import { beforeEach, describe, it } from 'vitest'; import { browserTracingIntegration, init, TraceClass, TraceDirective } from '../src/index'; import { _updateSpanAttributesForParametrizedUrl, getParameterizedRouteFromSnapshot } from '../src/tracing'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from 'vitest'; describe('browserTracingIntegration', () => { @@ -68,6 +74,10 @@ describe('Angular Tracing', () => { }); describe('TraceService', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('change the span name to route name if the the source is `url`', async () => { init({ integrations: [browserTracingIntegration()] }); @@ -81,7 +91,7 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.undefined.angular', + [SENTRY_ORIGIN]: 'auto.undefined.angular', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: route, // URL_FULL is resolved against jsdom's http://localhost origin @@ -106,7 +116,7 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SENTRY_SEGMENT_NAME_SOURCE]: 'sample-source', }, name: 'initial-span-name', diff --git a/packages/astro/package.json b/packages/astro/package.json index 84578b3f1ec7..80f457529f6d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,7 +53,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/astro/src/client/browserTracingIntegration.ts b/packages/astro/src/client/browserTracingIntegration.ts index 86e8a009e0cd..80545c379b2e 100644 --- a/packages/astro/src/client/browserTracingIntegration.ts +++ b/packages/astro/src/client/browserTracingIntegration.ts @@ -4,14 +4,9 @@ import { WINDOW, } from '@sentry/browser'; import type { Client, Integration, TransactionSource } from '@sentry/core'; -import { - debug, - hasSpanStreamingEnabled, - PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { debug, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; /** * Returns the value of a meta-tag @@ -44,7 +39,7 @@ export function browserTracingIntegration( name, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro', + [SENTRY_ORIGIN]: 'auto.pageload.astro', ...(source === 'route' && { [URL_TEMPLATE]: name }), }, }); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index b67a20a788f8..ed0b1b540bb4 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -120,7 +120,9 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 4116085ffb41..2e88b7fc160a 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -8,6 +8,7 @@ import { URL_FULL, URL_PATH, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; @@ -34,7 +35,6 @@ import { getCurrentScope, getTraceMetaTags, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startSpan, winterCGHeadersToDict, @@ -148,7 +148,7 @@ async function enhanceHttpServerSpan(ctx: APIContext, next: MiddlewareNext, root rootSpan.setAttributes({ // This is here for backwards compatibility, we used to set this here before method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + [SENTRY_ORIGIN]: 'auto.http.astro', }); if (parametrizedRoute) { @@ -220,7 +220,7 @@ async function instrumentRequestStartHttpServerSpan( const attributes: SpanAttributes = { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + [SENTRY_ORIGIN]: 'auto.http.astro', [SENTRY_SEGMENT_NAME_SOURCE]: source, [HTTP_REQUEST_METHOD]: method, // This is here for backwards compatibility, we used to set this here before diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 041034f9164e..25d81333f194 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -98,6 +98,7 @@ describe('Sentry client SDK', () => { expect.objectContaining({ routeProvider: expect.objectContaining({ resolveRoute: expect.any(Function) }) }), ); + getMainCarrier().__SENTRY__ = undefined; const routeProvider = { resolveRoute: () => '/custom', resolveCurrentRoute: () => '/custom' }; init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', routeProvider }); expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider })); diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index e6693785102d..5060f5316552 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -56,7 +56,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 5d0c37614a3e..6f17bcc02c0c 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -95,7 +95,9 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 50da891bf853..c7fd27419081 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -34,8 +34,8 @@ }, "dependencies": { "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", - "web-vitals": "^6.1.1" + "@sentry/conventions": "^0.26.0", + "web-vitals": "^6.2.3" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 0bb5302b12f5..402cb6bac8cc 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -119,5 +119,15 @@ export function fetch(...rest: Parameters): ReturnType): ReturnType { - return getNativeImplementation('setTimeout')(...rest); + try { + return getNativeImplementation('setTimeout')(...rest); + } catch (e) { + // The implementation retrieved from the (since removed) sandbox iframe may throw when invoked, + // e.g. Firefox throws `NS_ERROR_NOT_INITIALIZED` for timers of a detached window. + // In this case, fall back to (and cache) the window's own setTimeout. + DEBUG_BUILD && debug.warn('Could not use native setTimeout implementation, falling back to window.setTimeout: ', e); + const fallback = WINDOW.setTimeout.bind(WINDOW) as typeof WINDOW.setTimeout; + cachedImplementations.setTimeout = fallback; + return fallback(...rest); + } } diff --git a/packages/browser-utils/src/performance/entries.ts b/packages/browser-utils/src/performance/entries.ts index f28e60f1cb3e..ce03b8405d7c 100644 --- a/packages/browser-utils/src/performance/entries.ts +++ b/packages/browser-utils/src/performance/entries.ts @@ -6,7 +6,6 @@ import { getActiveSpan, parseUrl, RESOURCE_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, spanToJSON, filterCollectedUrl, @@ -28,6 +27,7 @@ import { URL_DOMAIN, URL_FULL, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { BROWSER_CACHE, @@ -123,7 +123,7 @@ export function startTrackingLongTasks(): void { name: UI_LONG_TASK_SPAN_NAME_FALLBACK, op: UI_LONG_TASK, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }); } @@ -164,7 +164,7 @@ export function startTrackingLongAnimationFrames(): void { const duration = msToSec(entry.duration); const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }; const initialScript = entry.scripts[0]; @@ -291,7 +291,7 @@ export function _addPaintSpan( name: entry.name, attributes: { [SENTRY_OP]: BROWSER_PAINT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', [BROWSER_PAINT_TYPE]: entry.name, }, }); @@ -370,7 +370,7 @@ function _addPerformanceNavigationTiming( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[op] : entry.name, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: filterCollectedUrl(entry.name), ...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}), }, @@ -411,7 +411,7 @@ function _addRequest( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_REQUEST] : entry.name, attributes: { [SENTRY_OP]: BROWSER_REQUEST, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: url, }, }); @@ -420,7 +420,7 @@ function _addRequest( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_RESPONSE] : entry.name, attributes: { [SENTRY_OP]: BROWSER_RESPONSE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: url, }, }); @@ -453,7 +453,7 @@ export function _addResourceSpans( } const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', }; const parsedUrl = parseUrl(resourceUrl); diff --git a/packages/browser-utils/src/web-vitals/emitSpan.ts b/packages/browser-utils/src/web-vitals/emitSpan.ts index ba56bf783f57..5e25931f4bf8 100644 --- a/packages/browser-utils/src/web-vitals/emitSpan.ts +++ b/packages/browser-utils/src/web-vitals/emitSpan.ts @@ -1,12 +1,5 @@ import type { Integration, Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - getCurrentScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, -} from '@sentry/core'; +import { getClient, getCurrentScope, getRootSpan, spanToJSON } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core/browser'; import { SENTRY_EXCLUSIVE_TIME, @@ -16,6 +9,8 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRANSACTION, USER_AGENT_ORIGINAL, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { WINDOW } from '../types'; import type { MetricNavigationType } from '../instrumentation/performanceObserver'; @@ -91,8 +86,8 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { const segmentName = segmentSpan ? spanToJSON(segmentSpan).name : getCurrentScope().getScopeData().transactionName; const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_ORIGIN]: origin, + [SENTRY_OP]: op, [SENTRY_EXCLUSIVE_TIME]: 0, [`browser.web_vital.${metricName}.value`]: value, // oxlint-disable-next-line typescript-eslint/no-deprecated @@ -103,7 +98,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { ...passedAttributes, }; - if (parentSpan && spanToJSON(parentSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') { + if (parentSpan && spanToJSON(parentSpan).attributes[SENTRY_OP] === 'pageload') { // for LCP and CLS, we collect the pageload span id as an attribute attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId; } diff --git a/packages/browser-utils/src/web-vitals/softNavs.ts b/packages/browser-utils/src/web-vitals/softNavs.ts index addab70c50b0..9c7c03560df5 100644 --- a/packages/browser-utils/src/web-vitals/softNavs.ts +++ b/packages/browser-utils/src/web-vitals/softNavs.ts @@ -1,6 +1,6 @@ import type { Client, Span } from '@sentry/core'; -import { debug, LRUMap, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; -import { BROWSER_NAVIGATION_ID } from '@sentry/conventions/attributes'; +import { debug, LRUMap, spanToJSON } from '@sentry/core'; +import { BROWSER_NAVIGATION_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../debug-build'; import type { PerformanceSoftNavigation } from '../instrumentation/performanceObserver'; import { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver'; @@ -113,7 +113,7 @@ export function startSoftNavigationCorrelation(client: Client): void { WINDOW.addEventListener('keydown', onInteraction, { capture: true, passive: true }); client.on('spanStart', span => { - if (spanToJSON(span).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'navigation') { + if (spanToJSON(span).attributes?.[SENTRY_OP] !== 'navigation') { return; } diff --git a/packages/browser-utils/src/web-vitals/spans.ts b/packages/browser-utils/src/web-vitals/spans.ts index 69bee44f19a3..0e60188b456f 100644 --- a/packages/browser-utils/src/web-vitals/spans.ts +++ b/packages/browser-utils/src/web-vitals/spans.ts @@ -7,7 +7,6 @@ import { getClient, getRootSpan, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, timestampInSeconds, UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK, @@ -35,6 +34,7 @@ import { BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, BROWSER_WEB_VITAL_INP_TARGET, UI_COMPONENT_NAME, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { _emitWebVitalSpan } from './emitSpan'; import { isValidLcpMetric } from './lcp'; @@ -96,10 +96,7 @@ function trackWebVitalPerNavigation( // `back-forward-cache` navigation type, so matching on that alone lets the first of them replace the // navigation span, and every later vital then hangs off a sibling vital instead. const attributes = spanToJSON(span).attributes; - if ( - attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === NAVIGATION && - attributes[BROWSER_NAVIGATION_TYPE] === 'back-forward-cache' - ) { + if (attributes?.[SENTRY_OP] === NAVIGATION && attributes[BROWSER_NAVIGATION_TYPE] === 'back-forward-cache') { bfcacheNavigationSpan = span; } }); diff --git a/packages/browser-utils/test/getNativeImplementation.test.ts b/packages/browser-utils/test/getNativeImplementation.test.ts new file mode 100644 index 000000000000..cb7dc59011e0 --- /dev/null +++ b/packages/browser-utils/test/getNativeImplementation.test.ts @@ -0,0 +1,53 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { clearCachedImplementation, setTimeout as sentrySetTimeout } from '../src/getNativeImplementation'; +import { WINDOW } from '../src/types'; + +describe('setTimeout', () => { + const originalSetTimeout = WINDOW.setTimeout; + + beforeEach(() => { + clearCachedImplementation('setTimeout'); + }); + + afterEach(() => { + WINDOW.setTimeout = originalSetTimeout; + clearCachedImplementation('setTimeout'); + vi.restoreAllMocks(); + }); + + it('falls back to window.setTimeout if the sandboxed iframe implementation throws', async () => { + // Simulate a wrapped (non-native) window.setTimeout, e.g. by a browser extension + const wrappedSetTimeout = vi.fn((...args: Parameters) => originalSetTimeout(...args)); + WINDOW.setTimeout = wrappedSetTimeout as unknown as typeof WINDOW.setTimeout; + + // Simulate Firefox throwing `NS_ERROR_NOT_INITIALIZED` when calling setTimeout of a detached iframe window + const detachedSetTimeout = vi.fn(() => { + const error = new Error(''); + error.name = 'NS_ERROR_NOT_INITIALIZED'; + throw error; + }); + const fakeIframe = { hidden: false, contentWindow: { setTimeout: detachedSetTimeout } }; + + const document = WINDOW.document as Document; + vi.spyOn(document, 'createElement').mockReturnValue(fakeIframe as unknown as HTMLElement); + vi.spyOn(document.head, 'appendChild').mockImplementation(node => node); + vi.spyOn(document.head, 'removeChild').mockImplementation(node => node); + + const callback = vi.fn(); + + expect(() => sentrySetTimeout(callback, 0)).not.toThrow(); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(1); + + // Subsequent calls use the cached fallback and do not hit the broken implementation again + sentrySetTimeout(callback, 0); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(2); + + await vi.waitFor(() => expect(callback).toHaveBeenCalledTimes(2)); + }); +}); diff --git a/packages/browser-utils/test/performance/browserMetrics.test.ts b/packages/browser-utils/test/performance/browserMetrics.test.ts index d01d7b3c779a..f9dcccabb0b4 100644 --- a/packages/browser-utils/test/performance/browserMetrics.test.ts +++ b/packages/browser-utils/test/performance/browserMetrics.test.ts @@ -1,13 +1,6 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SentrySpan, - setCurrentClient, - spanToJSON, -} from '@sentry/core'; +import { getClient, getMainCarrier, SentrySpan, setCurrentClient, spanToJSON } from '@sentry/core'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { _addNavigationSpans, @@ -246,8 +239,8 @@ describe('_addResourceSpans', () => { start_timestamp: timeOrigin + startTime, end_timestamp: timeOrigin + startTime + duration, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.css', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, ['http.response.size']: entry.transferSize, @@ -356,7 +349,7 @@ describe('_addResourceSpans', () => { _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465); expect(spans).toHaveLength(i + 1); - expect(spanToJSON(spans[i]!).attributes).toEqual(expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op })); + expect(spanToJSON(spans[i]!).attributes).toEqual(expect.objectContaining({ [SENTRY_OP]: op })); } }); @@ -429,8 +422,8 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.css', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, ['http.response.size']: entry.transferSize, @@ -468,8 +461,8 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.css', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', 'http.request.same_origin': true, @@ -520,8 +513,8 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_OP]: 'resource.css', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', 'http.request.same_origin': true, @@ -820,7 +813,7 @@ describe('_addNavigationSpans', () => { // `unloadEventStart`/`End` are 0 in the shared entry, so that span is never created. _addNavigationSpans(pageloadSpan, { ...entry, unloadEventStart: 1, unloadEventEnd: 2 }, 999, true); - const spanJson = spans.map(spanToJSON).find(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op); + const spanJson = spans.map(spanToJSON).find(span => span.attributes[SENTRY_OP] === op); expect(spanJson?.name).toBe(expectedName); expect(spanJson?.attributes['url.full']).toBe('https://santry.com/test?q=secret#frag'); diff --git a/packages/browser/package.json b/packages/browser/package.json index bbba7f804b41..6f806161aafd 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@sentry/browser-utils": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/feedback": "11.4.0", "@sentry/replay": "11.4.0", "@sentry/replay-canvas": "11.4.0", diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 6708e12549ba..48b09d3c1acc 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -78,7 +78,9 @@ export { } from '@sentry/core'; export { + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; diff --git a/packages/browser/src/integrations/fetchStreamPerformance.ts b/packages/browser/src/integrations/fetchStreamPerformance.ts index 2a5c0dc9c103..c021c959e883 100644 --- a/packages/browser/src/integrations/fetchStreamPerformance.ts +++ b/packages/browser/src/integrations/fetchStreamPerformance.ts @@ -1,4 +1,4 @@ -import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_DOMAIN, URL_FULL, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_CLIENT_STREAM } from '@sentry/conventions/op'; import type { IntegrationFn, Span } from '@sentry/core'; import { @@ -9,7 +9,6 @@ import { getUrlDomain, hasSpanStreamingEnabled, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stripDataUrlContent, filterCollectedUrl, } from '@sentry/core'; @@ -93,7 +92,7 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => { [HTTP_REQUEST_METHOD]: method, type: 'fetch', [SENTRY_OP]: HTTP_CLIENT_STREAM, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.stream', + [SENTRY_ORIGIN]: 'auto.http.browser.stream', }, }); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 2320f12cba6f..dd5cdb351a3c 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -13,7 +13,6 @@ import { instrumentFetchRequest, matchesTracePropagationTargets, parseUrl, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, setHttpStatus, spanIsIgnored, @@ -44,6 +43,7 @@ import { URL_FRAGMENT, URL_FULL, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; @@ -393,7 +393,7 @@ function xhrCallback( [URL_FULL]: filterCollectedUrl(sanitizedFullUrl), [SERVER_ADDRESS]: host, [URL_DOMAIN]: domain, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + [SENTRY_ORIGIN]: 'auto.http.browser', [SENTRY_OP]: HTTP_CLIENT, [URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(parsedUrl?.search)), [URL_FRAGMENT]: getUrlFragment(parsedUrl?.hash), diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index e9836c90308b..c1c55dbca8ca 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -391,6 +391,10 @@ describe('SentryBrowser', () => { }); describe('SentryBrowser initialization', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('should use window.SENTRY_RELEASE to set release on initialization if available', () => { global.SENTRY_RELEASE = { id: 'foobar' }; init({ dsn }); diff --git a/packages/browser/test/integrations/httpcontext.test.ts b/packages/browser/test/integrations/httpcontext.test.ts index 524c8742bf3a..555b68dd1c05 100644 --- a/packages/browser/test/integrations/httpcontext.test.ts +++ b/packages/browser/test/integrations/httpcontext.test.ts @@ -1,5 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { describe, expect, it, vi } from 'vitest'; -import { httpContextIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../../src/exports'; +import { httpContextIntegration } from '../../src/exports'; import type { Event, StreamedSpanJSON } from '@sentry/core'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; import { BrowserClient } from '../../src/client'; @@ -29,7 +30,7 @@ describe('httpContextIntegration', () => { const span: Partial = { is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + [SENTRY_OP]: 'http.client', }, }; @@ -39,7 +40,7 @@ describe('httpContextIntegration', () => { expect(span.attributes).not.toHaveProperty('url.full'); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + [SENTRY_OP]: 'http.client', 'http.request.header.referer': ['https://example.com'], 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, @@ -52,7 +53,7 @@ describe('httpContextIntegration', () => { const span: Partial = { is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }, }; @@ -61,7 +62,7 @@ describe('httpContextIntegration', () => { integration.processSpan!(span as StreamedSpanJSON, browserClient); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'http.request.header.referer': ['https://example.com'], 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, @@ -74,14 +75,14 @@ describe('httpContextIntegration', () => { const span: Partial = { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click', + [SENTRY_OP]: 'ui.click', }, }; integration.processSpan!(span as StreamedSpanJSON, new BrowserClient(getDefaultBrowserClientOptions())); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click', + [SENTRY_OP]: 'ui.click', 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, }); @@ -161,13 +162,13 @@ describe('httpContextIntegration', () => { ); const span: Partial = { is_segment: true, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload' }, + attributes: { [SENTRY_OP]: 'pageload' }, }; httpContextIntegration().processSpan!(span as StreamedSpanJSON, client); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'sentry.is_localhost': false, 'url.full': 'https://example.com', }); @@ -179,13 +180,13 @@ describe('httpContextIntegration', () => { ); const span: Partial = { is_segment: true, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload' }, + attributes: { [SENTRY_OP]: 'pageload' }, }; httpContextIntegration().processSpan!(span as StreamedSpanJSON, client); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'url.full': 'https://example.com', 'http.request.header.referer': ['[Filtered]'], 'user_agent.original': USER_AGENT, diff --git a/packages/browser/test/profiling/UIProfiler.test.ts b/packages/browser/test/profiling/UIProfiler.test.ts index 38ac888f4d1e..a977693e4189 100644 --- a/packages/browser/test/profiling/UIProfiler.test.ts +++ b/packages/browser/test/profiling/UIProfiler.test.ts @@ -21,8 +21,7 @@ function getBaseOptionsForTraceLifecycle(sendMock: Mock, enableTracing = tr describe('Browser Profiling v2 trace lifecycle', () => { afterEach(async () => { - const client = Sentry.getClient(); - await client?.close(); + await Sentry.close(); // reset profiler constructor (window as any).Profiler = undefined; vi.restoreAllMocks(); @@ -568,7 +567,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { } // End Session 1 - await client?.close(); + await Sentry.close(); // Session 2 (new init simulates new user session) const send2 = vi.fn().mockResolvedValue(undefined); @@ -737,8 +736,7 @@ function getBaseOptionsForManualLifecycle(sendMock: Mock, enableTracing = t describe('Browser Profiling v2 manual lifecycle', () => { afterEach(async () => { - const client = Sentry.getClient(); - await client?.close(); + await Sentry.close(); // reset profiler constructor (window as any).Profiler = undefined; vi.restoreAllMocks(); diff --git a/packages/browser/test/profiling/integration.test.ts b/packages/browser/test/profiling/integration.test.ts index 0bd7e96d8b1d..88e68f5f03bb 100644 --- a/packages/browser/test/profiling/integration.test.ts +++ b/packages/browser/test/profiling/integration.test.ts @@ -7,14 +7,19 @@ import { getClient, spanToStaticSpanJSON, getActiveSpan, + getCurrentScope, browserTracingIntegration, browserProfilingIntegration, } from '../../src/index'; import { debug } from '@sentry/core'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { BrowserClient } from '../../src/index'; describe('BrowserProfilingIntegration', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('profiles an already active pageload span in trace lifecycle mode', async () => { const stopProfile = vi.fn().mockResolvedValue({ frames: [{ name: 'pageload_fn', line: 1, column: 1 }], diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 7b983af3635e..79c029c0eb32 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -34,6 +34,7 @@ export class MockIntegration implements Integration { describe('init', () => { afterEach(() => { + SentryCore.getCurrentScope().setClient(undefined); vi.restoreAllMocks(); }); diff --git a/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts b/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts index 27cc3089b309..d2b542a5392f 100644 --- a/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts +++ b/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts @@ -2,11 +2,11 @@ * @vitest-environment jsdom */ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { browserPerformanceTimeOrigin, getActiveSpan, getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient, spanToJSON, } from '@sentry/core'; @@ -66,7 +66,7 @@ describe('bfcache restore, then the first history navigation of the document', ( WINDOW.dispatchEvent(event); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), ); // Past the redirect threshold, so the navigation is judged on the pageload guard alone rather @@ -75,7 +75,7 @@ describe('bfcache restore, then the first history navigation of the document', ( WINDOW.history.pushState({}, '', '/after-restore'); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser' }), ); }); }); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index b61518c581f5..0061ec0dde23 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -9,8 +9,6 @@ import { getDynamicSamplingContextFromSpan, getMainCarrier, metrics, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, spanIsSampled, @@ -35,7 +33,13 @@ import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/linkedTrace import { bfcacheMetricsIntegration } from '../../src/integrations/bfcacheMetrics'; import * as webVitalsModule from '../../src/integrations/webVitals'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; const oldTextEncoder = global.window.TextEncoder; const oldTextDecoder = global.window.TextDecoder; @@ -176,8 +180,8 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -320,8 +324,8 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -355,8 +359,8 @@ describe('browserTracingIntegration', () => { name: 'Navigation', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/test', @@ -398,8 +402,8 @@ describe('browserTracingIntegration', () => { name: 'Navigation', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/test2', @@ -443,8 +447,8 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -478,8 +482,8 @@ describe('browserTracingIntegration', () => { expect(getSpanDescendants(span).map(span => spanToJSON(span))).toContainEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation.redirect', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_OP]: 'navigation.redirect', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [URL_FULL]: 'https://example.com/test', [URL_PATH]: '/test', }, @@ -539,8 +543,8 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', @@ -606,7 +610,7 @@ describe('browserTracingIntegration', () => { name: 'test span', attributes: { testy: 'yes', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }); @@ -615,8 +619,8 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', @@ -917,8 +921,8 @@ describe('browserTracingIntegration', () => { expect(span).toBeDefined(); expect(spanToJSON(span).attributes).toEqual( expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache', 'browser.navigation.type': 'back-forward-cache', }), ); @@ -955,7 +959,7 @@ describe('browserTracingIntegration', () => { firePageShow(true); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), ); }); @@ -1045,8 +1049,8 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), @@ -1093,7 +1097,7 @@ describe('browserTracingIntegration', () => { name: 'test span', attributes: { testy: 'yes', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }); @@ -1102,8 +1106,8 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', diff --git a/packages/bun/package.json b/packages/bun/package.json index 951bdbd8d3f1..6ac2e3fad1a4 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-utils": "11.4.0" }, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d5b1d11ee4e5..e96e052656cb 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -116,7 +116,9 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 080251c7dba3..1bd340703aec 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -12,15 +12,15 @@ import { HTTP_SPAN_NAME_FALLBACK, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, - startSpan, + SPAN_STATUS_ERROR, + startSpanManual, winterCGRequestToRequestData, withIsolationScope, filterCollectedUrl, filterCollectedUrlQuery, } from '@sentry/core'; -import { getClientIPAddress } from '@sentry/core/server'; +import { classifyResponseStreaming, getClientIPAddress } from '@sentry/core/server'; import type { Server, ServeOptions } from 'bun'; import { CLIENT_ADDRESS, @@ -36,8 +36,10 @@ import { URL_PORT, URL_QUERY, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; +import { monitorStream } from '../utils/streaming'; const INTEGRATION_NAME = 'BunServer' as const; @@ -294,7 +296,7 @@ function wrapRequestHandler( baggage: request.headers.get('baggage'), }, () => - startSpan( + startSpanManual( { attributes: { ...attributes, [SENTRY_OP]: HTTP_SERVER }, // With span streaming, span names have to be low cardinality, so we can't fall back to the URL path. @@ -303,7 +305,7 @@ function wrapRequestHandler( ? `${request.method} ${routeName}` : request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK, }, - async span => { + async (span, endSpan) => { try { const response = (await target.apply(thisArg, args)) as Response | undefined; if (response?.status) { @@ -319,14 +321,28 @@ function wrapRequestHandler( ); } } + if (response?.body && !response.body.locked && classifyResponseStreaming(response).isStreaming) { + const body = monitorStream(response.body, endSpan, error => { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { mechanism: { type: 'auto.http.bun.serve', handled: false } }); + }); + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + endSpan(); return response; } catch (e) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); captureException(e, { mechanism: { type: 'auto.http.bun.serve', handled: false, }, }); + endSpan(); throw e; } }, @@ -355,7 +371,7 @@ function getSpanAttributesFromParsedUrl( request: Request, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', + [SENTRY_ORIGIN]: 'auto.http.bun.serve', [HTTP_REQUEST_METHOD]: request.method || 'GET', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }; diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index bd936eae1adb..d1aba1b7aebc 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -2,6 +2,7 @@ import * as os from 'node:os'; import type { Integration, Options } from '@sentry/core'; import { applySdkMetadata, + conversationIdIntegration, dedupeIntegration, eventFiltersIntegration, functionToStringIntegration, @@ -51,6 +52,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { linkedErrorsIntegration(), dedupeIntegration(), requestDataIntegration(), + conversationIdIntegration(), // Native Wrappers consoleIntegration(), httpIntegration(), diff --git a/packages/bun/src/utils/streaming.ts b/packages/bun/src/utils/streaming.ts new file mode 100644 index 000000000000..d19466a6a035 --- /dev/null +++ b/packages/bun/src/utils/streaming.ts @@ -0,0 +1,40 @@ +/** Monitor the source without reading ahead or treating cancellation as a source failure. */ +export function monitorStream( + stream: ReadableStream, + onDone: () => void, + onError: (error: unknown) => void, +): ReadableStream { + const reader = stream.getReader(); + // A cancelled reader closes normally; only a source failure should be captured as an error. + void reader.closed.then(onDone, error => { + onError(error); + onDone(); + }); + + return new ReadableStream( + { + async pull(controller) { + try { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + reader.releaseLock(); + } else { + controller.enqueue(value); + } + } catch (error) { + controller.error(error); + reader.releaseLock(); + } + }, + async cancel(reason) { + try { + await reader.cancel(reason); + } finally { + reader.releaseLock(); + } + }, + }, + { highWaterMark: 0 }, + ); +} diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 2fced80fd67a..17983138e5ad 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -1,4 +1,5 @@ -import { type Integration } from '@sentry/core'; +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; +import { getCurrentScope, type Integration } from '@sentry/core'; import * as sentryServerUtils from '@sentry/server-utils'; import type { Mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; @@ -8,6 +9,9 @@ import { getDefaultIntegrationsWithoutPerformance, init, initWithoutDefaultIntegrations, + spanToJSON, + startSpan, + withScope, } from '../src'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -25,6 +29,7 @@ describe('init()', () => { let mockGetTracingIntegrations: Mock<() => Integration[]>; beforeEach(() => { + getCurrentScope().setClient(undefined); mockGetTracingIntegrations = spyOn(sentryServerUtils, 'getTracingIntegrations'); }); @@ -127,6 +132,19 @@ describe('init()', () => { expect(integrations?.map(({ name }) => name)).toContain('Some mock integration 4.1'); expect(integrations?.map(({ name }) => name)).toContain('Some mock integration 4.3'); }); + + it('adds the conversation id of the scope to gen_ai spans', () => { + // The default integrations alone must do it, without the tracing integrations. + mockGetTracingIntegrations.mockImplementation(() => []); + init({ dsn: PUBLIC_DSN, tracesSampleRate: 1 }); + + withScope(scope => { + scope.setConversationId('conversation-1'); + startSpan({ name: 'chat', op: 'gen_ai.chat' }, span => { + expect(spanToJSON(span).attributes[GEN_AI_CONVERSATION_ID]).toBe('conversation-1'); + }); + }); + }); }); describe('runtime', () => { diff --git a/packages/bun/test/integrations/bunHttpServer.test.ts b/packages/bun/test/integrations/bunHttpServer.test.ts index 48d5f255d821..9c679e69749f 100644 --- a/packages/bun/test/integrations/bunHttpServer.test.ts +++ b/packages/bun/test/integrations/bunHttpServer.test.ts @@ -1,6 +1,6 @@ import http from 'node:http'; -import { HTTP_REQUEST_METHOD } from '@sentry/conventions/attributes'; -import { getActiveSpan, getCurrentScope, getTraceData, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; +import { HTTP_REQUEST_METHOD, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, getCurrentScope, getTraceData, spanToJSON } from '@sentry/core'; import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { init } from '../../src'; @@ -22,6 +22,7 @@ function currentTraceId(): string | undefined { describe('Bun HTTP Server Integration', () => { beforeAll(() => { + getCurrentScope().setClient(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, @@ -51,7 +52,7 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span?.attributes[SENTRY_OP]).toBe('http.server'); // No router resolves a route here, so with span streaming the name is the request method. expect(span?.name).toBe('GET'); expect(span?.attributes['url.path']).toBe('/users'); @@ -82,7 +83,7 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span?.attributes[SENTRY_OP]).toBe('http.server'); expect(span?.name).toBe('QUERY'); expect(span?.attributes['url.path']).toBe('/search'); expect(span?.attributes[HTTP_REQUEST_METHOD]).toBe('QUERY'); diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 391b3c981c59..0a25b655a892 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -8,12 +8,16 @@ import { instrumentBunServe } from '../../src/integrations/bunserver'; describe('Bun Serve Integration', () => { const mockSpan = SentryCore.startInactiveSpan({ name: 'test span' }); const setAttributesSpy = spyOn(mockSpan, 'setAttributes'); + const setStatusSpy = spyOn(mockSpan, 'setStatus'); + const captureExceptionSpy = spyOn(SentryCore, 'captureException'); const continueTraceSpy = spyOn(SentryCore, 'continueTrace'); - const startSpanSpy = spyOn(SentryCore, 'startSpan').mockImplementation((_opts, cb) => { - return cb(mockSpan as unknown as SentryCore.Span); + const endSpanSpy = spyOn(mockSpan, 'end'); + const startSpanSpy = spyOn(SentryCore, 'startSpanManual').mockImplementation((_opts, cb) => { + return cb(mockSpan as unknown as SentryCore.Span, () => mockSpan.end()); }); const setupClient = (options?: BunOptions): void => { + SentryCore.getCurrentScope().setClient(undefined); init({ dsn: 'https://username@domain/123', defaultIntegrations: false, @@ -29,8 +33,11 @@ describe('Bun Serve Integration', () => { beforeEach(() => { startSpanSpy.mockClear(); + endSpanSpy.mockReset(); continueTraceSpy.mockClear(); setAttributesSpy.mockClear(); + setStatusSpy.mockClear(); + captureExceptionSpy.mockClear(); // Header attributes are only collected while a client is active, so every test sets up its own instead of // relying on one leaking in from whichever test file `bun test` happened to run first. setupClient(); @@ -46,6 +53,149 @@ describe('Bun Serve Integration', () => { port += 1; }); + test.each(['fetch', 'route'])( + 'keeps a streaming %s response span open until the last chunk is consumed', + async mode => { + let controller: ReadableStreamDefaultController; + const source = new ReadableStream({ + start(value) { + controller = value; + }, + }); + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const handler = () => + new Response(source, { status: 201, headers: { 'content-type': 'text/event-stream', 'x-stream': 'events' } }); + const server = Bun.serve({ + port, + ...(mode === 'fetch' ? { fetch: handler } : { routes: { '/events': { GET: handler } } }), + }); + try { + controller!.enqueue(new TextEncoder().encode('data: first\n\n')); + const response = await fetch(`http://localhost:${port}/events`); + const reader = response.body!.getReader(); + expect(response.status).toBe(201); + expect(response.headers.get('x-stream')).toBe('events'); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n'); + expect(endSpanSpy).toHaveBeenCalledTimes(0); + + controller!.enqueue(new TextEncoder().encode('data: last\n\n')); + controller!.close(); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: last\n\n'); + expect((await reader.read()).done).toBe(true); + await ended; + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }, + ); + + test.each(['client disconnected', new Error('client disconnected')])( + 'ends the span without capturing a cancelled stream: %s', + async reason => { + let cancelled: unknown; + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const source = new ReadableStream({ + cancel(reason) { + cancelled = reason; + }, + }); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'text/event-stream' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + const reader = response.body!.getReader(); + const pendingRead = reader.read(); + await reader.cancel(reason); + expect(await pendingRead).toEqual({ done: true, value: undefined }); + await ended; + expect(cancelled).toBe(reason); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(setStatusSpy).not.toHaveBeenCalledWith({ + code: SentryCore.SPAN_STATUS_ERROR, + message: 'internal_error', + }); + } finally { + await server.stop(true); + } + }, + ); + + test('preserves backpressure instead of reading ahead of the response consumer', async () => { + let pulls = 0; + const source = new ReadableStream( + { + pull(controller) { + pulls += 1; + controller.enqueue(new TextEncoder().encode(`data: ${pulls}\n\n`)); + }, + }, + { highWaterMark: 0 }, + ); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'text/event-stream' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + expect(pulls).toBe(0); + const reader = response.body!.getReader(); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: 1\n\n'); + expect(pulls).toBe(1); + expect(endSpanSpy).not.toHaveBeenCalled(); + + await reader.cancel(); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + } finally { + await server.stop(true); + } + }); + + test('captures a response stream error and marks its span as failed', async () => { + let controller: ReadableStreamDefaultController; + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const source = new ReadableStream({ + start(value) { + controller = value; + }, + }); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'application/x-ndjson' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + const error = new Error('stream interrupted'); + controller!.error(error); + await ended; + await expect(response.text()).rejects.toBe(error); + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.bun.serve', handled: false }, + }); + expect(setStatusSpy).toHaveBeenCalledWith({ code: SentryCore.SPAN_STATUS_ERROR, message: 'internal_error' }); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }); + + test('ends a non-streaming response span before the body is consumed', async () => { + const server = Bun.serve({ port, fetch: () => Response.json({ ok: true }) }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/status`)); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(await response.json()).toEqual({ ok: true }); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }); + test('generates a transaction around a request', async () => { const server = Bun.serve({ async fetch(_req) { @@ -371,6 +521,7 @@ describe('Bun Serve Integration', () => { expect(await initialResponse.text()).toBe('Initial handler'); expect(startSpanSpy).toHaveBeenCalledTimes(1); startSpanSpy.mockClear(); + endSpanSpy.mockReset(); // Reload server with new handler server.reload({ diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts index 0f414bc7495e..56814809d1ae 100644 --- a/packages/bun/test/sdk.test.ts +++ b/packages/bun/test/sdk.test.ts @@ -1,4 +1,5 @@ import type { BaseTransportOptions, Envelope, Event, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { describe, expect, test } from 'bun:test'; import type { NodeClient } from '../src/index'; import { init } from '../src/index'; @@ -26,6 +27,7 @@ describe('Bun SDK', () => { test('SDK works as expected', async () => { let client: NodeClient | undefined; + getCurrentScope().setClient(undefined); expect(() => { client = init(initOptions); }).not.toThrow(); diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index dd47fb96ebc7..1368b018d732 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/opentelemetry": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/cloudflare/src/baseSdk.ts b/packages/cloudflare/src/baseSdk.ts index 94bdb6c17be3..0375e7f94601 100644 --- a/packages/cloudflare/src/baseSdk.ts +++ b/packages/cloudflare/src/baseSdk.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { debug, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { _INTERNAL_isClientClosed, debug, getCurrentScope, setCurrentClient } from '@sentry/core'; import { consoleIntegration, conversationIdIntegration, @@ -94,7 +94,9 @@ export function initWithDefaultIntegrations( const cached = getCachedClient(); const cacheEnabled = options.cacheClient !== false; - if (cacheEnabled && cached) { + // A closed client drops all data. `close()` marks the client before it + // flushes, so this also skips a client that is still closing. + if (cacheEnabled && cached && !_INTERNAL_isClientClosed(cached)) { if (DEBUG_BUILD && cached.getOptions().dsn !== options.dsn) { debug.warn( '[Sentry] init() was called with a different DSN than the cached client of this isolate; the cached client keeps its DSN. Pass `cacheClient: false` for per-invocation options.', @@ -135,6 +137,12 @@ export function initWithDefaultIntegrations( } /*! rollup-include-development-only-end */ + if (!cacheEnabled) { + // `cacheClient: false` asks for a new client on each call, so replacing + // the bound client is expected here and must not warn. + getCurrentScope().setClient(undefined); + } + const client = initAndBind(CloudflareClient, clientOptions) as CloudflareClient; if (cacheEnabled && client) { diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index af2b4481553d..541b507a7d56 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -3,7 +3,7 @@ import { RPC } from '@sentry/conventions/op'; import { getDefaultIsolationScope, getIsolationScope, isObjectLike, startNewTrace } from '@sentry/core'; import type { DurableObject } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { getInstrumented, markAsInstrumented } from './instrument'; import { instrumentDurableObjectHandlers } from './instrumentations/instrumentDurableObjectHandlers'; @@ -53,7 +53,7 @@ export function constructInstrumentedDurableObject context: InstrumentedDurableObjectContext; frameworkManagedMethods: ReadonlySet; } { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const options = getFinalOptions(optionsCallback(env), env); // See InstrumentedDurableObjectContext — `ctx` is widened to `any` so the concrete // `DurableObjectState` type never enters the checker's relation graph in this module. diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 59f8713b181e..1d3aef44a03c 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -86,7 +86,9 @@ export { instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, diff --git a/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts b/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts index bf64488ad90c..88ea17db8eb5 100644 --- a/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts +++ b/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts @@ -1,6 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; -import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { debug, startSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { AGENT_SPAN_ORIGIN, type AgentInternals, getAgentAttributes, setAgentConversationId } from './types'; @@ -35,7 +35,7 @@ export function instrumentAgentCallableRpc(obj: AgentInternals): void { name: method, attributes: { [SENTRY_OP]: RPC, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AGENT_SPAN_ORIGIN, + [SENTRY_ORIGIN]: AGENT_SPAN_ORIGIN, ...getAgentAttributes(thisArg), }, }, diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts index 976412bae83d..e1d1a3fc1025 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts @@ -1,7 +1,7 @@ import type { DurableObjectStorage, SyncKvStorage, SqlStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { getClient, isThenable, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { getClient, isThenable, startSpan } from '@sentry/core'; import type { CloudflareClientOptions } from '../client'; import { getStorageKeys, targetsCloudflareInternalKey } from '../utils/internalStorageKey'; import { storeSpanContext } from '../utils/traceLinks'; @@ -75,7 +75,7 @@ export function instrumentDurableObjectStorage( name: `durable_object_storage_${methodName}`, attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': methodName, }, diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts index c8b94d14fef4..eb21b57a2fbf 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts @@ -1,7 +1,7 @@ import type { SyncKvStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { startSpan } from '@sentry/core'; const SYNC_KV_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list'] as const; @@ -28,7 +28,7 @@ export function instrumentDurableObjectSyncKvStorage(syncKv: SyncKvStorage): Syn name: `durable_object_storage_kv_${methodName}`, attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', // keeping the value as close as possible to the Cloudflare Worker KV instrumentation // https://github.com/cloudflare/workerd/blob/6b8b11787e2b2a800ab0edd0690bfab3857b0529/src/workerd/api/sync-kv.c%2B%2B#L19 'db.system.name': 'cloudflare-durable-object-sql', diff --git a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts index 706960562f88..276cd0be5f8d 100644 --- a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts @@ -1,21 +1,14 @@ import type { SqlStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; -import { - getActiveSpan, - getClient, - hasSpansEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanIsSampled, - startSpan, -} from '@sentry/core'; +import { getActiveSpan, getClient, hasSpansEnabled, spanIsSampled, startSpan } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import type { CloudflareClientOptions } from '../client'; import { mayTargetCloudflareInternalTable, targetsCloudflareInternalTable } from '../utils/internalSqlQuery'; const SPAN_ATTRIBUTES = { [SENTRY_OP]: DB_QUERY, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', }; diff --git a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts index 1071d79226c6..34c9d7562ed3 100644 --- a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts +++ b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts @@ -1,6 +1,6 @@ import type { RpcStub, WorkerEntrypoint } from 'cloudflare:workers'; import { RPC } from '@sentry/conventions/op'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../utils/asyncContextStrategy'; import type { CloudflareOptions } from '../client'; import { markAsInstrumented } from '../instrument'; import { getFinalOptions } from '../options'; @@ -151,7 +151,7 @@ export function instrumentWorkerEntrypoint< // Set up AsyncLocalStorage strategy ONCE at instrumentation time, not per-request // This is critical - calling this per-request would create a new AsyncLocalStorage // each time, breaking scope isolation for concurrent requests - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const InstrumentedClass = new Proxy(WorkerEntrypointClass, { construct(target, [ctx, env]) { diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index d8410ce32a45..42b01428816c 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -1,16 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { D1Database, D1DatabaseSession, D1PreparedStatement, D1Response } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import type { Span, SpanAttributes, StartSpanOptions } from '@sentry/core'; -import { - addBreadcrumb, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, -} from '@sentry/core'; +import { addBreadcrumb, getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import { ensureInstrumented } from '../../instrument'; @@ -148,7 +141,7 @@ function createStartSpanOptions(queryText: string | undefined, type: D1QueryType 'db.operation.name': type, 'db.query.text': queryText, 'db.query.summary': querySummary, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', }, }; } @@ -187,7 +180,7 @@ function instrumentBatch( 'db.operation.name': 'batch', 'db.query.text': queryText || undefined, 'db.operation.batch.size': statements.length, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', }, }, async () => { diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts index 285be9fc1313..b12f01474d6a 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts @@ -1,9 +1,9 @@ import type { MessageBatch } from '@cloudflare/workers-types'; import type { AnyExportedHandler } from '../../types'; import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS } from '@sentry/conventions/op'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, withIsolationScope } from '@sentry/core'; +import { captureException, startSpan, withIsolationScope } from '@sentry/core'; import type { CloudflareOptions } from '../../client'; import { flushAndDispose } from '../../flush'; import { ensureInstrumented } from '../../instrument'; @@ -45,7 +45,7 @@ function wrapQueueHandler( 'messaging.operation.name': 'process', 'messaging.batch.message_count': batch.messages.length, 'messaging.message.retry.count': batch.messages.reduce((acc, message) => acc + message.attempts - 1, 0), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue', + [SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts index d1b3de4ba107..3460ea32c633 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts @@ -1,7 +1,7 @@ import type { MessageSendRequest, Queue, QueueSendBatchOptions, QueueSendOptions } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { QUEUE_PUBLISH } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { startSpan } from '@sentry/core'; const ORIGIN = 'auto.faas.cloudflare.queue'; @@ -26,7 +26,7 @@ function startPublishSpan( ...(messageCount !== undefined && { 'messaging.batch.message_count': messageCount }), 'messaging.message.body.size': bodySize, [SENTRY_OP]: QUEUE_PUBLISH, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }, callback, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts b/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts index 7d6ad24ae3e2..a2ffdf72e50a 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts @@ -7,6 +7,7 @@ import { CLOUDFLARE_R2_REQUEST_PART_NUMBER, CLOUDFLARE_R2_REQUEST_PREFIX, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { OBJECT_DELETE, @@ -19,7 +20,7 @@ import { OBJECT_PUT, OBJECT_UPLOAD_PART, } from '@sentry/conventions/op'; -import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { isObjectLike, startSpan } from '@sentry/core'; const ORIGIN = 'auto.faas.cloudflare.r2'; @@ -67,7 +68,7 @@ function createSpanOptions(bindingName: string, r2Op: R2OperationKey, key?: stri ...(isR2ListOptions(key) && key.prefix !== undefined && { [CLOUDFLARE_R2_REQUEST_PREFIX]: key.prefix }), ...(isR2ListOptions(key) && key.delimiter !== undefined && { [CLOUDFLARE_R2_REQUEST_DELIMITER]: key.delimiter }), [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }; } diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index a90a236e4bf1..0d65340b306b 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -39,7 +39,7 @@ function getEnvVar>(env: unknown, varName: key * @returns The final options. */ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions { - if (typeof env !== 'object' || env === null) { + if (!isObjectLike(env)) { return userOptions; } diff --git a/packages/cloudflare/src/utils/asyncContextStrategy.ts b/packages/cloudflare/src/utils/asyncContextStrategy.ts new file mode 100644 index 000000000000..d4afe1369f4e --- /dev/null +++ b/packages/cloudflare/src/utils/asyncContextStrategy.ts @@ -0,0 +1,12 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; + +/** + * Installs the AsyncLocalStorage strategy unless an OpenTelemetry strategy is installed. Both use the same storage, + * so a switch during a request breaks it (#24603). + */ +export function setAsyncLocalStorageAsyncContextStrategyForWorker(): void { + if (!getAsyncContextStrategy(getMainCarrier()).withActiveSpan) { + setAsyncLocalStorageAsyncContextStrategy(); + } +} diff --git a/packages/cloudflare/src/withSentry.ts b/packages/cloudflare/src/withSentry.ts index d26cc30da60c..6c991af6ed38 100644 --- a/packages/cloudflare/src/withSentry.ts +++ b/packages/cloudflare/src/withSentry.ts @@ -1,4 +1,4 @@ -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import { instrumentExportedHandlerEmail } from './instrumentations/worker/instrumentEmail'; import { instrumentExportedHandlerFetch } from './instrumentations/worker/instrumentFetch'; import { instrumentExportedHandlerQueue } from './instrumentations/worker/instrumentQueue'; @@ -37,7 +37,7 @@ export function withSentry< return instrumentWorkerEntrypoint(optionsCallback as any, handler); } - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); try { // oxlint-disable-next-line typescript/no-explicit-any diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 62442cadf32d..ec065bca348e 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -1,15 +1,12 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + CODE_FUNCTION_NAME, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import type { PropagationContext, Scope } from '@sentry/core'; -import { - captureException, - flush, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - withIsolationScope, - withScope, -} from '@sentry/core'; +import { captureException, flush, getCurrentScope, startSpan, withIsolationScope, withScope } from '@sentry/core'; import type { WorkflowDelayDuration, WorkflowEntrypoint, @@ -22,7 +19,7 @@ import type { WorkflowStepRollbackOptions, WorkflowTimeoutDuration, } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { markAsInstrumented } from './instrument'; @@ -146,7 +143,7 @@ class WrappedWorkflowStep implements WorkflowStep { typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay, 'cloudflare.workflow.retries.limit': config?.retries?.limit, 'cloudflare.workflow.attempt': attempt, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow', + [SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, @@ -254,7 +251,7 @@ export function instrumentWorkflowWithSentry< get(obj, prop, receiver) { if (prop === 'run') { return async function (event: WorkflowEvent

, step: WorkflowStep): Promise { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); return withInvocationIsolationScope(async isolationScope => { const waitUntil = getOriginalWaitUntil(context).bind(context); diff --git a/packages/cloudflare/src/wrapRequestHandlerWithInit.ts b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts index 18f2ce63ddb2..3599b88f4243 100644 --- a/packages/cloudflare/src/wrapRequestHandlerWithInit.ts +++ b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts @@ -18,6 +18,7 @@ import { startSpanManual, winterCGHeadersToDict, } from '@sentry/core'; +import { classifyResponseStreaming } from '@sentry/core/server'; import { captureIncomingRequestBody } from './integrations/httpServer'; import { flushDeferredChannelEvents } from './orchestrion-deferred-channels'; import type { CloudflareClient, CloudflareOptions } from './client'; @@ -26,7 +27,6 @@ import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; import { getInvocationState, getInvocationWaitUntil } from './utils/invocationContext'; import { withInvocationIsolationScope } from './utils/invocationScope'; -import { classifyResponseStreaming } from './utils/streaming'; function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { // Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry) diff --git a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts index af5161e5bcde..8a17c0031aac 100644 --- a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { instrumentDurableObjectStorage } from '../src/instrumentations/instrumentDurableObjectStorage'; @@ -32,7 +32,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'get', }, @@ -53,7 +53,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'get', }, @@ -76,7 +76,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'put', }, @@ -97,7 +97,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'put', }, @@ -120,7 +120,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'delete', }, @@ -141,7 +141,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'delete', }, @@ -164,7 +164,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_list', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'list', }, @@ -187,7 +187,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_setAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'setAlarm', }, @@ -271,7 +271,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_getAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'getAlarm', }, @@ -292,7 +292,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_deleteAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'deleteAlarm', }, @@ -315,7 +315,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'SELECT', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT ?', @@ -439,7 +439,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'get', }, diff --git a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts index f6135c2f919c..b75cc0e0e245 100644 --- a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { instrumentDurableObjectSyncKvStorage } from '../src/instrumentations/instrumentDurableObjectSyncKvStorage'; @@ -21,7 +21,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'get', }, @@ -64,7 +64,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'put', }, @@ -96,7 +96,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'delete', }, @@ -129,7 +129,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_list', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'list', }, diff --git a/packages/cloudflare/test/instrumentSqlStorage.test.ts b/packages/cloudflare/test/instrumentSqlStorage.test.ts index 8d76316460f3..edf590f84a0f 100644 --- a/packages/cloudflare/test/instrumentSqlStorage.test.ts +++ b/packages/cloudflare/test/instrumentSqlStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import * as serverUtils from '@sentry/server-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -27,7 +27,7 @@ describe('instrumentSqlStorage', () => { name: 'SELECT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT * FROM users WHERE id = ?', @@ -51,7 +51,7 @@ describe('instrumentSqlStorage', () => { name: 'SELECT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT * FROM users WHERE name = ? AND age > ?', @@ -84,7 +84,7 @@ describe('instrumentSqlStorage', () => { name: 'INSERT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'INSERT INTO users (name, email) VALUES (?, ?)', @@ -281,7 +281,7 @@ describe('instrumentSqlStorage', () => { name: 'exec', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', }, diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 249da91d8cd1..ae30155c3c6a 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -1174,6 +1174,7 @@ describe('cached client (cacheClient)', () => { beforeEach(() => { vi.clearAllMocks(); _clearGlobalClientCache(); + SentryCore.getCurrentScope().setClient(undefined); }); // `init()` resolves defaults into the options object it is given, so each call @@ -1221,6 +1222,7 @@ describe('cached client (cacheClient)', () => { test('clears cache with _clearGlobalClientCache', async () => { const client1 = init(makeOptions()); _clearGlobalClientCache(); + SentryCore.getCurrentScope().setClient(undefined); const client2 = init(makeOptions()); expect(client2).not.toBe(client1); }); diff --git a/packages/cloudflare/test/sdk.test.ts b/packages/cloudflare/test/sdk.test.ts index 40dbca79fb84..d7820ecc343e 100644 --- a/packages/cloudflare/test/sdk.test.ts +++ b/packages/cloudflare/test/sdk.test.ts @@ -151,6 +151,45 @@ describe('cacheClient', () => { expect(getClient()).toBe(cached); }); + test('sets up a new cached client after close()', async () => { + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + } as const; + + const first = init({ ...options }); + await SentryCore.close(); + const second = init({ ...options }); + + expect(second).not.toBe(first); + expect(second?.getOptions().enabled).not.toBe(false); + expect(getClient()).toBe(second); + expect(init({ ...options })).toBe(second); + }); + + test('sets up a new cached client while the cached client is closing', async () => { + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + } as const; + + const first = init({ ...options }); + const closing = first!.close(); + const second = init({ ...options }); + await closing; + + expect(second).not.toBe(first); + expect(second?.getOptions().enabled).not.toBe(false); + expect(init({ ...options })).toBe(second); + }); + + test('keeps the cached client when a client that is not cached closes', async () => { + const cached = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + const uncached = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + + await uncached?.close(); + + expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(cached); + }); + test('caches a client without a DSN and reuses it', () => { // A disabled (DSN-less) client is still created once per isolate, not per invocation. const first = init({}); @@ -189,6 +228,26 @@ describe('cacheClient', () => { expect(flushSpy).toHaveBeenCalled(); }); + test('does not warn about a repeated init when cacheClient is disabled', () => { + const originalWarn = SentryCore.originalConsoleMethods.warn; + const warn = vi.fn(); + SentryCore.originalConsoleMethods.warn = warn; + + try { + const first = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + const second = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + + expect(second).not.toBe(first); + expect(warn).not.toHaveBeenCalled(); + } finally { + if (originalWarn) { + SentryCore.originalConsoleMethods.warn = originalWarn; + } else { + delete SentryCore.originalConsoleMethods.warn; + } + } + }); + test('does not flush eagerly when cacheClient is disabled', async () => { const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); @@ -343,6 +402,7 @@ describe('getDefaultIntegrations', () => { }); test('installs an integration registered after init via the module-injected event', async () => { + resetSdk(); const { mysqlIntegration } = await import('@sentry/server-utils'); const client = init({}); expect(client?.getIntegrationByName('Mysql')).toBeUndefined(); diff --git a/packages/cloudflare/test/utils/asyncContextStrategy.test.ts b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts new file mode 100644 index 000000000000..7f6346035a6c --- /dev/null +++ b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts @@ -0,0 +1,28 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import { afterEach, describe, expect, it } from 'vitest'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../../src/utils/asyncContextStrategy'; +import { resetSdk } from '../testUtils'; + +describe('setAsyncLocalStorageAsyncContextStrategyForWorker', () => { + afterEach(() => { + resetSdk(); + }); + + it('installs the AsyncLocalStorage strategy', () => { + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + const strategy = getAsyncContextStrategy(getMainCarrier()); + expect(strategy.getTracingChannelBinding).toBeDefined(); + expect(strategy.withActiveSpan).toBeUndefined(); + }); + + it('keeps an installed OpenTelemetry strategy', () => { + setOpenTelemetryContextAsyncContextStrategy(); + const openTelemetryStrategy = getAsyncContextStrategy(getMainCarrier()); + + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + expect(getAsyncContextStrategy(getMainCarrier())).toBe(openTelemetryStrategy); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index dd9e7ffda7e3..f8d6607e58a6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -94,6 +94,6 @@ "zod": "^3.24.1" }, "dependencies": { - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" } } diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 12d02b348bb2..9f77aceb9ed5 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -67,6 +67,20 @@ const DO_NOT_SEND_EVENT_SYMBOL = Symbol.for('SentryDoNotSendEventError'); // Default interval for flushing logs and metrics (5 seconds) const DEFAULT_FLUSH_INTERVAL = 5000; +// Clones of a scope keep their own reference to its client, so unbinding a +// closed client from one scope does not reach the others. The mark lives on +// the client under a `Symbol.for` key, so every copy of the SDK can read it. +const CLOSED_CLIENT_SYMBOL = Symbol.for('SentryClosedClient'); + +/** + * Whether `close()` was called on the given client. + * + * @hidden + */ +export function isClientClosed(client: Client): boolean { + return CLOSED_CLIENT_SYMBOL in client; +} + interface InternalError { message: string; [INTERNAL_ERROR_SYMBOL]: true; @@ -488,6 +502,7 @@ export abstract class Client { */ // @ts-expect-error - PromiseLike is a subset of Promise public async close(timeout?: number): PromiseLike { + Object.defineProperty(this, CLOSED_CLIENT_SYMBOL, { value: true }); const result = await this.flush(timeout); this.getOptions().enabled = false; this.emit('close'); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 12623aa224d9..19e9509ea3a0 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -195,7 +195,8 @@ export async function flush(timeout?: number): Promise { } /** - * Call `close()` on the current client, if there is one. See {@link Client.close}. + * Call `close()` on the current client, if there is one, and unbind it from the current scope. + * See {@link Client.close}. * * @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this * parameter will cause the client to wait until all events are sent before disabling itself. @@ -203,9 +204,15 @@ export async function flush(timeout?: number): Promise { * doesn't (or if there's no client defined). */ export async function close(timeout?: number): Promise { - const client = getClient(); + const scope = getCurrentScope(); + const client = scope.getClient(); if (client) { - return client.close(timeout); + const result = await client.close(timeout); + // Unbind the client, so a later `init()` sets up a new one. + if (scope.getClient() === client) { + scope.setClient(undefined); + } + return result; } DEBUG_BUILD && debug.warn('Cannot flush events and disable SDK. No client defined.'); return Promise.resolve(false); diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index aaf83ef25d82..76ef3892bea1 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -9,11 +9,11 @@ import { URL_FRAGMENT, URL_FULL, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import type { Client } from './client'; import { getClient } from './currentScopes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { setHttpStatus, SPAN_STATUS_ERROR, spanIsIgnored } from './tracing'; import { startInactiveSpan } from './tracing/trace'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; @@ -385,7 +385,7 @@ function getFetchSpanAttributes( type: 'fetch', // oxlint-disable-next-line typescript/no-deprecated [HTTP_REQUEST_METHOD]: method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SENTRY_ORIGIN]: spanOrigin, [SENTRY_OP]: HTTP_CLIENT, [URL_DOMAIN]: domain, }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20b52be30603..d3bfe71b64a4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -58,8 +58,13 @@ export { Scope } from './scope'; export type { CaptureContext, ScopeContext, ScopeData } from './scope'; export { notifyEventProcessors } from './eventProcessors'; export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint, SENTRY_API_VERSION } from './api'; -export { Client } from './client'; -export { initAndBind, setCurrentClient } from './sdk'; +export { Client, isClientClosed as _INTERNAL_isClientClosed } from './client'; +export { + getActiveClient as _INTERNAL_getActiveClient, + initAndBind, + setCurrentClient, + warnIfClientIsActive as _INTERNAL_warnIfClientIsActive, +} from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from './transports/multiplexed'; diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index 3ef8edbf0a49..8427c699e031 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -39,7 +39,6 @@ import { continueTrace, startSpanManual } from '../../tracing/trace'; import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { HTTP_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { safeMathRandom } from '../../utils/randomSafeContext'; import type { SpanStatus } from '../../types/spanStatus'; import { @@ -66,6 +65,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import { filterCollectedUrl, filterCollectedUrlQuery } from '../../utils/data-collection/filterCollectedUrl'; @@ -326,7 +326,7 @@ function buildServerSpanWrap( attributes: { // Sentry-specific attributes [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.server', + [SENTRY_ORIGIN]: 'auto.http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_KIND]: 'server', // Network attributes diff --git a/packages/core/src/integrations/mcp-server/spans.ts b/packages/core/src/integrations/mcp-server/spans.ts index 9159ce497122..b380da55e5a8 100644 --- a/packages/core/src/integrations/mcp-server/spans.ts +++ b/packages/core/src/integrations/mcp-server/spans.ts @@ -6,13 +6,12 @@ */ import { getClient } from '../../currentScopes'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MCP_NOTIFICATION_CLIENT_TO_SERVER, MCP_NOTIFICATION_SERVER_TO_CLIENT, MCP_SERVER, } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK, MCP_SERVER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; import { startSpan } from '../../tracing/trace'; @@ -72,7 +71,7 @@ function buildSentryAttributes(type: McpSpanConfig['type']): Record = {}) => { diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index e7f20432b91a..98cd8f894182 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,5 +1,6 @@ import type { Client } from './client'; -import { getCurrentScope } from './currentScopes'; +import { isClientClosed } from './client'; +import { getClient, getCurrentScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { ClientOptions } from './types/options'; import { consoleSandbox, debug } from './utils/debug-logger'; @@ -29,6 +30,8 @@ export function initAndBind( }); } } + warnIfClientIsActive(); + const scope = getCurrentScope(); scope.update(options.initialScope); @@ -38,6 +41,35 @@ export function initAndBind( return client; } +/** + * Returns the client bound to the current scope, unless it is closed. + * + * @hidden + */ +export function getActiveClient(): C | undefined { + const client = getClient(); + return client && !isClientClosed(client) ? client : undefined; +} + +/** + * Warns when `init()` runs while a client is still bound. The new client + * replaces it, but the old client stays alive, so the two can mix state. + * + * @hidden + */ +export function warnIfClientIsActive(): void { + if (getActiveClient()) { + consoleSandbox(() => { + // TODO(#24883): Point apps that share a page with another app to the + // isolated client helper, once that PR settles its name. + // eslint-disable-next-line no-console + console.warn( + '[Sentry] `Sentry.init()` was called more than once. The new client replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` only once, or call `await Sentry.close()` before you call it again.', + ); + }); + } +} + /** * Make the given client the current client. */ diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index f77eb63b8f56..c9e64cc6e781 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -16,11 +16,13 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE = 'sentry.prev /** * Use this attribute to represent the operation of a span. + * @deprecated Use `SENTRY_OP` from `@sentry/conventions/attributes` instead. */ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op'; /** * Use this attribute to represent the origin of a span. + * @deprecated Use `SENTRY_ORIGIN` from `@sentry/conventions/attributes` instead. */ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin'; diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index a34657db8d6a..e44bb9c2df8b 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -41,3 +41,5 @@ export type { } from './integrations/http/types'; export { createFetchIntegration } from './integrations/fetch'; export type { FetchIntegrationOptions } from './integrations/fetch'; +export { classifyResponseStreaming } from './utils/responseStreaming'; +export type { StreamingGuess } from './utils/responseStreaming'; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index abd27b52edf1..f0285f63eb27 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,12 +1,14 @@ /* eslint-disable max-lines */ import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; -import { SENTRY_PROFILE_ID, SENTRY_EXCLUSIVE_TIME, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; import { - SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '../semanticAttributes'; + SENTRY_PROFILE_ID, + SENTRY_EXCLUSIVE_TIME, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '../semanticAttributes'; import type { Client } from '../client'; import type { TransactionEvent } from '../types/event'; import type { SpanLink } from '../types/link'; @@ -102,8 +104,8 @@ export class SentrySpan implements Span { this._attributes = {}; this.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op, + [SENTRY_ORIGIN]: 'manual', + [SENTRY_OP]: spanContext.op, ...spanContext.attributes, }); @@ -270,14 +272,14 @@ export class SentrySpan implements Span { return { data: this._attributes, description: this._name, - op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + op: this._attributes[SENTRY_OP], parent_span_id: this._parentSpanId, span_id: this._spanId, start_timestamp: this._startTime, status: getStatusMessage(this._status), timestamp: this._endTime, trace_id: this._traceId, - origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + origin: this._attributes[SENTRY_ORIGIN] as SpanOrigin | undefined, profile_id: this._attributes[SENTRY_PROFILE_ID] as string | undefined, exclusive_time: this._attributes[SENTRY_EXCLUSIVE_TIME] as number | undefined, measurements: timedEventsToMeasurements(this._events), diff --git a/packages/core/src/tracing/spans/beforeSendSpan.ts b/packages/core/src/tracing/spans/beforeSendSpan.ts index b2fcd8261079..4acc765f19b2 100644 --- a/packages/core/src/tracing/spans/beforeSendSpan.ts +++ b/packages/core/src/tracing/spans/beforeSendSpan.ts @@ -2,7 +2,6 @@ import { DEBUG_BUILD } from '../../debug-build'; import type { BeforeSendStaticSpanCallback, BeforeSendStreamedSpanCallback } from '../../types/options'; import type { SpanJSON, StreamedSpanJSON } from '../../types/span'; import { addNonEnumerableProperty } from '../../utils/object'; -import { consoleSandbox } from '../../utils/debug-logger'; import { safeCallback } from '../../utils/safeCallback'; /** @@ -57,7 +56,6 @@ export function isStaticBeforeSendSpanCallback(callback: unknown): callback is B return !!callback && typeof callback === 'function' && '_static' in callback && !!callback._static; } -let hasShownSpanDropWarning = false; /** * Apply a user-provided beforeSendSpan callback to a span JSON. */ @@ -72,18 +70,5 @@ export function applyBeforeSendSpanCallback beforeSendSpan(span), () => span, ); - if (modifiedSpan) { - return modifiedSpan; - } - - if (!hasShownSpanDropWarning) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); - }); - hasShownSpanDropWarning = true; - } - return span; + return modifiedSpan || span; } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 1aa3818a9f1c..ff312712ee46 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -6,8 +6,8 @@ import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; import type { ClientOptions } from '../types/options'; import type { SentrySpanArguments, Span, SpanTimeInput } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; @@ -448,7 +448,7 @@ function parseSentrySpanArguments(options: StartSpanOptions): SentrySpanArgument // constructor only adds it after the sampling decision. An explicit `sentry.op` attribute wins. if (options.op) { initialCtx.attributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: options.op, + [SENTRY_OP]: options.op, ...options.attributes, }; } @@ -633,7 +633,7 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se return shouldIgnoreSpan( { description: spanArguments.name || '', - op: spanArguments.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] || spanArguments.op, + op: spanArguments.attributes?.[SENTRY_OP] || spanArguments.op, attributes: spanArguments.attributes, }, ignoreSpans, diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 55e428f60458..d84f6f16f17b 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -5,11 +5,11 @@ import { SENTRY_SEGMENT_NAME_SOURCE, TRPC_PROCEDURE_PATH, TRPC_PROCEDURE_TYPE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; import { getClient, withIsolationScope } from './currentScopes'; import { captureException } from './exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { startSpanManual } from './tracing/trace'; import { normalize } from './utils/normalize'; import { setNormalizationDepthOverrideHint } from './utils/normalizationHints'; @@ -97,7 +97,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { attributes: { [SENTRY_OP]: RPC, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc', + [SENTRY_ORIGIN]: 'auto.rpc.trpc', [RPC_SYSTEM_NAME]: 'trpc', [RPC_METHOD]: String(path), [TRPC_PROCEDURE_PATH]: String(path), diff --git a/packages/cloudflare/src/utils/streaming.ts b/packages/core/src/utils/responseStreaming.ts similarity index 100% rename from packages/cloudflare/src/utils/streaming.ts rename to packages/core/src/utils/responseStreaming.ts diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 2bf38cdb5864..e1615b9bf5c4 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -5,12 +5,13 @@ import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; import type { Scope } from '../scope'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_STATUS_MESSAGE } from '@sentry/conventions/attributes'; import { - SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '../semanticAttributes'; + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_STATUS_MESSAGE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { isStatusErrorMessageValid, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import { getCapturedScopesOnSpan } from '../tracing/utils'; @@ -190,8 +191,8 @@ export function spanToStaticSpanJSON(span: Span): SpanJSON { // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time timestamp: spanTimeInputToSeconds(endTime) || undefined, status: getStatusMessage(status), - op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], - origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + op: attributes[SENTRY_OP], + origin: attributes[SENTRY_ORIGIN] as SpanOrigin | undefined, links: convertSpanLinksForEnvelope(links), }; } diff --git a/packages/core/src/utils/url.ts b/packages/core/src/utils/url.ts index d7f45141a342..3a0a5fca186c 100644 --- a/packages/core/src/utils/url.ts +++ b/packages/core/src/utils/url.ts @@ -11,8 +11,8 @@ import { SENTRY_SEGMENT_NAME_SOURCE, URL_SCHEME, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { Client } from '../client'; import type { SpanAttributes } from '../types/span'; import type { TransactionSource } from '../types/transaction'; @@ -210,7 +210,7 @@ export function getHttpSpanDetailsFromUrlObject( client?: Client, ): [name: string, attributes: SpanAttributes] { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SENTRY_ORIGIN]: spanOrigin, }; let nameSource: TransactionSource = 'url'; diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index e2c3269c4929..c77bdce7d24c 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1711,51 +1711,6 @@ describe('Client', () => { expect(loggerLogSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.'); }); - test('does not discard span and warn when returning null from `beforeSendSpan', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - - // @ts-expect-error - intentionally violating the type signature here - const beforeSendSpan = withStaticSpan(vi.fn(() => null)); - - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); - const client = new TestClient(options); - - const transaction: Event = { - transaction: '/dogs/are/great', - type: 'transaction', - spans: [ - { - description: 'first span', - span_id: '9e15bf99fbe4bc80', - start_timestamp: 1591603196.637835, - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - data: {}, - status: 'ok', - }, - { - description: 'second span', - span_id: 'aa554c1f506b0783', - start_timestamp: 1591603196.637835, - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - data: {}, - status: 'ok', - }, - ], - }; - client.captureEvent(transaction); - - expect(beforeSendSpan).toHaveBeenCalledTimes(3); - const capturedEvent = TestClient.instance!.event!; - expect(capturedEvent.spans).toHaveLength(2); - expect(client['_outcomes']).toEqual({}); - - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); - consoleWarnSpy.mockRestore(); - }); - test("doesn't throw if the `beforeSendSpan` callback throws", () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error').mockImplementation(() => undefined); const error = new Error('beforeSendSpan is broken'); diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 8ca2f6cda39d..8388c1d86434 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,9 +1,11 @@ -import { beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest'; import type { Client } from '../../src/client'; -import { getCurrentScope } from '../../src/currentScopes'; +import { getClient, getCurrentScope, withScope } from '../../src/currentScopes'; +import { close } from '../../src/exports'; import { captureCheckIn } from '../../src/monitor'; import { installedIntegrations } from '../../src/integration'; -import { initAndBind, setCurrentClient } from '../../src/sdk'; +import { originalConsoleMethods } from '../../src/utils/debug-logger'; +import { getActiveClient, initAndBind, setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types/integration'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; @@ -90,6 +92,151 @@ describe('SDK', () => { const client = initAndBind(TestClient, options); expect(client).not.toBeUndefined(); }); + + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + test('does not warn on the first call', () => { + initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + test('warns and replaces the active client', () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + test('does not warn after close() in a child scope', async () => { + initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await withScope(() => close()); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + + test('does not warn after client.close()', async () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await first.close(); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + + test('does not warn after close()', async () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await close(); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + }); + }); +}); + +describe('getActiveClient', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + test('returns the bound client', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(getActiveClient()).toBe(client); + }); + + test('returns undefined when no client is bound', () => { + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a closed client that is still bound', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + + expect(getClient()).toBe(client); + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a client that is still closing', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + void client.close(); + + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a client that was closed twice', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + await client.close(); + + expect(getActiveClient()).toBeUndefined(); + }); + + test('sees a client closed through another copy of the SDK as closed', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + + vi.resetModules(); + const otherCopy = await import('../../src/client'); + + expect(otherCopy.isClientClosed(client)).toBe(true); + }); + + test('returns a client that was created with enabled: false', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enabled: false })); + + expect(getActiveClient()).toBe(client); + }); +}); + +describe('close', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + test('unbinds the closed client', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await close(); + + expect(client.getOptions().enabled).toBe(false); + expect(getClient()).toBeUndefined(); + }); + + test('does not unbind a client that was bound while closing', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const other = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + vi.spyOn(client, 'close').mockImplementation(async () => { + setCurrentClient(other); + return true; + }); + + await close(); + + expect(getClient()).toBe(other); }); }); diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 06f83bea6b0a..23cdaba20cf6 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -3,8 +3,6 @@ import type { Contexts, Span, StreamedSpanJSON } from '../../../../src'; import { captureSpan, debug, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanStreamingIntegration, startInactiveSpan, @@ -33,6 +31,8 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; describe('captureSpan', () => { @@ -77,11 +77,11 @@ describe('captureSpan', () => { type: 'string', value: 'stream', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -173,11 +173,11 @@ describe('captureSpan', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -269,11 +269,11 @@ describe('captureSpan', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -350,8 +350,8 @@ describe('captureSpan', () => { is_segment: true, attributes: { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.client' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SEGMENT_NAME]: { value: 'my-span', type: 'string' }, [SENTRY_SEGMENT_ID]: { value: span.spanContext().spanId, type: 'string' }, @@ -507,8 +507,7 @@ describe('captureSpan', () => { expect(beforeSendSpan).not.toHaveBeenCalled(); }); - it('logs a warning if the beforeSendSpan callback returns null', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + it('keeps the span if the beforeSendSpan callback returns null', () => { const beforeSendSpan = vi.fn(() => null as unknown as StreamedSpanJSON); const client = new TestClient( @@ -522,16 +521,17 @@ describe('captureSpan', () => { }), ); - const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } }); - span.end(); - - captureSpan(span, client); + const span = withScope(scope => { + scope.setClient(client); + const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } }); + span.end(); + return span; + }); - expect(consoleWarnSpy).toHaveBeenCalledWith( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); + const serialized = captureSpan(span, client); - consoleWarnSpy.mockRestore(); + expect(serialized.span_id).toBe(span.spanContext().spanId); + expect(serialized.name).toBe('my-span'); }); it('keeps the span and logs an error if the beforeSendSpan callback throws', () => { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index e36effb86317..5d7ab473b1b8 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -6,8 +6,6 @@ import { getMainCarrier, getTraceData, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setAsyncContextStrategy, setCurrentClient, spanToJSON, @@ -33,7 +31,7 @@ import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '. import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; import { SUPPRESS_TRACING_KEY } from '../../../src/tracing/constants'; import { resetGlobals } from '../../testutils'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; const enum Type { Sync = 'sync', @@ -110,14 +108,14 @@ describe('startSpan', () => { }); try { await startSpan({ name: 'GET users/[id]' }, span => { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); + span.setAttribute(SENTRY_OP, 'http.server'); return callback(); }); } catch { // } - expect(spanToJSON(_span!).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('http.server'); + expect(spanToJSON(_span!).attributes[SENTRY_OP]).toEqual('http.server'); }); it('creates a span with correct description', async () => { @@ -156,7 +154,7 @@ describe('startSpan', () => { try { await startSpan({ name: 'GET users/[id]' }, () => { return startSpan({ name: 'SELECT * from users' }, childSpan => { - childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); + childSpan.setAttribute(SENTRY_OP, 'db.query'); return callback(); }); }); @@ -168,7 +166,7 @@ describe('startSpan', () => { const spans = getSpanDescendants(_span!); expect(spans).toHaveLength(2); - expect(spanToJSON(spans[1]!).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('db.query'); + expect(spanToJSON(spans[1]!).attributes[SENTRY_OP]).toEqual('db.query'); }); it('correctly sets the span origin', async () => { @@ -180,7 +178,7 @@ describe('startSpan', () => { await startSpan( { name: 'GET users/[id]', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser' }, + attributes: { [SENTRY_ORIGIN]: 'auto.http.browser' }, }, () => { return callback(); diff --git a/packages/core/test/lib/utils/responseStreaming.test.ts b/packages/core/test/lib/utils/responseStreaming.test.ts new file mode 100644 index 000000000000..5ec7779bd7e8 --- /dev/null +++ b/packages/core/test/lib/utils/responseStreaming.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vitest'; +import { classifyResponseStreaming } from '../../../src/utils/responseStreaming'; + +describe('classifyResponseStreaming', () => { + test.each([ + ['text/event-stream; charset=utf-8', true], + ['application/x-ndjson', true], + ['application/ndjson', true], + ['application/stream+json', true], + ['TEXT/EVENT-STREAM', true], + ['text/plain', true], + ['text/html', false], + ['application/json', false], + ['application/octet-stream', false], + ])('classifies %s without reading the body', (contentType, isStreaming) => { + const response = new Response(new ReadableStream(), { headers: { 'content-type': contentType } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming }); + expect(response.bodyUsed).toBe(false); + expect(response.body?.locked).toBe(false); + }); + + test('treats plain text with a known length as non-streaming', () => { + const response = new Response('ready', { headers: { 'content-type': 'text/plain', 'content-length': '5' } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming: false }); + }); + + test('treats a bodyless response as non-streaming even with streaming headers', () => { + const response = new Response(null, { status: 204, headers: { 'content-type': 'text/event-stream' } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming: false }); + }); +}); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index e8ae4379b86f..032a09ce1667 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,11 +1,9 @@ -import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE } from '@sentry/conventions/attributes'; +import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, test } from 'vitest'; import { convertSpanLinksForEnvelope, getCurrentScope, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, SentrySpan, setCurrentClient, @@ -344,7 +342,7 @@ describe('spanToStaticSpanJSON', () => { traceId: 'abcd', startTimestamp: 123, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, }); span.setStatus({ code: SPAN_STATUS_OK }); @@ -400,8 +398,8 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', }, status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, }); @@ -417,8 +415,8 @@ describe('spanToStaticSpanJSON', () => { data: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', }, status: 'unknown_error', }); @@ -438,7 +436,7 @@ describe('spanToStaticSpanJSON', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', }, }); }); @@ -452,7 +450,7 @@ describe('spanToStaticSpanJSON', () => { traceId: 'abcd', startTimestamp: 123, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', attr1: 'value1', attr2: 2, attr3: true, @@ -488,8 +486,8 @@ describe('spanToStaticSpanJSON', () => { attr2: 2, attr3: true, attr4: [1, 2, 3], - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -586,8 +584,8 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -616,8 +614,8 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', [SENTRY_STATUS_MESSAGE]: 'unknown_error', }, links: [ @@ -683,8 +681,8 @@ describe('spanToStaticSpanJSON', () => { attr2: 2, attr3: true, attr4: [1, 2, 3], - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_OP]: 'test op', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -712,8 +710,8 @@ describe('spanToStaticSpanJSON', () => { attr2: { type: 'integer', value: 2 }, attr3: { type: 'boolean', value: true }, attr4: { type: 'array', value: [1, 2, 3] }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test op' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, + [SENTRY_OP]: { type: 'string', value: 'test op' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, }, links: [ { diff --git a/packages/deno/package.json b/packages/deno/package.json index 07d8ea02edb6..8e7676619553 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -28,7 +28,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0" diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 8db992d91e6a..090eca082909 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -81,7 +81,9 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, startSession, diff --git a/packages/deno/src/opentelemetry/tracer.ts b/packages/deno/src/opentelemetry/tracer.ts index 795f24eb9be4..ce3516feece4 100644 --- a/packages/deno/src/opentelemetry/tracer.ts +++ b/packages/deno/src/opentelemetry/tracer.ts @@ -1,6 +1,7 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpanManual } from '@sentry/core'; +import { isObjectLike, startInactiveSpan, startSpanManual } from '@sentry/core'; /** * Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans. @@ -34,7 +35,7 @@ class SentryDenoTracer implements Tracer { name, attributes: { ...options?.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', 'sentry.deno_tracer': true, }, }); @@ -57,14 +58,14 @@ class SentryDenoTracer implements Tracer { context?: unknown, fn?: F, ): ReturnType { - const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions; + const opts = (isObjectLike(options) ? options : {}) as SpanOptions; const spanOpts = { ...opts, name, attributes: { ...opts.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', 'sentry.deno_tracer': true, }, }; diff --git a/packages/deno/src/utils/streaming.ts b/packages/deno/src/utils/streaming.ts index 491131e6e093..ca6f778b749e 100644 --- a/packages/deno/src/utils/streaming.ts +++ b/packages/deno/src/utils/streaming.ts @@ -1,46 +1,5 @@ import type { Span } from '@sentry/core'; - -export type StreamingGuess = { - isStreaming: boolean; -}; - -/** - * Classifies a Response as streaming or non-streaming. - * - * Heuristics: - * - No body → not streaming - * - Known streaming Content-Types → streaming (SSE, NDJSON, JSON streaming) - * - text/plain without Content-Length → streaming (some AI APIs) - * - Otherwise → not streaming (conservative default, including HTML/SSR) - * - * We avoid probing the stream to prevent blocking on transform streams (like injectTraceMetaTags) - * or SSR streams that may not have data ready immediately. - */ -export function classifyResponseStreaming(res: Response): StreamingGuess { - if (!res.body) { - return { isStreaming: false }; - } - - const contentType = res.headers.get('content-type') ?? ''; - const contentLength = res.headers.get('content-length'); - - // Streaming: Known streaming content types - // - text/event-stream: Server-Sent Events (Vercel AI SDK, real-time APIs) - // - application/x-ndjson, application/ndjson: Newline-delimited JSON - // - application/stream+json: JSON streaming - // - text/plain (without Content-Length): Some AI APIs use this for streaming text - if ( - /^text\/event-stream\b/i.test(contentType) || - /^application\/(x-)?ndjson\b/i.test(contentType) || - /^application\/stream\+json\b/i.test(contentType) || - (/^text\/plain\b/i.test(contentType) && !contentLength) - ) { - return { isStreaming: true }; - } - - // Default: treat as non-streaming - return { isStreaming: false }; -} +import { classifyResponseStreaming } from '@sentry/core/server'; /** * Tee a stream, and end the provided span when the stream ends. diff --git a/packages/deno/test/sdk.test.ts b/packages/deno/test/sdk.test.ts index ad7152cb21ba..fe85e2ff9f45 100644 --- a/packages/deno/test/sdk.test.ts +++ b/packages/deno/test/sdk.test.ts @@ -1,9 +1,19 @@ import { assertNotEquals } from 'https://deno.land/std@0.202.0/assert/assert_not_equals.ts'; import { assertArrayIncludes } from 'https://deno.land/std@0.212.0/assert/assert_array_includes.ts'; -import { getDefaultIntegrations, init, spanStreamingIntegration } from '../build/esm/index.js'; +import { + getCurrentScope, + getDefaultIntegrations, + init as initSdk, + spanStreamingIntegration, +} from '../build/esm/index.js'; import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +function init(options) { + getCurrentScope().setClient(undefined); + return initSdk(options); +} + Deno.test('init() should return client', () => { assertNotEquals(init({}), undefined); }); diff --git a/packages/effect/package.json b/packages/effect/package.json index b6858b5636df..06be83c3a28a 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0" }, diff --git a/packages/effect/src/logger.ts b/packages/effect/src/logger.ts index f0a55b3bf20e..a5fc7fe6923d 100644 --- a/packages/effect/src/logger.ts +++ b/packages/effect/src/logger.ts @@ -1,9 +1,10 @@ -import { isObjectLike, logger as sentryLogger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { isObjectLike, logger as sentryLogger } from '@sentry/core'; import * as Logger from 'effect/Logger'; import type * as LogLevel from 'effect/LogLevel'; const LOG_ATTRIBUTES = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.log.effect', + [SENTRY_ORIGIN]: 'auto.log.effect', }; function getLogLevelTag(logLevel: LogLevel.LogLevel): LogLevel.LogLevel | 'Warning' { diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts index 2e72e3c61fc0..f25ce111b180 100644 --- a/packages/effect/src/tracer.ts +++ b/packages/effect/src/tracer.ts @@ -1,5 +1,5 @@ /* oxlint-disable max-lines */ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; import type { Span, StartSpanOptions } from '@sentry/core'; import { @@ -9,7 +9,6 @@ import { getCurrentScope, getDefaultCurrentScope, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startNewTrace, withActiveSpan, withScope, @@ -390,7 +389,7 @@ function createSentrySpan( // Setting these to `undefined` would strip the core defaults instead of leaving them in place. attributes: { ...(op && { [SENTRY_OP]: op }), - ...(origin && { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin }), + ...(origin && { [SENTRY_ORIGIN]: origin }), }, }, parent, diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts index a7aace022282..c1f4a767da93 100644 --- a/packages/effect/test/tracer.test.ts +++ b/packages/effect/test/tracer.test.ts @@ -1,7 +1,7 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; import * as sentryCoreBrowser from '@sentry/core/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { ServerRuntimeClient } from '@sentry/core/server'; import { Effect } from 'effect'; import * as Tracer from 'effect/Tracer'; @@ -201,10 +201,10 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('my-operation'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); - expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN); - expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_OP); + expect(attributes?.[SENTRY_ORIGIN]).toBeUndefined(); + expect(attributes?.[SENTRY_OP]).toBeUndefined(); + expect(attributes).not.toHaveProperty(SENTRY_ORIGIN); + expect(attributes).not.toHaveProperty(SENTRY_OP); }), ); @@ -212,8 +212,8 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('http.server GET /api/users'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect'); + expect(attributes?.[SENTRY_OP]).toBe('http.server'); }), ); @@ -221,8 +221,8 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('http.client GET https://api.example.com'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.client'); + expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect'); + expect(attributes?.[SENTRY_OP]).toBe('http.client'); }), ); diff --git a/packages/elysia/package.json b/packages/elysia/package.json index bf70f6fec0ec..1480039acf3c 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -34,7 +34,7 @@ "dependencies": { "@sentry/bun": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "elysia": "^1.4.0" diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index cf35f6de4eb8..17baec308ba1 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -95,7 +95,9 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/elysia/src/withElysia.ts b/packages/elysia/src/withElysia.ts index c68c192e3fa4..ee8ac91f8cb5 100644 --- a/packages/elysia/src/withElysia.ts +++ b/packages/elysia/src/withElysia.ts @@ -5,6 +5,7 @@ import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HANDLER, HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; @@ -18,8 +19,6 @@ import { getTraceData, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startInactiveSpan, startSpanManual, @@ -154,8 +153,8 @@ function instrumentLifecyclePhase( name: isStreamedRequestHandlerSpan ? context.route || REQUEST_HANDLER_SPAN_NAME_FALLBACK : phaseName, parentSpan: rootSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_OP]: op, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, }, }); @@ -169,8 +168,8 @@ function instrumentLifecyclePhase( name: isStreamedRequestHandlerSpan ? context.route || REQUEST_HANDLER_SPAN_NAME_FALLBACK : handlerName, parentSpan: phaseSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_OP]: op, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, // Streamed request handler spans are named after the route, so the // handler name has no other place to go. Anonymous handlers have no @@ -252,7 +251,7 @@ export function withElysia(app: T, options: ElysiaHandlerOp : `${request.method} ${new URL(request.url).pathname}`, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: filterCollectedUrl(request.url), [URL_PATH]: new URL(request.url).pathname, diff --git a/packages/ember/package.json b/packages/ember/package.json index 2498a3b6d1d1..8fc7337ceb9e 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -45,7 +45,7 @@ "@embroider/addon-shim": "^1.10.2", "@embroider/macros": "^1.20.2", "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "decorator-transforms": "^2.3.2" }, diff --git a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts index d598c41fa834..6f3df88fab75 100644 --- a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts +++ b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts @@ -5,7 +5,7 @@ import type { startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType, startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType, } from '@sentry/browser'; -import { getAbsoluteUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, WINDOW } from '@sentry/browser'; +import { getAbsoluteUrl, startInactiveSpan, WINDOW } from '@sentry/browser'; import { ROUTER_NAVIGATION_ROUTE_ID, SENTRY_SEGMENT_NAME_SOURCE, @@ -13,6 +13,7 @@ import { URL_FULL, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { ROUTER } from '@sentry/conventions/op'; import { @@ -77,7 +78,7 @@ export function instrumentEmberAppInstanceForPerformance( : url || WINDOW.location.pathname, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: routeInfo ? 'route' : 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember', + [SENTRY_ORIGIN]: 'auto.pageload.ember', ...(routeInfo?.name && { [ROUTER_NAVIGATION_ROUTE_ID]: routeInfo.name }), ...(url ? _getRouteUrlAttributes(client, url, routeInfo?.params) : {}), toRoute: routeInfo?.name, @@ -121,7 +122,7 @@ export function instrumentEmberAppInstanceForPerformance( name: transactionName, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember', + [SENTRY_ORIGIN]: 'auto.navigation.ember', ...(toRoute && { [ROUTER_NAVIGATION_ROUTE_ID]: toRoute }), ...urlAttributes, fromRoute, @@ -154,7 +155,7 @@ export function instrumentEmberAppInstanceForPerformance( transitionSpan = startInactiveSpan({ attributes: { [SENTRY_OP]: ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', }, // With span streaming, span names have to be low cardinality, and Ember gives us no route // template for the transition itself, so it's the fallback. diff --git a/packages/ember/src/utils/instrumentEmberGlobals.ts b/packages/ember/src/utils/instrumentEmberGlobals.ts index 498953a062f1..31c76d10af51 100644 --- a/packages/ember/src/utils/instrumentEmberGlobals.ts +++ b/packages/ember/src/utils/instrumentEmberGlobals.ts @@ -1,8 +1,8 @@ import { subscribe } from '@ember/instrumentation'; import { scheduleOnce } from '@ember/runloop'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_RESOLVE, UI_TASK } from '@sentry/conventions/op'; -import { getActiveSpan, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; +import { getActiveSpan, getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, @@ -101,7 +101,7 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number startInactiveSpan({ attributes: { [SENTRY_OP]: UI_TASK, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', 'ember.runloop.queue': queue, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -168,7 +168,7 @@ export function _processComponentRenderAfter( startTime: begin.now, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', [UI_COMPONENT_NAME]: name, }, onlyIfParent: true, @@ -253,7 +253,7 @@ function _instrumentInitialLoad(): void { name: hasSpanStreaming ? UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, startTime, diff --git a/packages/ember/src/utils/instrumentRoutePerformance.ts b/packages/ember/src/utils/instrumentRoutePerformance.ts index 1515c7ed3f90..f7c2bdd77eac 100644 --- a/packages/ember/src/utils/instrumentRoutePerformance.ts +++ b/packages/ember/src/utils/instrumentRoutePerformance.ts @@ -1,7 +1,7 @@ import { startSpan } from '@sentry/browser'; -import { CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled } from '@sentry/core'; import type Route from '@ember/routing/route'; @@ -43,7 +43,7 @@ export function instrumentRoutePerformance(BaseRoute return startSpan( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: hookName, ...(isStreaming && { [SENTRY_DESCRIPTION]: fullRouteName }), diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index 4c4ec2510b87..414250dbf4a0 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -50,6 +50,7 @@ interface DrawCommand { function drawRect(command: DrawCommand, ctx: CanvasRenderingContext2D, color: string): void { switch (command.type) { case 'highlight': { + ctx.save(); // creates a shadow around ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = 50; @@ -60,6 +61,7 @@ function drawRect(command: DrawCommand, ctx: CanvasRenderingContext2D, color: st // cut out the inside of the rectangle ctx.clearRect(command.x, command.y, command.w, command.h); + ctx.restore(); break; } @@ -229,8 +231,8 @@ export function ScreenshotEditorFactory({ const boundingRect = mouseRef.current.getBoundingClientRect(); const startingPoint: DrawCommand = { type: action, - x: e.offsetX / scaleFactor, - y: e.offsetY / scaleFactor, + x: (e.clientX - boundingRect.x) / scaleFactor, + y: (e.clientY - boundingRect.y) / scaleFactor, w: 0, h: 0, }; diff --git a/packages/feedback/test/core/mockSdk.ts b/packages/feedback/test/core/mockSdk.ts index 5e7a65b38212..a15667791f4c 100644 --- a/packages/feedback/test/core/mockSdk.ts +++ b/packages/feedback/test/core/mockSdk.ts @@ -1,4 +1,5 @@ import type { Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { vi } from 'vitest'; import type { TestClientOptions } from '../../src/core/TestClient'; import { getDefaultClientOptions, init } from '../../src/core/TestClient'; @@ -43,6 +44,8 @@ class MockTransport implements Transport { * */ export async function mockSdk({ sentryOptions }: MockSdkParams = {}): Promise { + getCurrentScope().setClient(undefined); + init({ ...getDefaultClientOptions(), dsn: 'https://dsn@ingest.f00.f00/1', diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 727a74f7b261..def490e4f5d9 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -36,7 +36,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts index e7b1f6c7e21b..5fae82f33b51 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts @@ -8,6 +8,7 @@ import { GCP_FUNCTION_CONTEXT_SOURCE, GCP_FUNCTION_CONTEXT_SPECVERSION, GCP_FUNCTION_CONTEXT_TIME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import { @@ -15,7 +16,6 @@ import { getClient, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; @@ -64,7 +64,7 @@ function _wrapCloudEventFunction( [FAAS_NAME]: functionName, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: context.type, [GCP_FUNCTION_CONTEXT_ID]: context.id, [GCP_FUNCTION_CONTEXT_SOURCE]: context.source, diff --git a/packages/google-cloud-serverless/src/gcpfunction/http.ts b/packages/google-cloud-serverless/src/gcpfunction/http.ts index a90cb8ad00fd..3775752497ef 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/http.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/http.ts @@ -5,6 +5,7 @@ import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import { @@ -14,7 +15,6 @@ import { hasSpanStreamingEnabled, httpRequestToRequestData, isString, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, setHttpStatus, stripUrlQueryAndFragment, @@ -73,7 +73,7 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial): [FAAS_NAME]: functionName, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: hasSpanStreaming ? 'component' : 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', // The method and path used to be the span name; they stay on the span so that // information survives the low-cardinality rename. [HTTP_REQUEST_METHOD]: reqMethod || undefined, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d789d83c91a8..eb4fa0495540 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -96,7 +96,9 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts b/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts index d41e1cb38ad7..e0cb9816f13f 100644 --- a/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts +++ b/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts @@ -1,7 +1,7 @@ -import { RPC_METHOD, RPC_SERVICE, RPC_SYSTEM_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { RPC_METHOD, RPC_SERVICE, RPC_SYSTEM_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { GRPC } from '@sentry/conventions/op'; import type { Client, IntegrationFn } from '@sentry/core'; -import { defineIntegration, fill, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { defineIntegration, fill, getClient } from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; import type { EventEmitter } from 'events'; @@ -106,7 +106,7 @@ export function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodNa onlyIfParent: true, attributes: { [SENTRY_OP]: GRPC, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + [SENTRY_ORIGIN]: 'auto.grpc.serverless', [RPC_SYSTEM_NAME]: 'grpc', [RPC_SERVICE]: serviceIdentifier, [RPC_METHOD]: methodName, diff --git a/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts b/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts index 91e99e031840..6359ed876333 100644 --- a/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts +++ b/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts @@ -1,5 +1,12 @@ import type * as common from '@google-cloud/common'; -import { HTTP_REQUEST_METHOD, SENTRY_OP, SERVER_ADDRESS, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { + HTTP_REQUEST_METHOD, + SENTRY_OP, + SERVER_ADDRESS, + URL_DOMAIN, + URL_FULL, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import type { Client, IntegrationFn } from '@sentry/core'; import { @@ -9,7 +16,6 @@ import { hasSpanStreamingEnabled, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, filterCollectedUrl, stripUrlQueryAndFragment, @@ -72,7 +78,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: httpMethod, [SERVER_ADDRESS]: serverAddress, [URL_DOMAIN]: serverAddress, diff --git a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts index 514a288f9531..a6bb52702d6a 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts @@ -8,11 +8,12 @@ import { GCP_FUNCTION_CONTEXT_SOURCE, GCP_FUNCTION_CONTEXT_SPECVERSION, GCP_FUNCTION_CONTEXT_TIME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { wrapCloudEventFunction } from '../../src/gcpfunction/cloud_events'; import type { CloudEventFunction, CloudEventFunctionWithCallback } from '../../src/gcpfunction/general'; @@ -94,7 +95,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -126,7 +127,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -159,7 +160,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -203,7 +204,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -247,7 +248,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -276,7 +277,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -319,7 +320,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', diff --git a/packages/google-cloud-serverless/test/gcpfunction/events.test.ts b/packages/google-cloud-serverless/test/gcpfunction/events.test.ts index 95d1b6972924..c69251441b54 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/events.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/events.test.ts @@ -7,11 +7,12 @@ import { GCP_FUNCTION_CONTEXT_EVENT_ID, GCP_FUNCTION_CONTEXT_RESOURCE, GCP_FUNCTION_CONTEXT_TIMESTAMP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client, Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { wrapEventFunction } from '../../src/gcpfunction/events'; import type { EventFunction, EventFunctionWithCallback } from '../../src/gcpfunction/general'; @@ -88,7 +89,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -116,7 +117,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -149,7 +150,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -181,7 +182,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -211,7 +212,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -239,7 +240,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -268,7 +269,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', diff --git a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts index 881fc6867bd6..a0704860c8bf 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts @@ -5,11 +5,12 @@ import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client, Integration } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; import type { HttpFunction, Request, Response } from '../../src/gcpfunction/general'; import { wrapHttpFunction } from '../../src/gcpfunction/http'; @@ -107,7 +108,7 @@ describe('GCPFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', [HTTP_REQUEST_METHOD]: 'POST', [URL_PATH]: '/path', }, @@ -134,7 +135,7 @@ describe('GCPFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', [HTTP_REQUEST_METHOD]: 'POST', [URL_PATH]: '/path', }, diff --git a/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts b/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts index 954f186a34d4..9d9aa0dcedf7 100644 --- a/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts +++ b/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; -import { createTransport, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { createTransport, setCurrentClient } from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { GrpcFunction, GrpcFunctionObject, Stub } from '../../src/integrations/google-cloud-grpc'; @@ -145,7 +145,7 @@ describe('GoogleCloudGrpc tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: 'grpc', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + [SENTRY_ORIGIN]: 'auto.grpc.serverless', 'rpc.system.name': 'grpc', 'rpc.service': 'test-service', 'rpc.method': 'unaryMethod', diff --git a/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts b/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts index 3a0859914daf..a7d550eea515 100644 --- a/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts +++ b/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts @@ -1,7 +1,13 @@ import { BigQuery } from '@google-cloud/bigquery'; -import { HTTP_REQUEST_METHOD, SENTRY_OP, SERVER_ADDRESS, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { + HTTP_REQUEST_METHOD, + SENTRY_OP, + SERVER_ADDRESS, + URL_DOMAIN, + URL_FULL, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { createTransport, NodeClient, setCurrentClient } from '@sentry/node'; import * as fs from 'fs'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -93,7 +99,7 @@ describe('GoogleCloudHttp tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: 'POST', [SERVER_ADDRESS]: 'bigquery.googleapis.com', [URL_DOMAIN]: 'bigquery.googleapis.com', @@ -105,7 +111,7 @@ describe('GoogleCloudHttp tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: 'GET', [SERVER_ADDRESS]: 'bigquery.googleapis.com', [URL_DOMAIN]: 'bigquery.googleapis.com', diff --git a/packages/hono/package.json b/packages/hono/package.json index 9746eaac01f1..d0d6c45cc709 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -74,7 +74,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/server-utils": "11.4.0" }, "peerDependencies": { diff --git a/packages/hono/src/bun/sdk.ts b/packages/hono/src/bun/sdk.ts index 26f9c987348b..6b6b2170f085 100644 --- a/packages/hono/src/bun/sdk.ts +++ b/packages/hono/src/bun/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, consoleSandbox, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox } from '@sentry/core'; import { init as initBun } from '@sentry/bun'; import type { HonoBunOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -13,7 +13,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. */ export function init(options: HonoBunOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console diff --git a/packages/hono/src/deno/sdk.ts b/packages/hono/src/deno/sdk.ts index 1c40bdb3bdbf..144a4391ee1e 100644 --- a/packages/hono/src/deno/sdk.ts +++ b/packages/hono/src/deno/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, consoleSandbox, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox } from '@sentry/core'; import { init as initDeno } from '@sentry/deno'; import type { HonoDenoOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -13,7 +13,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. */ export function init(options: HonoDenoOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console diff --git a/packages/hono/src/node/sdk.ts b/packages/hono/src/node/sdk.ts index 61c8afe32f35..6203900e0db6 100644 --- a/packages/hono/src/node/sdk.ts +++ b/packages/hono/src/node/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, debug, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import type { HonoNodeOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -11,7 +11,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * This function should be called in an `instrument.ts` file loaded via `--import` to set up Sentry globally for the application. */ export function init(options: HonoNodeOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { existingClient.getOptions().debug && debug.log('Sentry is already initialized, skipping re-initialization.'); return existingClient; diff --git a/packages/hono/test/bun/middleware.test.ts b/packages/hono/test/bun/middleware.test.ts index f2ef49fa19a8..a8ce3baac746 100644 --- a/packages/hono/test/bun/middleware.test.ts +++ b/packages/hono/test/bun/middleware.test.ts @@ -26,14 +26,14 @@ vi.mock('@sentry/core', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore applySdkMetadata: vi.fn(actual.applySdkMetadata), - getClient: vi.fn(() => undefined), + _INTERNAL_getActiveClient: vi.fn(() => undefined), // Pass-through so console.warn calls inside consoleSandbox are observable in tests consoleSandbox: vi.fn((cb: () => unknown) => cb()), }; }); const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; -const getClientMock = SentryCore.getClient as Mock; +const getActiveClientMock = SentryCore._INTERNAL_getActiveClient as Mock; describe('Hono Bun Middleware', () => { beforeEach(() => { @@ -64,7 +64,7 @@ describe('Hono Bun Middleware', () => { }); it('calls init from @sentry/bun when no client exists yet', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -173,7 +173,7 @@ describe('Hono Bun Middleware', () => { describe('double-init guard', () => { it('does not re-initialize when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -183,7 +183,7 @@ describe('Hono Bun Middleware', () => { it('returns the existing client when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(fakeClient); expect(initBunMock).not.toHaveBeenCalled(); @@ -192,7 +192,7 @@ describe('Hono Bun Middleware', () => { it('emits a console.warn directing to remove the duplicate init call when Sentry is already initialized', () => { const warnSpy = vi.spyOn(console, 'warn'); const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -204,7 +204,7 @@ describe('Hono Bun Middleware', () => { it('does not emit a console.warn when no client exists yet', () => { const warnSpy = vi.spyOn(console, 'warn'); - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -213,7 +213,7 @@ describe('Hono Bun Middleware', () => { }); it('always calls init regardless of whether a client already exists', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/hono/test/deno/middleware.test.ts b/packages/hono/test/deno/middleware.test.ts index 84506ab494fa..3da909433d72 100644 --- a/packages/hono/test/deno/middleware.test.ts +++ b/packages/hono/test/deno/middleware.test.ts @@ -26,14 +26,14 @@ vi.mock('@sentry/core', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore applySdkMetadata: vi.fn(actual.applySdkMetadata), - getClient: vi.fn(() => undefined), + _INTERNAL_getActiveClient: vi.fn(() => undefined), // Pass-through so console.warn calls inside consoleSandbox are observable in tests consoleSandbox: vi.fn((cb: () => unknown) => cb()), }; }); const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; -const getClientMock = SentryCore.getClient as Mock; +const getActiveClientMock = SentryCore._INTERNAL_getActiveClient as Mock; describe('Hono Deno Middleware', () => { beforeEach(() => { @@ -64,7 +64,7 @@ describe('Hono Deno Middleware', () => { }); it('calls init from @sentry/deno when no client exists yet', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -173,7 +173,7 @@ describe('Hono Deno Middleware', () => { describe('double-init guard', () => { it('does not re-initialize when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -183,7 +183,7 @@ describe('Hono Deno Middleware', () => { it('returns the existing client when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(fakeClient); expect(initDenoMock).not.toHaveBeenCalled(); @@ -192,7 +192,7 @@ describe('Hono Deno Middleware', () => { it('emits a console.warn directing to remove the duplicate init call when Sentry is already initialized', () => { const warnSpy = vi.spyOn(console, 'warn'); const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -204,7 +204,7 @@ describe('Hono Deno Middleware', () => { it('does not emit a console.warn when no client exists yet', () => { const warnSpy = vi.spyOn(console, 'warn'); - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -213,7 +213,7 @@ describe('Hono Deno Middleware', () => { }); it('always calls init regardless of whether a client already exists', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/hono/test/node/middleware.test.ts b/packages/hono/test/node/middleware.test.ts index 4355fb10e2e8..307eefe05500 100644 --- a/packages/hono/test/node/middleware.test.ts +++ b/packages/hono/test/node/middleware.test.ts @@ -127,8 +127,8 @@ describe('Hono Node Middleware', () => { describe('double-init guard', () => { it('skips re-initialization when a client already exists', () => { const fakeClient = { getOptions: () => ({}) }; - const getClientSpy = vi - .spyOn(SentryCore, 'getClient') + const getActiveClientSpy = vi + .spyOn(SentryCore, '_INTERNAL_getActiveClient') .mockReturnValue(fakeClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -136,17 +136,17 @@ describe('Hono Node Middleware', () => { expect(result).toBe(fakeClient); expect(initNodeMock).not.toHaveBeenCalled(); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); it('initializes normally when no client exists yet', () => { - const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const getActiveClientSpy = vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(initNodeMock).toHaveBeenCalledTimes(1); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); }); @@ -208,8 +208,8 @@ describe('Hono Node Middleware', () => { describe('double-init guard', () => { it('skips re-initialization when a client already exists', () => { const fakeClient = { getOptions: () => ({}) }; - const getClientSpy = vi - .spyOn(SentryCore, 'getClient') + const getActiveClientSpy = vi + .spyOn(SentryCore, '_INTERNAL_getActiveClient') .mockReturnValue(fakeClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -218,17 +218,17 @@ describe('Hono Node Middleware', () => { expect(initNodeMock).not.toHaveBeenCalled(); expect(applySdkMetadataMock).not.toHaveBeenCalled(); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); it('initializes normally when no client exists yet', () => { - const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const getActiveClientSpy = vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(initNodeMock).toHaveBeenCalledTimes(1); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); }); }); diff --git a/packages/hono/test/node/sdk.test.ts b/packages/hono/test/node/sdk.test.ts index c85b7c951b36..d5045d7ff343 100644 --- a/packages/hono/test/node/sdk.test.ts +++ b/packages/hono/test/node/sdk.test.ts @@ -26,7 +26,7 @@ const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; describe('Hono Node SDK – init()', () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); }); // ─── Happy path ─────────────────────────────────────────────────────────── @@ -136,7 +136,7 @@ describe('Hono Node SDK – init()', () => { it('returns the existing client without re-initializing when already set up', () => { const existingClient = { getOptions: () => ({ debug: false }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -148,7 +148,7 @@ describe('Hono Node SDK – init()', () => { it('logs a debug message when skipping re-initialization', () => { const logSpy = vi.spyOn(SentryCore.debug, 'log'); const existingClient = { getOptions: () => ({ debug: true }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -158,7 +158,7 @@ describe('Hono Node SDK – init()', () => { it('does not log when debug is false and skipping re-initialization', () => { const logSpy = vi.spyOn(SentryCore.debug, 'log'); const existingClient = { getOptions: () => ({ debug: false }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 2f37b1ad369c..126eb82be976 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-utils": "11.4.0" diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index e4690c988d7a..4d5762481d2f 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,6 +1,6 @@ import type { MonitorConfig } from '@sentry/core'; -import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { CODE_FUNCTION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { captureException } from '@sentry/core'; import * as Sentry from '@sentry/node'; import { startSpan } from '@sentry/node'; import { isExpectedError } from './helpers'; @@ -42,8 +42,8 @@ export function SentryTraced(op: string = 'function') { op: op, name: propertyKey, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_OP]: op, [CODE_FUNCTION_NAME]: propertyKey, }, }, diff --git a/packages/nestjs/src/helpers.ts b/packages/nestjs/src/helpers.ts index 58d03dda019f..56f9a5704b32 100644 --- a/packages/nestjs/src/helpers.ts +++ b/packages/nestjs/src/helpers.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Determines if the exception is an expected NestJS control flow error. * - HttpException have getStatus and getResponse methods: https://github.com/nestjs/nest/blob/master/packages/microservices/exceptions/rpc-exception.ts @@ -10,17 +12,15 @@ * @returns `true` if the exception is expected and should not be reported to Sentry, otherwise `false`. */ export function isExpectedError(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - // HttpException if ( - typeof ex.getStatus === 'function' && - typeof ex.getResponse === 'function' && - typeof ex.initMessage === 'function' + typeof exception.getStatus === 'function' && + typeof exception.getResponse === 'function' && + typeof exception.initMessage === 'function' ) { return true; } @@ -39,10 +39,9 @@ export function isExpectedError(exception: unknown): boolean { * We use duck-typing to avoid importing from `@nestjs/websockets` or `@nestjs/microservices`. */ export function isWsOrRpcException(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - return typeof ex.getError === 'function' && typeof ex.initMessage === 'function'; + return typeof exception.getError === 'function' && typeof exception.initMessage === 'function'; } diff --git a/packages/nestjs/src/integrations/helpers.ts b/packages/nestjs/src/integrations/helpers.ts index 8759a78b31ee..5056413e911a 100644 --- a/packages/nestjs/src/integrations/helpers.ts +++ b/packages/nestjs/src/integrations/helpers.ts @@ -5,16 +5,11 @@ import { MESSAGING_SYSTEM, SENTRY_DESCRIPTION, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, MIDDLEWARE, QUEUE_PROCESS } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; -import { - addNonEnumerableProperty, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - withActiveSpan, -} from '@sentry/core'; +import { addNonEnumerableProperty, getClient, hasSpanStreamingEnabled, withActiveSpan } from '@sentry/core'; import type { CatchTarget, InjectableTarget, NextFunction, Observable, Subscription } from './types'; /** A function of unknown signature, matching the methods/handlers we wrap. */ @@ -100,7 +95,7 @@ export function getMiddlewareSpanOptions( name: name ?? target.name ?? 'unknown', attributes: { [SENTRY_OP]: MIDDLEWARE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: middlewareOrigin(componentType), + [SENTRY_ORIGIN]: middlewareOrigin(componentType), }, }; } @@ -124,7 +119,7 @@ export function getEventSpanOptions(event: string): { attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: event, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs', + [SENTRY_ORIGIN]: 'auto.event.nestjs', // Relay infers a `function` span's description from `code.function.name` alone, which drops the prefix. ...(isStreamed && { [SENTRY_DESCRIPTION]: description }), }, @@ -157,7 +152,7 @@ export function getBullMQProcessSpanOptions(queueName: string | undefined): { name, attributes: { [SENTRY_OP]: QUEUE_PROCESS, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.queue.nestjs.bullmq', + [SENTRY_ORIGIN]: 'auto.queue.nestjs.bullmq', [MESSAGING_SYSTEM]: 'bullmq', [MESSAGING_OPERATION_TYPE]: PROCESS_OPERATION, [MESSAGING_DESTINATION_NAME]: queueName, diff --git a/packages/nestjs/src/integrations/wrap-route.ts b/packages/nestjs/src/integrations/wrap-route.ts index c65b6e2b69ce..406591804d49 100644 --- a/packages/nestjs/src/integrations/wrap-route.ts +++ b/packages/nestjs/src/integrations/wrap-route.ts @@ -4,6 +4,7 @@ import { HTTP_ROUTE, SENTRY_OP, URL_FULL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HANDLER } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; @@ -11,7 +12,6 @@ import { getClient, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, filterCollectedUrl, } from '@sentry/core'; @@ -47,7 +47,7 @@ export function getAppCreationSpanOptions( attributes: { component: NESTJS_COMPONENT, [SENTRY_OP]: FUNCTION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [AttributeNames.TYPE]: NestType.APP_CREATION, [AttributeNames.VERSION]: moduleVersion || undefined, [AttributeNames.MODULE]: moduleName || undefined, @@ -68,7 +68,7 @@ export function wrapRouteHandler(callback: AnyFn, moduleVersion?: string): AnyFn const attributes: SpanAttributes = { component: NESTJS_COMPONENT, [SENTRY_OP]: HANDLER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, [AttributeNames.CALLBACK]: callback.name, [AttributeNames.VERSION]: moduleVersion || undefined, @@ -111,7 +111,7 @@ export function wrapRequestContextHandler( const httpRoute = req.route?.path || req.routeOptions?.url || req.routerPath; const attributes: SpanAttributes = { component: NESTJS_COMPONENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: spanName, [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index b646806532b4..a8e56a9d8a66 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -8,7 +8,7 @@ import type { } from '@nestjs/common'; import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common'; import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; -import { captureException, debug, getDefaultIsolationScope, getIsolationScope } from '@sentry/core'; +import { captureException, debug, getDefaultIsolationScope, getIsolationScope, isObjectLike } from '@sentry/core'; import type { Observable } from 'rxjs'; import { isExpectedError, isWsOrRpcException } from './helpers'; @@ -166,7 +166,7 @@ class SentryGlobalFilter extends BaseExceptionFilter { if (isWsOrRpcException(exception)) { const result = (exception as { getError: () => unknown }).getError(); - const response = typeof result === 'object' && result !== null ? result : { status: 'error', message: result }; + const response = isObjectLike(result) ? result : { status: 'error', message: result }; client.emit?.('exception', response); return; } diff --git a/packages/nestjs/test/decorators.test.ts b/packages/nestjs/test/decorators.test.ts index 9640244de437..661f00b89182 100644 --- a/packages/nestjs/test/decorators.test.ts +++ b/packages/nestjs/test/decorators.test.ts @@ -1,7 +1,6 @@ import 'reflect-metadata'; -import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as core from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SentryCron, SentryExceptionCaptured, SentryTraced } from '../src/decorators'; import * as helpers from '../src/helpers'; @@ -41,8 +40,8 @@ describe('SentryTraced decorator', () => { op: 'test-operation', name: 'testMethod', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test-operation', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_OP]: 'test-operation', [CODE_FUNCTION_NAME]: 'testMethod', }, }, @@ -75,8 +74,8 @@ describe('SentryTraced decorator', () => { op: 'function', // default value name: 'testDefaultOp', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'testDefaultOp', }, }, @@ -109,8 +108,8 @@ describe('SentryTraced decorator', () => { op: 'sync-operation', name: 'syncMethod', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'sync-operation', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_OP]: 'sync-operation', [CODE_FUNCTION_NAME]: 'syncMethod', }, }, diff --git a/packages/nestjs/test/integrations/span-origin.test.ts b/packages/nestjs/test/integrations/span-origin.test.ts index 8eba4fb68e18..b1d0f3bc551f 100644 --- a/packages/nestjs/test/integrations/span-origin.test.ts +++ b/packages/nestjs/test/integrations/span-origin.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { describe, expect, it } from 'vitest'; import { getBullMQProcessSpanOptions, @@ -8,15 +8,15 @@ import { } from '../../src/integrations/helpers'; function middlewareOrigin(componentType?: string): unknown { - return getMiddlewareSpanOptions({ name: 'X' }, undefined, componentType).attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getMiddlewareSpanOptions({ name: 'X' }, undefined, componentType).attributes[SENTRY_ORIGIN]; } function eventOrigin(): unknown { - return getEventSpanOptions('x').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getEventSpanOptions('x').attributes[SENTRY_ORIGIN]; } function bullmqOrigin(): unknown { - return getBullMQProcessSpanOptions('q').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getBullMQProcessSpanOptions('q').attributes[SENTRY_ORIGIN]; } describe('NestJS span origin', () => { diff --git a/packages/nestjs/test/sentry-global-filter.test.ts b/packages/nestjs/test/sentry-global-filter.test.ts index d5f772992854..90c340aa2992 100644 --- a/packages/nestjs/test/sentry-global-filter.test.ts +++ b/packages/nestjs/test/sentry-global-filter.test.ts @@ -11,7 +11,8 @@ vi.mock('../src/helpers', () => ({ isWsOrRpcException: vi.fn(), })); -vi.mock('@sentry/core', () => ({ +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, captureException: vi.fn().mockReturnValue('mock-event-id'), getIsolationScope: vi.fn(), getDefaultIsolationScope: vi.fn(), diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index dd1455aa72e8..1f2d91d9a76f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -76,7 +76,7 @@ "@rollup/plugin-commonjs": "28.0.1", "@sentry/browser-utils": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/opentelemetry": "11.4.0", diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 37763c0e0607..6f43a282df11 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -2,7 +2,7 @@ // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ import type { Client, EventProcessor, Integration } from '@sentry/core'; -import { addEventProcessor, applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } from '@sentry/core'; +import { applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -22,8 +22,6 @@ export { captureUnderscoreErrorException } from '../common/pages-router-instrume export { browserTracingIntegration } from './browserTracingIntegration'; export { captureRouterTransitionStart } from './routing/appRouterRoutingInstrumentation'; -let clientIsInitialized = false; - const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesAssetPrefixPath: string; _sentryAssetPrefix?: string; @@ -37,16 +35,6 @@ declare const __SENTRY_TRACING__: boolean; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: BrowserOptions): Client | undefined { - if (clientIsInitialized) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] You are calling `Sentry.init()` more than once on the client. This can happen if you have both a `sentry.client.config.ts` and a `instrumentation-client.ts` file with `Sentry.init()` calls. It is recommended to call `Sentry.init()` once in `instrumentation-client.ts`.', - ); - }); - } - clientIsInitialized = true; - if (!DEBUG_BUILD && options.debug) { consoleSandbox(() => { // eslint-disable-next-line no-console @@ -88,10 +76,10 @@ export function init(options: BrowserOptions): Client | undefined { ? null : event; filterNextRedirectError.id = 'NextRedirectErrorFilter'; - addEventProcessor(filterNextRedirectError); + client?.addEventProcessor(filterNextRedirectError); if (process.env.NODE_ENV === 'development') { - addEventProcessor(devErrorSymbolicationEventProcessor); + client?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 1a7bb8a113de..4b7e9b1c7ae5 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -2,9 +2,9 @@ import type { Client, Span } from '@sentry/core'; import { GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, timestampInSeconds, } from '@sentry/core'; @@ -23,6 +23,7 @@ import { URL_FULL, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; @@ -104,7 +105,7 @@ export function appRouterInstrumentPageLoad(client: Client): void { // pageload should always start at timeOrigin (and needs to be in s, not ms) attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), }, @@ -179,7 +180,7 @@ export function appRouterInstrumentNavigation(client: Client): void { name: spanName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': `router.${navigationType}`, ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), @@ -216,7 +217,7 @@ export function appRouterInstrumentNavigation(client: Client): void { name: spanName, startTime: traversal?.startTime, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': traversal?.navigationType ?? 'browser.popstate', ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), @@ -250,8 +251,8 @@ export function appRouterInstrumentNavigation(client: Client): void { if (globalValue) { GLOBAL_OBJ_WITH_NEXT_ROUTER.next = new Proxy(globalValue, { set(target, p, newValue) { - if (p === 'router' && typeof newValue === 'object' && newValue !== null) { - patchRouter(client, newValue, currentRouterPatchingNavigationSpanRef); + if (p === 'router' && isObjectLike(newValue)) { + patchRouter(client, newValue as unknown as NextRouter, currentRouterPatchingNavigationSpanRef); } // @ts-expect-error we cannot possibly type this @@ -313,7 +314,7 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : transactionName), attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': `router.${routerFunctionName}`, ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), diff --git a/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts index 44d842b6193a..ddd14281bfea 100644 --- a/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts @@ -1,13 +1,8 @@ import type { Client, TransactionSource } from '@sentry/core'; -import { - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - stripUrlQueryAndFragment, -} from '@sentry/core'; +import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, stripUrlQueryAndFragment } from '@sentry/core'; import { getAbsoluteUrl, startBrowserTracingNavigationSpan } from '@sentry/react'; import RouterImport from 'next/router'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import { getNextRouteFromPathname } from './pagesRouterRoutingInstrumentation'; @@ -49,7 +44,7 @@ export function pagesRouterInstrumentNavigation(client: Client): void { name: spanSource === 'route' || !hasSpanStreamingEnabled(client) ? newLocation : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: spanSource, ...(spanSource === 'route' && { [URL_TEMPLATE]: newLocation }), }, diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 7a4ebaad3877..7335d0465bda 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -1,16 +1,10 @@ import type { Client } from '@sentry/core'; -import { - debug, - hasSpanStreamingEnabled, - PAGELOAD_SPAN_NAME_FALLBACK, - parseBaggageHeader, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { debug, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK, parseBaggageHeader } from '@sentry/core'; import { startBrowserTracingPageLoadSpan, WINDOW } from '@sentry/react'; import type { NEXT_DATA } from 'next/dist/shared/lib/utils'; import type { ParsedUrlQuery } from 'querystring'; import { DEBUG_BUILD } from '../../common/debug-build'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { PAGELOAD } from '@sentry/conventions/op'; const globalObject = WINDOW; @@ -113,7 +107,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void { name, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: route ? 'route' : 'url', ...(route && { [URL_TEMPLATE]: route }), ...(params && { ...params }), diff --git a/packages/nextjs/src/common/nextNavigationErrorUtils.ts b/packages/nextjs/src/common/nextNavigationErrorUtils.ts index 94960cc32ca9..9ad2674729ea 100644 --- a/packages/nextjs/src/common/nextNavigationErrorUtils.ts +++ b/packages/nextjs/src/common/nextNavigationErrorUtils.ts @@ -29,6 +29,20 @@ export function isNotFoundNavigationError(subject: unknown): boolean { return hasDigest(subject, digest => ['NEXT_NOT_FOUND', 'NEXT_HTTP_ERROR_FALLBACK;404'].includes(digest)); } +/** + * Returns the HTTP status of a Next.js `forbidden()` (403) or `unauthorized()` (401) error, if input is one. + * https://nextjs.org/docs/app/api-reference/functions/forbidden + */ +export function getAuthInterruptStatusCode(subject: unknown): 401 | 403 | undefined { + if (hasDigest(subject, digest => digest === 'NEXT_HTTP_ERROR_FALLBACK;403')) { + return 403; + } + if (hasDigest(subject, digest => digest === 'NEXT_HTTP_ERROR_FALLBACK;401')) { + return 401; + } + return undefined; +} + /** * Determines whether input is a Next.js redirect error. * https://beta.nextjs.org/docs/api-reference/redirect#redirect diff --git a/packages/nextjs/src/common/pages-router-instrumentation/_error.ts b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts index 80103b42568f..9efa068e4e0d 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/_error.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts @@ -1,12 +1,7 @@ -import { - captureException, - getIsolationScope, - httpRequestToRequestData, - isAlreadyCaptured, - withScope, -} from '@sentry/core'; +import { captureException, getIsolationScope, isAlreadyCaptured, withScope } from '@sentry/core'; import type { NextPageContext } from 'next'; import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; +import { pagesRouterRequestToRequestData } from '../utils/pagesRouterRequestToRequestData'; type ContextOrProps = { req?: NextPageContext['req']; @@ -62,7 +57,7 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP const eventId = withScope(scope => { if (req) { - const normalizedRequest = httpRequestToRequestData(req); + const normalizedRequest = pagesRouterRequestToRequestData(req); scope.setSDKProcessingMetadata({ normalizedRequest }); } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 77343c882239..d43567a87716 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -4,7 +4,6 @@ import { getActiveSpan, getCurrentScope, getRootSpan, - httpRequestToRequestData, objectify, setCapturedScopesOnSpan, withIsolationScope, @@ -12,6 +11,7 @@ import { import type { NextApiRequest } from 'next'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; +import { pagesRouterRequestToRequestData } from '../utils/pagesRouterRequestToRequestData'; import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; export type AugmentedNextApiRequest = NextApiRequest & { @@ -56,7 +56,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz return withIsolationScope(async isolationScope => { const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - isolationScope.setSDKProcessingMetadata({ normalizedRequest: httpRequestToRequestData(req) }); + isolationScope.setSDKProcessingMetadata({ normalizedRequest: pagesRouterRequestToRequestData(req) }); isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`); // We no longer create the transaction ourselves: it's the Next.js root span, which captured a different diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts index 02c54e76b3d6..7fda6a1da56e 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type App from 'next/app'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -45,23 +46,23 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI baggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof appGetInitialProps === 'object' && appGetInitialProps !== null) { + if (isObjectLike(appGetInitialProps)) { // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. - if (!(appGetInitialProps as Record).pageProps) { - (appGetInitialProps as Record).pageProps = {}; + if (!appGetInitialProps.pageProps) { + appGetInitialProps.pageProps = {}; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryTraceData = sentryTrace; + (appGetInitialProps.pageProps as Record)._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryBaggage = baggage; + (appGetInitialProps.pageProps as Record)._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts index 209ca486db15..1346b78ffd25 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; import { isBuild } from '../utils/isBuild'; @@ -47,15 +48,15 @@ export function wrapErrorGetInitialPropsWithSentry( sentryTrace?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof errorGetInitialProps === 'object' && errorGetInitialProps !== null) { + if (isObjectLike(errorGetInitialProps)) { if (sentryTrace) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - (errorGetInitialProps as Record)._sentryTraceData = sentryTrace; + errorGetInitialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (errorGetInitialProps as Record)._sentryBaggage = baggage; + errorGetInitialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts index f597692820b8..82fc76343785 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPage } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -43,15 +44,15 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro sentryTrace?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - if (typeof initialProps === 'object' && initialProps !== null) { + if (isObjectLike(initialProps)) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (initialProps as Record)._sentryTraceData = sentryTrace; + initialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (initialProps as Record)._sentryBaggage = baggage; + initialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts index 5512c6204410..f99faa528f91 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { GetServerSideProps } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -39,7 +40,7 @@ export function wrapGetServerSidePropsWithSentry( sentryTrace?: string; } = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType); - if (typeof serverSideProps === 'object' && serverSideProps !== null && 'props' in serverSideProps) { + if (isObjectLike(serverSideProps) && 'props' in serverSideProps) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { (serverSideProps.props as Record)._sentryTraceData = sentryTrace; diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index a2d2ab9ac0d2..6cb11040038f 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -1,6 +1,6 @@ -import { HTTP_TARGET, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { HTTP_TARGET, URL_FULL, URL_PATH, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { RawAttributes } from '@sentry/core'; -import { getClient, GLOBAL_OBJ, isSentryRequestUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span } from '@sentry/core'; +import { getClient, GLOBAL_OBJ, isSentryRequestUrl, type Span } from '@sentry/core'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; import { isPathnameUnderSentryTunnelRoute } from './tunnelPathnameMatch'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; @@ -32,7 +32,7 @@ export function dropMiddlewareTunnelRequests( const isMiddleware = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute'; // The fetch span could be originating from rewrites re-writing a tunnel request // So we want to filter it out - const isFetchSpan = attrs?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.node_fetch'; + const isFetchSpan = attrs?.[SENTRY_ORIGIN] === 'auto.http.node_fetch'; const isBaseServerHandleRequest = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest'; // If the span is not a middleware span, fetch span, or BaseServer.handleRequest span, return diff --git a/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts b/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts index e80079fe68bd..be2c8b998316 100644 --- a/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts +++ b/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToStaticSpanJSON } from '@sentry/core'; +import { spanToStaticSpanJSON } from '@sentry/core'; import type { MutableRootSpan } from '../../server/enhanceHandleRequestRootSpan'; /** @@ -24,7 +24,7 @@ export function createLiveRootSpanAdapter(span: Span): MutableRootSpan { span.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, source); }, setOp: (op: string) => { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, op); + span.setAttribute(SENTRY_OP, op); }, }; } diff --git a/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts b/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts new file mode 100644 index 000000000000..12203f438df2 --- /dev/null +++ b/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts @@ -0,0 +1,35 @@ +import type { RequestEventData } from '@sentry/core'; +import { httpRequestToRequestData, parseUrl } from '@sentry/core'; +import type { IncomingMessage } from 'http'; + +const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta'); + +type RequestWithNextMeta = IncomingMessage & { + [NEXT_REQUEST_META]?: { initURL?: unknown }; +}; + +/** + * Converts a Pages Router request into request data for events. + * + * Next.js strips `basePath` from `req.url` before running pages and API routes, but keeps the URL as it was originally + * requested in its internal request meta (`initURL`). We use that one so the reported URL matches what was requested. + */ +export function pagesRouterRequestToRequestData(req: RequestWithNextMeta): RequestEventData { + const requestData = httpRequestToRequestData(req); + + const initUrl = req[NEXT_REQUEST_META]?.initURL; + // `initURL` can be absolute, but its origin is built from the Next.js server's own hostname and port rather than the + // request headers, so we only keep path and query and let the headers decide the origin like everywhere else. + const originalUrl = typeof initUrl === 'string' ? parseUrl(initUrl).relative : undefined; + if (!originalUrl || originalUrl === req.url) { + return requestData; + } + + const { url, query_string } = httpRequestToRequestData({ + url: originalUrl, + headers: req.headers, + socket: req.socket, + }); + + return { ...requestData, url, query_string }; +} diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index 31e3fa698433..4ec79d9cabdf 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { debug, fill, flush, GLOBAL_OBJ, setHttpStatus } from '@sentry/core'; +import { debug, fill, flush, getAsyncContextStrategy, getMainCarrier, GLOBAL_OBJ, setHttpStatus } from '@sentry/core'; import { vercelWaitUntil } from '@sentry/core/server'; import type { ServerResponse } from 'http'; import { DEBUG_BUILD } from '../debug-build'; @@ -105,3 +105,20 @@ export function cloudflareWaitUntil(task: Promise): void { export function isCloudflareWaitUntilAvailable(): boolean { return typeof _getOpenNextCloudflareContext()?.waitUntil === 'function'; } + +/** + * Whether a request of `withSentry` from `@sentry/cloudflare` runs. A client of `init` would then replace its async + * context strategy while the request runs (#24603). + */ +export function isAsyncContextOwnedByCloudflare(): boolean { + const strategy = getAsyncContextStrategy(getMainCarrier()); + // The AsyncLocalStorage strategy of `@sentry/cloudflare` has no `withActiveSpan`. + const asyncLocalStorage = strategy.getTracingChannelBinding?.()?.asyncLocalStorage as + | { getStore(): unknown } + | undefined; + return ( + !strategy.withActiveSpan && + asyncLocalStorage?.getStore() !== undefined && + GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers' + ); +} diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index f340b1e69bf8..a0e6259d1404 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -6,11 +6,11 @@ import { getIsolationScope, getRootSpan, getTraceData, - httpRequestToRequestData, isThenable, } from '@sentry/core'; import type { IncomingMessage, ServerResponse } from 'http'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; +import { pagesRouterRequestToRequestData } from './pagesRouterRequestToRequestData'; /** * Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown. @@ -68,7 +68,7 @@ export function withTracedServerSideDataFetcher Pr this: unknown, ...args: Parameters ): Promise<{ data: ReturnType; sentryTrace?: string; baggage?: string }> { - const normalizedRequest = httpRequestToRequestData(req); + const normalizedRequest = pagesRouterRequestToRequestData(req); getCurrentScope().setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`); getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 29b2ac5c757a..3f4587f80a35 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -6,9 +6,9 @@ import { getActiveSpan, getClient, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan, @@ -17,6 +17,7 @@ import { import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; import { DEBUG_BUILD } from './debug-build'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -27,6 +28,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; @@ -138,15 +140,18 @@ async function withServerActionInstrumentationImplementation { // oxlint-disable-next-line typescript/await-thenable -- callback may be async at runtime const result = await handleCallbackErrors(callback, error => { + const authInterruptStatusCode = getAuthInterruptStatusCode(error); if (isNotFoundNavigationError(error)) { // We don't want to report "not-found"s span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (authInterruptStatusCode) { + span.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); } else if (isRedirectNavigationError(error)) { // Redirects are normal Next.js control flow, not errors. Mark the span as OK and end it // early so the surrounding `startSpan` error handler doesn't override the status to diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index d90678ded6ed..29d609dae31d 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -3,6 +3,7 @@ import { captureException, getActiveSpan, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, SPAN_STATUS_ERROR, SPAN_STATUS_OK, @@ -10,6 +11,7 @@ import { } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -59,6 +61,12 @@ export function wrapGenerationFunctionWithSentry a return; } + const authInterruptStatusCode = getAuthInterruptStatusCode(error); + if (authInterruptStatusCode) { + span?.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); + return; + } + if (isRedirectNavigationError(error)) { // We don't want to report redirects span?.setStatus({ code: SPAN_STATUS_OK }); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index cbefbc2f079c..7ed7b15eb0ba 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -16,6 +16,7 @@ import { withScope, } from '@sentry/core'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -83,6 +84,7 @@ export function wrapRouteHandlerWithSentry any>( const response: Response = await handleCallbackErrors( () => originalFunction.apply(thisArg, args), error => { + const authInterruptStatusCode = getAuthInterruptStatusCode(error); // Next.js throws errors when calling `redirect()`. We don't wanna report these. if (isRedirectNavigationError(error)) { // Don't do anything @@ -93,6 +95,13 @@ export function wrapRouteHandlerWithSentry any>( if (rootSpan) { setHttpStatus(rootSpan, 404); } + } else if (authInterruptStatusCode) { + if (activeSpan) { + setHttpStatus(activeSpan, authInterruptStatusCode); + } + if (rootSpan) { + setHttpStatus(rootSpan, authInterruptStatusCode); + } } else if (isPrerenderControlFlowError(error)) { // Next.js aborts prerenders by rejecting the promises it handed out. React discards those // rejections, so they are expected, do not affect the response, and must not be reported. diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 6deaef200f67..9f96f5a912c1 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -3,12 +3,14 @@ import { captureException, getActiveSpan, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, SPAN_STATUS_ERROR, SPAN_STATUS_OK, winterCGHeadersToDict, } from '@sentry/core'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -54,6 +56,12 @@ export function wrapServerComponentWithSentry any> return; } + const authInterruptStatusCode = getAuthInterruptStatusCode(error); + if (authInterruptStatusCode) { + span?.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); + return; + } + if (isRedirectNavigationError(error)) { // We don't want to report redirects span?.setStatus({ code: SPAN_STATUS_OK }); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index c971bbafec91..e582927fd6e8 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -3,6 +3,7 @@ /* eslint-disable import/export */ import { applySdkMetadata, + debug, getGlobalScope, getIsolationScope, getRootSpan, @@ -10,8 +11,6 @@ import { hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, registerSpanErrorInstrumentation, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; @@ -30,7 +29,12 @@ import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunn import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan'; import { getNormalizedRequestFromAttributes } from '../common/utils/getNormalizedRequestFromAttributes'; import { isBuild } from '../common/utils/isBuild'; -import { flushSafelyWithTimeout, isCloudflareWaitUntilAvailable, waitUntil } from '../common/utils/responseEnd'; +import { + flushSafelyWithTimeout, + isAsyncContextOwnedByCloudflare, + isCloudflareWaitUntilAvailable, + waitUntil, +} from '../common/utils/responseEnd'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; @@ -41,6 +45,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; @@ -106,6 +111,26 @@ export function init(options: VercelEdgeOptions = {}): void { ]; opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans]; + // Before the Cloudflare check, so that the events of the client of `withSentry` get the tag too. + try { + // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js + if (process.turbopack) { + getGlobalScope().setTag('turbopack', true); + getGlobalScope().setAttribute('turbopack', true); + } + } catch { + // Noop + // The statement above can throw because process is not defined on the client + } + + if (isAsyncContextOwnedByCloudflare()) { + DEBUG_BUILD && + debug.log( + 'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.', + ); + return; + } + // Use appropriate SDK metadata based on the runtime environment if (isRunningOnCloudflare) { applySdkMetadata(opts, 'nextjs', ['nextjs', 'cloudflare']); @@ -159,13 +184,13 @@ export function init(options: VercelEdgeOptions = {}): void { // Mark all spans generated by Next.js as 'auto' & server if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); span.setAttribute(SENTRY_KIND, 'server'); } // Make sure middleware spans get the right op if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, MIDDLEWARE); + span.setAttribute(SENTRY_OP, MIDDLEWARE); span.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, 'url'); } @@ -214,15 +239,4 @@ export function init(options: VercelEdgeOptions = {}): void { waitUntil(flushSafelyWithTimeout()); }); - - try { - // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js - if (process.turbopack) { - getGlobalScope().setTag('turbopack', true); - getGlobalScope().setAttribute('turbopack', true); - } - } catch { - // Noop - // The statement above can throw because process is not defined on the client - } } diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index d7ce162236fb..3b5ac2d0bd72 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,14 +1,12 @@ // import/export got a false positive, and affects most of our index barrel files // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ -import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; -import type { EventProcessor } from '@sentry/core'; +import type { EventProcessor, Scope } from '@sentry/core'; import { + _INTERNAL_getActiveClient, applySdkMetadata, debug, - getClient, getGlobalScope, - getRootSpan, getVercelEnv, GLOBAL_OBJ, } from '@sentry/core'; @@ -17,20 +15,12 @@ import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sent import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils'; -import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; -import { isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; -import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; +import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; -import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; -import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; -import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; -import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; -import { handleOnSpanStart } from './handleOnSpanStart'; +import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from './serverSpanHooks'; import { prepareSafeIdGeneratorContext } from './prepareSafeIdGeneratorContext'; import { nextjsUseCacheIntegration } from './useCacheInstrumentation'; -import { maybeCompleteCronCheckIn } from './vercelCronsMonitoring'; -import { maybeCleanupQueueSpan } from './vercelQueuesMonitoring'; export * from '@sentry/node'; @@ -159,22 +149,7 @@ export function init(options: NodeOptions): NodeClient | undefined { ...cloudflareConfig, }; - const nextjsIgnoreSpans: NonNullable = [ - // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) - /^GET (\/.*)?\/_next\/static\//, - // Dev source-map fetch endpoints - /\/__nextjs_original-stack-frame/, - // Pages router /404 - /^\/404$/, - // App router /404 and /_not-found segments (any HTTP method) - /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, - // Root transactions named "NextServer.getRequestHandler" containing useless tracing - /^NextServer\.getRequestHandler$/, - // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION - // (set in `dropMiddlewareTunnelRequests` during `spanStart`) - { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, - ]; - opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans]; + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...NEXTJS_SERVER_IGNORE_SPANS]; if (DEBUG_BUILD && opts.debug) { debug.enable(); @@ -182,56 +157,37 @@ export function init(options: NodeOptions): NodeClient | undefined { DEBUG_BUILD && debug.log('Initializing SDK...'); - if (sdkAlreadyInitialized()) { + // The client of `withSentry` from `@sentry/cloudflare` handles this request, so `init` creates none. The global + // parts below still apply. + const isOwnedByCloudflare = isAsyncContextOwnedByCloudflare(); + const existingClient = _INTERNAL_getActiveClient(); + if (existingClient && !isOwnedByCloudflare) { DEBUG_BUILD && debug.log('SDK already initialized'); - return; + return existingClient; } // Use appropriate SDK metadata based on the runtime environment applySdkMetadata(opts, 'nextjs', ['nextjs', cloudflareConfig ? 'cloudflare' : 'node']); - const client = nodeInit(opts); - - client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { - // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in - // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing - // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. - // We need to drop these spans. - if ( - // eslint-disable-next-line typescript/no-deprecated - (typeof spanAttributes[HTTP_TARGET] === 'string' && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_key') && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_client')) || - (typeof spanAttributes[URL_QUERY] === 'string' && - spanAttributes[URL_QUERY].includes('sentry_key') && - spanAttributes[URL_QUERY].includes('sentry_client')) - ) { - samplingDecision.decision = false; - } - }); - - client?.on('spanStart', span => handleOnSpanStart(span, client)); - - // Normalize name/op/source/status on the request root span at span end, before it is serialized into - // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles - // pick up the changes from one place, and the cron/queue hooks below see the finalized status. - client?.on('spanEnd', span => { - if (span !== getRootSpan(span)) { - return; - } - - const mutableRootSpan = createLiveRootSpanAdapter(span); - enhanceHandleRequestRootSpan(mutableRootSpan); - enhanceMiddlewareRootSpan(mutableRootSpan); - backfillHttpServerStatus(span); - }); + const client = isOwnedByCloudflare ? undefined : nodeInit(opts); - client?.on('spanEnd', maybeCompleteCronCheckIn); - client?.on('spanEnd', maybeCleanupQueueSpan); + if (client) { + addNextjsServerSpanHooks(client); + } - getGlobalScope().addEventProcessor( + // On the client, not the global scope, so a later `init()` after + // `close()` does not stack another copy. In a request of `withSentry` on + // Workers, `init` creates no client, so the global scope gets them, which + // reaches every client of `withSentry`, unless an earlier `init` added them. + let eventProcessorTarget: NodeClient | Scope | undefined = client; + if (isOwnedByCloudflare) { + const globalScope = getGlobalScope(); + const isAdded = globalScope + .getScopeData() + .eventProcessors.some(processor => processor.id === 'DropReactControlFlowErrors'); + eventProcessorTarget = isAdded ? undefined : globalScope; + } + eventProcessorTarget?.addEventProcessor( Object.assign( ((event, hint) => { if (event.type !== undefined) { @@ -273,12 +229,8 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - client?.on('preprocessEvent', event => { - setUrlProcessingMetadata(event); - }); - if (process.env.NODE_ENV === 'development') { - getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); + eventProcessorTarget?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { @@ -292,15 +244,18 @@ export function init(options: NodeOptions): NodeClient | undefined { // The statement above can throw because process is not defined on the client } - DEBUG_BUILD && debug.log('SDK successfully initialized'); + if (isOwnedByCloudflare) { + DEBUG_BUILD && + debug.log( + 'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.', + ); + } else { + DEBUG_BUILD && debug.log('SDK successfully initialized'); + } return client; } -function sdkAlreadyInitialized(): boolean { - return !!getClient(); -} - export * from '../common'; export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/serverSpanHooks.ts similarity index 56% rename from packages/nextjs/src/server/handleOnSpanStart.ts rename to packages/nextjs/src/server/serverSpanHooks.ts index 5a7f3ea59772..fd6eb7fb2154 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/serverSpanHooks.ts @@ -3,24 +3,99 @@ import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, + HTTP_TARGET, + SENTRY_ORIGIN, + URL_QUERY, } from '@sentry/conventions/attributes'; -import type { Client, Span } from '@sentry/core'; +import type { Client, Options, Span } from '@sentry/core'; import { getIsolationScope, getRootSpan, hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; +import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan'; +import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils'; -import { maybeStartCronCheckIn } from './vercelCronsMonitoring'; -import { maybeEnrichQueueConsumerSpan, maybeEnrichQueueProducerSpan } from './vercelQueuesMonitoring'; +import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; +import { maybeCompleteCronCheckIn, maybeStartCronCheckIn } from './vercelCronsMonitoring'; +import { + maybeCleanupQueueSpan, + maybeEnrichQueueConsumerSpan, + maybeEnrichQueueProducerSpan, +} from './vercelQueuesMonitoring'; + +export const NEXTJS_SERVER_IGNORE_SPANS: NonNullable = [ + // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) + /^GET (\/.*)?\/_next\/static\//, + // Dev source-map fetch endpoints + /\/__nextjs_original-stack-frame/, + // Pages router /404 + /^\/404$/, + // App router /404 and /_not-found segments (any HTTP method) + /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, + // Root transactions named "NextServer.getRequestHandler" containing useless tracing + /^NextServer\.getRequestHandler$/, + // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION + // (set in `dropMiddlewareTunnelRequests` during `spanStart`) + { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, +]; + +/** + * Registers the hooks that name, enrich and filter the spans and events of the Next.js server on `client`. + */ +export function addNextjsServerSpanHooks(client: Client): void { + client.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { + // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in + // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing + // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. + // We need to drop these spans. + if ( + // eslint-disable-next-line typescript/no-deprecated + (typeof spanAttributes[HTTP_TARGET] === 'string' && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_key') && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_client')) || + (typeof spanAttributes[URL_QUERY] === 'string' && + spanAttributes[URL_QUERY].includes('sentry_key') && + spanAttributes[URL_QUERY].includes('sentry_client')) + ) { + samplingDecision.decision = false; + } + }); + + client.on('spanStart', span => handleOnSpanStart(span, client)); + + // Normalize name/op/source/status on the request root span at span end, before it is serialized into + // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles + // pick up the changes from one place, and the cron/queue hooks below see the finalized status. + client.on('spanEnd', span => { + if (span !== getRootSpan(span)) { + return; + } + + const mutableRootSpan = createLiveRootSpanAdapter(span); + enhanceHandleRequestRootSpan(mutableRootSpan); + enhanceMiddlewareRootSpan(mutableRootSpan); + backfillHttpServerStatus(span); + }); + + client.on('spanEnd', maybeCompleteCronCheckIn); + client.on('spanEnd', maybeCleanupQueueSpan); + + client.on('preprocessEvent', event => { + setUrlProcessingMetadata(event); + }); +} /** * Handles the on span start event for Next.js spans. @@ -96,13 +171,13 @@ export function handleOnSpanStart(span: Span, client: Client): void { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }); } - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); } // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans // with patterns (e.g. http.server spans) that will produce confusing data. if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); } if (isRootSpan) { diff --git a/packages/nextjs/src/server/useCacheInstrumentation.ts b/packages/nextjs/src/server/useCacheInstrumentation.ts index ac0a32373790..e51e5e148e05 100644 --- a/packages/nextjs/src/server/useCacheInstrumentation.ts +++ b/packages/nextjs/src/server/useCacheInstrumentation.ts @@ -6,6 +6,7 @@ import { CACHE_OPERATION, CACHE_TAGS, CACHE_TTL, + CODE_FILE_PATH, SENTRY_LINK_TYPE, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; @@ -26,6 +27,7 @@ import { timestampInSeconds, } from '@sentry/core'; import { DEBUG_BUILD } from '../common/debug-build'; +import { getCacheFunctionSourceFile } from './useCacheSourceFile'; // Next.js shares its `use cache` handlers across bundles via `globalThis` // (`next/src/server/use-cache/handlers.ts`). This module can load once per bundle, so all @@ -102,7 +104,12 @@ function shouldRecordCacheSpan(): boolean { return !!activeSpan && spanIsSampled(activeSpan); } -function startCacheSpan(op: typeof CACHE_GET | typeof CACHE_PUT, digest: string, callback: (span: Span) => T): T { +function startCacheSpan( + op: typeof CACHE_GET | typeof CACHE_PUT, + digest: string, + extraAttributes: Record, + callback: (span: Span) => T, +): T { const client = getClient(); return startSpan( @@ -114,6 +121,7 @@ function startCacheSpan(op: typeof CACHE_GET | typeof CACHE_PUT, digest: stri [SENTRY_ORIGIN]: CACHE_SPAN_ORIGIN, [CACHE_KEY]: [digest], [CACHE_OPERATION]: CACHE_OPERATION_NAMES[op], + ...extraAttributes, }, }, callback, @@ -250,7 +258,7 @@ function instrumentHandler(handler: unknown): void { return originalGet.call(this, cacheKey, softTags); } const digest = keyDigest(cacheKey); - return startCacheSpan(CACHE_GET, digest, span => + return startCacheSpan(CACHE_GET, digest, {}, span => // `Promise.resolve` because custom handlers may return the entry synchronously. Promise.resolve(originalGet.call(this, cacheKey, softTags)).then(entry => { try { @@ -273,9 +281,11 @@ function instrumentHandler(handler: unknown): void { } const digest = keyDigest(cacheKey); + const sourceFile = getCacheFunctionSourceFile(cacheKey); + // The handler drains `pendingEntry` (the still-streaming entry) before storing, so this // span covers producing and storing the entry, not just the write. - return startCacheSpan(CACHE_PUT, digest, span => + return startCacheSpan(CACHE_PUT, digest, sourceFile ? { [CODE_FILE_PATH]: sourceFile } : {}, span => // Only a successful write becomes a fill origin: a failed write leaves no entry or the // previous one (whose origin still stands). A dropped span (`ignoreSpans`) never // reaches Sentry, so a link to it would be broken. diff --git a/packages/nextjs/src/server/useCacheSourceFile.ts b/packages/nextjs/src/server/useCacheSourceFile.ts new file mode 100644 index 000000000000..0530a0cd3495 --- /dev/null +++ b/packages/nextjs/src/server/useCacheSourceFile.ts @@ -0,0 +1,134 @@ +// Next.js' runtime manifest registry +// https://github.com/vercel/next.js/blob/8e0700c74474498a07b33f58da1c1316f740eb19/packages/next/src/server/app-render/manifests-singleton.ts#L56-L59 +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); + +type ManifestEntries = Record; + +type GlobalWithManifests = typeof globalThis & { + [NEXT_MANIFESTS_SINGLETON]?: { + serverActionsManifest?: { + node?: ManifestEntries; + edge?: ManifestEntries; + }; + }; +}; + +interface MultipartField { + content: string; + /** Index of the first character after this field. */ + end: number; +} + +/** + * Reads one field of a multipart cache key, starting at `start`. + * A field is `:`, with the length in lowercase hex counting UTF-16 code units. + * https://github.com/vercel/next.js/blob/8e0700c74474498a07b33f58da1c1316f740eb19/packages/next/src/server/use-cache/use-cache-wrapper.ts#L1815-L1847 + * + * Returns `undefined` when the framing is broken. + */ +function readMultipartField(cacheKey: string, start: number): MultipartField | undefined { + // The characters before the next `:` must be a non-empty hex length. + const colon = cacheKey.indexOf(':', start); + if (colon <= start || !/^[0-9a-f]+$/.test(cacheKey.slice(start, colon))) { + return undefined; + } + + // A length that points past the end of the key means the key is truncated. + const contentStart = colon + 1; + const contentEnd = contentStart + parseInt(cacheKey.slice(start, colon), 16); + if (contentEnd > cacheKey.length) { + return undefined; + } + + return { content: cacheKey.slice(contentStart, contentEnd), end: contentEnd }; +} + +/** + * Cache keys with arguments that do not serialize to JSON (a page's `params` promise, a layout's + * `children`) are serialized FormData: pairs of length-prefixed fields, a field name followed by + * its content. React stores the key parts JSON in the field named `"0"`, which is not necessarily + * the first field. Returns that JSON text, or `undefined` for a malformed key. + */ +function readKeyPartsFromMultipartKey(cacheKey: string): string | undefined { + let position = 0; + while (position < cacheKey.length) { + // Each pair is the field name, then the field content. + const name = readMultipartField(cacheKey, position); + const content = name && readMultipartField(cacheKey, name.end); + if (name === undefined || content === undefined) { + return undefined; + } + + if (name.content === '0') { + return content.content; + } + position = content.end; + } + return undefined; +} + +/** + * Picks the function id out of the decoded key parts. Next.js 16.3 puts the id at index 1 + * (`[buildId, id, args]` in prod; dev appends a fourth part). 16.4 canary moves it to index 0 + * (`[id, args, …]`). Returns `undefined` for any other shape. + */ +function readFunctionIdFromKeyParts(keyParts: unknown): string | undefined { + if (!Array.isArray(keyParts)) { + return undefined; + } + // An args array at index 1 marks the canary shape, where the id sits at index 0. + const functionId = Array.isArray(keyParts[1]) ? keyParts[0] : keyParts[1]; + return typeof functionId === 'string' ? functionId : undefined; +} + +/** + * Manifest filenames start at the repo root, but only the path inside the project is useful. + * The repo-root-to-project part equals the tail of `process.cwd()`, because `next dev`, + * `next start`, and the standalone server all run in the project directory (the standalone + * `server.js` chdirs into its mirrored copy). Unknown layouts keep the full path. + */ +function toProjectRelativePath(filename: string): string { + // Split the cwd on both separators so Windows paths work. The manifest always uses `/`. + const cwdSegments = process.cwd().split(/[\\/]/).filter(Boolean); + const fileSegments = filename.split('/'); + + // Drop the longest filename prefix that matches the cwd tail. Longest first, so the whole + // repo prefix goes, not just a part of it. At least one segment always remains. + const maxOverlap = Math.min(fileSegments.length - 1, cwdSegments.length); + for (let overlap = maxOverlap; overlap > 0; overlap--) { + const cwdTail = cwdSegments.slice(-overlap); + if (cwdTail.every((segment, index) => segment === fileSegments[index])) { + return fileSegments.slice(overlap).join('/'); + } + } + return filename; +} + +/** + * Resolves the source file of the `use cache` function behind a cache key, through the server-reference manifest. + * With Turbopack on Next.js 16.3 the manifest only covers component-tree functions, so route handlers resolve + * to `undefined`. Webpack and 16.4 canary include route handlers. + * Any unexpected key or manifest shape returns `undefined`, never a wrong file. + */ +export function getCacheFunctionSourceFile(cacheKey: string): string | undefined { + try { + // The key is `encodeReply(keyParts)`: a plain JSON array when all function arguments + // serialize to JSON, the multipart form otherwise. + const keyPartsJson = cacheKey.startsWith('[') ? cacheKey : readKeyPartsFromMultipartKey(cacheKey); + if (!keyPartsJson) { + return undefined; + } + + const functionId = readFunctionIdFromKeyParts(JSON.parse(keyPartsJson)); + if (functionId === undefined) { + return undefined; + } + + // The manifest has one section per runtime. `NEXT_RUNTIME` is only `'edge'` on edge. + const manifest = (globalThis as GlobalWithManifests)[NEXT_MANIFESTS_SINGLETON]?.serverActionsManifest; + const filename = manifest?.[process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node']?.[functionId]?.filename; + return typeof filename === 'string' && filename ? toProjectRelativePath(filename) : undefined; + } catch { + return undefined; + } +} diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index f9367080f699..03cd83dbe8b6 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,7 +1,7 @@ import type { Integration } from '@sentry/core'; import { debug, getMainCarrier, GLOBAL_OBJ, SentryNonRecordingSpan, spanToJSON } from '@sentry/core'; import * as SentryReact from '@sentry/react'; -import { getClient, WINDOW } from '@sentry/react'; +import { close, getClient, getIsolationScope, WINDOW } from '@sentry/react'; import { JSDOM } from 'jsdom'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; @@ -243,6 +243,22 @@ describe('Client init()', () => { expect(init({})).not.toBeUndefined(); }); + it('registers its event processors on the client', () => { + const client = init({}); + + expect(client?.getEventProcessors().map(processor => processor.id)).toContain('NextRedirectErrorFilter'); + }); + + it('does not add event processors to the isolation scope after close()', async () => { + const isolationProcessorCount = getIsolationScope().getScopeData().eventProcessors.length; + + init({}); + await close(); + init({}); + + expect(getIsolationScope().getScopeData().eventProcessors.length).toBe(isolationProcessorCount); + }); + describe('environment option', () => { const originalEnv = process.env.SENTRY_ENVIRONMENT; const originalNodeEnv = process.env.NODE_ENV; diff --git a/packages/nextjs/test/common/liveRootSpanAdapter.test.ts b/packages/nextjs/test/common/liveRootSpanAdapter.test.ts index cd2f7a9fcac3..3e935f4fe8f9 100644 --- a/packages/nextjs/test/common/liveRootSpanAdapter.test.ts +++ b/packages/nextjs/test/common/liveRootSpanAdapter.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SentrySpan, spanToStaticSpanJSON } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SentrySpan, spanToStaticSpanJSON } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { createLiveRootSpanAdapter } from '../../src/common/utils/liveRootSpanAdapter'; @@ -12,7 +12,7 @@ describe('createLiveRootSpanAdapter', () => { expect(adapter.attributes.foo).toBe('bar'); adapter.setOp('http.server'); - expect(spanToStaticSpanJSON(span).data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(spanToStaticSpanJSON(span).data[SENTRY_OP]).toBe('http.server'); }); it('renames the span without stamping source=custom (preserves an existing source)', () => { diff --git a/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts b/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts index 072893b8f53b..e0e2c482b737 100644 --- a/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts +++ b/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -21,6 +22,22 @@ describe('isNotFoundNavigationError', () => { }); }); +describe('getAuthInterruptStatusCode', () => { + it.each([ + ['NEXT_HTTP_ERROR_FALLBACK;403', 403], + ['NEXT_HTTP_ERROR_FALLBACK;401', 401], + ])('returns the status for the %s digest', (digest, status) => { + expect(getAuthInterruptStatusCode(errorWithDigest(digest))).toBe(status); + expect(getAuthInterruptStatusCode(new Error('wrapped', { cause: errorWithDigest(digest) }))).toBe(status); + }); + + it('returns undefined for other errors', () => { + expect(getAuthInterruptStatusCode(errorWithDigest('NEXT_HTTP_ERROR_FALLBACK;404'))).toBeUndefined(); + expect(getAuthInterruptStatusCode(new Error('boom'))).toBeUndefined(); + expect(getAuthInterruptStatusCode({ digest: 'NEXT_HTTP_ERROR_FALLBACK;403' })).toBeUndefined(); + }); +}); + describe('isRedirectNavigationError', () => { it('detects a redirect digest', () => { expect(isRedirectNavigationError(errorWithDigest('NEXT_REDIRECT;/some-path'))).toBe(true); diff --git a/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts b/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts new file mode 100644 index 000000000000..8c730d180f62 --- /dev/null +++ b/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts @@ -0,0 +1,56 @@ +import type { IncomingMessage } from 'http'; +import { describe, expect, it } from 'vitest'; +import { pagesRouterRequestToRequestData } from '../../../src/common/utils/pagesRouterRequestToRequestData'; + +function createRequest(url: string, initURL?: unknown): IncomingMessage { + const req = { + method: 'GET', + url, + headers: { host: 'example.com' }, + socket: {}, + } as unknown as IncomingMessage; + + if (initURL !== undefined) { + (req as unknown as Record)[Symbol.for('NextInternalRequestMeta')] = { initURL }; + } + + return req; +} + +describe('pagesRouterRequestToRequestData', () => { + it('uses `req.url` when there is no Next.js request meta', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar?q=1'))).toMatchObject({ + url: 'http://example.com/foo/bar?q=1', + query_string: 'q=1', + method: 'GET', + }); + }); + + it('restores the basePath from a relative `initURL`', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar?q=1', '/base/foo/bar?q=1'))).toMatchObject({ + url: 'http://example.com/base/foo/bar?q=1', + query_string: 'q=1', + method: 'GET', + }); + }); + + it('restores the basePath for the basePath root', () => { + expect(pagesRouterRequestToRequestData(createRequest('/', '/base'))).toMatchObject({ + url: 'http://example.com/base', + }); + }); + + it('takes only path and query from an absolute `initURL` and keeps the origin from the request headers', () => { + expect( + pagesRouterRequestToRequestData(createRequest('/foo/bar', 'http://localhost:3000/base/foo/bar')), + ).toMatchObject({ + url: 'http://example.com/base/foo/bar', + }); + }); + + it('ignores a non-string `initURL`', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar', 123))).toMatchObject({ + url: 'http://example.com/foo/bar', + }); + }); +}); diff --git a/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts b/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts index aed2553c7baa..8556f5bda8df 100644 --- a/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts +++ b/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts @@ -40,6 +40,8 @@ describe('wrapServerComponentWithSentry', () => { it.each([ ['not-found', 'NEXT_NOT_FOUND'], ['redirect', 'NEXT_REDIRECT;/somewhere'], + ['forbidden', 'NEXT_HTTP_ERROR_FALLBACK;403'], + ['unauthorized', 'NEXT_HTTP_ERROR_FALLBACK;401'], ['hanging prerender promise', 'HANGING_PROMISE_REJECTION'], ['prerender interruption', 'NEXT_PRERENDER_INTERRUPTED'], ['dynamic server usage', 'DYNAMIC_SERVER_USAGE'], @@ -53,6 +55,18 @@ describe('wrapServerComponentWithSentry', () => { expect(captureException).not.toHaveBeenCalled(); }); + it.each([ + ['NEXT_HTTP_ERROR_FALLBACK;403', 'permission_denied'], + ['NEXT_HTTP_ERROR_FALLBACK;401', 'unauthenticated'], + ])('marks the active span for the %s auth interrupt', async (digest, message) => { + vi.spyOn(SentryCore, 'captureException').mockImplementation(() => ''); + const { setStatus } = mockActiveSpan({} as Span); + + await runWrapped(Object.assign(new Error('control flow'), { digest })); + + expect(setStatus).toHaveBeenCalledWith({ code: SentryCore.SPAN_STATUS_ERROR, message }); + }); + it.each(['NEXT_NOT_FOUND', 'NEXT_REDIRECT;/somewhere', 'HANGING_PROMISE_REJECTION'])( 'does not capture the %s control flow error when there is no active span', async digest => { diff --git a/packages/nextjs/test/edgeSdk.test.ts b/packages/nextjs/test/edgeSdk.test.ts index 8d4fee1f926e..2cbd0ef0070b 100644 --- a/packages/nextjs/test/edgeSdk.test.ts +++ b/packages/nextjs/test/edgeSdk.test.ts @@ -1,5 +1,6 @@ import type { Integration } from '@sentry/core'; -import { GLOBAL_OBJ } from '@sentry/core'; +import { getGlobalScope, getMainCarrier, GLOBAL_OBJ, withIsolationScope } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils'; import * as SentryVercelEdge from '@sentry/vercel-edge'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached'; @@ -20,7 +21,13 @@ describe('Edge init()', () => { }); afterEach(() => { + SentryVercelEdge.getCurrentScope().setClient(undefined); vi.clearAllMocks(); + vi.unstubAllGlobals(); + + getMainCarrier().__SENTRY__ = undefined; + + delete (process as { turbopack?: boolean }).turbopack; }); it('inits the Vercel Edge SDK', () => { @@ -52,6 +59,25 @@ describe('Edge init()', () => { ); }); + it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(vercelEdgeInit).not.toHaveBeenCalled(); + }); + + it('sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + (process as { turbopack?: boolean }).turbopack = true; + + withIsolationScope(() => init({})); + + expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true); + }); + describe('integrations', () => { // Options passed by `@sentry/nextjs`'s `init` to `@sentry/vercel-edge`'s `init` after modifying them type ModifiedInitOptions = { integrations?: Integration[]; defaultIntegrations: Integration[] }; diff --git a/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts b/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts index c25f9d79e650..743bb32b8d02 100644 --- a/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts +++ b/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts @@ -3,8 +3,8 @@ import { HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, HTTP_STATUS_CODE, + SENTRY_OP, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../../src/common/nextSpanAttributes'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../../src/common/span-attributes-with-logic-attached'; @@ -35,7 +35,7 @@ describe('enhanceHandleRequestRootSpan', () => { enhanceHandleRequestRootSpan(span); expect(getName()).toBe('GET /api/foo'); expect(getOp()).toBeUndefined(); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.attributes[SENTRY_OP]).toBeUndefined(); }); it('sets http.server op and source=route for parameterized routes', () => { @@ -50,7 +50,7 @@ describe('enhanceHandleRequestRootSpan', () => { enhanceHandleRequestRootSpan(span); expect(getOp()).toBe('http.server'); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span.attributes[SENTRY_OP]).toBe('http.server'); expect(getName()).toBe('GET /api/users/[id]'); expect(span.attributes[SENTRY_SEGMENT_NAME_SOURCE]).toBe('route'); expect(span.attributes[ATTR_NEXT_ROUTE]).toBe('/api/users/[id]'); @@ -192,14 +192,14 @@ describe('enhanceHandleRequestRootSpan', () => { name = n; }, setOp: (op: string) => { - attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op; + attributes[SENTRY_OP] = op; }, }; enhanceHandleRequestRootSpan(span); expect(name).toBe('middleware GET'); - expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('middleware'); + expect(attributes[SENTRY_OP]).toBe('middleware'); }); it('rewrites GET /_error using the http.target attribute', () => { diff --git a/packages/nextjs/test/server/useCacheInstrumentation.test.ts b/packages/nextjs/test/server/useCacheInstrumentation.test.ts index 51037a67c005..587b57bf9820 100644 --- a/packages/nextjs/test/server/useCacheInstrumentation.test.ts +++ b/packages/nextjs/test/server/useCacheInstrumentation.test.ts @@ -33,6 +33,7 @@ import { _instrumentUseCacheHandlers } from '../../src/server/useCacheInstrument const NEXT_CACHE_HANDLERS_MAP = Symbol.for('@next/cache-handlers-map'); const NEXT_PRIVATE_CACHE_HANDLER = Symbol.for('@next/cache-handlers-private'); +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); const SENTRY_CACHE_INSTRUMENTED = Symbol.for('sentry.nextjs.cacheHandlersInstrumented'); const SENTRY_WRAPPED_HANDLERS = Symbol.for('sentry.nextjs.wrappedCacheHandlers'); const SENTRY_CACHE_ORIGINS = Symbol.for('sentry.nextjs.cacheOrigins'); @@ -77,6 +78,7 @@ describe('instrumentUseCacheHandlers', () => { for (const symbol of [ NEXT_CACHE_HANDLERS_MAP, NEXT_PRIVATE_CACHE_HANDLER, + NEXT_MANIFESTS_SINGLETON, SENTRY_CACHE_INSTRUMENTED, SENTRY_WRAPPED_HANDLERS, SENTRY_CACHE_ORIGINS, @@ -484,6 +486,62 @@ describe('instrumentUseCacheHandlers', () => { }); }); + describe('source file on `cache.put`', () => { + const functionId = 'c05120808bb68f6400d039e720226869fb1f079019'; + const jsonCacheKey = JSON.stringify(['build-id', functionId, [['arg'], {}]]); + + function setManifest(filename: unknown): void { + setGlobal(NEXT_MANIFESTS_SINGLETON, { + serverActionsManifest: { node: { [functionId]: { filename } } }, + }); + } + + it('starts the `cache.put` span with `code.file.path` when the key parses and the manifest knows the function', async () => { + setManifest('app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx'); + const handler = installWithDefaultHandler(); + + await handler.set(jsonCacheKey, Promise.resolve({})); + + // In the start options (not set afterwards), so samplers and span processors see it. + expect(mocks.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'cache.put', + attributes: expect.objectContaining({ + 'code.file.path': 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + }), + }), + expect.any(Function), + ); + }); + + it('does not put `code.file.path` on `cache.get` spans', async () => { + setManifest('app/page.tsx'); + const handler = installWithDefaultHandler({ timestamp: nowMs() }); + + await handler.get(jsonCacheKey); + + expect(mocks.startSpan).not.toHaveBeenCalledWith( + expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }), + expect.any(Function), + ); + expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything()); + }); + + // Key parsing, manifest lookup, and path shortening are covered in `useCacheSourceFile.test.ts`. + it('still writes the entry and omits `code.file.path` when the key does not resolve', async () => { + setManifest('app/page.tsx'); + const handler = installWithDefaultHandler(); + + await expect(handler.set('multipart-encoded-key', Promise.resolve({}))).resolves.toBeUndefined(); + + expect(mocks.startSpan).not.toHaveBeenCalledWith( + expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }), + expect.any(Function), + ); + expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything()); + }); + }); + it('creates a `cache.put` span around handler writes', async () => { const handler = installWithDefaultHandler(); diff --git a/packages/nextjs/test/server/useCacheSourceFile.test.ts b/packages/nextjs/test/server/useCacheSourceFile.test.ts new file mode 100644 index 000000000000..16082bcf21b2 --- /dev/null +++ b/packages/nextjs/test/server/useCacheSourceFile.test.ts @@ -0,0 +1,236 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getCacheFunctionSourceFile } from '../../src/server/useCacheSourceFile'; + +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); + +const FUNCTION_ID = 'c05120808bb68f6400d039e720226869fb1f079019'; +const LAYOUT_FILE = 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx'; + +const stableKey = JSON.stringify(['build-id', FUNCTION_ID, [['arg'], { locale: 'en' }]]); +const canaryKey = JSON.stringify([FUNCTION_ID, [['arg'], { locale: 'en' }], 'implementation-hash']); +const rootPayload = JSON.stringify([ + 'build-id', + FUNCTION_ID, + [[], { children: { then: '$undefined' }, params: '$@1' }], +]); + +function setManifest(filename: unknown, runtime: 'node' | 'edge' = 'node', functionId: string = FUNCTION_ID): void { + (globalThis as Record)[NEXT_MANIFESTS_SINGLETON] = { + serverActionsManifest: { [runtime]: { [functionId]: { filename } } }, + }; +} + +// Mirrors `encodeFormData` in Next.js' `use-cache-wrapper.ts`: each field is +// `:`, lengths counting UTF-16 code units. +function multipartField(name: string, content: string): string { + return `${name.length.toString(16)}:${name}${content.length.toString(16)}:${content}`; +} + +describe('getCacheFunctionSourceFile', () => { + beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue('/repo/apps/web'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + Reflect.deleteProperty(globalThis, NEXT_MANIFESTS_SINGLETON); + }); + + describe('JSON cache keys', () => { + it.each([ + ['the stable key shape `[buildId, functionId, args]`', stableKey], + ['the canary key shape `[functionId, args, implementationHash]`', canaryKey], + ])('resolves the source file for %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(LAYOUT_FILE); + }); + + it.each([ + ['an empty array', '[]'], + ['a one-element array', JSON.stringify(['build-id'])], + ['a stable-shaped key whose function id is not a string', JSON.stringify(['build-id', 42, [['arg'], {}]])], + ['a canary-shaped key whose function id is not a string', JSON.stringify([42, [['arg'], {}], 'hash'])], + ['invalid JSON that starts like an array', '[truncated'], + ])('resolves no file for %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + }); + + describe('multipart cache keys', () => { + it.each([ + ['the key parts in the first field', multipartField('0', rootPayload) + multipartField('1', '{"id":"some-id"}')], + ['the key parts in a later field', multipartField('1', 'binary-ish :: data') + multipartField('0', rootPayload)], + [ + 'a preceding field whose content looks like framing', + multipartField('1', '3:abc') + multipartField('0', rootPayload), + ], + ['a preceding field with empty content', multipartField('1', '') + multipartField('0', rootPayload)], + [ + 'a preceding field with non-ASCII content counted in UTF-16 code units', + multipartField('1', 'héllo 😀 wörld') + multipartField('0', rootPayload), + ], + ])('resolves the source file for a key with %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(LAYOUT_FILE); + }); + + it.each([ + ['an empty key', ''], + ['a field length that is not hex', `x:0${rootPayload}`], + ['an uppercase field length (Next.js emits lowercase)', `A:aaaaaaaaaa${multipartField('0', rootPayload)}`], + ['a field length pointing past the end of the key', `1:0ffff:${rootPayload}`], + ['a key truncated inside a field', multipartField('0', rootPayload).slice(0, 10)], + ['a field name without a content field', '1:0'], + ['no field named "0"', multipartField('1', '{"id":"some-id"}')], + ['an empty field named "0"', multipartField('0', '') + multipartField('1', '{"id":"some-id"}')], + ['key parts that are a JSON object instead of an array', multipartField('0', '{"not":"an array"}')], + ])('resolves no file for a key with %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + }); + + describe('manifest lookup', () => { + it('resolves no file when the manifest singleton is missing', () => { + expect(getCacheFunctionSourceFile(stableKey)).toBeUndefined(); + }); + + it.each([ + [ + 'the manifest does not know the function id', + (): void => setManifest(LAYOUT_FILE), + JSON.stringify(['build-id', 'other-id', []]), + ], + ['the manifest entry has no filename', (): void => setManifest(undefined), stableKey], + ['the manifest filename is not a string', (): void => setManifest(42), stableKey], + ['the manifest filename is empty', (): void => setManifest(''), stableKey], + ])('resolves no file when %s', (_label, prepare, cacheKey) => { + prepare(); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + + it('reads the edge section of the manifest on the edge runtime', () => { + vi.stubEnv('NEXT_RUNTIME', 'edge'); + setManifest(LAYOUT_FILE, 'edge'); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(LAYOUT_FILE); + }); + + it('ignores the node section of the manifest on the edge runtime', () => { + vi.stubEnv('NEXT_RUNTIME', 'edge'); + setManifest(LAYOUT_FILE, 'node'); + + expect(getCacheFunctionSourceFile(stableKey)).toBeUndefined(); + }); + + it('reads the node section of the manifest when `NEXT_RUNTIME` is `nodejs`', () => { + vi.stubEnv('NEXT_RUNTIME', 'nodejs'); + setManifest(LAYOUT_FILE, 'node'); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(LAYOUT_FILE); + }); + }); + + describe('project-relative paths', () => { + it.each([ + [ + 'strips a monorepo prefix that matches the cwd tail', + '/repo/dev-packages/e2e-tests/test-applications/my-app', + 'dev-packages/e2e-tests/test-applications/my-app/app/(group)/page.tsx', + 'app/(group)/page.tsx', + ], + ['keeps a `src/app` layout intact', '/repo/apps/web', 'apps/web/src/app/page.tsx', 'src/app/page.tsx'], + [ + 'is not confused by an `app` directory inside the prefix', + '/repo/packages/app', + 'packages/app/app/page.tsx', + 'app/page.tsx', + ], + ['strips the longest match when the cwd tail repeats', '/repo/a/b/a/b', 'a/b/a/b/page.tsx', 'page.tsx'], + ['splits a Windows cwd on backslashes', 'C:\\repo\\apps\\web', 'apps/web/app/page.tsx', 'app/page.tsx'], + ['ignores a trailing separator on the cwd', '/repo/apps/web/', 'apps/web/app/page.tsx', 'app/page.tsx'], + [ + 'keeps a file outside the app directory', + '/repo/apps/web', + 'apps/web/lib/cached-queries.ts', + 'lib/cached-queries.ts', + ], + [ + 'keeps the full path when the cwd does not match', + '/somewhere/else', + 'dev-packages/my-app/app/page.tsx', + 'dev-packages/my-app/app/page.tsx', + ], + ['never strips the whole path', '/repo/my-app', 'my-app', 'my-app'], + ])('%s', (_label, cwd, filename, expected) => { + vi.spyOn(process, 'cwd').mockReturnValue(cwd); + setManifest(filename); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(expected); + }); + }); + + // Keys and manifest filenames captured verbatim from the `nextjs-16-cacheComponents` e2e app + // (Next 16.3 dev + prod, 16.4.0-canary.57 prod) on 2026-10-02; only the machine-specific part + // of the cwd is generalized. See `_proj-cache-components/research/cache-key-capture-howto.md`. + describe('real-world keys', () => { + const APP_PREFIX = 'dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents'; + + it.each([ + [ + 'a dev JSON key with four key parts', + '["development","80c108a36ae1b809cefcec4ecb7ede5df718f8b93a",[],"0"]', + '80c108a36ae1b809cefcec4ecb7ede5df718f8b93a', + 'app/cache/page.tsx', + ], + [ + 'a dev multipart layout key', + '1:063:["development","c0c36245c7b284678b232881c45d061cc5ce45f174",[{"params":"$@1","children":"$T"}],"0"]1:1c:{"id":"123"}', + 'c0c36245c7b284678b232881c45d061cc5ce45f174', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a prod JSON key (stable shape)', + '["8SLlD3QlHgVCUdTRW5hzL","c02767c889ce99f14e39a7637aae5cfabf6a939d9a",["default-id"]]', + 'c02767c889ce99f14e39a7637aae5cfabf6a939d9a', + 'app/use-cache-page/page.tsx', + ], + [ + 'a prod multipart layout key (stable shape)', + '1:069:["8SLlD3QlHgVCUdTRW5hzL","c0a941ad6a1202c965f53812b0e13854f898cba7c9",[{"params":"$@1","children":"$T"}]]1:1c:{"id":"123"}', + 'c0a941ad6a1202c965f53812b0e13854f898cba7c9', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a canary JSON key, third part an array rather than a hash string', + '["c02767c889ce99f14e39a7637aae5cfabf6a939d9a",["default-id"],["eWyFndmUdIGUjY7hj9qJJ"]]', + 'c02767c889ce99f14e39a7637aae5cfabf6a939d9a', + 'app/use-cache-page/page.tsx', + ], + [ + 'a canary multipart layout key', + '1:06b:["c0a941ad6a1202c965f53812b0e13854f898cba7c9",[{"params":"$@1","children":"$T"}],["eWyFndmUdIGUjY7hj9qJJ"]]1:1c:{"id":"123"}', + 'c0a941ad6a1202c965f53812b0e13854f898cba7c9', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a canary route-handler key (route handlers have manifest entries on 16.4+)', + '["c06943f936da1e0a20710aae915fb68dbaa3527c74",["default-id"],["eWyFndmUdIGUjY7hj9qJJ"]]', + 'c06943f936da1e0a20710aae915fb68dbaa3527c74', + 'app/api/use-cache/route.ts', + ], + ])('resolves %s', (_label, cacheKey, functionId, appRelativePath) => { + vi.spyOn(process, 'cwd').mockReturnValue(`/repo/${APP_PREFIX}`); + setManifest(`${APP_PREFIX}/${appRelativePath}`, 'node', functionId); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(appRelativePath); + }); + }); +}); diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 25ba979026b3..3ac99be0037f 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,6 +1,7 @@ import type { Integration } from '@sentry/core'; -import { GLOBAL_OBJ, getMainCarrier } from '@sentry/core'; -import { getCurrentScope } from '@sentry/node'; +import { GLOBAL_OBJ, getMainCarrier, withIsolationScope } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils'; +import { close, getCurrentScope, getGlobalScope } from '@sentry/node'; import * as SentryNode from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached'; @@ -18,10 +19,12 @@ function findIntegrationByName(integrations: Integration[] = [], name: string): describe('Server init()', () => { afterEach(() => { vi.clearAllMocks(); + vi.unstubAllGlobals(); getMainCarrier().__SENTRY__ = undefined; delete process.env.VERCEL; + delete (process as { turbopack?: boolean }).turbopack; }); it('inits the Node SDK', () => { @@ -67,6 +70,82 @@ describe('Server init()', () => { expect(nodeInit).toHaveBeenCalledTimes(1); }); + it('registers its event processors on the client', () => { + const client = init({}); + + expect(client?.getEventProcessors().map(processor => processor.id)).toContain('DropReactControlFlowErrors'); + }); + + it('does not add event processors to the global scope after close()', async () => { + const globalProcessorCount = getGlobalScope().getScopeData().eventProcessors.length; + + init({}); + await close(); + init({}); + + expect(getGlobalScope().getScopeData().eventProcessors.length).toBe(globalProcessorCount); + }); + + it('returns the existing client if already initialized', () => { + const first = init({}); + const second = init({}); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(nodeInit).not.toHaveBeenCalled(); + }); + + it('sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + (process as { turbopack?: boolean }).turbopack = true; + + withIsolationScope(() => init({})); + + expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true); + }); + + it('adds its event processors to the global scope once on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => { + init({}); + init({}); + }); + + expect( + getGlobalScope() + .getScopeData() + .eventProcessors.filter(processor => processor.id === 'DropReactControlFlowErrors'), + ).toHaveLength(1); + }); + + it('inits on Cloudflare Workers outside of a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + init({}); + + expect(nodeInit).toHaveBeenCalledTimes(1); + }); + + it('inits outside of Cloudflare Workers when an AsyncLocalStorage strategy is installed', () => { + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(nodeInit).toHaveBeenCalledTimes(1); + }); + // TODO: test `vercel` tag when running on Vercel // Can't just add the test and set env variables, since the value in `index.server.ts` // is resolved when importing. @@ -154,6 +233,16 @@ describe('Server init()', () => { }); }); + it('initializes again after the existing client is closed', async () => { + const first = init({}); + await first?.close(); + const second = init({}); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); diff --git a/packages/nitro/package.json b/packages/nitro/package.json index bee0d4e70df6..7ef2e39fd3d9 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/nitro/src/runtime/hooks/captureStorageEvents.ts b/packages/nitro/src/runtime/hooks/captureStorageEvents.ts index de1b9399efdf..2b768d93226d 100644 --- a/packages/nitro/src/runtime/hooks/captureStorageEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureStorageEvents.ts @@ -1,5 +1,5 @@ import * as dc from 'node:diagnostics_channel'; -import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import { CACHE_OPERATION_NAMES, @@ -7,7 +7,6 @@ import { GLOBAL_OBJ, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; @@ -95,7 +94,7 @@ function setupStorageTracingChannel(operation: TracedOperation): void { attributes: { [SENTRY_OP]: OPERATION_SPAN_OPS[operation], [CACHE_OPERATION]: cacheOperationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [CACHE_KEY]: cacheKeys.length > 1 ? cacheKeys : cacheKeys[0], 'db.operation.name': operation, 'db.collection.name': mountBase(data), diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts index da1f517acc7c..2752e2f63d79 100644 --- a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts @@ -1,5 +1,5 @@ import * as dc from 'node:diagnostics_channel'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import { isObjectLike, @@ -12,7 +12,6 @@ import { httpHeadersToSpanAttributes, HTTP_SPAN_NAME_FALLBACK, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, type Span, startInactiveSpan, @@ -121,7 +120,7 @@ function setupH3TracingChannels(): void { : spanName, attributes: { ...urlAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.h3', + [SENTRY_ORIGIN]: 'auto.http.nitro.h3', [SENTRY_OP]: data?.type === 'middleware' ? MIDDLEWARE : HTTP_SERVER, }, }); @@ -199,7 +198,7 @@ function setupSrvxTracingChannels(): void { attributes: { ...urlAttributes, ...headerAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SENTRY_ORIGIN]: 'auto.http.nitro.srvx', [SENTRY_OP]: data.middleware ? MIDDLEWARE : HTTP_SERVER, 'server.port': data.server.options.port, }, @@ -238,7 +237,7 @@ function setupSrvxTracingChannels(): void { name: `${data.middleware?.handler.name ?? 'unknown'} - ${data.request.method} ${data.request._url?.pathname}`, attributes: { ...urlAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SENTRY_ORIGIN]: 'auto.http.nitro.srvx', [SENTRY_OP]: MIDDLEWARE, }, parentSpan: requestParentSpans.get(data.request) || undefined, diff --git a/packages/node/package.json b/packages/node/package.json index ac2983f9092c..2e1a8d5c71ba 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -78,7 +78,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/opentelemetry": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index deed6c7c0b65..1a1105557d9f 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -113,7 +113,9 @@ export { setAttributes, setUser, setConversationId, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index c2effd4a23f1..d341a3d9ffa3 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -14,12 +14,10 @@ * `error.type` (the syscall error code, e.g. `ENOENT`). */ -import { ERROR_TYPE } from '@sentry/conventions/attributes'; +import { ERROR_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { FILE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, getActiveSpan, startInactiveSpan, @@ -101,8 +99,8 @@ function getSpanAttributes( config: FsInstrumentationConfig, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: SPAN_OP, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN, + [SENTRY_OP]: SPAN_OP, + [SENTRY_ORIGIN]: SPAN_ORIGIN, }; if (!config.recordFilePaths) { diff --git a/packages/node/src/integrations/http/httpServerSpansIntegration.ts b/packages/node/src/integrations/http/httpServerSpansIntegration.ts index 4712bdd61e7a..b690854c87d2 100644 --- a/packages/node/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node/src/integrations/http/httpServerSpansIntegration.ts @@ -24,6 +24,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { Event, Integration, IntegrationFn, Span, SpanAttributes, SpanStatus } from '@sentry/core'; @@ -39,7 +40,6 @@ import { httpHeadersToSpanAttributes, getContentLengthFromHeaders, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, stripUrlQueryAndFragment, isTracingSuppressed, @@ -168,7 +168,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions [SENTRY_KIND]: 'server', [SENTRY_OP]: HTTP_SERVER, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', + [SENTRY_ORIGIN]: 'auto.http.http_server', [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, [URL_FULL]: filterCollectedUrl(fullUrl, client), [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, diff --git a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts index 0def1c3be01d..4b9f9f82324c 100644 --- a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts +++ b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts @@ -31,7 +31,6 @@ import { parseUrl, safeCallback, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, stripDataUrlContent, @@ -59,6 +58,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; @@ -220,7 +220,7 @@ function onRequestCreated(config: NodeFetchOptions, { request }: RequestMessage) [URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(requestUrl.search)), [URL_FRAGMENT]: getUrlFragment(requestUrl.hash), [URL_SCHEME]: urlScheme, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node_fetch', + [SENTRY_ORIGIN]: 'auto.http.node_fetch', }; // Sanitize data URLs to prevent long base64 strings in span attributes diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index b740fa3e6612..57cf3871256a 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -1,5 +1,6 @@ import type { Integration, Options } from '@sentry/core'; import { + _INTERNAL_warnIfClientIsActive, applySdkMetadata, consoleSandbox, conversationIdIntegration, @@ -40,6 +41,7 @@ import { getSpotlightConfig } from '../utils/spotlight'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initOpenTelemetry } from './initOtel'; +import { setupVercelKeepAlive } from './vercel'; /** * Get the base default integrations shared by all Node SDK default-integration sets. @@ -161,6 +163,8 @@ function _init( const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl); + _INTERNAL_warnIfClientIsActive(); + const scope = getCurrentScope(); scope.update(clientOptions.initialScope); @@ -189,13 +193,8 @@ function _init( updateScopeFromEnvVariables(); - // Ensure we flush events when vercel functions are ended - // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal if (process.env.VERCEL) { - process.on('SIGTERM', async () => { - // We have 500ms for processing here, so we try to make sure to have enough time to send the events - await client.flush(200); - }); + setupVercelKeepAlive(client); } // Add Node SDK specific OpenTelemetry setup. `setupEventContextTrace` reads the active span from the diff --git a/packages/node/src/sdk/vercel.ts b/packages/node/src/sdk/vercel.ts new file mode 100644 index 000000000000..9a18c5e1c9a2 --- /dev/null +++ b/packages/node/src/sdk/vercel.ts @@ -0,0 +1,86 @@ +import type { Client, Span } from '@sentry/core'; +import { getActiveSpan, getRootSpan, GLOBAL_OBJ } from '@sentry/core'; +import type { HttpServerResponse } from '@sentry/core/server'; +import { _INTERNAL_safeUnref } from '@sentry/core/server'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; + +// On Vercel, `http.server.request.start` fires without the request context, while this channel fires with it. +const HTTP_ON_SERVER_RESPONSE_FINISH = 'http.server.response.finish'; + +// Some frameworks end the request's root span shortly after the response closes. +const REQUEST_END_TIMEOUT_MS = 2000; + +interface VercelRequestContextGlobal { + get?(): { waitUntil?: (task: Promise) => void } | undefined; +} + +/** + * Keeps Vercel Node.js functions alive until the SDK has sent the telemetry of each request. + * + * Vercel can suspend a function as soon as the response is sent, so buffered events, spans, logs and metrics + * (and the timers that flush them) arrive late or never. When a response + * finishes, we register one `waitUntil` that waits for the response to close and the request's root span to + * end (2 seconds at most), and then flushes the client. + */ +export function setupVercelKeepAlive(client: Client): void { + // Ensure we flush events when vercel functions are ended + // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal + process.on('SIGTERM', async () => { + // We have 500ms for processing here, so we try to make sure to have enough time to send the events + await client.flush(200); + }); + + subscribeDiagnosticsChannel(HTTP_ON_SERVER_RESPONSE_FINISH, message => { + const { response } = message as { response?: HttpServerResponse }; + const requestContextGlobal: VercelRequestContextGlobal | undefined = + // @ts-expect-error Vercel sets this global, so `GLOBAL_OBJ` does not type it + GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; + const requestContext = requestContextGlobal?.get?.(); + if (!response || !requestContext?.waitUntil) { + return; + } + + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan && getRootSpan(activeSpan); + + requestContext.waitUntil( + waitForRequestEnd(client, response, rootSpan, REQUEST_END_TIMEOUT_MS).then(() => client.flush(2000)), + ); + }); +} + +function waitForRequestEnd( + client: Client, + response: HttpServerResponse, + rootSpan: Span | undefined, + timeout: number, +): Promise { + return new Promise(resolve => { + let responseClosed = false; + // Ended and unsampled spans are not recording. + let rootSpanEnded = !rootSpan?.isRecording(); + + const done = (): void => { + clearTimeout(timer); + unsubscribe(); + resolve(); + }; + const doneIfRequestEnded = (): void => { + if (responseClosed && rootSpanEnded) { + done(); + } + }; + + const timer = _INTERNAL_safeUnref(setTimeout(done, timeout)); + const unsubscribe = client.on('spanEnd', span => { + if (span === rootSpan) { + rootSpanEnded = true; + doneIfRequestEnded(); + } + }); + response.once('close', () => { + responseClosed = true; + doneIfRequestEnded(); + }); + }); +} diff --git a/packages/node/src/utils/addOriginToSpan.ts b/packages/node/src/utils/addOriginToSpan.ts index 2a23710fa7cf..afb1c8872aaf 100644 --- a/packages/node/src/utils/addOriginToSpan.ts +++ b/packages/node/src/utils/addOriginToSpan.ts @@ -1,8 +1,8 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Span } from '@opentelemetry/api'; import type { SpanOrigin } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; /** Adds an origin to an OTEL Span. */ export function addOriginToSpan(span: Span, origin: SpanOrigin): void { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin); + span.setAttribute(SENTRY_ORIGIN, origin); } diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index a4bf5e3281ff..681c988eef10 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { ErrorEvent, TransactionEvent } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Sentry from '../../src'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -38,7 +37,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -168,7 +167,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -460,7 +459,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, () => { diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index f278ccc726d7..807b16ac5468 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -10,8 +10,8 @@ import { URL_PATH, URL_QUERY, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { channel } from 'node:diagnostics_channel'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import type { UndiciRequest } from '../../src/integrations/node-fetch/types'; @@ -70,7 +70,7 @@ describe('instrumentUndici', () => { [URL_QUERY]: 'limit=10', [URL_FRAGMENT]: undefined, [URL_SCHEME]: 'https', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node_fetch', + [SENTRY_ORIGIN]: 'auto.http.node_fetch', [SERVER_ADDRESS]: 'api.example.com', [SERVER_PORT]: 443, }, diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 3a9af657e3d9..1063c856c21a 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,9 +1,9 @@ import type { Integration } from '@sentry/core'; -import { debug, SDK_VERSION } from '@sentry/core'; +import { debug, originalConsoleMethods, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import * as SentryServerUtils from '@sentry/server-utils'; import { afterEach, beforeEach, describe, expect, it, type Mock, type MockInstance, vi } from 'vitest'; -import { getClient, NodeClient } from '../../src/'; +import { close, getClient, NodeClient } from '../../src/'; import { init } from '../../src/sdk'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -283,6 +283,45 @@ describe('init()', () => { expect(client).toBeInstanceOf(NodeClient); }); + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + it('warns and replaces the active client', () => { + const first = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + const second = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + it('does not warn after close()', async () => { + init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + await close(); + const second = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + }); + it('registers a SIGTERM handler on Vercel', () => { const originalVercelEnv = process.env.VERCEL; process.env.VERCEL = '1'; diff --git a/packages/node/test/sdk/vercel.test.ts b/packages/node/test/sdk/vercel.test.ts new file mode 100644 index 000000000000..211feef8bc60 --- /dev/null +++ b/packages/node/test/sdk/vercel.test.ts @@ -0,0 +1,170 @@ +import { EventEmitter } from 'node:events'; +import type { Client, Span } from '@sentry/core'; +import type * as SentryCore from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; +import type * as SentryServerUtils from '@sentry/server-utils'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { setupVercelKeepAlive } from '../../src/sdk/vercel'; + +vi.mock('@sentry/core', async importOriginal => ({ + ...(await importOriginal()), + getActiveSpan: vi.fn(), +})); + +vi.mock('@sentry/server-utils', async importOriginal => ({ + ...(await importOriginal()), + subscribeDiagnosticsChannel: vi.fn(), +})); + +const REQUEST_CONTEXT = Symbol.for('@vercel/request-context'); + +function createClient() { + const spanEndCallbacks = new Set<(span: Span) => void>(); + return { + on: vi.fn((_hook: 'spanEnd', callback: (span: Span) => void) => { + spanEndCallbacks.add(callback); + return () => spanEndCallbacks.delete(callback); + }), + flush: vi.fn().mockResolvedValue(true), + endSpan: (span: Span) => spanEndCallbacks.forEach(callback => callback(span)), + }; +} + +function setActiveRootSpan(): Span { + const span = { isRecording: () => true } as unknown as Span; + vi.mocked(getActiveSpan).mockReturnValue(span); + return span; +} + +function finishResponse() { + const response = new EventEmitter(); + const [, onMessage] = vi.mocked(subscribeDiagnosticsChannel).mock.calls.at(-1)!; + onMessage({ request: new EventEmitter(), response }, 'http.server.response.finish'); + return response; +} + +describe('setupVercelKeepAlive', () => { + let waitUntil: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + waitUntil = vi.fn(); + (globalThis as any)[REQUEST_CONTEXT] = { get: () => ({ waitUntil }) }; + vi.spyOn(process, 'on').mockImplementation(() => process); + }); + + afterEach(() => { + (globalThis as any)[REQUEST_CONTEXT] = undefined; + vi.mocked(getActiveSpan).mockReturnValue(undefined); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('registers exactly one waitUntil per finished response', () => { + setupVercelKeepAlive(createClient() as unknown as Client); + + // Vercel does not publish `http.server.request.start` for function requests. + expect(subscribeDiagnosticsChannel).toHaveBeenCalledWith('http.server.response.finish', expect.any(Function)); + + finishResponse(); + expect(waitUntil).toHaveBeenCalledTimes(1); + + finishResponse(); + expect(waitUntil).toHaveBeenCalledTimes(2); + }); + + it('keeps the task pending until the response closes, then flushes the client once', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + const response = finishResponse(); + const task = waitUntil.mock.calls[0]![0] as Promise; + const settled = vi.fn(); + void task.then(settled); + + await vi.advanceTimersByTimeAsync(0); + expect(settled).not.toHaveBeenCalled(); + expect(client.flush).not.toHaveBeenCalled(); + + response.emit('close'); + await task; + + expect(client.flush).toHaveBeenCalledTimes(1); + expect(client.flush).toHaveBeenCalledWith(2000); + }); + + it('waits for the root span to end after the response closes, then flushes', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + const rootSpan = setActiveRootSpan(); + + const response = finishResponse(); + const task = waitUntil.mock.calls[0]![0] as Promise; + + response.emit('close'); + await vi.advanceTimersByTimeAsync(0); + expect(client.flush).not.toHaveBeenCalled(); + + client.endSpan(rootSpan); + await task; + + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('flushes after 2 seconds when the root span does not end', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + setActiveRootSpan(); + + const response = finishResponse(); + response.emit('close'); + + await vi.advanceTimersByTimeAsync(1999); + expect(client.flush).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1); + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('flushes after 2 seconds when the response does not close', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + finishResponse(); + + await vi.advanceTimersByTimeAsync(1999); + expect(client.flush).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1); + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('does nothing without a request context', () => { + (globalThis as any)[REQUEST_CONTEXT] = undefined; + setupVercelKeepAlive(createClient() as unknown as Client); + + finishResponse(); + + expect(waitUntil).not.toHaveBeenCalled(); + }); + + it('does nothing when the request context has no waitUntil', () => { + (globalThis as any)[REQUEST_CONTEXT] = { get: () => ({}) }; + setupVercelKeepAlive(createClient() as unknown as Client); + + expect(() => finishResponse()).not.toThrow(); + expect(waitUntil).not.toHaveBeenCalled(); + }); + + it('does not depend on a span', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + const response = finishResponse(); + response.emit('close'); + await (waitUntil.mock.calls[0]![0] as Promise); + + expect(client.flush).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 84bde2dc6d39..cdfefb544c1e 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -56,7 +56,7 @@ "@nuxt/kit": "^4.5.2", "@sentry/browser": "11.4.0", "@sentry/cloudflare": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/bundler-plugins": "11.4.0", diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index f5adf02ea0eb..270654bb9ff8 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -1,11 +1,10 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import { captureException, debug, getClient, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, type SpanAttributes, @@ -134,7 +133,7 @@ function withSpan( captureException(error, { mechanism: { handled: false, - type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + type: attributes[SENTRY_ORIGIN], }, }); @@ -166,7 +165,7 @@ function getSpanAttributes( const attributes: SpanAttributes = { [SENTRY_OP]: MIDDLEWARE, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nuxt', + [SENTRY_ORIGIN]: 'auto.middleware.nuxt', 'nuxt.middleware.name': middlewareName, 'nuxt.middleware.hook.name': hookName ?? 'handler', }; diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 8584d7ec123c..71149f91ff19 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -1,4 +1,4 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import { addBreadcrumb, @@ -7,7 +7,6 @@ import { debug, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, SPAN_STATUS_ERROR, startSpan, @@ -31,7 +30,7 @@ const patchedStatement = new WeakSet(); /** * The Sentry origin for the database plugin. */ -const SENTRY_ORIGIN = 'auto.db.nuxt'; +const NUXT_DATABASE_ORIGIN = 'auto.db.nuxt'; /** * Creates the Nitro database plugin setup by instrumenting the configured database instances. @@ -230,7 +229,7 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; di captureException(error, { mechanism: { handled: false, - type: SENTRY_ORIGIN, + type: NUXT_DATABASE_ORIGIN, }, }); @@ -275,7 +274,7 @@ function createStartSpanOptions( attributes: { [DB_QUERY_TEXT]: queryText, [DB_QUERY_SUMMARY]: querySummary, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, + [SENTRY_ORIGIN]: NUXT_DATABASE_ORIGIN, [SENTRY_OP]: DB_QUERY, ...data, }, diff --git a/packages/nuxt/src/runtime/utils/instrumentStorage.ts b/packages/nuxt/src/runtime/utils/instrumentStorage.ts index 7d6cc98177fc..1480c1b7b32d 100644 --- a/packages/nuxt/src/runtime/utils/instrumentStorage.ts +++ b/packages/nuxt/src/runtime/utils/instrumentStorage.ts @@ -1,4 +1,4 @@ -import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import { CACHE_OPERATION_NAMES, @@ -7,7 +7,6 @@ import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, type SpanAttributes, @@ -187,7 +186,7 @@ function createMethodWrapper( captureException(error, { mechanism: { handled: false, - type: options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + type: options.attributes?.[SENTRY_ORIGIN], }, }); @@ -239,7 +238,7 @@ function createSpanStartOptions( const attributes: SpanAttributes = { [SENTRY_OP]: cacheOperation, [CACHE_OPERATION]: cacheOperationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [CACHE_KEY]: keys.length > 1 ? keys : keys[0], 'db.operation.name': methodName, 'db.collection.name': mountBase.replace(/:$/, ''), diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index ec309246f176..0fc7abad204d 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,13 +1,12 @@ import * as path from 'node:path'; import type { Client, Event, EventProcessor } from '@sentry/core'; import { + _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox, debug, DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT, - getClient, - getGlobalScope, } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -34,14 +33,16 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { // Since the server config is bundled into the Nitro build, a `node --import` preload of a config // file initializes the SDK a second time. The first init wins so a preload keeps its semantics. - if (isNuxtServerInitialized()) { + // A closed client does not count, so `close()` lets a later init set up a new one. + const existingClient = _INTERNAL_getActiveClient(); + if (isNuxtServerInitialized() && existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( '[Sentry] The Sentry server SDK is already initialized, skipping a second initialization. The Sentry server config is bundled into the Nitro server build, so a `node --import` preload of the config file is no longer needed and can be removed.', ); }); - return getClient(); + return existingClient; } let isDevBuild = false; @@ -66,8 +67,10 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { markNuxtServerInitialized(); } - getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); - getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); + // On the client, not the global scope, so a later `init()` after + // `close()` does not stack another copy. + client?.addEventProcessor(lowQualityTransactionsFilter(options)); + client?.addEventProcessor(clientSourceMapErrorFilter(options)); return client; } diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index de7d7bb00bdc..d03c725e6c8c 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -22,7 +22,7 @@ export type SourceMapSetting = boolean | 'hidden' | 'inline'; export function setupSourceMaps( moduleOptions: SentryNuxtModuleOptions, nuxt: Nuxt, - addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void, + addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean; server?: boolean }) => void, ): void { // Warn here rather than in `getPluginOptions`, which runs once per bundler (Vite and Nitro's // Rollup) and would emit the same warning twice. @@ -73,13 +73,16 @@ export function setupSourceMaps( }); if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) { + // Also on the SSR build: Nitro's source maps reach the original files only through that build's maps. + addVitePlugin([validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled })], { + dev: false, + build: true, + }); + addVitePlugin( - [ - validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }), - // Vite plugin is added on the client and server side (plugin runs for both builds) - ...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), - ], - { dev: false, build: true }, // Only add source map plugin during build + sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), + // Nitro bundles the SSR output again, so debug IDs from the SSR build would point to the wrong maps. + { dev: false, build: true, server: false }, ); } @@ -162,7 +165,7 @@ export function getPluginOptions( sourcemaps: { disable: moduleOptions.sourcemaps?.disable, // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') - // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that source maps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). + // We cannot determine automatically how the build folder looks like (depends on the preset), so each plugin uploads its own build output (the vitePlugin for the Nuxt client and the rollupPlugin for Nitro). // If we could know where the server/client assets are located, we could do something like this (based on the Nitro preset): isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], assets: sourcemapsOptions.assets ?? undefined, ignore: sourcemapsOptions.ignore ?? undefined, diff --git a/packages/nuxt/test/client/sdk.test.ts b/packages/nuxt/test/client/sdk.test.ts index af285e4adedc..0f9ea837853f 100644 --- a/packages/nuxt/test/client/sdk.test.ts +++ b/packages/nuxt/test/client/sdk.test.ts @@ -9,6 +9,7 @@ describe('Nuxt Client SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + SentryBrowser.getCurrentScope().setClient(undefined); }); it('Adds Nuxt metadata to the SDK options', () => { diff --git a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts index fec51364d935..9d6a84fa318f 100644 --- a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts +++ b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, SentrySpan } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, SentrySpan } from '@sentry/core'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { updateRouteBeforeResponse } from '../../../src/runtime/hooks/updateRouteBeforeResponse'; @@ -15,7 +15,7 @@ vi.mock(import('@sentry/core'), async importOriginal => { describe('updateRouteBeforeResponse', () => { const mockRootSpan = new SentrySpan({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockRootSpan.updateName = vi.fn(); diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index d7fb10fe2eef..c91f26abf59b 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -1,3 +1,4 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import * as SentryCoreServer from '@sentry/core/server'; import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; @@ -187,7 +188,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'object-middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'object-middleware', }), }), @@ -218,7 +219,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'request-middleware.onRequest', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'request-middleware', 'nuxt.middleware.hook.name': 'onRequest', }), @@ -305,7 +306,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'response-middleware.onBeforeResponse', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'response-middleware', 'nuxt.middleware.hook.name': 'onBeforeResponse', }), @@ -517,7 +518,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'v2-middleware.middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'v2-middleware', 'nuxt.middleware.hook.name': 'middleware', 'nuxt.middleware.hook.index': 0, @@ -596,7 +597,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'api-middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'api-middleware', 'http.request.method': 'GET', 'http.route': '/test-path', diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index ba2c5f82818c..872dc5aeaaa3 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,7 +1,7 @@ -import type { Event, EventProcessor } from '@sentry/core'; +import type { Event } from '@sentry/core'; import { originalConsoleMethods } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node'; +import { getGlobalScope, SDK_VERSION } from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NUXT_DEV_MODE_FLAG, NUXT_PRERENDER_FLAG, NUXT_SERVER_INITIALIZED_FLAG } from '../../src/common/devMode'; import { init } from '../../src/server'; @@ -16,6 +16,7 @@ describe('Nuxt Server SDK', () => { // Each test needs a fresh init; the double-init guard would otherwise skip every later call. delete (globalThis as { __SENTRY_NUXT_SERVER_INITIALIZED__?: boolean }).__SENTRY_NUXT_SERVER_INITIALIZED__; delete (globalThis as { __SENTRY_NUXT_PRERENDER__?: boolean }).__SENTRY_NUXT_PRERENDER__; + SentryNode.getCurrentScope().setClient(undefined); }); it('Adds Nuxt metadata to the SDK options', () => { @@ -82,6 +83,26 @@ describe('Nuxt Server SDK', () => { } }); + it('initializes again after close()', async () => { + const firstClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await SentryNode.close(); + const secondClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(secondClient).toBeDefined(); + expect(secondClient).not.toBe(firstClient); + }); + + it('initializes again after the existing client is closed', async () => { + const firstClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await firstClient?.close(); + const secondClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(secondClient).toBeDefined(); + expect(secondClient).not.toBe(firstClient); + }); + it('marks a successful initialization for the double-init guard', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -256,22 +277,24 @@ describe('Nuxt Server SDK', () => { }); }); - it('registers an event processor', async () => { - let passedEventProcessors: EventProcessor[] = []; - const addEventProcessor = vi - .spyOn(getGlobalScope(), 'addEventProcessor') - .mockImplementation((eventProcessor: EventProcessor) => { - passedEventProcessors = [...passedEventProcessors, eventProcessor]; - return new Scope(); - }); - - init({ + it('registers its event processors on the client', () => { + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', }); - expect(addEventProcessor).toHaveBeenCalledTimes(2); - expect(passedEventProcessors[0]?.id).toEqual('NuxtLowQualityTransactionsFilter'); - expect(passedEventProcessors[1]?.id).toEqual('NuxtClientSourceMapErrorFilter'); + const ids = client?.getEventProcessors().map(processor => processor.id); + expect(ids).toContain('NuxtLowQualityTransactionsFilter'); + expect(ids).toContain('NuxtClientSourceMapErrorFilter'); + }); + + it('does not add event processors to the global scope after close()', async () => { + const globalProcessorCount = getGlobalScope().getScopeData().eventProcessors.length; + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await SentryNode.close(); + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(getGlobalScope().getScopeData().eventProcessors.length).toBe(globalProcessorCount); }); }); diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index 2ef55c0959ec..18f67df7ab40 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -22,16 +22,16 @@ vi.mock('@sentry/bundler-plugins/vite', () => ({ sentryVitePlugin: mockSentryVit vi.mock('@sentry/bundler-plugins/rollup', () => ({ sentryRollupPlugin: mockSentryRollupPlugin })); function createMockAddVitePlugin() { - let capturedPlugins: Plugin[] | null = null; + const capturedPlugins: Plugin[] = []; const mockAddVitePlugin = vi.fn((plugins: Plugin[]) => { - capturedPlugins = plugins; + capturedPlugins.push(...plugins); }); return { mockAddVitePlugin, - getCapturedPlugin: () => capturedPlugins?.[0] ?? null, - getCapturedPlugins: () => capturedPlugins, + getValidationPlugin: () => + capturedPlugins.find(plugin => plugin.name === 'sentry-nuxt-source-map-validation') ?? null, }; } @@ -121,15 +121,30 @@ describe('setupSourceMaps hooks', () => { }); describe('vite plugin registration', () => { - it('calls `addVitePlugin` when setupSourceMaps is called', async () => { + it('adds the source map validation plugin to the client and server builds', async () => { const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - expect(plugin?.name).toBe('sentry-nuxt-source-map-validation'); + expect(mockAddVitePlugin).toHaveBeenCalledWith( + [expect.objectContaining({ name: 'sentry-nuxt-source-map-validation' })], + { dev: false, build: true }, + ); + }); + + it('adds the Sentry Vite plugin to the client build only', async () => { + const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); + const { mockAddVitePlugin } = createMockAddVitePlugin(); + + setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); + + expect(mockAddVitePlugin).toHaveBeenCalledTimes(2); + expect(mockAddVitePlugin).toHaveBeenCalledWith([{ name: 'sentry-vite-plugin' }], { + dev: false, + build: true, + server: false, + }); }); it.each([ @@ -160,21 +175,6 @@ describe('setupSourceMaps hooks', () => { expect(mockAddVitePlugin).not.toHaveBeenCalled(); }); - - it.each([ - { label: 'server (SSR) build', buildConfig: { build: { ssr: true }, plugins: [] } }, - { label: 'client build', buildConfig: { build: { ssr: false }, plugins: [] } }, - ])('adds sentry vite plugin to vite config for $label in production', async () => { - const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugins } = createMockAddVitePlugin(); - - setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - - const plugins = getCapturedPlugins(); - expect(plugins).not.toBeNull(); - expect(plugins?.length).toBeGreaterThan(0); - expect(mockSentryVitePlugin).toHaveBeenCalled(); - }); }); describe('sentry vite plugin calls', () => { @@ -372,12 +372,12 @@ describe('setupSourceMaps hooks', () => { describe('debug logging', () => { it('logs a [Sentry] message in production mode', async () => { const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { (plugin.config as (config: UserConfig, env: ConfigEnv) => void)( { build: { ssr: false }, plugins: [] } as UserConfig, @@ -398,12 +398,12 @@ describe('setupSourceMaps hooks', () => { it('does not log a [Sentry] messages in prepare mode', async () => { const mockNuxt = createMockNuxt({ _prepare: true }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { (plugin.config as (config: UserConfig, env: ConfigEnv) => void)({ build: {}, plugins: [] } as UserConfig, { mode: 'production', diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index a4cd97e56a9d..6f8ba9677d45 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0" }, "peerDependencies": { diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 4ff73f163f0c..21f24a0f8555 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -1,16 +1,9 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { SpanContext } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; import { TraceState } from '../../src/utils/TraceState'; import type { Event, TransactionEvent } from '@sentry/core'; -import { - addBreadcrumb, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setTag, - startInactiveSpan, - startSpan, - withIsolationScope, -} from '@sentry/core'; +import { addBreadcrumb, setTag, startInactiveSpan, startSpan, withIsolationScope } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { SENTRY_TRACE_STATE_DSC } from '../../src/constants'; import { makeTraceState } from '../../src/utils/makeTraceState'; @@ -44,7 +37,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -168,7 +161,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, @@ -324,7 +317,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, @@ -438,7 +431,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 1b967dfc051a..448692f6a505 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,7 +1,7 @@ /* eslint-disable typescript/no-deprecated */ import type { Span, TimeInput } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Event, Scope } from '@sentry/core'; import { getCapturedScopesOnSpan, @@ -10,8 +10,6 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToJSON, startInactiveSpan, @@ -211,7 +209,7 @@ describe('trace', () => { span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); @@ -224,15 +222,15 @@ describe('trace', () => { op: 'my-op', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', }, }, span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'my-op', + [SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_OP]: 'my-op', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); }, @@ -257,7 +255,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', @@ -608,7 +606,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); @@ -618,7 +616,7 @@ describe('trace', () => { op: 'my-op', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', }, }); @@ -626,8 +624,8 @@ describe('trace', () => { expect(getSpanAttributes(span2)).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'my-op', + [SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_OP]: 'my-op', }); }); @@ -648,7 +646,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', @@ -981,7 +979,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', diff --git a/packages/react-router/package.json b/packages/react-router/package.json index db7af0bc228f..4042ab8a4ec0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -57,7 +57,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/react-router/src/client/createClientInstrumentation.ts b/packages/react-router/src/client/createClientInstrumentation.ts index faa39b4fbe75..fb77746120fc 100644 --- a/packages/react-router/src/client/createClientInstrumentation.ts +++ b/packages/react-router/src/client/createClientInstrumentation.ts @@ -9,7 +9,6 @@ import { GLOBAL_OBJ, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, SPAN_STATUS_ERROR, updateSpanName, @@ -32,6 +31,7 @@ import { SENTRY_OP, URL_FULL, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, MIDDLEWARE, NAVIGATION } from '@sentry/conventions/op'; @@ -119,7 +119,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': 'browser.popstate', }, }, @@ -164,7 +164,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': navigationType, }, }, @@ -211,7 +211,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': 'router.navigate', }, }, @@ -244,7 +244,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'fetcher', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the key. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -286,7 +286,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'clientLoader', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -320,7 +320,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'clientAction', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -356,7 +356,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: MIDDLEWARE, [CODE_FUNCTION_NAME]: 'clientMiddleware', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', 'react_router.route.id': routeId, 'http.route': routePattern, 'react_router.middleware.index': middlewareIndex, @@ -381,7 +381,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'lazy', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', }, }, async span => { diff --git a/packages/react-router/src/client/hydratedRouter.ts b/packages/react-router/src/client/hydratedRouter.ts index 70b8e195f090..a0bc90ff72e4 100644 --- a/packages/react-router/src/client/hydratedRouter.ts +++ b/packages/react-router/src/client/hydratedRouter.ts @@ -9,7 +9,6 @@ import { hasSpanStreamingEnabled, isThenable, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { DataRouter } from 'react-router'; @@ -22,7 +21,13 @@ import { resolveNavigateArg, updateSpanWithParameterizedRoute, } from './utils'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + URL_PATH, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { @@ -58,7 +63,7 @@ export function instrumentHydratedRouter(): void { // this event is for the currently active pageload normalizePathname(router.state.location.pathname) === normalizePathname(pageloadPath) ) { - pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.pageload.react_router'); + pageloadSpan.setAttribute(SENTRY_ORIGIN, 'auto.pageload.react_router'); updateSpanWithParameterizedRoute(pageloadSpan, router.state); } } @@ -187,7 +192,7 @@ function maybeCreateNavigationTransaction(name: string, url: string, source: 'ur attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router', + [SENTRY_ORIGIN]: 'auto.navigation.react_router', ...(source === 'route' ? { [URL_TEMPLATE]: name } : {}), }, }, diff --git a/packages/react-router/src/client/utils.ts b/packages/react-router/src/client/utils.ts index 94044c096c94..eeb029f44878 100644 --- a/packages/react-router/src/client/utils.ts +++ b/packages/react-router/src/client/utils.ts @@ -4,6 +4,7 @@ import { getClient, GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, filterCollectedUrl, } from '@sentry/core'; @@ -24,13 +25,13 @@ const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { * to avoid "[object Object]" transaction names. */ export function resolveNavigateArg(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } // Object `to` with pathname - const pathname = (target as Record).pathname; + const pathname = target.pathname; if (typeof pathname === 'string') { return pathname || '/'; } @@ -46,12 +47,12 @@ export function resolveNavigateArg(target: unknown): string { * destination the user is navigating to, including any query string. */ export function resolveNavigateUrl(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } - const { pathname, search, hash } = target as Record; + const { pathname, search, hash } = target; const path = typeof pathname === 'string' && pathname !== '' ? pathname : WINDOW.location?.pathname || '/'; return `${path}${typeof search === 'string' ? search : ''}${typeof hash === 'string' ? hash : ''}`; diff --git a/packages/react-router/src/cloudflare/index.ts b/packages/react-router/src/cloudflare/index.ts index 67db950b59fb..b94430728125 100644 --- a/packages/react-router/src/cloudflare/index.ts +++ b/packages/react-router/src/cloudflare/index.ts @@ -4,6 +4,10 @@ import { isPrerenderRequest } from '../server/serverBuild'; export * from '../client'; export { wrapSentryHandleRequest } from '../server/wrapSentryHandleRequest'; +export { + createSentryServerInstrumentation, + type CreateSentryServerInstrumentationOptions, +} from '../server/createServerInstrumentation'; /** * Injects Sentry trace meta tags into the HTML response by transforming the ReadableStream. diff --git a/packages/react-router/src/server/createServerInstrumentation.ts b/packages/react-router/src/server/createServerInstrumentation.ts index f6fdee0e0133..ebf16d85c87d 100644 --- a/packages/react-router/src/server/createServerInstrumentation.ts +++ b/packages/react-router/src/server/createServerInstrumentation.ts @@ -7,6 +7,7 @@ import { SENTRY_OP, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import { @@ -17,7 +18,6 @@ import { getRootSpan, hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, updateSpanName, @@ -82,7 +82,7 @@ export function createSentryServerInstrumentation( updateSpanName(existingRootSpan, unparameterizedName); existingRootSpan.setAttributes({ [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: filterCollectedUrl(info.request.url), [URL_PATH]: pathname, @@ -106,7 +106,7 @@ export function createSentryServerInstrumentation( name: unparameterizedName, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [HTTP_REQUEST_METHOD]: info.request.method, [URL_PATH]: pathname, @@ -156,7 +156,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'loader', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -191,7 +191,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'action', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -237,7 +237,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: MIDDLEWARE, [CODE_FUNCTION_NAME]: 'middleware', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', 'react_router.route.id': routeId, [HTTP_ROUTE]: routePattern, ...(middlewareName && { 'react_router.middleware.name': middlewareName }), @@ -264,7 +264,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'lazy', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', }, }, async span => { diff --git a/packages/react-router/src/server/wrapSentryHandleRequest.ts b/packages/react-router/src/server/wrapSentryHandleRequest.ts index f40d56cad735..2ffeb9102cbf 100644 --- a/packages/react-router/src/server/wrapSentryHandleRequest.ts +++ b/packages/react-router/src/server/wrapSentryHandleRequest.ts @@ -1,11 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { - getActiveSpan, - getCurrentScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - updateSpanName, -} from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { getActiveSpan, getCurrentScope, getRootSpan, updateSpanName } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import type { AppLoadContext, EntryContext, RouterContextProvider } from 'react-router'; import { registerServerBuildGlobal } from './serverBuild'; @@ -95,7 +89,7 @@ export function wrapSentryHandleRequest( rootSpan.setAttributes({ [HTTP_ROUTE]: routeName, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); } } diff --git a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts index 23744ed79722..3af80b186e14 100644 --- a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts +++ b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts @@ -67,7 +67,8 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo // check if release should be created if (release?.name) { try { - await sentry.release.create({ orgVersion: release.name }); + // The release API requires a project, and the one given to the SDK does not reach this call. + await sentry.release.create({ orgVersion: release.name, project }); } catch (error) { // eslint-disable-next-line no-console console.error('[Sentry] Could not create release', error); diff --git a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts index 6fa1f2ef1026..7fa8b1066cfb 100644 --- a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts +++ b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream'; -import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { getActiveSpan, getRootSpan, getTraceMetaTags, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { getActiveSpan, getRootSpan, getTraceMetaTags } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { getMetaTagTransformer } from '../../src/server/getMetaTagTransformer'; @@ -70,7 +70,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ [HTTP_ROUTE]: '/some-path', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); }); @@ -90,7 +90,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenLastCalledWith({ [HTTP_ROUTE]: '/', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); const nestedIndexContext = { @@ -109,7 +109,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenLastCalledWith({ [HTTP_ROUTE]: '/dashboard', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); }); diff --git a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts index 7f907d4e8aca..9bb18672ba3c 100644 --- a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts +++ b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts @@ -85,7 +85,10 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: 'v1.0.0' }); + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ + orgVersion: 'v1.0.0', + project: 'test-project', + }); }); it('resolves root-level BuildTimeOptionsBase options for release creation and source map upload', async () => { @@ -115,7 +118,7 @@ describe('sentryOnBuildEnd', () => { url: undefined, headers: undefined, }); - expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: '1.2.3' }); + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: '1.2.3', project: 'my-project' }); expect(mockSentrySdkInstance.sourcemap.upload).toHaveBeenCalledWith({ directory: '/build', release: '1.2.3', diff --git a/packages/react/package.json b/packages/react/package.json index 0addf1426f26..155b05373528 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,7 +59,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "react": "17.x || 18.x || 19.x", diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 7f2e07aaf9ec..508d9e508bdf 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -2,7 +2,6 @@ import { getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, timestampInSeconds, UI_MOUNT_SPAN_NAME_FALLBACK, @@ -10,7 +9,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, withActiveSpan, } from '@sentry/core'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_UPDATE } from '@sentry/conventions/op'; import * as React from 'react'; import { hoistNonReactStatics } from './hoist-non-react-statics'; @@ -66,7 +65,7 @@ class Profiler extends React.Component { onlyIfParent: true, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -102,7 +101,7 @@ class Profiler extends React.Component { startTime: now, attributes: { [SENTRY_OP]: UI_UPDATE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), 'ui.react.changed_props': changedProps, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), @@ -142,7 +141,7 @@ class Profiler extends React.Component { startTime, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -229,7 +228,7 @@ function useProfiler( onlyIfParent: true, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -257,7 +256,7 @@ function useProfiler( startTime, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index ff860013dd59..629b839d11ad 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -18,7 +18,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import * as React from 'react'; @@ -49,7 +48,7 @@ import { setNavigationContext, transactionNameHasWildcard, } from './utils'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; const reactRouterConfigByClient = new WeakMap(); @@ -756,7 +755,7 @@ export function createReactRouterV6CompatibleTracingIntegration( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.reactrouter${version ? `_v${version}` : ''}`, + [SENTRY_ORIGIN]: `auto.pageload.react.reactrouter${version ? `_v${version}` : ''}`, }, }); } @@ -1078,7 +1077,7 @@ export function handleNavigation(opts: { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`, + [SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`, ...(source === 'route' && { [URL_TEMPLATE]: placeholderEntry.routeName }), }, }); diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 2110d4835cf0..c843c6cd3a43 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -12,15 +12,13 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { ReactElement } from 'react'; import * as React from 'react'; import { hoistNonReactStatics } from './hoist-non-react-statics'; import type { Action, Location } from './types'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; // We need to disable eslint no-explicit-any because any is required for the @@ -165,7 +163,7 @@ function instrumentReactRouter( name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, + [SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, @@ -182,7 +180,7 @@ function instrumentReactRouter( name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, + [SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, @@ -287,7 +285,7 @@ function getActiveRootSpan(): Span | undefined { return undefined; } - const op = spanToJSON(rootSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const op = spanToJSON(rootSpan).attributes[SENTRY_OP]; // Only use this root span if it is a pageload or navigation span return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index 7e799b742aea..501c13f0d6bb 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -5,14 +5,9 @@ import { WINDOW, } from '@sentry/browser'; import type { Integration, TransactionSource } from '@sentry/core'; -import { - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK } from '@sentry/core'; import type { Location } from './types'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; // Many of the types below had to be mocked out to prevent typescript issues @@ -70,7 +65,7 @@ export function reactRouterV3BrowserTracingIntegration( name: source === 'route' || !hasSpanStreamingEnabled(client) ? localName : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: localName }), }, @@ -93,7 +88,7 @@ export function reactRouterV3BrowserTracingIntegration( source === 'route' || !hasSpanStreamingEnabled(client) ? localName : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: localName }), }, diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 3d4007d7d155..41eec4694aae 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -12,7 +12,6 @@ import { NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types'; import { PARAMS_KEY_BASE, @@ -22,6 +21,7 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; @@ -104,7 +104,7 @@ export function tanstackRouterBrowserTracingIntegration( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -161,7 +161,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/react/test/normalize.test.ts b/packages/react/test/normalize.test.ts index 994b6da96919..84fac26ffcf6 100644 --- a/packages/react/test/normalize.test.ts +++ b/packages/react/test/normalize.test.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import { normalize, setNormalizeStringifier } from '@sentry/core'; +import { getCurrentScope, normalize, setNormalizeStringifier } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { init } from '../src/sdk'; @@ -23,6 +23,7 @@ describe('@sentry/react init() normalize stringifier', () => { afterEach(() => { setNormalizeStringifier(undefined); + getCurrentScope().setClient(undefined); }); it("collapses React SyntheticEvent-like objects to '[SyntheticEvent]'", () => { diff --git a/packages/react/test/react-router.test.tsx b/packages/react/test/react-router.test.tsx index cb100633002f..b25323e691c2 100644 --- a/packages/react/test/react-router.test.tsx +++ b/packages/react/test/react-router.test.tsx @@ -5,14 +5,8 @@ * directly from `react` / `react-router` so `reactRouterBrowserTracingIntegration()` can be used * without passing them in. */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import { MemoryRouter, Route, Routes, useNavigate } from 'react-router'; @@ -78,9 +72,9 @@ describe('@sentry/react/react-router', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', // version-agnostic origin (no `_v6`/`_v7` suffix) - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/about'); @@ -120,8 +114,8 @@ describe('@sentry/react/react-router', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter', }, }); }); diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index 3711626a27c2..580cd11841df 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render, waitFor } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -667,8 +661,8 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/settings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -687,8 +681,8 @@ describe('React Router cross usage of wrappers', () => { const calls = mockStartBrowserTracingNavigationSpan.mock.calls; expect(calls[0]![1].name).toBe('/settings'); expect(calls[1]![1].name).toBe('/profile'); - expect(calls[0]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); - expect(calls[1]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); + expect(calls[0]![1].attributes[SENTRY_OP]).toBe('navigation'); + expect(calls[1]![1].attributes[SENTRY_OP]).toBe('navigation'); }); it('should create separate transactions for rapid consecutive navigations', async () => { @@ -786,8 +780,8 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -808,8 +802,8 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -864,7 +858,7 @@ describe('React Router cross usage of wrappers', () => { expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { name: '/settings/*', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }), }); }); @@ -912,7 +906,7 @@ describe('React Router cross usage of wrappers', () => { expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledWith(expect.any(BrowserClient), { name: '/details/*', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }), }); }); diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx index 10804904f5e5..7f712ad0f5d7 100644 --- a/packages/react/test/reactrouter-descendant-routes.test.tsx +++ b/packages/react/test/reactrouter-descendant-routes.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -177,8 +171,8 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/child/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -309,8 +303,8 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/:orgId/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -420,8 +414,8 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -481,8 +475,8 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -634,8 +628,8 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index e77d9584499b..29f54903d579 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -2,14 +2,8 @@ * @vitest-environment jsdom */ import { BrowserClient } from '@sentry/browser'; -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -108,8 +102,8 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SENTRY_OP]: 'pageload', }, }); }); @@ -145,8 +139,8 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_OP]: 'navigation', }, }); @@ -159,8 +153,8 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/features', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_OP]: 'navigation', }, }); }); @@ -201,8 +195,8 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/:userid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_OP]: 'navigation', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/users/:userid'); @@ -217,8 +211,8 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/teams/:teamId/details', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_OP]: 'navigation', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/teams/:teamId/details'); diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 98c4da18df5a..157e0e43efeb 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,20 +1,14 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-4'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV4BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -86,8 +80,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', + [SENTRY_OP]: 'pageload', }, }); }); @@ -131,8 +125,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); @@ -144,8 +138,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); }); @@ -203,8 +197,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); }); @@ -240,8 +234,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -285,8 +279,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -308,8 +302,8 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); @@ -357,8 +351,8 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); @@ -371,8 +365,8 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_OP]: 'navigation', }, }); }); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 489735c46d47..a36474c9a5a0 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,20 +1,14 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV5BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -86,8 +80,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', + [SENTRY_OP]: 'pageload', }, }); }); @@ -131,8 +125,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); @@ -144,8 +138,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); }); @@ -203,8 +197,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); }); @@ -240,8 +234,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -285,8 +279,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -308,8 +302,8 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); @@ -356,8 +350,8 @@ describe('browserTracingReactRouterV5', () => { name: '/organizations/:orgid/v1/:teamid', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', }, }); @@ -370,8 +364,8 @@ describe('browserTracingReactRouterV5', () => { name: '/organizations/:orgid', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid', }, }); diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index b4b94eb6c5ca..fc9e7e664644 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import type { RouteObject } from 'react-router-6'; @@ -214,8 +208,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -261,8 +255,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -298,8 +292,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); }); @@ -416,8 +410,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -503,8 +497,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -541,8 +535,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -581,8 +575,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -629,8 +623,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -679,8 +673,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -731,8 +725,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -771,8 +765,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -811,8 +805,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -883,8 +877,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); }); @@ -1035,8 +1029,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1087,8 +1081,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1139,8 +1133,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1197,8 +1191,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1279,8 +1273,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1359,8 +1353,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/param-page/:id/details/:superId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1516,8 +1510,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1556,8 +1550,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); diff --git a/packages/react/test/sdk.test.ts b/packages/react/test/sdk.test.ts index 1fd3d6ade5d1..8cd0bc9c1bc1 100644 --- a/packages/react/test/sdk.test.ts +++ b/packages/react/test/sdk.test.ts @@ -1,9 +1,13 @@ import * as SentryBrowser from '@sentry/browser'; import { version } from 'react'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../src/sdk'; describe('init', () => { + afterEach(() => { + SentryBrowser.getCurrentScope().setClient(undefined); + }); + it('sets the React version (if available) in the global scope', () => { const setContextSpy = vi.spyOn(SentryBrowser, 'setContext'); diff --git a/packages/react/test/tanstackrouter.test.ts b/packages/react/test/tanstackrouter.test.ts index 5ae27a3550c9..4922d3c39ca8 100644 --- a/packages/react/test/tanstackrouter.test.ts +++ b/packages/react/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -84,7 +83,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/posts/$postId', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/posts/$postId', 'url.path.parameter.postId': '999', diff --git a/packages/remix/package.json b/packages/remix/package.json index 24a0289d722c..57a70f33c25a 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,7 +78,7 @@ "@remix-run/router": "^1.23.4", "@sentry/browser": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/remix/scripts/createRelease.js b/packages/remix/scripts/createRelease.js index c6fe5cb9ee2f..e55e865b8fa3 100644 --- a/packages/remix/scripts/createRelease.js +++ b/packages/remix/scripts/createRelease.js @@ -25,7 +25,14 @@ async function createRelease(argv, URL_PREFIX, BUILD_PATH) { release = argv.release; } - await sentry.release.create({ orgVersion: release }); + // The release API requires a project, and the one given to the SDK does not reach this call. + const project = argv.project ?? process.env.SENTRY_PROJECT; + if (!project) { + throw new Error( + '[sentry] A project is required to create a release. Pass `--project` or set `SENTRY_PROJECT` as environment variable.', + ); + } + await sentry.release.create({ orgVersion: release, project }); try { await sentry.sourcemap.upload({ diff --git a/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs b/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs index 8cc963fd57ea..9f9df060fb30 100755 --- a/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs +++ b/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs @@ -21,6 +21,11 @@ let keepOutput = false; try { const values = parseOptions(); keepOutput = Boolean(values['keep-output'] || values['out-dir']); + const org = values.org ?? process.env.SENTRY_ORG; + const project = values.project ?? process.env.SENTRY_PROJECT; + if (!values['dry-run'] && !project) { + fail('a project is required for the upload. Pass `--project` or set `SENTRY_PROJECT` as environment variable.'); + } const { emitAssets } = await import('@sentry/remix/v3'); @@ -63,11 +68,11 @@ try { console.log('[sentry] --dry-run, skipping upload'); } else { const { createSentrySDK } = await import('sentry'); - const sentry = createSentrySDK({ url: values.url, org: values.org, project: values.project }); + const sentry = createSentrySDK({ url: values.url, org, project }); const release = values.release ?? (await sentry.release['propose-version']()).version; try { // The project has to be named here; the SDK default does not reach release creation. - await sentry.release.create({ orgVersion: release, project: values.project }); + await sentry.release.create({ orgVersion: release, project }); // Modules keep their served names (`.ts`, `.tsx`), which the upload skips by default. Only module // extensions go here: the upload pairs each module with the `.map` sibling `emitAssets` wrote. const ext = [...new Set(emitted.map(asset => path.extname(asset.file)))].join(','); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 7ee7d7654025..80132fd4a254 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -7,7 +7,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { isNodeEnv } from '@sentry/core/server'; import type { BrowserClient, browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; @@ -15,7 +14,7 @@ import { getClient, startBrowserTracingNavigationSpan, startBrowserTracingPageLo import * as React from 'react'; import { DEBUG_BUILD } from '../utils/debug-build'; import { hasManifest, maybeParameterizeRemixRoute } from './remixRouteParameterization'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; export type Params = { @@ -104,7 +103,7 @@ export function startPageloadSpan(client: Client): void { name: source === 'route' || !hasSpanStreamingEnabled(client) ? spanName : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix', + [SENTRY_ORIGIN]: 'auto.pageload.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: spanName }), }, @@ -129,7 +128,7 @@ function startNavigationSpan(matches: RouteMatch[], location: ReturnType name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix', + [SENTRY_ORIGIN]: 'auto.navigation.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 543dd239a67e..e012093aaf76 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -106,7 +106,9 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index f18b713dac6b..d35dceb18969 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -84,7 +84,9 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index 6ec3c819c9f2..0f5389854ff0 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -25,7 +25,6 @@ import { hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, spanToJSON, startSpan, @@ -49,6 +48,7 @@ import { SENTRY_OP, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER } from '@sentry/conventions/op'; @@ -145,7 +145,7 @@ function makeWrappedDocumentRequestFunction(instrumentTracing?: boolean) { attributes: { method: request.method, [URL_FULL]: filterCollectedUrl(request.url), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix', + [SENTRY_ORIGIN]: 'auto.function.remix', [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'documentRequest', @@ -231,7 +231,7 @@ function makeWrappedDataFunction( // module id stays on `router.navigation.route.id`. name: client && hasSpanStreamingEnabled(client) ? name : id, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix', + [SENTRY_ORIGIN]: 'auto.ui.remix', [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: id, [CODE_FUNCTION_NAME]: name, @@ -415,7 +415,7 @@ function wrapRequestHandler ServerBuild | Promise { name: spanName, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix', + [SENTRY_ORIGIN]: 'auto.http.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: HTTP_SERVER, [URL_FULL]: filterCollectedUrl(url.href), @@ -556,5 +556,10 @@ export function instrumentServer(options?: { instrumentTracing?: boolean }): voi return; } + // `init()` runs this again after `close()`, so skip a handler that is already wrapped. + if ((pkg.createRequestHandler as WrappedFunction).__sentry_original__) { + return; + } + fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler(options)); } diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 89c1216c9857..6ab1645ca503 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -9,7 +9,6 @@ import { isObjectLike, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startInactiveSpan, waitForTracingChannelBinding, @@ -28,6 +27,7 @@ import { SENTRY_OP, HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER } from '@sentry/conventions/op'; import { remixChannels } from '@sentry/server-utils/orchestrion/config'; @@ -161,7 +161,7 @@ function subscribeRequestHandler(): void { : 'remix.request', attributes: { [SENTRY_KIND]: 'server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: HTTP_SERVER, ...(hasUrlName && { [SENTRY_SEGMENT_NAME_SOURCE]: 'url' }), [CODE_FUNCTION_NAME]: 'requestHandler', @@ -201,7 +201,7 @@ function subscribeCallRouteLoader(): void { // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'loader' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'loader', @@ -240,7 +240,7 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined): // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'action' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'action', diff --git a/packages/remix/src/server/sdk.ts b/packages/remix/src/server/sdk.ts index 49a10e2065c8..95b2922ddd96 100644 --- a/packages/remix/src/server/sdk.ts +++ b/packages/remix/src/server/sdk.ts @@ -1,7 +1,7 @@ import type { Integration } from '@sentry/core'; -import { applySdkMetadata, debug } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit } from '@sentry/node'; import { DEBUG_BUILD } from '../utils/debug-build'; import type { RemixOptions } from '../utils/remixOptions'; import { instrumentServer } from './instrumentServer'; @@ -25,10 +25,11 @@ export function getRemixDefaultIntegrations(options: RemixOptions): Integration[ export function init(options: RemixOptions): NodeClient | undefined { applySdkMetadata(options, 'remix', ['remix', 'node']); - if (isInitialized()) { + const existingClient = _INTERNAL_getActiveClient(); + if (existingClient) { DEBUG_BUILD && debug.log('SDK already initialized'); - return; + return existingClient; } options.defaultIntegrations = getRemixDefaultIntegrations(options); diff --git a/packages/remix/src/v3/client/browserTracingIntegration.ts b/packages/remix/src/v3/client/browserTracingIntegration.ts index 38cd64183fb0..5e07ca200934 100644 --- a/packages/remix/src/v3/client/browserTracingIntegration.ts +++ b/packages/remix/src/v3/client/browserTracingIntegration.ts @@ -1,25 +1,42 @@ import { browserTracingIntegration as originalBrowserTracingIntegration, + getActiveSpan, + getCurrentScope, + getRootSpan, + getRouteProvider, + resolveCurrentRoute, + resolveRoute, startBrowserTracingNavigationSpan, WINDOW, } from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; import { + addFetchInstrumentationHandler, type Client, hasSpanStreamingEnabled, type Integration, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + type Span, + spanToJSON, } from '@sentry/core'; +import { parseRouteTiming, ROUTE_TIMING_NAME } from '../routeTiming'; + type Options = Parameters[0]; +// The request header the runtime sets on its fetch of a navigation's destination. +const FRAME_REQUEST_HEADER = 'x-remix-frame'; + /** * Browser tracing for Remix 3. * * Page loads stay with the upstream integration, because they are ordinary document loads. Navigations * do not: `remix/component` intercepts links and form submissions through the Navigation API and never touches * History, so the upstream handler never fires. + * + * Spans are named through the route provider. Routes reach it from the server, with each HTML + * response, so a span starts under a low cardinality name when its route is not known yet and is + * renamed once the response arrives. */ export function browserTracingIntegration(options: Options = {}): Integration { const integration = originalBrowserTracingIntegration({ ...options, instrumentNavigation: false }); @@ -29,6 +46,9 @@ export function browserTracingIntegration(options: Options = {}): Integration { afterAllSetup(client) { integration.afterAllSetup(client); + if (options.instrumentPageLoad !== false) { + nameActivePageLoadSpan(client); + } if (options.instrumentNavigation !== false) { instrumentNavigationApi(client); } @@ -36,6 +56,25 @@ export function browserTracingIntegration(options: Options = {}): Integration { }; } +/** The page load span the upstream integration just started, named after the route that served the document. */ +function nameActivePageLoadSpan(client: Client): void { + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + + const rootSpan = getRootSpan(activeSpan); + if (spanToJSON(rootSpan).attributes[SENTRY_OP] !== 'pageload') { + return; + } + + recordRoute(client, WINDOW.location?.pathname, getDocumentRoute()); + const route = resolveCurrentRoute(client); + if (route) { + applyRoute(rootSpan, route); + } +} + function instrumentNavigationApi(client: Client): void { const navigation = (WINDOW as WindowWithNavigation).navigation; if (!navigation) { @@ -43,30 +82,98 @@ function instrumentNavigationApi(client: Client): void { return; } + // The navigation span still waiting for its route, which arrives with the response to the runtime's + // fetch of the destination. There is at most one: starting a navigation ends the previous span. + let pending: { span: Span; url: string; pathname: string } | undefined; + + client.on('spanEnd', span => { + if (pending?.span === span) { + pending = undefined; + } + }); + navigation.addEventListener('navigate', event => { const url = event.destination?.url; if (!url || !isRuntimeNavigation(event, url)) { return; } - startBrowserTracingNavigationSpan( + const pathname = pathnameOf(url) || '/'; + // Known when the app loaded this route before. + const route = resolveRoute(url, client); + const span = startBrowserTracingNavigationSpan( client, { - // Remix 3 gives the browser no route to name this after: `remix/component` exposes no matched route - // and never matches client side. Passing the server's pattern down is tracked in (#24872). - name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathnameOf(url) || '/', + name: route ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname), attributes: { - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix_v3', + [SENTRY_SEGMENT_NAME_SOURCE]: route ? 'route' : 'url', + ...(route && { [URL_TEMPLATE]: route }), + [SENTRY_ORIGIN]: 'auto.navigation.remix_v3', }, }, // The span needs the destination: `location` still points at the previous page until the // navigation finishes. { url }, ); + + pending = span && !route ? { span, url, pathname } : undefined; + }); + + addFetchInstrumentationHandler(({ fetchData, headers, response }) => { + if (!pending || !response) { + return; + } + // Only the runtime's own fetch of the destination carries the route. The app's fetches, even to + // the same path, do not. + if (headers?.get(FRAME_REQUEST_HEADER) !== 'true') { + return; + } + if (pathnameOf(fetchData.url, WINDOW.location?.href) !== pending.pathname) { + return; + } + const { span, url, pathname } = pending; + pending = undefined; + + // The route belongs to the URL that answered. After a redirect that is not the one requested, and + // recording it under the requested path would misname every later visit there. + const servedPathname = pathnameOf(response.url, WINDOW.location?.href) || pathname; + recordRoute(client, servedPathname, parseRouteTiming(response.headers.get('server-timing'))); + const route = resolveRoute(response.url || url, client); + if (route) { + applyRoute(span, route); + } }); } +/** Hands a route the server reported to the provider, when it is one that takes them. */ +function recordRoute(client: Client, pathname: string | undefined, route: string | undefined): void { + const provider = getRouteProvider(client); + if (provider && 'record' in provider && typeof provider.record === 'function') { + provider.record(pathname, route); + } +} + +/** + * Renames a span that started under the fallback. The scope's transaction name follows, so errors + * group by route too, but only while this is still the current span. + */ +function applyRoute(span: Span, route: string): void { + span.updateName(route); + span.setAttributes({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: route }); + const activeSpan = getActiveSpan(); + if (activeSpan && getRootSpan(activeSpan) === span) { + getCurrentScope().setTransactionName(route); + } +} + +/** The route from the document response, read off the navigation timing entry. */ +function getDocumentRoute(): string | undefined { + const entry = WINDOW.performance?.getEntriesByType?.('navigation')[0] as + | { serverTiming?: Array<{ name: string; description: string }> } + | undefined; + return entry?.serverTiming?.find(timing => timing.name === ROUTE_TIMING_NAME)?.description; +} + /** * Whether the runtime will keep this navigation inside the current document. * @@ -87,9 +194,9 @@ function isSameOrigin(url: string): boolean { } } -function pathnameOf(url: string): string | undefined { +function pathnameOf(url: string, base?: string): string | undefined { try { - return new URL(url).pathname; + return new URL(url, base).pathname; } catch { return undefined; } diff --git a/packages/remix/src/v3/client/routeProvider.ts b/packages/remix/src/v3/client/routeProvider.ts new file mode 100644 index 000000000000..b9bef232cfbc --- /dev/null +++ b/packages/remix/src/v3/client/routeProvider.ts @@ -0,0 +1,12 @@ +import { type CachedRouteProvider, createCachedRouteProvider } from '@sentry/browser'; + +/** + * The route provider for Remix 3. + * + * The browser has no route table: `remix/component` never matches routes client side. The server + * reports the matched pattern on each HTML response instead, and the tracing integration records it + * here as documents and frames arrive. A URL the app has not loaded yet resolves to nothing. + */ +export function createRemixV3RouteProvider(): CachedRouteProvider { + return createCachedRouteProvider(); +} diff --git a/packages/remix/src/v3/client/sdk.ts b/packages/remix/src/v3/client/sdk.ts index 998bfc627a35..edbd2f290545 100644 --- a/packages/remix/src/v3/client/sdk.ts +++ b/packages/remix/src/v3/client/sdk.ts @@ -4,6 +4,7 @@ import { applySdkMetadata, type Client, type Integration } from '@sentry/core'; import { browserTracingIntegration } from './browserTracingIntegration'; import { instrumentClientRuntime } from './errors'; +import { createRemixV3RouteProvider } from './routeProvider'; /** * Default integrations for the Remix 3 client SDK. @@ -19,6 +20,8 @@ export function getDefaultIntegrations(options: BrowserOptions): Integration[] { /** Initialize the Sentry Remix 3 SDK in the browser. */ export function init(options: BrowserOptions): Client | undefined { const opts = { + // Set at `init` so the page load span can be named from it. An app with its own provider keeps it. + routeProvider: createRemixV3RouteProvider(), ...options, defaultIntegrations: options.defaultIntegrations ?? getDefaultIntegrations(options), }; diff --git a/packages/remix/src/v3/index.client.ts b/packages/remix/src/v3/index.client.ts index ffe5c04d3175..66da64fa8581 100644 --- a/packages/remix/src/v3/index.client.ts +++ b/packages/remix/src/v3/index.client.ts @@ -74,4 +74,5 @@ export type { BrowserOptions } from '@sentry/browser'; export { getDefaultIntegrations, init } from './client/sdk'; export { browserTracingIntegration } from './client/browserTracingIntegration'; +export { createRemixV3RouteProvider } from './client/routeProvider'; export { captureRuntimeErrors, instrumentClientRuntime } from './client/errors'; diff --git a/packages/remix/src/v3/routeTiming.ts b/packages/remix/src/v3/routeTiming.ts new file mode 100644 index 000000000000..483a867ed8af --- /dev/null +++ b/packages/remix/src/v3/routeTiming.ts @@ -0,0 +1,17 @@ +// The `Server-Timing` entry that carries the matched route pattern from the server to the browser. +// Shared by the server middleware, which writes it, and the browser SDK, which reads it, so the two +// cannot drift. Nothing in here may pull server code into the client bundle. + +export const ROUTE_TIMING_NAME = 'sentry-route'; + +/** The entry for a route, quoted per the header grammar. */ +export function formatRouteTiming(route: string): string { + return `${ROUTE_TIMING_NAME};desc="${route.replace(/["\\]/g, '\\$&')}"`; +} + +/** The route from a raw `Server-Timing` header value, or `undefined` when the entry is absent. */ +export function parseRouteTiming(header: string | null | undefined): string | undefined { + // Everything between the quotes, where a backslash escapes the next character. + const quoted = header?.match(new RegExp(`(?:^|,)\\s*${ROUTE_TIMING_NAME};desc="((?:[^"\\\\]|\\\\.)*)"`))?.[1]; + return quoted?.replace(/\\(.)/g, '$1'); +} diff --git a/packages/remix/src/v3/server/errorFilter.ts b/packages/remix/src/v3/server/errorFilter.ts index 9e98a2874a27..5a771746f8b0 100644 --- a/packages/remix/src/v3/server/errorFilter.ts +++ b/packages/remix/src/v3/server/errorFilter.ts @@ -1,4 +1,4 @@ -import { captureException } from '@sentry/core'; +import { captureException, isObjectLike } from '@sentry/core'; /** Decides whether a thrown value should become a Sentry issue. */ export type ShouldHandleError = (error: unknown) => boolean; @@ -8,11 +8,11 @@ export type ShouldHandleError = (error: unknown) => boolean; * outcome, not a fault, and the request still produces a span. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/remix/src/v3/server/middleware.ts b/packages/remix/src/v3/server/middleware.ts index 541adc696214..7ae9dc989362 100644 --- a/packages/remix/src/v3/server/middleware.ts +++ b/packages/remix/src/v3/server/middleware.ts @@ -10,6 +10,7 @@ import { winterCGRequestToRequestData, } from '@sentry/core'; +import { formatRouteTiming } from '../routeTiming'; import type { MatcherLike, MiddlewareLike, NextFunctionLike, RequestContextLike } from '../types'; import { captureRequestError } from './errorFilter'; import { resolveRoutePattern } from './route'; @@ -27,7 +28,10 @@ export function sentryRemixMiddleware(matcher: MatcherLike): MiddlewareLike { isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(context.request) }); // Applied before `next()` so anything captured while the handler runs already carries the route. - applyRoute(isolationScope, matcher, context); + const route = resolveRoutePattern(matcher, context); + if (route) { + applyRoute(isolationScope, route, context.method); + } let response; try { @@ -40,20 +44,18 @@ export function sentryRemixMiddleware(matcher: MatcherLike): MiddlewareLike { } setResponseStatus(response); + if (route) { + addRouteHeader(response, context.request, route); + } return response; }; } -function applyRoute(isolationScope: Scope, matcher: MatcherLike, context: RequestContextLike): void { - const route = resolveRoutePattern(matcher, context); - if (!route) { - // Nothing matched, so the router falls through to its 404 handler. Leaving the name alone keeps the - // raw URL out of it, which span streaming requires. - return; - } - - const name = `${context.method} ${route}`; +// When nothing matched, the router falls through to its 404 handler. Leaving the name alone keeps the +// raw URL out of it, which span streaming requires. +function applyRoute(isolationScope: Scope, route: string, method: string): void { + const name = `${method} ${route}`; isolationScope.setTransactionName(name); const activeSpan = getActiveSpan(); @@ -67,6 +69,22 @@ function applyRoute(isolationScope: Scope, matcher: MatcherLike, context: Reques rootSpan.setAttribute(HTTP_ROUTE, route); } +/** + * Tells the browser SDK which route served a document, so it can name its page load and navigation + * spans after the pattern. The browser has no route table of its own. Only HTML responses carry it, + * which is what document loads and the runtime's frame fetches ask for. + */ +function addRouteHeader(response: Response, request: Request, route: string): void { + if (!request.headers.get('accept')?.includes('text/html')) { + return; + } + try { + response.headers.append('Server-Timing', formatRouteTiming(route)); + } catch { + // Immutable headers, e.g. a response passed through from `fetch()`. + } +} + function setResponseStatus(response: Response): void { const activeSpan = getActiveSpan(); if (!activeSpan || typeof response?.status !== 'number') { diff --git a/packages/remix/test/index.client.test.ts b/packages/remix/test/index.client.test.ts index 1e0e26343494..70e8751496df 100644 --- a/packages/remix/test/index.client.test.ts +++ b/packages/remix/test/index.client.test.ts @@ -1,4 +1,4 @@ -import { getMainCarrier } from '@sentry/core'; +import { getCurrentScope, getMainCarrier } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { init } from '../src/index.client'; @@ -49,6 +49,7 @@ describe('Client init()', () => { expect.objectContaining({ routeProvider: expect.objectContaining({ resolveRoute: expect.any(Function) }) }), ); + getCurrentScope().setClient(undefined); const routeProvider = { resolveRoute: () => '/custom', resolveCurrentRoute: () => '/custom' }; init({ routeProvider }); expect(reactInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider })); diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index a6883b986e57..dabba59e4956 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -48,6 +48,24 @@ describe('Server init()', () => { expect(nodeInit).toHaveBeenCalledTimes(1); }); + it('returns the existing client if already initialized', () => { + const first = init({}); + const second = init({}); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it('initializes again after the existing client is closed', async () => { + const first = init({}); + await first?.close(); + const second = init({}); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); diff --git a/packages/remix/test/scripts/upload-sourcemaps.test.ts b/packages/remix/test/scripts/upload-sourcemaps.test.ts index cce1ed03f269..426199331c8c 100644 --- a/packages/remix/test/scripts/upload-sourcemaps.test.ts +++ b/packages/remix/test/scripts/upload-sourcemaps.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const createMock = vi.fn(); const uploadSourceMapsMock = vi.fn(); @@ -48,12 +48,16 @@ beforeEach(() => { proposeVersionMock.mockClear(); }); +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe('createRelease', () => { it('should use release param when given', async () => { - await createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build'); + await createRelease({ release: '0.1.2.3', project: 'my-project' }, '~/build/', 'public/build'); expect(proposeVersionMock).not.toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3', @@ -64,10 +68,10 @@ describe('createRelease', () => { }); it('should call `proposeVersion` when release param is not given.', async () => { - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(proposeVersionMock).toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3.4', @@ -80,6 +84,7 @@ describe('createRelease', () => { it('should use given buildPath and urlPrefix over the defaults when given.', async () => { await createRelease( { + project: 'my-project', urlPrefix: '~/build/', buildPath: 'public/build', }, @@ -88,7 +93,7 @@ describe('createRelease', () => { ); expect(proposeVersionMock).toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3.4', @@ -101,7 +106,7 @@ describe('createRelease', () => { it('logs an error when uploadSourceMaps fails', async () => { uploadSourceMapsMock.mockRejectedValue(new Error('Failed to upload sourcemaps')); - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', @@ -118,10 +123,26 @@ describe('createRelease', () => { it('logs an error when finalize fails', async () => { finalizeMock.mockRejectedValue(new Error('Failed to finalize release')); - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(consoleWarnSpy).toHaveBeenCalledWith('[sentry] Failed to finalize release.'); }); + + it('falls back to SENTRY_PROJECT for the release project', async () => { + vi.stubEnv('SENTRY_PROJECT', 'env-project'); + + await createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build'); + + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3', project: 'env-project' }); + }); + + it('fails before creating a release when no project is known', async () => { + vi.stubEnv('SENTRY_PROJECT', ''); + + await expect(createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build')).rejects.toThrow('--project'); + + expect(createMock).not.toHaveBeenCalled(); + }); }); // To avoid `--isolatedModules` flag as we're not importing diff --git a/packages/remix/test/server/instrumentServer.test.ts b/packages/remix/test/server/instrumentServer.test.ts index b26ef66e3cf1..504c8a17acae 100644 --- a/packages/remix/test/server/instrumentServer.test.ts +++ b/packages/remix/test/server/instrumentServer.test.ts @@ -9,8 +9,29 @@ import { import { FUNCTION } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import * as SentryCore from '@sentry/core'; +import * as SentryCoreServer from '@sentry/core/server'; +import type { WrappedFunction } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { instrumentBuild } from '../../src/server/instrumentServer'; +import { instrumentBuild, instrumentServer } from '../../src/server/instrumentServer'; + +describe('instrumentServer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('wraps `createRequestHandler` only once when called again', () => { + const createRequestHandler = vi.fn(); + const pkg = { createRequestHandler }; + vi.spyOn(SentryCoreServer, 'loadModule').mockReturnValue(pkg); + + instrumentServer(); + const wrapped = pkg.createRequestHandler; + instrumentServer(); + + expect(pkg.createRequestHandler).toBe(wrapped); + expect((pkg.createRequestHandler as WrappedFunction).__sentry_original__).toBe(createRequestHandler); + }); +}); describe('instrumentBuild', () => { afterEach(() => { diff --git a/packages/remix/test/v3/browserTracingIntegration.test.ts b/packages/remix/test/v3/browserTracingIntegration.test.ts index 2a78829b1107..1c9d660c34b3 100644 --- a/packages/remix/test/v3/browserTracingIntegration.test.ts +++ b/packages/remix/test/v3/browserTracingIntegration.test.ts @@ -1,35 +1,108 @@ import type { Client } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const startBrowserTracingNavigationSpan = vi.fn(); +import type * as SentryCore from '@sentry/core'; + +/** The span a start function returns: records the renames the integration applies. */ +function fakeSpan(op: string) { + return { op, updateName: vi.fn(), setAttributes: vi.fn() }; +} +type FakeSpan = ReturnType; + +let navigationSpan: FakeSpan | undefined; +let activeSpan: FakeSpan | undefined; +let fetchHandler: + | ((data: { + fetchData: { url: string }; + headers?: Headers; + endTimestamp?: number; + response?: { url: string; headers: Headers }; + }) => void) + | undefined; + +const startBrowserTracingNavigationSpan = vi.fn(() => navigationSpan); const upstreamIntegration = { name: 'BrowserTracing', afterAllSetup: vi.fn() }; const upstreamOptions: { instrumentNavigation?: boolean }[] = []; +/** What `createCachedRouteProvider` does, in miniature: answers from the routes it was told about. */ +const recorded = new Map(); +const routeProvider = { + resolveRoute: (url: { pathname: string }) => recorded.get(url.pathname), + resolveCurrentRoute: () => recorded.get((globalThis as { location?: { pathname: string } }).location?.pathname ?? ''), + record: (pathname: string | undefined, route: string | undefined) => { + if (pathname && route) { + recorded.set(pathname, route); + } + }, +}; +let registeredProvider: typeof routeProvider | undefined = routeProvider; +const scope = { setTransactionName: vi.fn() }; + vi.mock('@sentry/browser', () => ({ browserTracingIntegration: (options: { instrumentNavigation?: boolean }) => { upstreamOptions.push(options); return upstreamIntegration; }, startBrowserTracingNavigationSpan: (...args: unknown[]) => startBrowserTracingNavigationSpan(...args), + getActiveSpan: () => activeSpan, + getCurrentScope: () => scope, + getRootSpan: (span: unknown) => span, + getRouteProvider: () => registeredProvider, + resolveRoute: (url: string) => registeredProvider?.resolveRoute(new URL(url, 'https://app.test')), + resolveCurrentRoute: () => registeredProvider?.resolveCurrentRoute(), WINDOW: globalThis, })); +vi.mock('@sentry/core', async importOriginal => ({ + ...(await importOriginal()), + spanToJSON: (span: FakeSpan) => ({ attributes: { 'sentry.op': span.op } }), + addFetchInstrumentationHandler: (handler: typeof fetchHandler) => { + fetchHandler = handler; + }, +})); + const { browserTracingIntegration } = await import('../../src/v3/client/browserTracingIntegration'); +/** A response to the runtime's frame fetch, as the fetch instrumentation reports it. */ +function frameResponse(url: string, serverTiming?: string, servedUrl = url) { + return { + fetchData: { url }, + headers: new Headers({ 'x-remix-frame': 'true' }), + endTimestamp: 1, + response: { + url: new URL(servedUrl, 'https://app.test').href, + headers: new Headers(serverTiming ? { 'server-timing': serverTiming } : {}), + }, + }; +} + +/** A frame fetch that failed: settled, but with no response. */ +function failedFrameFetch(url: string) { + return { fetchData: { url }, headers: new Headers({ 'x-remix-frame': 'true' }), endTimestamp: 1 }; +} + interface FakeNavigateEvent { canIntercept: boolean; info?: unknown; destination?: { url: string }; } -const client = { getOptions: () => ({ traceLifecycle: 'stream' }) } as unknown as Client; +let spanEnd: ((span: unknown) => void) | undefined; +const client = { + getOptions: () => ({ traceLifecycle: 'stream' }), + on: (hook: string, callback: (span: unknown) => void) => { + if (hook === 'spanEnd') { + spanEnd = callback; + } + }, +} as unknown as Client; /** Sets up the integration over a fake Navigation API and returns a way to fire `navigate` events. */ -function setup(options = {}): (event: FakeNavigateEvent) => void { +function setup(options = {}, pathname = '/'): (event: FakeNavigateEvent) => void { let listener: ((event: FakeNavigateEvent) => void) | undefined; Object.assign(globalThis, { - location: { origin: 'https://app.test', pathname: '/' }, + location: { origin: 'https://app.test', pathname, href: `https://app.test${pathname}` }, navigation: { addEventListener: (_type: string, fn: (event: FakeNavigateEvent) => void) => { listener = fn; @@ -46,6 +119,198 @@ describe('browserTracingIntegration', () => { beforeEach(() => { startBrowserTracingNavigationSpan.mockClear(); upstreamOptions.length = 0; + navigationSpan = fakeSpan('navigation'); + activeSpan = undefined; + fetchHandler = undefined; + recorded.clear(); + registeredProvider = routeProvider; + scope.setTransactionName.mockClear(); + Object.assign(globalThis, { performance: { getEntriesByType: () => [] } }); + }); + + it('records the route the document reports, so the whole SDK can resolve it', () => { + activeSpan = fakeSpan('pageload'); + Object.assign(globalThis, { + performance: { + getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/users/:id' }] }], + }, + }); + + setup({}, '/users/1'); + + expect(recorded.get('/users/1')).toBe('/users/:id'); + }); + + it('names a navigation span from the provider right away when the route is already known', () => { + recorded.set('/users/12345', '/users/:id'); + const navigate = setup(); + + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( + client, + expect.objectContaining({ + name: '/users/:id', + attributes: expect.objectContaining({ 'sentry.segment.name.source': 'route', 'url.template': '/users/:id' }), + }), + { url: 'https://app.test/users/12345' }, + ); + }); + + it('still names spans from the response without a provider that records', () => { + registeredProvider = undefined; + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + }); + + it("names the page load span after the route the document's Server-Timing reports", () => { + activeSpan = fakeSpan('pageload'); + Object.assign(globalThis, { + performance: { + getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/users/:id' }] }], + }, + }); + + setup(); + + expect(activeSpan.updateName).toHaveBeenCalledWith('/users/:id'); + expect(activeSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.segment.name.source': 'route', + 'url.template': '/users/:id', + }); + expect(scope.setTransactionName).toHaveBeenCalledWith('/users/:id'); + }); + + it('leaves the page load span alone when the document reports no route', () => { + activeSpan = fakeSpan('pageload'); + + setup(); + + expect(activeSpan.updateName).not.toHaveBeenCalled(); + }); + + it('does not rename an active span that is not a page load', () => { + activeSpan = fakeSpan('navigation'); + Object.assign(globalThis, { + performance: { getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/' }] }] }, + }); + + setup(); + + expect(activeSpan.updateName).not.toHaveBeenCalled(); + }); + + it("names the navigation span after the route in the destination's response", () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + activeSpan = navigationSpan; + + fetchHandler?.(frameResponse('/users/12345', 'db;dur=3, sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + expect(navigationSpan?.setAttributes).toHaveBeenCalledWith({ + 'sentry.segment.name.source': 'route', + 'url.template': '/users/:id', + }); + // Errors captured from now on group by the route, not the path the span started under. + expect(scope.setTransactionName).toHaveBeenCalledWith('/users/:id'); + }); + + it('drops the response for a navigation whose span already ended', () => { + const first = fakeSpan('navigation'); + const second = fakeSpan('navigation'); + const navigate = setup(); + navigationSpan = first; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + navigationSpan = second; + navigate({ canIntercept: true, destination: { url: 'https://app.test/items/2' } }); + activeSpan = second; + // Starting the second navigation ended the first span. + spanEnd?.(first); + + fetchHandler?.(frameResponse('/items/2', 'sentry-route;desc="/items/:id"')); + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(second.updateName).toHaveBeenCalledWith('/items/:id'); + expect(first.updateName).not.toHaveBeenCalled(); + expect(scope.setTransactionName).toHaveBeenCalledTimes(1); + expect(scope.setTransactionName).toHaveBeenCalledWith('/items/:id'); + }); + + it('unquotes the route the header carries', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/files/x' } }); + + fetchHandler?.(frameResponse('/files/x', 'sentry-route;desc="/files/:name(\\"a\\\\b\\")"')); + + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/files/:name("a\\b")'); + }); + + it('records a redirected response under the path that served it, not the one requested', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/old/5' } }); + activeSpan = navigationSpan; + + fetchHandler?.(frameResponse('/old/5', 'sentry-route;desc="/users/:id"', '/users/5')); + + expect(recorded.get('/users/5')).toBe('/users/:id'); + expect(recorded.has('/old/5')).toBe(false); + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + }); + + it('still names a repeated navigation after the aborted first attempt reports its failure', () => { + const first = fakeSpan('navigation'); + const second = fakeSpan('navigation'); + const navigate = setup(); + navigationSpan = first; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + navigationSpan = second; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + activeSpan = second; + + // The Navigation API aborted the first intercept; its rejection arrives after the second queued. + fetchHandler?.(failedFrameFetch('/users/1')); + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(second.updateName).toHaveBeenCalledWith('/users/:id'); + expect(first.updateName).not.toHaveBeenCalled(); + }); + + it('forgets the pending span when it ends', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + spanEnd?.(navigationSpan); + + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + }); + + it("ignores the app's own fetches during a navigation", () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + // Same path as the navigation, but without the runtime's frame header: the app's own fetch. + fetchHandler?.({ ...frameResponse('/users/12345', 'sentry-route;desc="/users/:id"'), headers: new Headers() }); + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + + // The navigation is still waiting for its own response. + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + }); + + it('renames a navigation span once, from its own response only', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + fetchHandler?.(frameResponse('/users/12345')); + + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); }); it('leaves page loads to the upstream integration and takes over navigation', () => { @@ -62,7 +327,7 @@ describe('browserTracingIntegration', () => { expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( client, { - // Low cardinality, because the browser has no route patterns. + // Low cardinality until the server reports the route. name: 'Navigation', attributes: { 'sentry.segment.name.source': 'url', @@ -92,7 +357,7 @@ describe('browserTracingIntegration', () => { }); it('names the span after the path when span streaming is off', () => { - const staticClient = { getOptions: () => ({ traceLifecycle: 'static' }) } as unknown as Client; + const staticClient = { getOptions: () => ({ traceLifecycle: 'static' }), on: () => {} } as unknown as Client; let listener: ((event: FakeNavigateEvent) => void) | undefined; Object.assign(globalThis, { location: { origin: 'https://app.test', pathname: '/' }, diff --git a/packages/remix/test/v3/errorFilter.test.ts b/packages/remix/test/v3/errorFilter.test.ts index f307f8bdd4e4..ab17dbedadea 100644 --- a/packages/remix/test/v3/errorFilter.test.ts +++ b/packages/remix/test/v3/errorFilter.test.ts @@ -1,7 +1,11 @@ +import type * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const captureException = vi.fn(); -vi.mock('@sentry/core', () => ({ captureException: (...args: unknown[]) => captureException(...args) })); +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, + captureException: (...args: unknown[]) => captureException(...args), +})); const { captureRequestError, defaultShouldHandleError, isRequestAbort, setShouldHandleError } = await import('../../src/v3/server/errorFilter'); diff --git a/packages/remix/test/v3/middleware.test.ts b/packages/remix/test/v3/middleware.test.ts new file mode 100644 index 000000000000..88c8f1fc42db --- /dev/null +++ b/packages/remix/test/v3/middleware.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; + +import { sentryRemixMiddleware } from '../../src/v3/server/middleware'; +import type { MatcherLike, RequestContextLike } from '../../src/v3/types'; + +/** A matcher that resolves every URL to the one pattern, or to nothing. */ +function matcherFor(pattern: string | undefined): MatcherLike { + return { + add: () => {}, + matchAll: () => (pattern ? [{ data: { pattern: { source: pattern }, method: 'GET' }, params: {} }] : []), + }; +} + +function contextFor(url: string, headers: Record = {}): RequestContextLike { + return { request: new Request(url, { headers }), url: new URL(url), method: 'GET', params: {} }; +} + +describe('sentryRemixMiddleware', () => { + it('reports the route on HTML responses through Server-Timing', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/users/:id')); + + const response = await middleware( + contextFor('http://x/users/12345', { accept: 'text/html' }), + async () => new Response('', { headers: { 'content-type': 'text/html' } }), + ); + + expect(response.headers.get('server-timing')).toBe('sentry-route;desc="/users/:id"'); + }); + + it('quotes a pattern that contains header-breaking characters', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/files/:name("a\\b")')); + + const response = await middleware( + contextFor('http://x/files/x', { accept: 'text/html' }), + async () => new Response(''), + ); + + expect(response.headers.get('server-timing')).toBe('sentry-route;desc="/files/:name(\\"a\\\\b\\")"'); + }); + + it('keeps an existing Server-Timing header', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/')); + + const response = await middleware( + contextFor('http://x/', { accept: 'text/html' }), + async () => new Response('', { headers: { 'server-timing': 'db;dur=5' } }), + ); + + expect(response.headers.get('server-timing')).toBe('db;dur=5, sentry-route;desc="/"'); + }); + + it('adds nothing to a response the browser did not ask for as a document', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/api/items/:itemId')); + + const response = await middleware( + contextFor('http://x/api/items/1', { accept: 'application/json' }), + async () => new Response('{}'), + ); + + expect(response.headers.has('server-timing')).toBe(false); + }); + + it('adds nothing when no route matched', async () => { + const middleware = sentryRemixMiddleware(matcherFor(undefined)); + + const response = await middleware( + contextFor('http://x/nope', { accept: 'text/html' }), + async () => new Response('Not Found', { status: 404 }), + ); + + expect(response.headers.has('server-timing')).toBe(false); + }); + + it('leaves a response with immutable headers alone, without failing the request', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/')); + + // `Response.error()` is the one constructor that yields immutable headers, like a passed through + // `fetch()` response does. + const response = await middleware(contextFor('http://x/', { accept: 'text/html' }), async () => Response.error()); + + expect(response.headers.has('server-timing')).toBe(false); + }); +}); diff --git a/packages/remix/test/v3/routeTiming.test.ts b/packages/remix/test/v3/routeTiming.test.ts new file mode 100644 index 000000000000..4e681b686a87 --- /dev/null +++ b/packages/remix/test/v3/routeTiming.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { formatRouteTiming, parseRouteTiming } from '../../src/v3/routeTiming'; + +describe('route timing entry', () => { + it('round trips a route', () => { + expect(parseRouteTiming(formatRouteTiming('/users/:id'))).toBe('/users/:id'); + }); + + it('round trips a route with header-breaking characters', () => { + const route = '/files/:name("a\\b")'; + + expect(formatRouteTiming(route)).toBe('sentry-route;desc="/files/:name(\\"a\\\\b\\")"'); + expect(parseRouteTiming(formatRouteTiming(route))).toBe(route); + }); + + it('finds the entry among others', () => { + expect(parseRouteTiming(`db;dur=5, ${formatRouteTiming('/')}, cache;desc="hit"`)).toBe('/'); + }); + + it('is undefined without the entry', () => { + expect(parseRouteTiming('db;dur=5')).toBeUndefined(); + expect(parseRouteTiming(null)).toBeUndefined(); + }); +}); diff --git a/packages/remix/test/v3/sdk.test.ts b/packages/remix/test/v3/sdk.test.ts index 1436966ddb91..f5a0fdca9d3f 100644 --- a/packages/remix/test/v3/sdk.test.ts +++ b/packages/remix/test/v3/sdk.test.ts @@ -52,6 +52,21 @@ describe('the client entry', () => { // `@sentry/browser`'s installs span streaming on first use. Taking one from `@sentry/core` is // invisible until an app replaces the default integrations, because browser tracing installs span // streaming anyway. + it('registers a route provider that takes the routes the server reports', () => { + init({}); + + const { routeProvider } = browserInit.mock.calls.at(-1)?.[0] as { routeProvider: { record: unknown } }; + expect(routeProvider.record).toEqual(expect.any(Function)); + }); + + it("keeps the app's own route provider", () => { + const own = { resolveRoute: () => '/own', resolveCurrentRoute: () => '/own' }; + + init({ routeProvider: own }); + + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider: own })); + }); + it('re-exports @sentry/browser, never a @sentry/core lookalike', async () => { const [entry, browser] = await Promise.all([ import('../../src/v3/index.client'), diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 00771c363692..256fadb80c7c 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -58,7 +58,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry/rrweb": "2.43.2" + "@sentry/rrweb": "2.44.1" }, "dependencies": { "@sentry/replay": "11.4.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 2f67135e6161..340240b91bdf 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -73,8 +73,8 @@ "devDependencies": { "@babel/core": "^7.29.6", "@sentry-internal/replay-worker": "11.4.0", - "@sentry/rrweb": "2.43.2", - "@sentry/rrweb-snapshot": "2.43.2", + "@sentry/rrweb": "2.44.1", + "@sentry/rrweb-snapshot": "2.44.1", "fflate": "0.8.3", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.3.0" diff --git a/packages/replay-internal/test/mocks/mockSdk.ts b/packages/replay-internal/test/mocks/mockSdk.ts index 9ec20553529b..d4965874b0c9 100644 --- a/packages/replay-internal/test/mocks/mockSdk.ts +++ b/packages/replay-internal/test/mocks/mockSdk.ts @@ -1,4 +1,5 @@ import type { Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { vi } from 'vitest'; import type { Replay as ReplayIntegration } from '../../src/integration'; import type { ReplayContainer } from '../../src/replay'; @@ -71,6 +72,8 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } ...replayOptions, }); + getCurrentScope().setClient(undefined); + const client = init({ ...getDefaultClientOptions(), dsn: 'https://dsn@ingest.f00.f00/1', diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 759f0f0babc3..d92bab9cd8b9 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -97,7 +97,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0" }, "devDependencies": { diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index f45d875d8b6e..173ff3c638d4 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -1,12 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, - startSpanManual, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpan, startSpanManual } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_OPERATION_NAME, @@ -22,13 +15,14 @@ import { GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { getGenAiSpanOp, + getTokenUsageAttributes, resolveAIRecordingOptions, - setTokenUsageAttributes, wrapPromiseWithMethods, } from '../core/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; @@ -56,7 +50,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: 'anthropic', [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', + [SENTRY_ORIGIN]: 'auto.ai.anthropic', }; if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { @@ -131,12 +125,15 @@ function addMetadataAttributes(span: Span, response: AnthropicAiResponse): void }); if ('usage' in response && response.usage) { - setTokenUsageAttributes( - span, - response.usage.input_tokens, - response.usage.output_tokens, - response.usage.cache_creation_input_tokens, - response.usage.cache_read_input_tokens, + span.setAttributes( + getTokenUsageAttributes( + response.usage.input_tokens + + (response.usage.cache_creation_input_tokens ?? 0) + + (response.usage.cache_read_input_tokens ?? 0), + response.usage.output_tokens, + response.usage.cache_creation_input_tokens, + response.usage.cache_read_input_tokens, + ), ); } } diff --git a/packages/server-utils/src/ai/anthropic-ai/streaming.ts b/packages/server-utils/src/ai/anthropic-ai/streaming.ts index 3daf61c2b57e..20507ed4f3bf 100644 --- a/packages/server-utils/src/ai/anthropic-ai/streaming.ts +++ b/packages/server-utils/src/ai/anthropic-ai/streaming.ts @@ -83,7 +83,12 @@ function handleMessageMetadata(event: AnthropicAiStreamingEvent, state: Streamin if (message.model) state.responseModel = message.model; if (message.usage) { - if (typeof message.usage.input_tokens === 'number') state.promptTokens = message.usage.input_tokens; + if (typeof message.usage.input_tokens === 'number') { + state.promptTokens = + message.usage.input_tokens + + (message.usage.cache_creation_input_tokens ?? 0) + + (message.usage.cache_read_input_tokens ?? 0); + } if (typeof message.usage.cache_creation_input_tokens === 'number') state.cacheCreationInputTokens = message.usage.cache_creation_input_tokens; if (typeof message.usage.cache_read_input_tokens === 'number') diff --git a/packages/server-utils/src/ai/anthropic-ai/types.ts b/packages/server-utils/src/ai/anthropic-ai/types.ts index 1714349dcc48..bb768e63063e 100644 --- a/packages/server-utils/src/ai/anthropic-ai/types.ts +++ b/packages/server-utils/src/ai/anthropic-ai/types.ts @@ -40,8 +40,8 @@ type SuccessfulResponse = { usage?: { input_tokens: number; output_tokens: number; - cache_creation_input_tokens: number; - cache_read_input_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; }; error?: never; // This should help TypeScript infer the type correctly }; diff --git a/packages/server-utils/src/ai/core/utils.ts b/packages/server-utils/src/ai/core/utils.ts index d83c00edfd7f..49f76fb18998 100644 --- a/packages/server-utils/src/ai/core/utils.ts +++ b/packages/server-utils/src/ai/core/utils.ts @@ -12,8 +12,11 @@ import { GEN_AI_RESPONSE_STREAMING, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; @@ -100,47 +103,37 @@ export function isReadableStream(value: unknown): value is Readable } /** - * Set token usage attributes - * @param span - The span to add attributes to - * @param promptTokens - The number of prompt tokens - * @param completionTokens - The number of completion tokens - * @param cachedInputTokens - The number of cached input tokens - * @param cachedOutputTokens - The number of cached output tokens + * Build token usage attributes. Input tokens include cache reads and writes, which are subsets. */ -export function setTokenUsageAttributes( - span: Span, +export function getTokenUsageAttributes( promptTokens?: number, completionTokens?: number, - cachedInputTokens?: number, - cachedOutputTokens?: number, -): void { - if (promptTokens !== undefined) { - span.setAttributes({ - [GEN_AI_USAGE_INPUT_TOKENS]: promptTokens, - }); + cacheCreationInputTokens?: number, + cacheReadInputTokens?: number, + totalTokens?: number, +): Record { + const attributes: Record = {}; + + if (typeof promptTokens === 'number') { + attributes[GEN_AI_USAGE_INPUT_TOKENS] = promptTokens; } - if (completionTokens !== undefined) { - span.setAttributes({ - [GEN_AI_USAGE_OUTPUT_TOKENS]: completionTokens, - }); + if (typeof cacheCreationInputTokens === 'number') { + attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = cacheCreationInputTokens; } - if ( - promptTokens !== undefined || - completionTokens !== undefined || - cachedInputTokens !== undefined || - cachedOutputTokens !== undefined - ) { - /** - * Total input tokens in a request is the summation of `input_tokens`, - * `cache_creation_input_tokens`, and `cache_read_input_tokens`. - */ - const totalTokens = - (promptTokens ?? 0) + (completionTokens ?? 0) + (cachedInputTokens ?? 0) + (cachedOutputTokens ?? 0); - - span.setAttributes({ - [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, - }); + if (typeof cacheReadInputTokens === 'number') { + attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = cacheReadInputTokens; } + if (typeof completionTokens === 'number') { + attributes[GEN_AI_USAGE_OUTPUT_TOKENS] = completionTokens; + } + if (typeof totalTokens === 'number') { + attributes[GEN_AI_USAGE_TOTAL_TOKENS] = totalTokens; + } else if (typeof promptTokens === 'number' || typeof completionTokens === 'number') { + attributes[GEN_AI_USAGE_TOTAL_TOKENS] = + (typeof promptTokens === 'number' ? promptTokens : 0) + + (typeof completionTokens === 'number' ? completionTokens : 0); + } + return attributes; } /** One assistant turn for {@link setOutputMessagesAttribute}. */ @@ -227,6 +220,7 @@ export interface StreamResponseState { totalTokens?: number; cacheCreationInputTokens?: number; cacheReadInputTokens?: number; + reasoningOutputTokens?: number; } /** @@ -245,23 +239,18 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp if (state.responseId) attrs[GEN_AI_RESPONSE_ID] = state.responseId; if (state.responseModel) attrs[GEN_AI_RESPONSE_MODEL] = state.responseModel; - if (state.promptTokens !== undefined) attrs[GEN_AI_USAGE_INPUT_TOKENS] = state.promptTokens; - if (state.completionTokens !== undefined) attrs[GEN_AI_USAGE_OUTPUT_TOKENS] = state.completionTokens; - - // Use explicit total if provided (OpenAI, Google), otherwise compute from cache tokens (Anthropic) - if (state.totalTokens !== undefined) { - attrs[GEN_AI_USAGE_TOTAL_TOKENS] = state.totalTokens; - } else if ( - state.promptTokens !== undefined || - state.completionTokens !== undefined || - state.cacheCreationInputTokens !== undefined || - state.cacheReadInputTokens !== undefined - ) { - attrs[GEN_AI_USAGE_TOTAL_TOKENS] = - (state.promptTokens ?? 0) + - (state.completionTokens ?? 0) + - (state.cacheCreationInputTokens ?? 0) + - (state.cacheReadInputTokens ?? 0); + Object.assign( + attrs, + getTokenUsageAttributes( + state.promptTokens, + state.completionTokens, + state.cacheCreationInputTokens, + state.cacheReadInputTokens, + state.totalTokens, + ), + ); + if (typeof state.reasoningOutputTokens === 'number') { + attrs[GEN_AI_USAGE_REASONING_OUTPUT_TOKENS] = state.reasoningOutputTokens; } if (state.finishReasons.length) { diff --git a/packages/server-utils/src/ai/flue/index.ts b/packages/server-utils/src/ai/flue/index.ts index 67e59c9fcdc6..44c9ade4667c 100644 --- a/packages/server-utils/src/ai/flue/index.ts +++ b/packages/server-utils/src/ai/flue/index.ts @@ -1,20 +1,14 @@ import type { Span } from '@sentry/core'; +import { continueTrace, getActiveSpan, LRUMap, startSpan, withActiveSpan } from '@sentry/core'; import { - _INTERNAL_shouldSkipAiProviderWrapping, - _INTERNAL_skipAiProviderWrapping, - continueTrace, - getActiveSpan, - LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - withActiveSpan, -} from '@sentry/core'; -import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, GEN_AI_OPERATION_NAME } from '@sentry/conventions/attributes'; -import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; + GEN_AI_AGENT_NAME, + GEN_AI_CONVERSATION_ID, + GEN_AI_OPERATION_NAME, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; -import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants'; -import { OPENAI_INTEGRATION_NAME } from '../openai/constants'; +import { skipPiAiProviderIntegrations } from '../pi-ai/providers'; import { FLUE_INSTRUMENTATION_KEY, FLUE_OPERATION, FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; import type { SpanTracker } from './utils'; import { @@ -29,8 +23,6 @@ import type { FlueInstrumentation } from './types'; export type FlueOptions = GenAiOptions; -const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]; - /** * Build the object to hand to `instrument()` from `@flue/runtime`. * @@ -45,20 +37,16 @@ const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAM * which fall back to the current client's `dataCollection.genAI` settings and are read per event. */ export function createFlueInstrumentation(options: FlueOptions = {}): FlueInstrumentation { - // Flue drives the providers through `@earendil-works/pi-ai`, which bundles the `openai`, - // `@anthropic-ai/sdk` and `@google/genai` clients. Left alone they instrument the same call this - // reports as a turn, emitting a second `gen_ai.chat` beside ours. + // Flue drives the providers through `@earendil-works/pi-ai`, which sends its requests through the + // provider SDKs that Sentry instruments too (see `skipPiAiProviderIntegrations`). Left alone, their + // integrations report the same request a second time, as a second `gen_ai.chat` beside the turn's. // - // Applied on first use rather than here, for two reasons. Constructing the object proves nothing - // — if `instrument()` rejects it, suppressing the provider integrations would leave the app with - // no `gen_ai.chat` spans at all. And the registry is reset per client (`_setupIntegrations` - // clears it, and Cloudflare calls `init()` per request), so a one-shot call at module scope is - // wiped by the next `init()` and every later request double-reports. - const skipProviders = (): void => { - if (!SKIPPED_PROVIDERS.every(provider => _INTERNAL_shouldSkipAiProviderWrapping(provider))) { - _INTERNAL_skipAiProviderWrapping(SKIPPED_PROVIDERS); - } - }; + // The provider integrations are skipped on first use rather than here, for two reasons. + // Constructing the object proves nothing: if `instrument()` rejects it, suppressing the provider + // integrations would leave the app with no `gen_ai.chat` spans at all. And the registry is reset per + // client (`_setupIntegrations` clears it, and Cloudflare calls `init()` per request), so a one-shot + // call at module scope is wiped by the next `init()` and every later request double-reports. + const skipProviders = skipPiAiProviderIntegrations; // Keyed by the agent operation's own id, which is what the observations carry. That keeps // concurrent runs apart and gives a delegated subagent its own span: Flue nests a second `agent` @@ -104,7 +92,7 @@ export function createFlueInstrumentation(options: FlueOptions = {}): FlueInstru name: `invoke_agent ${ctx.agentName ?? 'agent'}`, op: getGenAiSpanOp('invoke_agent'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'invoke_agent', ...(ctx.agentName ? { [GEN_AI_AGENT_NAME]: ctx.agentName } : {}), ...(ctx.conversationId ? { [GEN_AI_CONVERSATION_ID]: ctx.conversationId } : {}), diff --git a/packages/server-utils/src/ai/flue/types.ts b/packages/server-utils/src/ai/flue/types.ts index bdbd58455518..30850dea9949 100644 --- a/packages/server-utils/src/ai/flue/types.ts +++ b/packages/server-utils/src/ai/flue/types.ts @@ -6,21 +6,7 @@ * `FlueExecutionContext` as of `@flue/runtime` 2.x. */ -/** Token counts and Flue-computed costs on a settled turn. */ -export interface FlueUsage { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - totalTokens?: number; - cost?: { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - total?: number; - }; -} +import type { PiAiUsage } from '../pi-ai/usage'; /** Mirrors `ModelRequestInfo`. */ export interface FlueModelRequestInfo { @@ -51,7 +37,8 @@ export interface FlueModelResponse { responseId?: string; responseModel?: string; output?: unknown; - usage?: FlueUsage; + /** pi-ai's usage, which Flue passes through with its own cost figures. */ + usage?: PiAiUsage; finishReason?: string; } @@ -88,6 +75,8 @@ export interface FlueObservation { request?: FlueModelRequest; args?: unknown; result?: unknown; + /** The tool result as the model sees it, on successful `tool` events. */ + effectiveResult?: unknown; response?: FlueModelResponse; } diff --git a/packages/server-utils/src/ai/flue/utils.ts b/packages/server-utils/src/ai/flue/utils.ts index a1b463cbfbd7..7f22256bf319 100644 --- a/packages/server-utils/src/ai/flue/utils.ts +++ b/packages/server-utils/src/ai/flue/utils.ts @@ -1,7 +1,7 @@ import type { LRUMap, Span } from '@sentry/core'; import { captureException, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, stringify, @@ -9,11 +9,6 @@ import { } from '@sentry/core'; import { GEN_AI_CONVERSATION_ID, - GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_COST_CACHE_READ_INPUT_TOKENS, - GEN_AI_COST_INPUT_TOKENS, - GEN_AI_COST_OUTPUT_TOKENS, - GEN_AI_COST_TOTAL_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_MESSAGES, @@ -30,17 +25,20 @@ import { GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_NAME, - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getGenAiSpanOp } from '../core/utils'; +import { + piAiAssistantMessageToGenAiMessage, + piAiContentToString, + piAiFinishReason, + piAiMessagesToGenAiMessages, +} from '../pi-ai/messages'; +import { setPiAiUsageAttributes } from '../pi-ai/usage'; import { FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; -import type { FlueErrorInfo, FlueModelRequestInfo, FlueObservation, FlueUsage } from './types'; +import type { FlueErrorInfo, FlueModelRequestInfo, FlueObservation } from './types'; /** * Flue persists the incoming W3C `traceparent` at admission and replays it on the agent operation. @@ -123,7 +121,7 @@ export function startTurnSpan(observation: FlueObservation, turnSpans: SpanTrack name: 'chat', op: getGenAiSpanOp('chat'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'chat', ...(observation.conversationId ? { [GEN_AI_CONVERSATION_ID]: observation.conversationId } : {}), // No conventional attribute for this; it is the only way to tell a compaction turn from a @@ -163,22 +161,25 @@ export function endTurnSpan(observation: FlueObservation, turnSpans: SpanTracker setRequestAttributes(span, observation.request); - const { responseId, finishReason } = observation.response ?? {}; + const responseId = observation.response?.responseId; if (responseId) { span.setAttribute(GEN_AI_RESPONSE_ID, responseId); } + const finishReason = piAiFinishReason(observation.response?.finishReason); if (finishReason) { // Serialized, not a raw array: the conventions declare this attribute's value type as `string`, // and that is what `ai/core`, Mastra, OpenAI and Vercel AI all write. span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, stringify([finishReason])); } - const output = observation.response?.output; - if (recordOutputs && output !== undefined) { - span.setAttribute(GEN_AI_OUTPUT_MESSAGES, stringify(output)); + const output = recordOutputs + ? piAiAssistantMessageToGenAiMessage(observation.response?.output, finishReason) + : undefined; + if (output) { + span.setAttribute(GEN_AI_OUTPUT_MESSAGES, stringify([output])); } - setUsageAttributes(span, observation.response?.usage, observation.isError); + setPiAiUsageAttributes(span, observation.response?.usage, observation.isError); if (observation.isError) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); @@ -186,39 +187,6 @@ export function endTurnSpan(observation: FlueObservation, turnSpans: SpanTracker span.end(); } -/** - * Flue reports token counts and its own computed costs on the same `usage` object, so both are set - * here. The cost figures have no equivalent in the provider SDKs' own instrumentation. - */ -export function setUsageAttributes(span: Span, usage: FlueUsage | undefined, isError?: boolean): void { - // A turn that failed before the provider billed anything reports every counter as 0. Writing - // those is noise that reads as a real zero-cost call, so skip the block entirely. - if (!usage || (isError && !usage.totalTokens)) { - return; - } - - const attributes: Record = {}; - const set = (key: string, value: number | undefined): void => { - if (typeof value === 'number') { - attributes[key] = value; - } - }; - - set(GEN_AI_USAGE_INPUT_TOKENS, usage.input); - set(GEN_AI_USAGE_OUTPUT_TOKENS, usage.output); - set(GEN_AI_USAGE_TOTAL_TOKENS, usage.totalTokens); - set(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheRead); - set(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, usage.cacheWrite); - - set(GEN_AI_COST_INPUT_TOKENS, usage.cost?.input); - set(GEN_AI_COST_OUTPUT_TOKENS, usage.cost?.output); - set(GEN_AI_COST_TOTAL_TOKENS, usage.cost?.total); - set(GEN_AI_COST_CACHE_READ_INPUT_TOKENS, usage.cost?.cacheRead); - set(GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, usage.cost?.cacheWrite); - - span.setAttributes(attributes); -} - /** * Tool spans hang off the agent invocation rather than the turn, matching how Flue's own * OpenTelemetry adapter projects them: siblings of `chat`, correlated to model output by tool call @@ -237,7 +205,7 @@ export function startToolSpan(observation: FlueObservation, toolSpans: SpanTrack name: `execute_tool ${toolName ?? 'unknown'}`, op: getGenAiSpanOp('execute_tool'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'execute_tool', ...(toolName ? { [GEN_AI_TOOL_NAME]: toolName } : {}), ...(observation.conversationId ? { [GEN_AI_CONVERSATION_ID]: observation.conversationId } : {}), @@ -257,8 +225,11 @@ export function endToolSpan(observation: FlueObservation, toolSpans: SpanTracker } toolSpans.remove(toolCallId); - if (recordOutputs && observation.result !== undefined) { - span.setAttribute(GEN_AI_TOOL_CALL_RESULT, stringify(observation.result)); + if (recordOutputs) { + const result = toolResultForModel(observation); + if (result !== undefined) { + span.setAttribute(GEN_AI_TOOL_CALL_RESULT, result); + } } if (observation.isError) { @@ -268,6 +239,25 @@ export function endToolSpan(observation: FlueObservation, toolSpans: SpanTracker span.end(); } +/** + * The result the model receives: Flue's `effectiveResult` when it has one, else the content blocks + * of its harness-level `result` (`{ content, details }`, whose `details` payload is tool-specific). + */ +function toolResultForModel(observation: FlueObservation): string | undefined { + const { effectiveResult } = observation; + if (effectiveResult !== undefined) { + if (typeof effectiveResult === 'string') { + return effectiveResult; + } + return Array.isArray(effectiveResult) ? piAiContentToString(effectiveResult) : stringify(effectiveResult); + } + const { result } = observation; + if (isObjectLike(result) && Array.isArray(result.content)) { + return piAiContentToString(result.content); + } + return result === undefined ? undefined : stringify(result); +} + /** * `turn_request` is the only event carrying the request's content — the settled `turn` reports * metadata alone — so input messages, system prompt and tool definitions are read from it. @@ -283,8 +273,9 @@ export function recordRequestContent(observation: FlueObservation, turnSpans: Sp if (input.systemPrompt) { span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, input.systemPrompt); } - if (input.messages) { - span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify(input.messages)); + const messages = piAiMessagesToGenAiMessages(input.messages); + if (messages.length) { + span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify(messages)); } if (input.tools?.length) { span.setAttribute(GEN_AI_TOOL_DEFINITIONS, stringify(input.tools)); diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index 01b7cfb0f6c0..16dbb01d98f9 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -4,7 +4,6 @@ import { getClient, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, startSpanManual, @@ -28,12 +27,11 @@ import { GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; -import { buildMethodPath, getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; +import { buildMethodPath, getGenAiSpanOp, getTokenUsageAttributes, resolveAIRecordingOptions } from '../core/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; @@ -113,7 +111,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: GOOGLE_GENAI_SYSTEM_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [SENTRY_ORIGIN]: 'auto.ai.google_genai', }; if (params) { @@ -205,20 +203,18 @@ export function addResponseAttributes(span: Span, response: GoogleGenAIResponse, // Add usage metadata if present if (response.usageMetadata && typeof response.usageMetadata === 'object') { const usage = response.usageMetadata; - if (typeof usage.promptTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_INPUT_TOKENS]: usage.promptTokenCount, - }); - } - if (typeof usage.candidatesTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_OUTPUT_TOKENS]: usage.candidatesTokenCount, - }); - } - if (typeof usage.totalTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_TOTAL_TOKENS]: usage.totalTokenCount, - }); + const hasOutput = typeof usage.candidatesTokenCount === 'number' || typeof usage.thoughtsTokenCount === 'number'; + span.setAttributes( + getTokenUsageAttributes( + usage.promptTokenCount, + hasOutput ? (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) : undefined, + undefined, + usage.cachedContentTokenCount, + usage.totalTokenCount, + ), + ); + if (typeof usage.thoughtsTokenCount === 'number') { + span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.thoughtsTokenCount); } } diff --git a/packages/server-utils/src/ai/google-genai/streaming.ts b/packages/server-utils/src/ai/google-genai/streaming.ts index 4937b0742b3e..3a2f720f2628 100644 --- a/packages/server-utils/src/ai/google-genai/streaming.ts +++ b/packages/server-utils/src/ai/google-genai/streaming.ts @@ -21,8 +21,11 @@ interface StreamingState { promptTokens?: number; /** Number of completion/output tokens used. */ completionTokens?: number; + candidateTokens?: number; + reasoningOutputTokens?: number; /** Number of total tokens used. */ totalTokens?: number; + cacheReadInputTokens?: number; /** Accumulated tool calls (finalized) */ toolCalls: Array>; /** Accumulated output message parts (for output recording). */ @@ -60,8 +63,13 @@ function handleResponseMetadata(chunk: GoogleGenAIResponse, state: StreamingStat const usage = chunk.usageMetadata; if (usage) { if (typeof usage.promptTokenCount === 'number') state.promptTokens = usage.promptTokenCount; - if (typeof usage.candidatesTokenCount === 'number') state.completionTokens = usage.candidatesTokenCount; + if (typeof usage.candidatesTokenCount === 'number') state.candidateTokens = usage.candidatesTokenCount; + if (typeof usage.thoughtsTokenCount === 'number') state.reasoningOutputTokens = usage.thoughtsTokenCount; + if (state.candidateTokens !== undefined || state.reasoningOutputTokens !== undefined) { + state.completionTokens = (state.candidateTokens ?? 0) + (state.reasoningOutputTokens ?? 0); + } if (typeof usage.totalTokenCount === 'number') state.totalTokens = usage.totalTokenCount; + if (typeof usage.cachedContentTokenCount === 'number') state.cacheReadInputTokens = usage.cachedContentTokenCount; } } diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index 1a8f61c867f6..e1c4fd25fd9b 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -1,10 +1,4 @@ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - stringify, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startSpan, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -12,6 +6,7 @@ import { GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; import { @@ -45,7 +40,7 @@ function extractEmbeddingAttributes(instance: unknown): Record const embeddingsInstance = (instance ?? {}) as Record; const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [SENTRY_OP]: GEN_AI_EMBEDDINGS, [GEN_AI_OPERATION_NAME]: 'embeddings', [GEN_AI_REQUEST_MODEL]: embeddingsInstance.model ?? 'unknown', diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index ea60eef3305e..9088e24cc523 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -1,12 +1,5 @@ /* eslint-disable max-lines */ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpanManual, - stringify, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpanManual, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_OPERATION_NAME, @@ -17,6 +10,7 @@ import { GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_NAME, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { resolveAIRecordingOptions } from '../core/utils'; @@ -34,6 +28,7 @@ import { extractLlmResponseAttributes, extractToolDefinitions, getAgentNameFromMetadata, + getConversationIdFromMetadata, getInvocationParams, } from './utils'; @@ -117,6 +112,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): : operationName, attributes: { ...getAgentNameFromMetadata(metadata), + ...getConversationIdFromMetadata(metadata), ...attributes, [SENTRY_OP]: GEN_AI_CHAT, }, @@ -167,6 +163,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): : operationName, attributes: { ...getAgentNameFromMetadata(metadata), + ...getConversationIdFromMetadata(metadata), ...attributes, [SENTRY_OP]: GEN_AI_CHAT, }, @@ -227,7 +224,8 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const chainName = runName || chain.name; const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', + ...getConversationIdFromMetadata(metadata), + [SENTRY_ORIGIN]: 'auto.ai.langchain', [GEN_AI_OPERATION_NAME]: 'invoke_agent', }; @@ -307,7 +305,8 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const toolName = runName || tool.name || 'unknown_tool'; const attributes: Record = { ...getAgentNameFromMetadata(metadata), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + ...getConversationIdFromMetadata(metadata), + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [GEN_AI_OPERATION_NAME]: 'execute_tool', [GEN_AI_TOOL_NAME]: toolName, }; diff --git a/packages/server-utils/src/ai/langchain/types.ts b/packages/server-utils/src/ai/langchain/types.ts index 352aecdea295..03039782c5bf 100644 --- a/packages/server-utils/src/ai/langchain/types.ts +++ b/packages/server-utils/src/ai/langchain/types.ts @@ -41,12 +41,27 @@ export interface LangChainMessage { }; role?: string; additional_kwargs?: Record; + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + total_tokens?: number; + input_token_details?: { + cache_read?: number; + cache_creation?: number; + }; + }; // LangChain serialized format lc?: number; id?: string[] | string; response_metadata?: { + [key: string]: unknown; model_name?: string; finish_reason?: string; + tokenUsage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; }; kwargs?: { [key: string]: unknown; diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index 4c29d93068ec..067a82fc2d75 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -1,9 +1,10 @@ /* eslint-disable max-lines */ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stringify } from '@sentry/core'; +import { isObjectLike, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, + GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -21,12 +22,10 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE, GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../core/gen-ai-attributes'; -import { extractSystemInstructions } from '../core/utils'; +import { extractSystemInstructions, getTokenUsageAttributes } from '../core/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -227,7 +226,7 @@ function baseRequestAttributes( [GEN_AI_PROVIDER_NAME]: stringify(system ?? 'langchain', String), [GEN_AI_OPERATION_NAME]: 'chat', [GEN_AI_REQUEST_MODEL]: stringify(modelName, String), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, ...extractCommonRequestAttributes(serialized, invocationParams, langSmithMetadata), }; } @@ -328,16 +327,46 @@ function addToolCallsAttributes(generations: LangChainMessage[][], attrs: Record } } -/** - * Adds token usage attributes, supporting both OpenAI (`tokenUsage`) and Anthropic (`usage`) formats. - * - Preserve zero values (0 tokens) by avoiding truthy checks. - * - Compute a total for Anthropic when not explicitly provided. - * - Include cache token metrics when present. - */ -function addTokenUsageAttributes( - llmOutput: LangChainLLMResult['llmOutput'], - attrs: Record, -): void { +export function extractMessageTokenUsageAttributes(message: LangChainMessage): Record { + const usage = message.usage_metadata; + if (isObjectLike(usage)) { + const details = isObjectLike(usage.input_token_details) ? usage.input_token_details : undefined; + return getTokenUsageAttributes( + usage.input_tokens, + usage.output_tokens, + details?.cache_creation, + details?.cache_read, + usage.total_tokens, + ); + } + + const metadata = message.response_metadata; + const tokenUsage = isObjectLike(metadata) && isObjectLike(metadata.tokenUsage) ? metadata.tokenUsage : undefined; + return getTokenUsageAttributes( + tokenUsage?.promptTokens, + tokenUsage?.completionTokens, + undefined, + undefined, + tokenUsage?.totalTokens, + ); +} + +function addTokenUsageAttributes(llmResult: LangChainLLMResult, attrs: Record): void { + const normalizedUsage: Record = {}; + const legacyMessageUsage: Record = {}; + for (const generations of llmResult.generations ?? []) { + // Candidate messages share the usage totals for their prompt. + const message = generations[0]?.message; + if (message) { + const usage = isObjectLike(message.usage_metadata) ? normalizedUsage : legacyMessageUsage; + for (const [key, value] of Object.entries(extractMessageTokenUsageAttributes(message))) { + usage[key] = (usage[key] ?? 0) + value; + } + } + } + Object.assign(attrs, Object.keys(normalizedUsage).length > 0 ? normalizedUsage : legacyMessageUsage); + + const llmOutput = llmResult.llmOutput; if (!llmOutput) return; const tokenUsage = llmOutput.tokenUsage; @@ -350,25 +379,47 @@ function addTokenUsageAttributes( } | undefined; - if (tokenUsage) { - setNumberIfDefined(attrs, GEN_AI_USAGE_INPUT_TOKENS, tokenUsage.promptTokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_OUTPUT_TOKENS, tokenUsage.completionTokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_TOTAL_TOKENS, tokenUsage.totalTokens); - } else if (anthropicUsage) { - setNumberIfDefined(attrs, GEN_AI_USAGE_INPUT_TOKENS, anthropicUsage.input_tokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_OUTPUT_TOKENS, anthropicUsage.output_tokens); - - // Compute total when not provided by the provider. - const input = Number(anthropicUsage.input_tokens); - const output = Number(anthropicUsage.output_tokens); - const total = (Number.isNaN(input) ? 0 : input) + (Number.isNaN(output) ? 0 : output); - if (total > 0) setNumberIfDefined(attrs, GEN_AI_USAGE_TOTAL_TOKENS, total); - - // Extra Anthropic cache metrics (present only when caching is enabled) - if (anthropicUsage.cache_creation_input_tokens !== undefined) - setNumberIfDefined(attrs, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, anthropicUsage.cache_creation_input_tokens); - if (anthropicUsage.cache_read_input_tokens !== undefined) - setNumberIfDefined(attrs, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, anthropicUsage.cache_read_input_tokens); + // llmOutput.tokenUsage can contain only the final stream chunk's counters. + if (anthropicUsage && Object.keys(normalizedUsage).length === 0 && Object.keys(legacyMessageUsage).length === 0) { + const { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens } = anthropicUsage; + const hasInput = + input_tokens !== undefined || cache_creation_input_tokens !== undefined || cache_read_input_tokens !== undefined; + Object.assign( + attrs, + getTokenUsageAttributes( + hasInput + ? (input_tokens ?? 0) + (cache_creation_input_tokens ?? 0) + (cache_read_input_tokens ?? 0) + : undefined, + output_tokens, + cache_creation_input_tokens, + cache_read_input_tokens, + ), + ); + } else if (tokenUsage && Object.keys(normalizedUsage).length === 0 && Object.keys(legacyMessageUsage).length === 0) { + Object.assign( + attrs, + getTokenUsageAttributes( + tokenUsage.promptTokens, + tokenUsage.completionTokens, + undefined, + undefined, + tokenUsage.totalTokens, + ), + ); + } + if (anthropicUsage) { + if ( + typeof anthropicUsage.cache_creation_input_tokens === 'number' && + !(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS in attrs) + ) { + attrs[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = anthropicUsage.cache_creation_input_tokens; + } + if ( + typeof anthropicUsage.cache_read_input_tokens === 'number' && + !(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS in attrs) + ) { + attrs[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = anthropicUsage.cache_read_input_tokens; + } } } @@ -424,7 +475,7 @@ export function extractLlmResponseAttributes( } } - addTokenUsageAttributes(llmResult.llmOutput, attrs); + addTokenUsageAttributes(llmResult, attrs); const llmOutput = llmResult.llmOutput; @@ -462,6 +513,41 @@ export function getAgentNameFromMetadata(metadata?: Record): Re return attrs; } +/** + * Metadata keys under which LangChain apps carry a conversation id, in order of precedence. + * `thread_id` is the LangGraph checkpointer key and one of the two keys LangSmith groups threads by, + * `session_id` is the other, and `sessionId` is what `RunnableWithMessageHistory` requires in JS. + */ +const CONVERSATION_ID_METADATA_KEYS = ['thread_id', 'session_id', 'sessionId'] as const; + +function findConversationIdEntry(source?: Record): [string, string | number] | undefined { + for (const key of CONVERSATION_ID_METADATA_KEYS) { + const value = source?.[key]; + if ((typeof value === 'string' && value) || (typeof value === 'number' && Number.isFinite(value))) { + return [key, value]; + } + } + return undefined; +} + +/** + * Derive `gen_ai.conversation.id` from run metadata, which child runs inherit from their parent. + * An id from `Sentry.setConversationId()` still wins, since `conversationIdIntegration` applies it on `spanStart`. + */ +export function getConversationIdFromMetadata(metadata?: Record): Record { + const entry = findConversationIdEntry(metadata); + return entry ? { [GEN_AI_CONVERSATION_ID]: String(entry[1]) } : {}; +} + +/** + * The conversation id entry from `config.configurable`, to spread under the run metadata. + * `@langchain/core` >= 1.1.40 only copies `configurable` into metadata for LangSmith tracers, not for our handler. + */ +export function getConversationIdMetadataFromConfig(config: Record): Record { + const entry = findConversationIdEntry(config.configurable as Record | undefined); + return entry ? { [entry[0]]: entry[1] } : {}; +} + export function extractToolDefinitions(extraParams?: Record): string | undefined { const tools = (extraParams?.invocation_params as Record)?.tools ?? diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 00825cbd6206..c5b2da14ffc1 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -1,8 +1,7 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; import { GEN_AI_AGENT_NAME, - GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PIPELINE_NAME, @@ -10,6 +9,7 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { extractSystemInstructions, resolveAIRecordingOptions } from '../core/utils'; @@ -25,7 +25,11 @@ import { setResponseAttributes, wrapToolsWithSpans, } from './utils'; -import { _INTERNAL_mergeLangChainCallbackHandler } from '../langchain/utils'; +import { + getConversationIdMetadataFromConfig, + _INTERNAL_mergeLangChainCallbackHandler, + getConversationIdFromMetadata, +} from '../langchain/utils'; let _insideCreateReactAgent = false; @@ -95,11 +99,14 @@ export function instrumentCompiledGraphInvoke( return new Proxy(originalInvoke, { apply(target, thisArg, args: unknown[]): Promise { const modelName = llm?.modelName ?? llm?.model; + const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; return startSpan( { name: 'invoke_agent', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, + // Set before `spanStart`, so an id from `Sentry.setConversationId()` wins, as on the child spans + ...getConversationIdFromMetadata(config?.configurable as Record | undefined), + [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, [SENTRY_OP]: GEN_AI_INVOKE_AGENT, [GEN_AI_OPERATION_NAME]: 'invoke_agent', }, @@ -118,15 +125,6 @@ export function instrumentCompiledGraphInvoke( span.setAttribute(GEN_AI_REQUEST_MODEL, modelName); } - // Extract thread_id from the config (second argument) - // LangGraph uses config.configurable.thread_id for conversation/session linking - const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; - const configurable = config?.configurable as Record | undefined; - const threadId = configurable?.thread_id; - if (threadId && typeof threadId === 'string') { - span.setAttribute(GEN_AI_CONVERSATION_ID, threadId); - } - // Inject callback handler and agent name into invoke config if (sentryCallbackHandler) { const invokeConfig = (args[1] ?? {}) as Record; @@ -134,6 +132,7 @@ export function instrumentCompiledGraphInvoke( const existingMetadata = (invokeConfig.metadata ?? {}) as Record; invokeConfig.metadata = { + ...getConversationIdMetadataFromConfig(invokeConfig), ...existingMetadata, __sentry_langgraph__: true, ...(typeof graphName === 'string' ? { lc_agent_name: graphName } : {}), diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 86e43d2d33a0..dd6d7e61358e 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; import type { Span, SpanAttributes } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -12,15 +12,22 @@ import { GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_DESCRIPTION, GEN_AI_TOOL_NAME, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { BaseChatModel, LangChainMessage } from '../langchain/types'; -import { normalizeLangChainMessages } from '../langchain/utils'; +import { + extractMessageTokenUsageAttributes, + getConversationIdFromMetadata, + normalizeLangChainMessages, +} from '../langchain/utils'; import { LANGGRAPH_ORIGIN } from './constants'; import type { CompiledGraph, LangGraphOptions, LangGraphTool } from './types'; @@ -75,7 +82,7 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, const wrappedInvoke = new Proxy(originalInvoke as (...args: unknown[]) => unknown, { apply(target, thisArg, args: unknown[]): unknown { const spanAttributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, + [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, [SENTRY_OP]: GEN_AI_EXECUTE_TOOL, [GEN_AI_OPERATION_NAME]: 'execute_tool', [GEN_AI_TOOL_NAME]: toolName, @@ -88,6 +95,7 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, if (typeof callAgentName === 'string') { spanAttributes[GEN_AI_AGENT_NAME] = callAgentName; } + Object.assign(spanAttributes, getConversationIdFromMetadata(callConfig?.metadata as Record)); if (toolDescription) { spanAttributes[GEN_AI_TOOL_DESCRIPTION] = toolDescription; @@ -183,45 +191,17 @@ export function extractTokenUsageFromMessage(message: LangChainMessage): { inputTokens: number; outputTokens: number; totalTokens: number; + cacheCreationInputTokens?: number; + cacheReadInputTokens?: number; } { - const msg = message as Record; - let inputTokens = 0; - let outputTokens = 0; - let totalTokens = 0; - - // Extract from usage_metadata (newer format) - if (msg.usage_metadata && typeof msg.usage_metadata === 'object') { - const usage = msg.usage_metadata as Record; - if (typeof usage.input_tokens === 'number') { - inputTokens = usage.input_tokens; - } - if (typeof usage.output_tokens === 'number') { - outputTokens = usage.output_tokens; - } - if (typeof usage.total_tokens === 'number') { - totalTokens = usage.total_tokens; - } - return { inputTokens, outputTokens, totalTokens }; - } - - // Fallback: Extract from response_metadata.tokenUsage - if (msg.response_metadata && typeof msg.response_metadata === 'object') { - const metadata = msg.response_metadata as Record; - if (metadata.tokenUsage && typeof metadata.tokenUsage === 'object') { - const tokenUsage = metadata.tokenUsage as Record; - if (typeof tokenUsage.promptTokens === 'number') { - inputTokens = tokenUsage.promptTokens; - } - if (typeof tokenUsage.completionTokens === 'number') { - outputTokens = tokenUsage.completionTokens; - } - if (typeof tokenUsage.totalTokens === 'number') { - totalTokens = tokenUsage.totalTokens; - } - } - } - - return { inputTokens, outputTokens, totalTokens }; + const attributes = extractMessageTokenUsageAttributes(message); + return { + inputTokens: attributes[GEN_AI_USAGE_INPUT_TOKENS] ?? 0, + outputTokens: attributes[GEN_AI_USAGE_OUTPUT_TOKENS] ?? 0, + totalTokens: attributes[GEN_AI_USAGE_TOTAL_TOKENS] ?? 0, + cacheCreationInputTokens: attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS], + cacheReadInputTokens: attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS], + }; } /** @@ -302,6 +282,8 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag let totalInputTokens = 0; let totalOutputTokens = 0; let totalTokens = 0; + let cacheCreationInputTokens: number | undefined; + let cacheReadInputTokens: number | undefined; // Extract metadata from messages for (const message of newMessages) { @@ -310,6 +292,12 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag totalInputTokens += tokens.inputTokens; totalOutputTokens += tokens.outputTokens; totalTokens += tokens.totalTokens; + if (tokens.cacheCreationInputTokens !== undefined) { + cacheCreationInputTokens = (cacheCreationInputTokens ?? 0) + tokens.cacheCreationInputTokens; + } + if (tokens.cacheReadInputTokens !== undefined) { + cacheReadInputTokens = (cacheReadInputTokens ?? 0) + tokens.cacheReadInputTokens; + } // Extract model metadata (last message's metadata wins for model/finish_reason) extractModelMetadata(span, message); @@ -325,4 +313,10 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag if (totalTokens > 0) { span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS, totalTokens); } + if (cacheCreationInputTokens !== undefined) { + span.setAttribute(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, cacheCreationInputTokens); + } + if (cacheReadInputTokens !== undefined) { + span.setAttribute(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cacheReadInputTokens); + } } diff --git a/packages/server-utils/src/ai/mastra/index.ts b/packages/server-utils/src/ai/mastra/index.ts index 10ca720d976c..0bfcdd712777 100644 --- a/packages/server-utils/src/ai/mastra/index.ts +++ b/packages/server-utils/src/ai/mastra/index.ts @@ -5,12 +5,10 @@ import { getActiveSpan, getClient, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; -import { GEN_AI_RESPONSE_MODEL } from '@sentry/conventions/attributes'; +import { GEN_AI_RESPONSE_MODEL, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../debug-build'; import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; @@ -144,8 +142,8 @@ export class SentryMastraExporter implements MastraObservabilityExporter { parentSpan: parentSpan ?? activeSpan, attributes: { ...this._attributesFor(span), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOperation(span.type)?.op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MASTRA_ORIGIN, + [SENTRY_OP]: getOperation(span.type)?.op, + [SENTRY_ORIGIN]: MASTRA_ORIGIN, }, }); diff --git a/packages/server-utils/src/ai/mistral/index.ts b/packages/server-utils/src/ai/mistral/index.ts index 96a335d0ebf2..4f6ba7b25f7e 100644 --- a/packages/server-utils/src/ai/mistral/index.ts +++ b/packages/server-utils/src/ai/mistral/index.ts @@ -1,10 +1,4 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, - startSpanManual, - stringify, -} from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan, startSpanManual, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -14,6 +8,7 @@ import { GEN_AI_PROVIDER_NAME, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { @@ -55,7 +50,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: MISTRAL_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MISTRAL_ORIGIN, + [SENTRY_ORIGIN]: MISTRAL_ORIGIN, [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: streaming, }; diff --git a/packages/server-utils/src/ai/openai/index.ts b/packages/server-utils/src/ai/openai/index.ts index 7bed7ab27743..981da4281ef5 100644 --- a/packages/server-utils/src/ai/openai/index.ts +++ b/packages/server-utils/src/ai/openai/index.ts @@ -3,7 +3,6 @@ import { DEBUG_BUILD } from '../../debug-build'; import { getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, startSpanManual, @@ -19,6 +18,7 @@ import { GEN_AI_REQUEST_MODEL, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { @@ -67,7 +67,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', }; if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { diff --git a/packages/server-utils/src/ai/openai/streaming.ts b/packages/server-utils/src/ai/openai/streaming.ts index 66b5e11491f6..0828e755e4cb 100644 --- a/packages/server-utils/src/ai/openai/streaming.ts +++ b/packages/server-utils/src/ai/openai/streaming.ts @@ -31,6 +31,8 @@ interface StreamingState { completionTokens: number | undefined; /** Total number of tokens used (prompt + completion). */ totalTokens: number | undefined; + cacheReadInputTokens: number | undefined; + cacheCreationInputTokens: number | undefined; /** * Accumulated tool calls from Chat Completion streaming, indexed by tool call index. * @see https://platform.openai.com/docs/guides/function-calling?api-mode=chat#streaming @@ -96,6 +98,9 @@ function processChatCompletionChunk(chunk: ChatCompletionChunk, state: Streaming state.promptTokens = chunk.usage.prompt_tokens; state.completionTokens = chunk.usage.completion_tokens; state.totalTokens = chunk.usage.total_tokens; + state.cacheReadInputTokens = chunk.usage.prompt_tokens_details?.cached_tokens ?? state.cacheReadInputTokens; + state.cacheCreationInputTokens = + chunk.usage.prompt_tokens_details?.cache_write_tokens ?? state.cacheCreationInputTokens; } for (const choice of chunk.choices ?? []) { @@ -179,6 +184,9 @@ function processResponsesApiEvent( state.promptTokens = response.usage.input_tokens; state.completionTokens = response.usage.output_tokens; state.totalTokens = response.usage.total_tokens; + state.cacheReadInputTokens = response.usage.input_tokens_details?.cached_tokens ?? state.cacheReadInputTokens; + state.cacheCreationInputTokens = + response.usage.input_tokens_details?.cache_write_tokens ?? state.cacheCreationInputTokens; } if (response.status) { @@ -215,6 +223,8 @@ export async function* instrumentStream( promptTokens: undefined, completionTokens: undefined, totalTokens: undefined, + cacheReadInputTokens: undefined, + cacheCreationInputTokens: undefined, chatCompletionToolCalls: {}, responsesApiToolCalls: [], }; diff --git a/packages/server-utils/src/ai/openai/types.ts b/packages/server-utils/src/ai/openai/types.ts index efc62b6e967a..cb57ea84b7a0 100644 --- a/packages/server-utils/src/ai/openai/types.ts +++ b/packages/server-utils/src/ai/openai/types.ts @@ -49,6 +49,7 @@ export interface OpenAIResponseObject { input_tokens: number; input_tokens_details?: { cached_tokens?: number; + cache_write_tokens?: number; }; output_tokens: number; output_tokens_details?: { @@ -231,6 +232,7 @@ export interface ChatCompletionChunk { prompt_tokens_details: { audio_tokens: number; cached_tokens: number; + cache_write_tokens?: number; }; }; } diff --git a/packages/server-utils/src/ai/openai/utils.ts b/packages/server-utils/src/ai/openai/utils.ts index 429340530e2c..49b39eb85013 100644 --- a/packages/server-utils/src/ai/openai/utils.ts +++ b/packages/server-utils/src/ai/openai/utils.ts @@ -1,4 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ +import { isObjectLike } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_CONVERSATION_ID, @@ -12,6 +13,8 @@ import { GEN_AI_RESPONSE_MODEL, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, @@ -90,6 +93,15 @@ export function addResponseAttributes(span: Span, result: unknown, recordOutputs if (typeof usage.total_tokens === 'number') { attrs[GEN_AI_USAGE_TOTAL_TOKENS] = usage.total_tokens; } + const inputDetails = usage.prompt_tokens_details ?? usage.input_tokens_details; + if (isObjectLike(inputDetails)) { + if (typeof inputDetails.cached_tokens === 'number') { + attrs[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = inputDetails.cached_tokens; + } + if (typeof inputDetails.cache_write_tokens === 'number') { + attrs[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = inputDetails.cache_write_tokens; + } + } } // Finish reasons from choices (chat completions) diff --git a/packages/server-utils/src/ai/pi-ai/messages.ts b/packages/server-utils/src/ai/pi-ai/messages.ts new file mode 100644 index 000000000000..c5060cdb1181 --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/messages.ts @@ -0,0 +1,104 @@ +import { isObjectLike, stringify } from '@sentry/core'; +import type { GenAiMessage, GenAiMessagePart } from '../types'; + +/** + * Map pi-ai messages (`@earendil-works/pi-ai`, which Flue and pi-durable send) to the gen_ai + * conventions. Sentry renders neither pi-ai's `toolResult` role nor its `toolCall` parts. + * + * System messages are left out: their prompt belongs in `gen_ai.system_instructions`. Messages with + * a role the conventions do not have are left out too. + */ +export function piAiMessagesToGenAiMessages(messages: unknown): GenAiMessage[] { + if (!Array.isArray(messages)) { + return []; + } + + const mapped: GenAiMessage[] = []; + for (const message of messages) { + if ( + !isObjectLike(message) || + (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'toolResult') + ) { + continue; + } + + if (message.role === 'toolResult') { + mapped.push({ + role: 'tool', + parts: [ + { + type: 'tool_call_response', + id: message.toolCallId, + name: message.toolName, + result: piAiContentToString(message.content), + }, + ], + }); + continue; + } + + const parts = piAiContentToParts(message.content); + if (parts.length) { + mapped.push({ role: message.role, parts }); + } + } + return mapped; +} + +/** The `gen_ai.output.messages` entry of a pi-ai assistant message, or `undefined` when it has no content. */ +export function piAiAssistantMessageToGenAiMessage(message: unknown, finishReason?: string): GenAiMessage | undefined { + const parts = isObjectLike(message) ? piAiContentToParts(message.content) : []; + if (!parts.length) { + return undefined; + } + return { role: 'assistant', parts, ...(finishReason ? { finish_reason: finishReason } : {}) }; +} + +/** pi-ai names a tool-calling stop `toolUse`; the conventions call it `tool_call`. */ +export function piAiFinishReason(stopReason: unknown): string | undefined { + if (typeof stopReason !== 'string' || !stopReason) { + return undefined; + } + return stopReason === 'toolUse' ? 'tool_call' : stopReason; +} + +/** Text-only content as its text, joined the way pi-ai joins it; anything else as its mapped parts. */ +export function piAiContentToString(content: unknown): string | undefined { + const parts = piAiContentToParts(content); + return parts.every(part => part.type === 'text') ? parts.map(part => part.content).join('\n') : stringify(parts); +} + +function piAiContentToParts(content: unknown): GenAiMessagePart[] { + if (typeof content === 'string') { + return content ? [{ type: 'text', content }] : []; + } + if (!Array.isArray(content)) { + return []; + } + return content.map(piAiPartToGenAiPart).filter((part): part is GenAiMessagePart => part !== undefined); +} + +/** An image is reported by its media type only: its `data` is base64, which the conventions require to be dropped. */ +function piAiPartToGenAiPart(part: unknown): GenAiMessagePart | undefined { + if (!isObjectLike(part)) { + return undefined; + } + + switch (part.type) { + case 'text': + // An empty text block makes Sentry render the message as "(no value)" instead of its tool calls. + return typeof part.text === 'string' && part.text.trim() ? { type: 'text', content: part.text } : undefined; + case 'thinking': + // A redacted thinking block carries an encrypted payload, not text. + return typeof part.thinking === 'string' && part.thinking && !part.redacted + ? { type: 'reasoning', content: part.thinking } + : undefined; + case 'toolCall': + return { type: 'tool_call', id: part.id, name: part.name, arguments: stringify(part.arguments ?? {}, String) }; + case 'image': + return { type: 'blob', mime_type: part.mimeType }; + default: + // Part kinds we don't know yet render as JSON rather than disappearing. + return { type: 'object', content: part }; + } +} diff --git a/packages/server-utils/src/ai/pi-ai/providers.ts b/packages/server-utils/src/ai/pi-ai/providers.ts new file mode 100644 index 000000000000..524afe79d573 --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/providers.ts @@ -0,0 +1,27 @@ +import { _INTERNAL_shouldSkipAiProviderWrapping, _INTERNAL_skipAiProviderWrapping } from '@sentry/core'; +import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; +import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants'; +import { OPENAI_INTEGRATION_NAME } from '../openai/constants'; + +// pi-ai sends its requests through the `openai`, `@anthropic-ai/sdk` and `@google/genai` clients. +// Left alone, those integrations report the same request a second time beside the `chat` span of +// the framework that sent it. Bedrock requests go through `@aws-sdk/client-bedrock-runtime`, which +// `awsIntegration` still reports; that one has no skip yet. +const PI_AI_PROVIDER_INTEGRATIONS = [ + OPENAI_INTEGRATION_NAME, + ANTHROPIC_AI_INTEGRATION_NAME, + GOOGLE_GENAI_INTEGRATION_NAME, +]; + +/** + * Stop the provider SDK integrations from reporting the requests pi-ai sends. The skip is + * process-wide: the provider SDKs do not know which of their calls pi-ai made. + * + * The registry is reset per client, so callers apply this on the first request instead of once at + * setup, or the next `init()` would undo it. + */ +export function skipPiAiProviderIntegrations(): void { + if (!PI_AI_PROVIDER_INTEGRATIONS.every(provider => _INTERNAL_shouldSkipAiProviderWrapping(provider))) { + _INTERNAL_skipAiProviderWrapping(PI_AI_PROVIDER_INTEGRATIONS); + } +} diff --git a/packages/server-utils/src/ai/pi-ai/usage.ts b/packages/server-utils/src/ai/pi-ai/usage.ts new file mode 100644 index 000000000000..4bfb8869e72e --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/usage.ts @@ -0,0 +1,75 @@ +import type { Span } from '@sentry/core'; +import { + GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_COST_CACHE_READ_INPUT_TOKENS, + GEN_AI_COST_INPUT_TOKENS, + GEN_AI_COST_OUTPUT_TOKENS, + GEN_AI_COST_TOTAL_TOKENS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; + +/** + * Token counts and costs of a pi-ai response. `input` and `cost.input` leave the cache tokens out; + * `reasoning` is part of `output`. + */ +export interface PiAiUsage { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + reasoning?: number; + totalTokens?: number; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + }; +} + +/** + * Set the token and cost attributes of a pi-ai response. The conventions count cached tokens in + * `gen_ai.usage.input_tokens` and their cost in `gen_ai.cost.input_tokens`, so both are summed here. + * + * `unbilled` is true for a response the provider may not have billed: a failed request, or one the + * provider parked to answer later. Its counters are all 0, which would read as a real zero-cost + * call, so nothing is set for it unless it reports tokens. + */ +export function setPiAiUsageAttributes(span: Span, usage: PiAiUsage | undefined, unbilled?: boolean): void { + if (!usage || (unbilled && !usage.totalTokens)) { + return; + } + + const attributes: Record = {}; + const set = (key: string, value: number | undefined): void => { + if (typeof value === 'number') { + attributes[key] = value; + } + }; + + set(GEN_AI_USAGE_INPUT_TOKENS, sum(usage.input, usage.cacheRead, usage.cacheWrite)); + set(GEN_AI_USAGE_OUTPUT_TOKENS, usage.output); + set(GEN_AI_USAGE_TOTAL_TOKENS, usage.totalTokens); + set(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheRead); + set(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, usage.cacheWrite); + set(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoning); + + set(GEN_AI_COST_INPUT_TOKENS, sum(usage.cost?.input, usage.cost?.cacheRead, usage.cost?.cacheWrite)); + set(GEN_AI_COST_OUTPUT_TOKENS, usage.cost?.output); + set(GEN_AI_COST_TOTAL_TOKENS, usage.cost?.total); + set(GEN_AI_COST_CACHE_READ_INPUT_TOKENS, usage.cost?.cacheRead); + set(GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, usage.cost?.cacheWrite); + + span.setAttributes(attributes); +} + +function sum(...values: (number | undefined)[]): number | undefined { + const numbers = values.filter((value): value is number => typeof value === 'number'); + return numbers.length ? numbers.reduce((total, value) => total + value, 0) : undefined; +} diff --git a/packages/server-utils/src/ai/types.ts b/packages/server-utils/src/ai/types.ts new file mode 100644 index 000000000000..a2e35318a238 --- /dev/null +++ b/packages/server-utils/src/ai/types.ts @@ -0,0 +1,10 @@ +/** A message in the gen_ai conventions shape, see https://develop.sentry.dev/sdk/telemetry/traces/modules/ai-agents/. */ +export interface GenAiMessage { + role: 'assistant' | 'system' | 'tool' | 'user'; + parts: GenAiMessagePart[]; + finish_reason?: string; +} + +export type GenAiMessagePart = Record & { + type: 'blob' | 'object' | 'reasoning' | 'text' | 'tool_call' | 'tool_call_response'; +}; diff --git a/packages/server-utils/src/ai/typesafe/index.ts b/packages/server-utils/src/ai/typesafe/index.ts index 328853cf5b02..af4115b0137d 100644 --- a/packages/server-utils/src/ai/typesafe/index.ts +++ b/packages/server-utils/src/ai/typesafe/index.ts @@ -8,18 +8,13 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { GEN_AI_EVALUATE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - stringify, - withActiveSpan, -} from '@sentry/core'; +import { isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, stringify, withActiveSpan } from '@sentry/core'; import type { GenAiOptions } from '../core/utils'; -import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; +import { resolveAIRecordingOptions } from '../core/utils'; import { TYPESAFE_ORIGIN, TYPESAFE_PROVIDER_NAME } from './constants'; /** @@ -34,7 +29,7 @@ export function startEvaluateSpan(request: unknown, client: unknown, recordInput return startInactiveSpan({ name: model ? `evaluate ${model}` : 'evaluate', - op: getGenAiSpanOp('evaluate'), + op: GEN_AI_EVALUATE, attributes: getRequestAttributes(params, model, recordInputs), }); } @@ -45,7 +40,7 @@ function getRequestAttributes( recordInputs: boolean, ): SpanAttributes { return { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: TYPESAFE_ORIGIN, + [SENTRY_ORIGIN]: TYPESAFE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'evaluate', [GEN_AI_PROVIDER_NAME]: TYPESAFE_PROVIDER_NAME, ...(model ? { [GEN_AI_REQUEST_MODEL]: model } : {}), diff --git a/packages/server-utils/src/ai/vercel-ai/index.ts b/packages/server-utils/src/ai/vercel-ai/index.ts index 76a355fdc588..3296eb18a73f 100644 --- a/packages/server-utils/src/ai/vercel-ai/index.ts +++ b/packages/server-utils/src/ai/vercel-ai/index.ts @@ -1,6 +1,5 @@ import type { SpanAttributeValue } from '@sentry/core'; import { - GEN_AI_CONVERSATION_ID, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, @@ -10,8 +9,8 @@ import { import type { OpenAiProviderMetadata, ProviderMetadata } from './vercel-ai-attributes'; /** - * Derive the `gen_ai.usage.*` cache/reasoning/prediction token attributes and `gen_ai.conversation.id` - * from an AI SDK `providerMetadata` object. + * Derive the `gen_ai.usage.*` cache/reasoning/prediction token attributes from an AI SDK + * `providerMetadata` object. * * Used by the `ai` >= 7 tracing-channel subscriber, which receives `providerMetadata` as an object on * the channel result. Pass the already-parsed object; unknown/empty input yields `{}`. @@ -39,7 +38,6 @@ export function getProviderMetadataAttributes(providerMetadata: unknown): Record 'gen_ai.usage.output_tokens.prediction_rejected', openaiMetadata.rejectedPredictionTokens, ); - setAttributeIfDefined(attributes, GEN_AI_CONVERSATION_ID, openaiMetadata.responseId); } if (metadata.anthropic) { diff --git a/packages/server-utils/src/ai/workers-ai/index.ts b/packages/server-utils/src/ai/workers-ai/index.ts index eb3a7c5407cb..ef49c2c4b89a 100644 --- a/packages/server-utils/src/ai/workers-ai/index.ts +++ b/packages/server-utils/src/ai/workers-ai/index.ts @@ -37,7 +37,7 @@ function instrumentRun( const [model, inputs, runOptions] = args as [unknown, unknown, Record | undefined]; - const operationName = getOperationName(inputs); + const operationName = getOperationName(model, inputs); const requestAttributes = extractRequestAttributes(model, inputs, operationName); const modelName = typeof model === 'string' && model ? model : 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index 67645a456988..66dd3d567c8c 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -14,41 +14,41 @@ import { GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; -import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stringify } from '@sentry/core'; +import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS, GEN_AI_EVALUATE } from '@sentry/conventions/op'; +import { isObjectLike, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; -import { - extractSystemInstructions, - getGenAiSpanOp, - setOutputMessagesAttribute, - setTokenUsageAttributes, -} from '../core/utils'; +import { extractSystemInstructions, getTokenUsageAttributes, setOutputMessagesAttribute } from '../core/utils'; import { addResponseAttributes as addEvaluateResponseAttributes, getEvaluationInputMessages } from '../typesafe'; // Re-exported so `workers-ai/streaming.ts` keeps importing it from this module. export { setOutputMessagesAttribute }; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from './constants'; import type { WorkersAiInput, WorkersAiOutput } from './types'; -/** - * Determine the gen_ai operation name from the inputs passed to `AI.run`. - * Workers AI exposes a single `run` method, so we infer the operation from the input shape. - */ export type WorkersAiOperationName = 'chat' | 'embeddings' | 'evaluate'; export const WORKERS_AI_OPERATION_SPAN_OPS: Record = { chat: GEN_AI_CHAT, embeddings: GEN_AI_EMBEDDINGS, - evaluate: getGenAiSpanOp('evaluate'), + evaluate: GEN_AI_EVALUATE, }; -export function getOperationName(inputs: unknown): WorkersAiOperationName { +/** + * Model ID prefixes of the evaluation models (TypeSafe Jev and Cloudflare Clef), traced as `evaluate` spans. + */ +const EVALUATE_MODEL_PREFIXES = ['typesafe/jev', '@cf/cloudflare/clef']; + +/** + * Determine the gen_ai operation name from the model and inputs passed to `AI.run`. + * Workers AI exposes a single `run` method, so we infer the operation from the model ID and the input shape. + */ +export function getOperationName(model: unknown, inputs: unknown): WorkersAiOperationName { + if (typeof model === 'string' && EVALUATE_MODEL_PREFIXES.some(prefix => model.startsWith(prefix))) { + return 'evaluate'; + } if (inputs && typeof inputs === 'object') { - // TypeSafe evaluation models (e.g. `typesafe/jev`) - if ('state' in inputs && 'questions' in inputs) { - return 'evaluate'; - } if ('messages' in inputs || 'prompt' in inputs) { return 'chat'; } @@ -70,7 +70,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: typeof model === 'string' ? model : 'unknown', }; @@ -173,7 +173,7 @@ export function addResponseAttributes( const response = result as WorkersAiOutput; if (response.usage) { - setTokenUsageAttributes(span, response.usage.prompt_tokens, response.usage.completion_tokens); + span.setAttributes(getTokenUsageAttributes(response.usage.prompt_tokens, response.usage.completion_tokens)); } if (recordOutputs) { diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index b67b546d8b2f..9b227d7e0e2c 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -7,7 +7,6 @@ import { getClient, getTraceData, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, timestampInSeconds, @@ -26,6 +25,7 @@ import { SERVER_ADDRESS, SERVER_PORT, URL_FULL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op'; import { amqplibModuleNames } from '../orchestrion/config/amqplib'; @@ -464,7 +464,7 @@ function startPublishSpan(data: AmqpChannelContext): Span { [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_VALUE_SEND, [MESSAGING_MESSAGE_ID]: options?.messageId as string | undefined, [ATTR_MESSAGING_CONVERSATION_ID]: options?.correlationId as string | undefined, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PUBLISHER_ORIGIN, + [SENTRY_ORIGIN]: PUBLISHER_ORIGIN, }, }); @@ -510,7 +510,7 @@ function startConsumeSpan(queue: string | undefined, msg: ConsumeMessage, channe [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_VALUE_PROCESS, [MESSAGING_MESSAGE_ID]: msg.properties?.messageId as string | undefined, [ATTR_MESSAGING_CONVERSATION_ID]: msg.properties?.correlationId as string | undefined, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN, + [SENTRY_ORIGIN]: CONSUMER_ORIGIN, }, }); } diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index 33517d4730c2..d14d3cbb2f6a 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -1,4 +1,4 @@ -import { GEN_AI_REQUEST_MODEL } from '@sentry/conventions/attributes'; +import { GEN_AI_REQUEST_MODEL, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { @@ -7,7 +7,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; @@ -115,7 +114,7 @@ function createGenAiSpan( const attributes = extractRequestAttributes(args, operation, recordInputs); const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); const span = startInactiveSpan({ diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index eb966dcd56c6..25a75931deb7 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -1,12 +1,13 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, startInactiveSpan } from '@sentry/core'; import { _AWS_REQUEST_ID as AWS_REQUEST_ID, AWS_REQUEST_EXTENDED_ID, CLOUD_REGION, SENTRY_KIND, HTTP_RESPONSE_STATUS_CODE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; import { CHANNELS } from '../../orchestrion/channels'; @@ -111,7 +112,7 @@ function instrumentAwsSdk(servicesExtensions: ServicesExtensions): void { op: requestMetadata.spanOp || RPC, attributes: { [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AWS_SDK_ORIGIN, + [SENTRY_ORIGIN]: AWS_SDK_ORIGIN, ...extractAttributesFromNormalizedRequest(normalizedRequest), ...requestMetadata.spanAttributes, }, diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index ac928f3ceeca..89decb7338e9 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -6,6 +6,7 @@ import { DB_OPERATION_NAME, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import type { IntegrationFn, Span, StartSpanOptions } from '@sentry/core'; @@ -15,7 +16,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpan, waitForTracingChannelBinding, @@ -113,7 +113,7 @@ function makeSpanOptions( // the vendored OTel instrumentation. The `batch` runs off a deferred tick with no obvious network // peer, so it gets no kind. [SENTRY_KIND]: operation === 'batch' ? undefined : 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_OPERATION_NAME]: operation, // A loader batches and caches one entity type, so it is the closest thing dataloader has to a // collection. Unnamed loaders report nothing. diff --git a/packages/server-utils/src/integrations/express/instrumentation.ts b/packages/server-utils/src/integrations/express/instrumentation.ts index ae442c454602..753ebbc90660 100644 --- a/packages/server-utils/src/integrations/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/express/instrumentation.ts @@ -1,5 +1,5 @@ import type * as diagnosticsChannel from 'node:diagnostics_channel'; -import { HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { HTTP_ROUTE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { @@ -12,7 +12,6 @@ import { hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, stringMatchesSomePattern, withActiveSpan, @@ -340,7 +339,7 @@ function getSpanForLayer(data: HandleChannelContext, options: ExpressIntegration ? matchedRoute || REQUEST_HANDLER_SPAN_NAME_FALLBACK : name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: EXPRESS_TYPE_TO_SPAN_OP[type], [ATTR_EXPRESS_NAME]: name, [ATTR_EXPRESS_TYPE]: type, diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 92f71c908fe4..6d34c4f66f2d 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -21,6 +21,7 @@ import { HTTP_ROUTE, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; @@ -30,7 +31,6 @@ import { getIsolationScope, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, startSpan, @@ -168,7 +168,7 @@ function onRequest(this: any, request: any, _reply: any, hookDone: () => void): getIsolationScope().setTransactionName(`${method} ${routeName}`); const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: HANDLER, [HTTP_REQUEST_METHOD]: request.method, [URL_PATH]: request.url, @@ -359,7 +359,7 @@ function handlerWrapper(handler: AnyFn, hookName: string, spanAttributes: Record attributes: { ...spanAttributes, [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, parentSpan, }, diff --git a/packages/server-utils/src/integrations/firebase/firestore.ts b/packages/server-utils/src/integrations/firebase/firestore.ts index 161daca6abaf..43928bd504b0 100644 --- a/packages/server-utils/src/integrations/firebase/firestore.ts +++ b/packages/server-utils/src/integrations/firebase/firestore.ts @@ -8,10 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import type { FirebaseApp, FirebaseOptions, FirestoreReference, FirestoreSettings } from './firestore-types'; /** @@ -24,7 +25,7 @@ export function startFirestoreSpan(spanName: string, reference: FirestoreReferen attributes: { [SENTRY_OP]: DB_QUERY, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.firestore', + [SENTRY_ORIGIN]: 'auto.firebase.firestore', [DB_OPERATION_NAME]: spanName, ...buildAttributes(reference), }, diff --git a/packages/server-utils/src/integrations/firebase/functions.ts b/packages/server-utils/src/integrations/firebase/functions.ts index c8a919ef6fa8..75fd439a3cd6 100644 --- a/packages/server-utils/src/integrations/firebase/functions.ts +++ b/packages/server-utils/src/integrations/firebase/functions.ts @@ -5,6 +5,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; @@ -13,7 +14,6 @@ import { flush, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, SPAN_STATUS_ERROR, startSpanManual, @@ -65,7 +65,7 @@ function wrapHandler(handler: Handler, triggerType: string): Handler { const enhancedTriggerType = `firebase.function.${triggerType}`; const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FUNCTIONS_ORIGIN, + [SENTRY_ORIGIN]: FUNCTIONS_ORIGIN, [FAAS_NAME]: functionName, [FAAS_TRIGGER]: triggerType, 'faas.provider': 'firebase', diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index 40d0f4c530a1..f5236f5a364c 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -1,8 +1,8 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, startInactiveSpan } from '@sentry/core'; import { CHANNELS } from '../orchestrion/channels'; import { genericPoolModuleNames } from '../orchestrion/config/generic-pool'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -41,7 +41,7 @@ function instrumentGenericPool(): void { name: 'generic-pool.acquire', attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.generic_pool', + [SENTRY_ORIGIN]: 'auto.db.generic_pool', }, }), ); diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index 6ddaf8757d6a..b191c503df5b 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -7,7 +7,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startInactiveSpan, } from '@sentry/core'; @@ -116,7 +115,7 @@ function createGenAiSpan( const attributes = extractRequestAttributes(operation, params, data.self, recordInputs); const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); const span = startInactiveSpan({ diff --git a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts index 68f7c832fe14..8ea8a5d91944 100644 --- a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts @@ -1,14 +1,13 @@ import type { TracingChannel } from 'node:diagnostics_channel'; -import { GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE } from '@sentry/conventions/attributes'; -import { GRAPHQL } from '@sentry/conventions/op'; import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; + GRAPHQL_DOCUMENT, + GRAPHQL_OPERATION_NAME, + GRAPHQL_OPERATION_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { GRAPHQL } from '@sentry/conventions/op'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { GRAPHQL_FIELD_NAME, @@ -159,8 +158,8 @@ function setupParseChannel(tracingChannel: GraphqlTracingChannelFactory): void { return startInactiveSpan({ name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_PARSE}` : SPAN_NAME_PARSE, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_PARSE, }, }); @@ -176,8 +175,8 @@ function setupValidateChannel(tracingChannel: GraphqlTracingChannelFactory): voi return startInactiveSpan({ name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_VALIDATE}` : SPAN_NAME_VALIDATE, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_VALIDATE, [GRAPHQL_DOCUMENT]: collectGraphqlDocument(data.document), }, @@ -212,8 +211,8 @@ function setupOperationChannel( ? streamedName : getOperationSpanName(data.operationType, data.operationName, fallbackName), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_EXECUTE, [GRAPHQL_OPERATION_TYPE]: data.operationType, [GRAPHQL_OPERATION_NAME]: data.operationName || undefined, @@ -254,8 +253,8 @@ function setupResolveChannel(tracingChannel: GraphqlTracingChannelFactory, ignor ? `GraphQL ${PROCESSING_TYPE_RESOLVE}` : `${SPAN_NAME_RESOLVE} ${data.fieldPath}`, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: data.fieldName, [GRAPHQL_FIELD_PATH]: data.fieldPath, diff --git a/packages/server-utils/src/integrations/graphql/resolvers.ts b/packages/server-utils/src/integrations/graphql/resolvers.ts index 8e028d939ee8..c432b06b79ec 100644 --- a/packages/server-utils/src/integrations/graphql/resolvers.ts +++ b/packages/server-utils/src/integrations/graphql/resolvers.ts @@ -6,14 +6,13 @@ * call runs). Resolver spans use the same origin/op/field attributes as the native subscriber. */ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan, @@ -182,8 +181,8 @@ function createFieldIfNotExists( function createResolverSpan(info: GraphQLResolveInfo, path: string[], parentSpan?: Span): Span { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: info.fieldName, [GRAPHQL_FIELD_PATH]: path.join('.'), diff --git a/packages/server-utils/src/integrations/graphql/spans.ts b/packages/server-utils/src/integrations/graphql/spans.ts index 16f27d8a5a9c..329407428a81 100644 --- a/packages/server-utils/src/integrations/graphql/spans.ts +++ b/packages/server-utils/src/integrations/graphql/spans.ts @@ -5,17 +5,16 @@ * arguments of the injected `parse`/`validate`/`execute` calls rather than graphql's native events. */ -import { GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE } from '@sentry/conventions/attributes'; +import { + GRAPHQL_DOCUMENT, + GRAPHQL_OPERATION_NAME, + GRAPHQL_OPERATION_TYPE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; import type { GraphqlDocumentNode } from './types'; import { collectGraphqlDocument, getOperationSpanName, hasResultErrors, renameRootSpanWithOperation } from './utils'; import { @@ -40,8 +39,8 @@ import type { } from './types'; const BASE_ATTRIBUTES = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: GRAPHQL, } as const; export function startParseSpan(): Span { diff --git a/packages/server-utils/src/integrations/hapi/hapi-utils.ts b/packages/server-utils/src/integrations/hapi/hapi-utils.ts index 21c27d91eec5..5c7afa11a268 100644 --- a/packages/server-utils/src/integrations/hapi/hapi-utils.ts +++ b/packages/server-utils/src/integrations/hapi/hapi-utils.ts @@ -16,10 +16,9 @@ import { isObjectLike, REQUEST_HANDLER_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import type { HapiRequest, @@ -179,7 +178,7 @@ function startMetadataSpan(metadata: SpanMetadata, original: () => unknown): unk attributes: { ...metadata.attributes, [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.hapi', + [SENTRY_ORIGIN]: 'auto.http.hapi', }, }, original, diff --git a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts index 71eab3bbb3fd..c166faea6384 100644 --- a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts +++ b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Default implementation of the `shouldHandleError` callback. * @@ -9,11 +11,11 @@ * Hono's `HTTPException`, third-party middleware errors, and custom error subclasses. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index 6c30f31ccafb..d62e949125ef 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -1,9 +1,9 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { createRequire } from 'node:module'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { debug, defineIntegration, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { Env, GetConnInfo, Hono, MiddlewareHandler } from './honoTypes'; import { CHANNELS } from '../../orchestrion/channels'; @@ -171,7 +171,7 @@ function instrumentInternalRequests(): void { name: `${method} ${extractPathname(input)}`, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, + [SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, }, }); }, diff --git a/packages/server-utils/src/integrations/hono/patchAppRequest.ts b/packages/server-utils/src/integrations/hono/patchAppRequest.ts index bc54e3716b32..15bb20cc21fd 100644 --- a/packages/server-utils/src/integrations/hono/patchAppRequest.ts +++ b/packages/server-utils/src/integrations/hono/patchAppRequest.ts @@ -1,13 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; -import { - debug, - getActiveSpan, - getOriginalFunction, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - type WrappedFunction, -} from '@sentry/core'; +import { debug, getActiveSpan, getOriginalFunction, startSpan, type WrappedFunction } from '@sentry/core'; import { INTERNAL_REQUEST_ORIGIN } from './constants'; import type { Env, Hono } from './honoTypes'; import { DEBUG_BUILD } from '../../debug-build'; @@ -104,7 +97,7 @@ export function patchAppRequest(app: Hono): void { onlyIfParent: true, attributes: { [SENTRY_OP]: INTERNAL_REQUEST_OP, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, + [SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, }, }, () => { diff --git a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts index e3e451247614..86f96e05039c 100644 --- a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts +++ b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts @@ -1,10 +1,9 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import { getActiveSpan, getOriginalFunction, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, type WrappedFunction, @@ -43,7 +42,7 @@ export function wrapMiddlewareWithSpan(handler: MiddlewareHandler): MiddlewareHa parentSpan: rootSpan, attributes: { [SENTRY_OP]: MIDDLEWARE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN, + [SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN, }, }); diff --git a/packages/server-utils/src/integrations/kafkajs/spans.ts b/packages/server-utils/src/integrations/kafkajs/spans.ts index 4aef66e08395..3ed0ca5a84bd 100644 --- a/packages/server-utils/src/integrations/kafkajs/spans.ts +++ b/packages/server-utils/src/integrations/kafkajs/spans.ts @@ -18,6 +18,7 @@ import { MESSAGING_SYSTEM, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH, QUEUE_RECEIVE } from '@sentry/conventions/op'; import type { Span, SpanAttributes, SpanLink } from '@sentry/core'; @@ -25,8 +26,8 @@ import { getClient, getTraceData, hasSpanStreamingEnabled, + isObjectLike, propagationContextFromHeaders, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -135,7 +136,7 @@ export function startConsumerSpan({ topic, message, operationType, links, attrib [ATTR_MESSAGING_KAFKA_OFFSET]: message?.offset as string | undefined, // Mirror the upstream behavior of only tagging per-message processing spans (not the batch // receiving span, which carries no message) with the auto origin. - ...(message ? { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN } : {}), + ...(message ? { [SENTRY_ORIGIN]: CONSUMER_ORIGIN } : {}), }, }); } @@ -155,7 +156,7 @@ export function startProducerSpan(topic: string, message: Message): Span { message.partition !== undefined ? String(message.partition) : undefined, [MESSAGING_OPERATION_NAME]: 'send', [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_TYPE_VALUE_SEND, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRODUCER_ORIGIN, + [SENTRY_ORIGIN]: PRODUCER_ORIGIN, }, }); @@ -183,7 +184,7 @@ export function applyErrorToSpans(spans: Span[], reason: unknown): void { let errorType: string = ERROR_TYPE_VALUE_OTHER; if (typeof reason === 'string' || reason === undefined) { errorMessage = reason; - } else if (typeof reason === 'object' && reason !== null && Object.prototype.hasOwnProperty.call(reason, 'message')) { + } else if (isObjectLike(reason) && Object.prototype.hasOwnProperty.call(reason, 'message')) { errorMessage = (reason as { message?: string }).message; errorType = (reason as { constructor: { name: string } }).constructor.name; } diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 5d85d1491682..781d26aa70c8 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -10,7 +10,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, truncate, @@ -28,6 +27,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../debug-build'; @@ -182,7 +182,7 @@ function subscribeQuery(): void { const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, 'knex.version': data.moduleVersion, [DB_SYSTEM_NAME]: dbSystem, [ATTR_DB_SQL_TABLE]: table, diff --git a/packages/server-utils/src/integrations/koa/index.ts b/packages/server-utils/src/integrations/koa/index.ts index 3ee982041147..e3f1c10690c0 100644 --- a/packages/server-utils/src/integrations/koa/index.ts +++ b/packages/server-utils/src/integrations/koa/index.ts @@ -10,10 +10,9 @@ import { getIsolationScope, hasSpanStreamingEnabled, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; -import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; @@ -213,7 +212,7 @@ function patchLayer( attributes: { ...metadata.attributes, [SENTRY_OP]: layerType === LAYER_TYPE.MIDDLEWARE ? MIDDLEWARE : ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }, () => { diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index d8513a80b926..e623bbbed9f0 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -7,7 +7,7 @@ import { createLangChainCallbackHandler } from '../ai/langchain'; import { LANGCHAIN_INTEGRATION_NAME } from '../ai/langchain/constants'; import { _INTERNAL_getLangChainEmbeddingsSpanOptions } from '../ai/langchain/embeddings'; import type { LangChainOptions } from '../ai/langchain/types'; -import { _INTERNAL_mergeLangChainCallbackHandler } from '../ai/langchain/utils'; +import { getConversationIdMetadataFromConfig, _INTERNAL_mergeLangChainCallbackHandler } from '../ai/langchain/utils'; import { MISTRAL_INTEGRATION_NAME } from '../ai/mistral/constants'; import { OPENAI_INTEGRATION_NAME } from '../ai/openai/constants'; import { GROQ_INTEGRATION_NAME } from './groq'; @@ -91,6 +91,11 @@ function instrumentChatModels(options: LangChainOptions): void { } callOptions.callbacks = _INTERNAL_mergeLangChainCallbackHandler(callOptions.callbacks, sentryHandler); + + const conversationIdMetadata = getConversationIdMetadataFromConfig(callOptions); + if (Object.keys(conversationIdMetadata).length) { + callOptions.metadata = { ...conversationIdMetadata, ...(callOptions.metadata as Record) }; + } }; for (const channelName of [CHANNELS.LANGCHAIN_CHAT_MODEL_INVOKE, CHANNELS.LANGCHAIN_CHAT_MODEL_STREAM]) { diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index a6b9f6f21541..5a41a8296aa3 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -1,6 +1,6 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration } from '@sentry/core'; +import { debug, defineIntegration, isObjectLike } from '@sentry/core'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { createLangChainCallbackHandler } from '../ai/langchain'; import { instrumentCompiledGraphInvoke } from '../ai/langgraph'; @@ -95,7 +95,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { function getFirstArgObject(args: unknown[] | undefined): Record | undefined { const first = (args ?? [])[0]; - return typeof first === 'object' && first !== null ? (first as Record) : undefined; + return isObjectLike(first) ? first : undefined; } /** diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index cfd1c5c70995..9856ef51651d 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -176,7 +176,7 @@ function instrumentExecuteWithContext(): void { } function attachExporter(instance: unknown, options: MastraOptions): void { - if (typeof instance !== 'object' || instance === null || registered.has(instance)) { + if (!isObjectLike(instance) || registered.has(instance)) { return; } diff --git a/packages/server-utils/src/integrations/mistral.ts b/packages/server-utils/src/integrations/mistral.ts index 117f0f965c0b..f70d026cf358 100644 --- a/packages/server-utils/src/integrations/mistral.ts +++ b/packages/server-utils/src/integrations/mistral.ts @@ -1,11 +1,7 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - defineIntegration, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { _INTERNAL_shouldSkipAiProviderWrapping, defineIntegration, startInactiveSpan } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; import { addRequestAttributes, extractRequestAttributes } from '../ai/mistral'; import { MISTRAL_INTEGRATION_NAME, MISTRAL_ORIGIN } from '../ai/mistral/constants'; @@ -86,7 +82,7 @@ function createGenAiSpan( const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(args, operation, recordInputs, streaming); - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = MISTRAL_ORIGIN; + attributes[SENTRY_ORIGIN] = MISTRAL_ORIGIN; const span = startInactiveSpan({ name: getSpanName(operation, attributes), diff --git a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts index 455b86653ac5..2f0a86721314 100644 --- a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts @@ -8,16 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, isObjectLike, startInactiveSpan } from '@sentry/core'; // `db.connection_string` is not part of `@sentry/conventions`, so it stays inlined to match // what `@opentelemetry/instrumentation-mongodb` emitted. @@ -127,7 +122,7 @@ export function getSpanAttributes( origin: string, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SENTRY_ORIGIN]: origin, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MONGODB, [DB_NAMESPACE]: dbName, [DB_COLLECTION_NAME]: dbCollection, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts index 7e4230b84fbe..9739ba070096 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts @@ -9,16 +9,10 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { - debug, - getClient, - hasSpanStreamingEnabled, - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { debug, getClient, hasSpanStreamingEnabled, isObjectLike, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -138,7 +132,7 @@ function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName return startInactiveSpan({ name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, [DB_OPERATION_NAME]: data.operation, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts index b279343435f8..d22e7c8a1063 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts @@ -8,10 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startInactiveSpan } from '@sentry/core'; const DB_SYSTEM_NAME_VALUE_MONGODB = 'mongodb'; @@ -59,7 +60,7 @@ export function startMongooseLegacySpan({ [SERVER_PORT]: collection?.conn?.port, [DB_OPERATION_NAME]: operation, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SENTRY_ORIGIN]: origin, }; const client = getClient(); diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index ccbbad5a1565..e4049b65d30a 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -9,6 +9,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Scope } from '@sentry/core'; @@ -19,7 +20,6 @@ import { getClient, getCurrentScope, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; @@ -102,7 +102,7 @@ function instrumentMysql(): void { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.mysql', + [SENTRY_ORIGIN]: 'auto.db.mysql', [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, portIsNumber ? portNumber : undefined, database), ...(database ? { [DB_NAMESPACE]: database } : {}), ...(user ? { [DB_USER]: user } : {}), diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index 4d01b681f1ce..b8f6501a26ba 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -5,7 +5,6 @@ import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -26,6 +25,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; @@ -96,7 +96,7 @@ function subscribeQueryChannel(channelName: ChannelName): void { name, attributes: { [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, [DB_QUERY_TEXT]: queryText || undefined, diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 71264c73565f..6ba7d1699d01 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -8,9 +8,10 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { sanitizeSqlQueryWithSummary } from '../../utils/sql'; @@ -109,7 +110,7 @@ function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelN return startInactiveSpan({ name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, [DB_QUERY_TEXT]: queryText, @@ -132,7 +133,7 @@ function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channe return startInactiveSpan({ name: spanName, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, [DB_NAMESPACE]: data.database || undefined, diff --git a/packages/server-utils/src/integrations/openai-compatible.ts b/packages/server-utils/src/integrations/openai-compatible.ts index be98c3de7a9e..2c23c711b60f 100644 --- a/packages/server-utils/src/integrations/openai-compatible.ts +++ b/packages/server-utils/src/integrations/openai-compatible.ts @@ -4,10 +4,9 @@ import { _INTERNAL_shouldSkipAiProviderWrapping, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { GEN_AI_PROVIDER_NAME } from '@sentry/conventions/attributes'; +import { GEN_AI_PROVIDER_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; import { wrapApiPromiseResponse } from '../ai/core/apiPromise'; import { addRequestAttributes, extractRequestAttributes } from '../ai/openai'; @@ -117,7 +116,7 @@ function createGenAiSpan( // `extractRequestAttributes` defaults the provider to openai; override it for the concrete provider. const attributes = extractRequestAttributes(args, operation, recordInputs); attributes[GEN_AI_PROVIDER_NAME] = provider.providerName; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = provider.origin; + attributes[SENTRY_ORIGIN] = provider.origin; const model = (params?.model as string) || 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/integrations/openai.ts b/packages/server-utils/src/integrations/openai.ts index ada6e60411a2..a5375794033e 100644 --- a/packages/server-utils/src/integrations/openai.ts +++ b/packages/server-utils/src/integrations/openai.ts @@ -1,3 +1,4 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { @@ -5,7 +6,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; @@ -93,7 +93,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(args, operation, recordInputs); - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const model = (params?.model as string) || 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index afb8d5ecc099..5d3b865cd20d 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -7,6 +7,7 @@ import { ERROR_TYPE, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Span } from '@sentry/core'; @@ -15,7 +16,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -299,7 +299,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { attributes: { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_POSTGRES, [DB_QUERY_TEXT]: sanitizedSqlQuery, [DB_QUERY_SUMMARY]: querySummary, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 1e7f0fdeb018..5d93ac17769f 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -9,6 +9,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Scope, SpanAttributes } from '@sentry/core'; @@ -19,7 +20,6 @@ import { getClient, getCurrentScope, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; @@ -191,7 +191,7 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp attributes: { [SENTRY_OP]: DB, ...getConnectionAttributes(params), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_QUERY_TEXT]: queryText || undefined, [DB_QUERY_SUMMARY]: querySummary, [ATTR_PG_PLAN]: typeof queryConfig?.name === 'string' ? queryConfig.name : undefined, diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index b85f7c7d8d45..656b7cf8fc75 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -1,14 +1,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { Span } from '@sentry/core'; -import { - getActiveSpan, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanIsIgnored, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; -import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, getRootSpan, spanIsIgnored, startInactiveSpan, withActiveSpan } from '@sentry/core'; +import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { prismaChannels, PRISMA_LAZY_TERMINALS } from '../../orchestrion/config/prisma'; import type { TracingChannelLifeCycleOptions } from '../../tracing-channel'; @@ -84,7 +77,7 @@ function startOperationSpan(method: string, data: PrismaTerminalChannelContext): name: OPERATION_SPAN_NAME, parentSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + [SENTRY_ORIGIN]: PRISMA_ORIGIN, [SENTRY_OP]: DB, [DB_OPERATION_NAME]: method, [DB_COLLECTION_NAME]: table ?? model, diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 0cfe6cf471d0..218e7c788134 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -20,7 +20,6 @@ import { getClient, hasSpanStreamingEnabled, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpanManual, } from '@sentry/core'; @@ -34,6 +33,7 @@ import { DB_SYSTEM_NAME, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getSqlQuerySummary, sanitizeSqlQuery, type SqlDialect, toSqlDialect } from '../../utils/sql'; @@ -98,7 +98,7 @@ function registerPrismaSpan(id: string, span: Span): void { function buildSpanAttributes(name: string, attributes: Record | undefined): SpanAttributes { const merged: SpanAttributes = { ...(attributes as SpanAttributes | undefined), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + [SENTRY_ORIGIN]: PRISMA_ORIGIN, }; // Prisma itself emits the deprecated `db.system` on older versions, so both spellings are checked diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index fb19018aa598..4140156cee57 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -8,6 +8,7 @@ import { SERVER_ADDRESS, SERVER_PORT, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; @@ -15,7 +16,6 @@ import { isObjectLike, defineIntegration, getActiveSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan, @@ -104,7 +104,7 @@ function nodeRedisAttributes(options: NodeRedisClientOptions | undefined): SpanA [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, [SERVER_ADDRESS]: host, ...(port != null ? { [SERVER_PORT]: port } : {}), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; } @@ -160,7 +160,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { const client = data.self as LegacyRedisClient | undefined; const attributes: SpanAttributes = { [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; if (client?.connection_options?.host != null) { diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index f50060a29a8e..0e91d59c03bb 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -7,10 +7,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; @@ -50,7 +51,7 @@ function connectionAttributes(host: string | undefined, port: number | undefined [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, ...(host != null ? { [SERVER_ADDRESS]: host } : {}), ...(port != null ? { [SERVER_PORT]: port } : {}), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; } diff --git a/packages/server-utils/src/integrations/redis/redis-cache.ts b/packages/server-utils/src/integrations/redis/redis-cache.ts index ac74bee358f4..bdc860872a6d 100644 --- a/packages/server-utils/src/integrations/redis/redis-cache.ts +++ b/packages/server-utils/src/integrations/redis/redis-cache.ts @@ -7,17 +7,11 @@ import { NETWORK_PEER_PORT, SERVER_ADDRESS, SERVER_PORT, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - CACHE_OPERATION_NAMES, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - spanToJSON, - truncate, -} from '@sentry/core'; +import { CACHE_OPERATION_NAMES, getClient, hasSpanStreamingEnabled, spanToJSON, truncate } from '@sentry/core'; // Runtime-agnostic on purpose: imports nothing from `node:*` and touches `Buffer` only inside // function bodies (never at module load), so bundling this into an edge runtime is safe and the @@ -174,7 +168,7 @@ export function getRedisCacheAttributes( } const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation, + [SENTRY_OP]: cacheOperation, [CACHE_KEY]: safeKey, [CACHE_OPERATION]: CACHE_OPERATION_NAMES[cacheOperation], }; @@ -208,7 +202,7 @@ export function getRedisCacheAttributes( * meaningless. */ export function applyCacheResponseAttributes(span: Span, response: unknown): void { - const op = spanToJSON(span).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const op = spanToJSON(span).attributes[SENTRY_OP]; if (op !== CACHE_GET && op !== CACHE_PUT) { return; } diff --git a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts index 090b02d7272e..de092c109bd7 100644 --- a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts @@ -7,10 +7,11 @@ import { SERVER_ADDRESS, SERVER_PORT, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; import { applyCacheResponseAttributes, getRedisCacheAttributes } from './redis-cache'; @@ -148,7 +149,7 @@ function setupCommandChannel( port: data.serverPort, }); const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB_QUERY, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, [DB_OPERATION_NAME]: data.command, @@ -182,7 +183,7 @@ function setupBatchChannel( return startInactiveSpan({ name: operation, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB_QUERY, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, [DB_OPERATION_NAME]: operation, @@ -201,7 +202,7 @@ function setupConnectChannel(tracingChannel: RedisTracingChannelFactory, channel return startInactiveSpan({ name: 'redis-connect', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, ...(data.serverAddress != null ? { [SERVER_ADDRESS]: data.serverAddress } : {}), diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index a2b3ea352791..21ff279dd13f 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -9,7 +9,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -23,6 +22,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { CHANNELS } from '../orchestrion/channels'; @@ -138,7 +138,7 @@ function subscribeQuery(channelName: string, operation: string): void { const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MSSQL, [DB_NAMESPACE]: databaseName, // `>=4` uses the `authentication` object; older versions expose `userName` directly. diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 78b4763f0240..e9a75ade26b6 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -25,9 +25,11 @@ import { GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS, + GEN_AI_EVALUATE, GEN_AI_EXECUTE_TOOL, GEN_AI_GENERATE_CONTENT, GEN_AI_INVOKE_AGENT, @@ -37,9 +39,9 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { _INTERNAL_skipAiProviderWrapping, captureException, + getActiveSpan, getClient, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, spanToJSON, spanToTraceContext, @@ -65,9 +67,6 @@ const AI_SDK_TELEMETRY_TRACING_CHANNEL = 'ai:telemetry'; const ORIGIN = 'auto.vercelai.channel'; -// Not yet in `@sentry/conventions`. -const GEN_AI_EVALUATE = 'gen_ai.evaluate'; - // `gen_ai.operation.name` values, keyed to the span op they map to. const GEN_AI_OPERATION_SPAN_OPS = { embeddings: GEN_AI_EMBEDDINGS, @@ -86,6 +85,7 @@ type GenAiOperation = keyof typeof GEN_AI_OPERATION_SPAN_OPS; const VERCEL_AI_OPERATION_ID_ATTRIBUTE = 'vercel.ai.operationId'; const VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE = 'vercel.ai.model.provider'; const VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE = 'vercel.ai.settings.maxRetries'; +const VERCEL_AI_TELEMETRY_METADATA_ATTRIBUTE_PREFIX = 'vercel.ai.telemetry.metadata.'; // Tracks the top-level operationId (and whether it streams) per `callId` so a model-call span can // name its `doGenerate`/`doStream` operation the same way the OTel integration does. `isStream` is @@ -142,6 +142,31 @@ export function clearOperationCallId(callId: string): void { invokeAgentSpanByCallId.delete(callId); } +/** + * The OpenAI Conversations API id from `providerOptions.openai.conversation` (or `azure`): the one + * provider-level value that is the same on every turn. A `Sentry.setConversationId()` value still wins, + * since `conversationIdIntegration` writes it on `spanStart`, after these start attributes. + */ +function getOpenAiConversationId(providerOptions: unknown): string | undefined { + if (!isObjectLike(providerOptions)) { + return undefined; + } + for (const key of ['openai', 'azure']) { + const options = providerOptions[key]; + const conversation = isObjectLike(options) ? asString(options.conversation) : undefined; + if (conversation) { + return conversation; + } + } + return undefined; +} + +/** The `gen_ai.conversation.id` already on the active span, which for a child event is its operation span. */ +function getActiveSpanConversationId(): string | undefined { + const active = getActiveSpan(); + return active ? asString(spanToJSON(active).attributes[GEN_AI_CONVERSATION_ID]) : undefined; +} + /** * `providerMetadata` is last-step only; drop derived usage on spans that report an aggregate. * @@ -216,7 +241,8 @@ export type ChannelEventType = | 'embed' | 'embedMany' | 'rerank' - | 'experimental_evaluate'; + | 'experimental_evaluate' + | 'experimental_decide'; /** * The context object the AI SDK passes through one tracing-channel call. It is the same object @@ -417,11 +443,20 @@ export function createSpanFromMessage( recordToolDescriptions(callId, event.tools); } - const baseAttributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + // Only an operation's start event carries `providerOptions`; its model-call and tool events start + // while the operation span is active, so they inherit the id from it. A root operation never + // inherits, so a nested call (e.g. inside a tool's `execute`) is not folded into the outer conversation. + const conversationId = + getOpenAiConversationId(event.providerOptions) ?? + (ROOT_OPERATION_TYPES.has(type) ? undefined : getActiveSpanConversationId()); + + const baseAttributes: SpanAttributes = { + [SENTRY_ORIGIN]: ORIGIN, + ...telemetryMetadataAttributes(event.telemetryMetadata), ...(provider ? { [GEN_AI_PROVIDER_NAME]: provider, [VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE]: provider } : {}), ...(modelId ? { [GEN_AI_REQUEST_MODEL]: modelId } : {}), ...(maxRetries !== undefined ? { [VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE]: maxRetries } : {}), + ...(conversationId ? { [GEN_AI_CONVERSATION_ID]: conversationId } : {}), }; switch (type) { @@ -437,7 +472,7 @@ export function createSpanFromMessage( return buildModelCallSpan(event, baseAttributes, recordInputs, callId, modelId); case 'executeTool': - return buildToolSpan(event, recordInputs); + return buildToolSpan(event, recordInputs, conversationId); case 'embed': case 'embedMany': { // `embed` carries a single `value`; `embedMany` a `values` array — both map to the embeddings input. @@ -449,7 +484,9 @@ export function createSpanFromMessage( } case 'rerank': return startGenAiSpan('rerank', modelId, baseAttributes); + // `ai` 7.0.128 renamed `experimental_evaluate` to `experimental_decide`; older 7.x still publishes the old name. case 'experimental_evaluate': + case 'experimental_decide': return startGenAiSpan('evaluate', modelId, { ...baseAttributes, ...(recordInputs @@ -466,6 +503,22 @@ export function createSpanFromMessage( } } +/** + * `experimental_telemetry.metadata` (`ai` <= 6) as `vercel.ai.telemetry.metadata.`, the names the OTel + * integration produced from the SDK's `ai.telemetry.metadata.*`. Only the orchestrion adapter sets + * `event.telemetryMetadata`; `ai` 7 has no `telemetry.metadata`. + */ +function telemetryMetadataAttributes(metadata: unknown): Record { + const attributes: Record = {}; + if (isObjectLike(metadata)) { + for (const [key, value] of Object.entries(metadata)) { + if (value === null || value === undefined) continue; + attributes[`${VERCEL_AI_TELEMETRY_METADATA_ATTRIBUTE_PREFIX}${key}`] = String(value); + } + } + return attributes; +} + /** Start a `gen_ai.` span named ` ` (or just `` when no suffix). */ function startGenAiSpan(operation: GenAiOperation, suffix: string | undefined, attributes: SpanAttributes): Span { return startInactiveSpan({ @@ -523,7 +576,11 @@ function buildModelCallSpan( }); } -function buildToolSpan(event: Record, recordInputs: boolean): Span { +function buildToolSpan( + event: Record, + recordInputs: boolean, + conversationId: string | undefined, +): Span { const toolCall = isObjectLike(event.toolCall) ? event.toolCall : {}; const toolName = asString(toolCall.toolName); const toolCallId = asString(event.toolCallId) ?? asString(toolCall.toolCallId); @@ -533,11 +590,12 @@ function buildToolSpan(event: Record, recordInputs: boolean): S const description = recordInputs && toolName ? resolveToolDescription(asString(event.callId), toolName, event.tools) : undefined; return startGenAiSpan('execute_tool', toolName, { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, ...(toolName ? { [GEN_AI_TOOL_NAME]: toolName } : {}), ...(toolCallId ? { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: toolCallId } : {}), ...(description ? { [GEN_AI_TOOL_DESCRIPTION]: description } : {}), ...(recordInputs && toolInput !== undefined ? { [GEN_AI_TOOL_CALL_ARGUMENTS]: stringify(toolInput) } : {}), + ...(conversationId ? { [GEN_AI_CONVERSATION_ID]: conversationId } : {}), }); } @@ -609,17 +667,11 @@ export function enrichSpanOnEnd( span.setAttribute(GEN_AI_RESPONSE_MODEL, responseModel); } - // Provider-specific cache/reasoning/prediction token breakdowns and `gen_ai.conversation.id`. - // The channel exposes `providerMetadata` as an object (the OTel path parses it from a string); - // both share `getProviderMetadataAttributes` so the emitted shape is identical. + // Provider-specific cache/reasoning/prediction token breakdowns. The channel exposes `providerMetadata` + // as an object (the OTel path parses it from a string); both share `getProviderMetadataAttributes` so + // the emitted shape is identical. const providerMetadata = (result as { providerMetadata?: unknown }).providerMetadata; const providerAttributes = getProviderMetadataAttributes(providerMetadata); - // Don't overwrite a conversation id already set on span start (e.g. by `conversationIdIntegration` - // from a user-set scope value); the provider-derived id is only a fallback. Matches the OTel path. - if (GEN_AI_CONVERSATION_ID in providerAttributes && spanToJSON(span).attributes[GEN_AI_CONVERSATION_ID]) { - // oxlint-disable-next-line typescript/no-dynamic-delete - delete providerAttributes[GEN_AI_CONVERSATION_ID]; - } dropLastStepOnlyUsage(providerAttributes, type); span.setAttributes(providerAttributes); @@ -636,7 +688,7 @@ function getOutputMessages( result: Record, finishReason: string | undefined, ): string | undefined { - if (type === 'experimental_evaluate') { + if (type === 'experimental_evaluate' || type === 'experimental_decide') { return stringify([{ type: 'evaluation', answers: withProviderConfidence(result) }]); } // `languageModelCall` exposes the response as a `content` parts array; top-level results expose diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index ea64ae6aacd9..daf4ee24584c 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -107,6 +107,9 @@ const callIdBySpan = new WeakMap(); // child `generate_content` span (whose event would fall back to the global default). v7's channel forwards // these flags on every event, so this keeps v6 identical. const recordingBySpan = new WeakMap>(); +// The operation's `experimental_telemetry.metadata`, keyed by its span, so the model-call span records it +// too. The OTel integration put it on both, but not on tool-call spans. +const telemetryMetadataBySpan = new WeakMap(); interface OperationErrorInfo { // The span active when the operation was invoked (its call site, e.g. the enclosing request/`main` span). // When an operation's error bubbles out unhandled it reaches the global handler outside any span, so — as @@ -163,6 +166,7 @@ export function subscribeVercelAiOrchestrionChannels( ...modelFields(callOptions.model), maxRetries: callOptions.maxRetries, value: callOptions.value, + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }), @@ -180,6 +184,7 @@ export function subscribeVercelAiOrchestrionChannels( ...modelFields(callOptions.model), maxRetries: callOptions.maxRetries, values: callOptions.values, + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }), @@ -263,6 +268,7 @@ function bindOperation( callIdBySpan.set(span, callId); } recordingBySpan.set(span, recording(telemetry)); + telemetryMetadataBySpan.set(span, telemetry.metadata); // v5 has no `executeToolCall` channel, so patch each tool's `execute` to emit the tool-call span. // Inert on v6 (guarded inside `patchToolExecute` when the parent is `executeToolCall`'s own span). if (isObjectLike(callOptions.tools)) { @@ -490,6 +496,7 @@ function patchModelMethod( // top-level `tools` array. Reading both keeps `tool.definitions` populated on the model-call span. tools: callArgs.tools ?? (isObjectLike(callArgs.mode) ? callArgs.mode.tools : undefined), messages: callArgs.prompt, + telemetryMetadata: telemetryMetadataBySpan.get(parent), // Inherit the enclosing operation's per-call recording flags so inputs/tools/outputs are recorded on // the model-call span whenever they are on the parent `invoke_agent` span. ...recordingBySpan.get(parent), @@ -669,6 +676,10 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject' // Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string // `prompt` becomes a single user message, matching the SDK's own normalization. messages: normalizePromptMessages(options), + // v7's native start event carries `providerOptions`; the shared core reads the OpenAI + // Conversations API id from it. + providerOptions: options.providerOptions, + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }); diff --git a/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts new file mode 100644 index 000000000000..aa62ff8d78ba --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts @@ -0,0 +1,126 @@ +import { + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { EventEmitter } from 'node:events'; +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { addResponseAttributes } from '../../../../src/ai/anthropic-ai'; +import { instrumentAsyncIterableStream, instrumentMessageStream } from '../../../../src/ai/anthropic-ai/streaming'; +import type { AnthropicAiStreamingEvent } from '../../../../src/ai/anthropic-ai/types'; + +function createMockSpan(): { span: Span; attributes: Record } { + const attributes: Record = {}; + let ended = false; + const span = { + isRecording: () => !ended, + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + setAttributes: (values: Record) => { + Object.assign(attributes, values); + }, + end: () => { + ended = true; + }, + } as unknown as Span; + return { span, attributes }; +} + +describe.each(['response', 'async iterable', 'message stream'] as const)('Anthropic token usage (%s)', mode => { + it.each([ + { + name: 'includes cache reads and writes in input tokens', + cache: { cache_read_input_tokens: 2048, cache_creation_input_tokens: 512 }, + inputTokens: 2600, + totalTokens: 2720, + cacheAttributes: { + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512, + }, + }, + { + name: 'includes cache reads when cache writes are absent', + cache: { cache_read_input_tokens: 2048 }, + inputTokens: 2088, + totalTokens: 2208, + cacheAttributes: { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048 }, + }, + { + name: 'includes cache writes when cache reads are absent', + cache: { cache_creation_input_tokens: 512 }, + inputTokens: 552, + totalTokens: 672, + cacheAttributes: { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512 }, + }, + { + name: 'preserves zero cache counts', + cache: { cache_read_input_tokens: 0, cache_creation_input_tokens: 0 }, + inputTokens: 40, + totalTokens: 160, + cacheAttributes: { + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 0, + }, + }, + { + name: 'omits cache attributes when cache usage is absent', + cache: {}, + inputTokens: 40, + totalTokens: 160, + cacheAttributes: {}, + }, + ])('$name', async ({ cache, inputTokens, totalTokens, cacheAttributes }) => { + const { span, attributes } = createMockSpan(); + const response = { + id: 'msg_cache_usage', + type: 'message' as const, + model: 'claude-sonnet-4-6', + role: 'assistant', + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 40, output_tokens: 120, ...cache }, + }; + const events: AnthropicAiStreamingEvent[] = [ + { type: 'message_start', message: { ...response, usage: { ...response.usage, output_tokens: 1 } } }, + { type: 'message_delta', usage: { output_tokens: 60 } }, + { type: 'message_delta', usage: { output_tokens: 120 } }, + { type: 'message_stop' }, + ]; + + if (mode === 'response') { + addResponseAttributes(span, response, false); + } else if (mode === 'async iterable') { + const stream = (async function* () { + yield* events; + })(); + for await (const _ of instrumentAsyncIterableStream(stream, span, false)) { + void _; + } + } else { + const stream = instrumentMessageStream(new EventEmitter(), span, false); + for (const event of events) { + stream.emit('streamEvent', event); + } + stream.emit('message', response); + stream.emit('message', response); + } + + expect(attributes).toEqual({ + [GEN_AI_RESPONSE_ID]: 'msg_cache_usage', + [GEN_AI_RESPONSE_MODEL]: 'claude-sonnet-4-6', + [GEN_AI_USAGE_INPUT_TOKENS]: inputTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + ...cacheAttributes, + ...(mode === 'response' ? {} : { [GEN_AI_RESPONSE_STREAMING]: true }), + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts new file mode 100644 index 000000000000..6e88ac2f7421 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts @@ -0,0 +1,186 @@ +import { + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { addResponseAttributes as addOpenAiResponseAttributes } from '../../../../src/ai/openai/utils'; +import { instrumentStream as instrumentOpenAiStream } from '../../../../src/ai/openai/streaming'; +import { addResponseAttributes as addGoogleResponseAttributes } from '../../../../src/ai/google-genai'; +import { instrumentStream as instrumentGoogleStream } from '../../../../src/ai/google-genai/streaming'; + +describe.each(['openai chat', 'openai responses', 'google'] as const)('cache usage (%s)', provider => { + describe.each(['response', 'stream'] as const)('%s', mode => { + it.each([ + { name: 'records cache subsets without increasing input or total tokens', cacheRead: 2048, cacheWrite: 512 }, + { name: 'preserves zero cache counts', cacheRead: 0, cacheWrite: 0 }, + { name: 'omits cache attributes when details are absent', cacheRead: undefined, cacheWrite: undefined }, + ])('$name', async ({ cacheRead, cacheWrite }) => { + const attributes: Record = {}; + const span = { + isRecording: () => true, + setAttributes: (values: Record) => Object.assign(attributes, values), + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + end: () => {}, + } as unknown as Span; + const details = + cacheRead === undefined ? undefined : { cached_tokens: cacheRead, cache_write_tokens: cacheWrite }; + const openAiResponse = { + id: 'response_cache_usage', + model: 'gpt-4o', + choices: [], + usage: + provider === 'openai chat' + ? { prompt_tokens: 2600, completion_tokens: 120, total_tokens: 2720, prompt_tokens_details: details } + : { input_tokens: 2600, output_tokens: 120, total_tokens: 2720, input_tokens_details: details }, + }; + const googleResponse = { + usageMetadata: { + promptTokenCount: 2600, + candidatesTokenCount: 120, + totalTokenCount: 2720, + cachedContentTokenCount: cacheRead, + }, + }; + + if (mode === 'response') { + if (provider === 'google') { + addGoogleResponseAttributes(span, googleResponse, false); + } else { + addOpenAiResponseAttributes(span, openAiResponse, false); + } + } else if (provider === 'google') { + const stream = (async function* () { + yield googleResponse; + yield googleResponse; + yield {}; + })(); + for await (const _ of instrumentGoogleStream(stream, span, false)) { + void _; + } + } else { + const event = + provider === 'openai chat' + ? { ...openAiResponse, object: 'chat.completion.chunk' } + : { type: 'response.completed', response: openAiResponse }; + const responseWithoutCacheDetails = { + ...openAiResponse, + usage: { ...openAiResponse.usage, prompt_tokens_details: undefined, input_tokens_details: undefined }, + }; + const stream = (async function* () { + yield event; + yield event; + yield provider === 'openai chat' + ? { ...responseWithoutCacheDetails, object: 'chat.completion.chunk' } + : { type: 'response.completed', response: responseWithoutCacheDetails }; + })(); + for await (const _ of instrumentOpenAiStream(stream, span, false)) { + void _; + } + } + + expect(attributes).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + ...(cacheRead === undefined ? {} : { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: cacheRead }), + ...(provider === 'google' || cacheWrite === undefined + ? {} + : { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: cacheWrite }), + ...(provider === 'google' + ? {} + : { [GEN_AI_RESPONSE_ID]: 'response_cache_usage', [GEN_AI_RESPONSE_MODEL]: 'gpt-4o' }), + ...(mode === 'response' ? {} : { [GEN_AI_RESPONSE_STREAMING]: true }), + }); + }); + }); +}); + +describe.each(['response', 'stream'] as const)('Google reasoning usage (%s)', mode => { + it.each([ + { + name: 'includes reasoning in output', + candidates: 120, + thoughts: 80, + total: 2800, + output: 200, + expectedTotal: 2800, + }, + { + name: 'computes a total including reasoning', + candidates: 120, + thoughts: 80, + total: undefined, + output: 200, + expectedTotal: 2800, + }, + { + name: 'preserves zero reasoning usage', + candidates: 120, + thoughts: 0, + total: 2720, + output: 120, + expectedTotal: 2720, + }, + { + name: 'records reasoning without response candidates', + candidates: undefined, + thoughts: 80, + total: 2680, + output: 80, + expectedTotal: 2680, + }, + ])('$name', async ({ candidates, thoughts, total, output, expectedTotal }) => { + const attributes: Record = {}; + const span = { + isRecording: () => true, + setAttributes: (values: Record) => Object.assign(attributes, values), + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + end: () => {}, + } as unknown as Span; + const response = { + usageMetadata: { + promptTokenCount: 2600, + cachedContentTokenCount: 2048, + candidatesTokenCount: candidates, + thoughtsTokenCount: thoughts, + totalTokenCount: total, + }, + }; + + if (mode === 'response') { + addGoogleResponseAttributes(span, response, false); + } else { + const stream = (async function* () { + yield { usageMetadata: { candidatesTokenCount: candidates } }; + yield { usageMetadata: { thoughtsTokenCount: thoughts } }; + yield response; + yield response; + yield {}; + })(); + for await (const _ of instrumentGoogleStream(stream, span, false)) { + void _; + } + } + + expect(attributes).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_OUTPUT_TOKENS]: output, + [GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]: thoughts, + [GEN_AI_USAGE_TOTAL_TOKENS]: expectedTotal, + ...(mode === 'stream' ? { [GEN_AI_RESPONSE_STREAMING]: true } : {}), + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/flue.test.ts b/packages/server-utils/test/ai/lib/tracing/flue.test.ts index 5817e1033871..bb3712d6ac32 100644 --- a/packages/server-utils/test/ai/lib/tracing/flue.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/flue.test.ts @@ -338,6 +338,55 @@ describe('createFlueInstrumentation', () => { expect(json?.data['gen_ai.cost.total_tokens']).toBe(0.001199); }); + // pi-ai keeps the cached tokens out of `input`; the conventions count them in. + it('counts cached tokens into the input tokens and their cost into the input cost', async () => { + const usage = { + input: 37, + output: 2, + cacheRead: 52, + cacheWrite: 38, + totalTokens: 129, + cost: { input: 0.000037, output: 0.00001, cacheRead: 0.0000052, cacheWrite: 0.0000475, total: 0.0000997 }, + }; + + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe(turn({ response: { ...turn().response, usage } }), {}); + }); + + const json = findSpan('chat claude-haiku-4.5'); + expect(json?.data['gen_ai.usage.input_tokens']).toBe(127); + expect(json?.data['gen_ai.usage.cache_read.input_tokens']).toBe(52); + expect(json?.data['gen_ai.usage.cache_creation.input_tokens']).toBe(38); + expect(json?.data['gen_ai.cost.input_tokens']).toBe(0.000037 + 0.0000052 + 0.0000475); + expect(json?.data['gen_ai.cost.total_tokens']).toBe(0.0000997); + }); + + it('reports a tool-calling turn with the finish reason of the conventions', async () => { + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe( + turn({ + response: { + ...turn().response, + finishReason: 'toolUse', + output: { + role: 'assistant', + content: [{ type: 'toolCall', id: 'c1', name: 'get_weather', arguments: { city: 'Berlin' } }], + }, + }, + }), + {}, + ); + }); + + const json = findSpan('chat claude-haiku-4.5'); + expect(json?.data['gen_ai.response.finish_reasons']).toBe('["tool_call"]'); + expect(json?.data['gen_ai.output.messages']).toBe( + '[{"role":"assistant","parts":[{"type":"tool_call","id":"c1","name":"get_weather","arguments":"{\\"city\\":\\"Berlin\\"}"}],"finish_reason":"tool_call"}]', + ); + }); + it('records the model-call tuning and provider endpoint', async () => { await withAgent(() => { instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1', purpose: 'agent' }, {}); @@ -404,7 +453,15 @@ describe('createFlueInstrumentation', () => { await instr.interceptor(AGENT_OP, AGENT_CTX, async () => { instr.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); instr.observe(requestContent, {}); - instr.observe(turn({ response: { ...turn().response, output: { role: 'assistant' } } }), {}); + instr.observe( + turn({ + response: { + ...turn().response, + output: { role: 'assistant', content: [{ type: 'text', text: 'It is sunny.' }] }, + }, + }), + {}, + ); instr.observe( { type: 'tool_start', @@ -415,8 +472,16 @@ describe('createFlueInstrumentation', () => { }, {}, ); + // `result` is the harness-level shape; `effectiveResult` is what the model receives. instr.observe( - { type: 'tool', toolCallId: 'c1', toolName: 'get_weather', result: 'sunny', operationId: 'op_1' }, + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + result: { content: [{ type: 'text', text: '"sunny"' }], details: { output: 'sunny' } }, + effectiveResult: 'sunny', + operationId: 'op_1', + }, {}, ); }); @@ -427,8 +492,11 @@ describe('createFlueInstrumentation', () => { const chat = findSpan('chat claude-haiku-4.5'); expect(chat?.data['gen_ai.system_instructions']).toBe('You are helpful.'); - expect(chat?.data['gen_ai.input.messages']).toContain('"role":"user"'); - expect(chat?.data['gen_ai.output.messages']).toContain('"role":"assistant"'); + // Mapped from pi-ai's message shape to the conventions, which is what Sentry renders. + expect(chat?.data['gen_ai.input.messages']).toBe('[{"role":"user","parts":[{"type":"text","content":"hi"}]}]'); + expect(chat?.data['gen_ai.output.messages']).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"It is sunny."}],"finish_reason":"stop"}]', + ); expect(chat?.data['gen_ai.tool.definitions']).toContain('get_weather'); const tool = findSpan('execute_tool get_weather'); @@ -436,6 +504,118 @@ describe('createFlueInstrumentation', () => { expect(tool?.data['gen_ai.tool.call.result']).toBe('sunny'); }); + it('records the content blocks of a tool result that has no effective result', async () => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + result: { + content: [ + { type: 'text', text: 'sunny' }, + { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }, + ], + details: { customTool: 'get_weather' }, + }, + operationId: 'op_1', + }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe( + '[{"type":"text","content":"sunny"},{"type":"blob","mime_type":"image/png"}]', + ); + }); + + it('maps an effective result made of several content blocks', async () => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + effectiveResult: [ + { type: 'text', text: 'sunny' }, + { type: 'image', data: '[image data omitted from event]', mimeType: 'image/png' }, + ], + operationId: 'op_1', + }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe( + '[{"type":"text","content":"sunny"},{"type":"blob","mime_type":"image/png"}]', + ); + }); + + it.each([ + ['an empty string', { effectiveResult: '' }], + ['an empty text block', { effectiveResult: [{ type: 'text', text: '' }] }], + ['no content blocks', { result: { content: [], details: {} } }], + ])('records an empty tool result given as %s', async (_label, fields) => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { type: 'tool', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1', ...fields }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe(''); + }); + + it('maps a tool result in the request and a tool call in the response', async () => { + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe( + { + ...requestContent, + request: { + ...requestContent.request, + input: { + messages: [ + { role: 'user', content: 'Weather in Berlin?' }, + { + role: 'assistant', + content: [{ type: 'toolCall', id: 'c1', name: 'get_weather', arguments: { city: 'Berlin' } }], + }, + { + role: 'toolResult', + toolCallId: 'c1', + toolName: 'get_weather', + content: [{ type: 'text', text: 'sunny' }], + isError: false, + }, + ], + }, + }, + }, + {}, + ); + instrumentation.observe(turn(), {}); + }); + + expect( + JSON.parse(String(findSpan('chat claude-haiku-4.5')?.data['gen_ai.input.messages'])).map( + (message: { role: string }) => message.role, + ), + ).toEqual(['user', 'assistant', 'tool']); + }); + it('omits inputs when recordInputs is false but keeps outputs', async () => { await record(createFlueInstrumentation({ recordInputs: false })); diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts new file mode 100644 index 000000000000..a9c7d9da2295 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts @@ -0,0 +1,264 @@ +import { + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { describe, expect, it } from 'vitest'; +import { extractLlmResponseAttributes } from '../../../../src/ai/langchain/utils'; +import type { LangChainLLMResult } from '../../../../src/ai/langchain/types'; + +const rawUsage = { + input_tokens: 40, + output_tokens: 120, + cache_read_input_tokens: 2048, + cache_creation_input_tokens: 512, +}; +const normalizedUsage = { + input_tokens: 2600, + output_tokens: 120, + total_tokens: 2720, + input_token_details: { cache_read: 2048, cache_creation: 512 }, +}; +const expectedUsage = { + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512, +}; + +describe('LangChain token usage', () => { + it('normalizes raw Anthropic input usage including cache reads and writes', () => { + const result = { generations: [], llmOutput: { usage: rawUsage } }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('does not add raw usage onto normalized message usage', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { usage: rawUsage }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('records normalized message usage when a streamed result has no llmOutput', () => { + const result = { generations: [[{ message: { usage_metadata: normalizedUsage } }]] }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('preserves normalized totals when tokenUsage contains only the final stream chunk', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { tokenUsage: { promptTokens: 0, completionTokens: 120, totalTokens: 120 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('prefers normalized message usage when both raw and generic usage are present', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { + usage: rawUsage, + tokenUsage: { promptTokens: 40, completionTokens: 120, totalTokens: 160 }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('normalizes raw Anthropic usage instead of using exclusive generic totals', () => { + const result = { + generations: [], + llmOutput: { + usage: rawUsage, + tokenUsage: { promptTokens: 40, completionTokens: 120, totalTokens: 160 }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it.each([ + { name: 'inclusive totals', promptTokens: 2600, completionTokens: 120, totalTokens: 2720 }, + { name: 'zero counts', promptTokens: 0, completionTokens: 0, totalTokens: 0 }, + ])('preserves legacy message $name over raw Anthropic usage', ({ promptTokens, completionTokens, totalTokens }) => { + const result = { + generations: [ + [ + { + message: { + response_metadata: { tokenUsage: { promptTokens, completionTokens, totalTokens } }, + }, + }, + ], + ], + llmOutput: { usage: { ...rawUsage, input_tokens: 0, output_tokens: 5 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + ...expectedUsage, + [GEN_AI_USAGE_INPUT_TOKENS]: promptTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS]: completionTokens, + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + }); + }); + + it('preserves raw cache details when normalized usage reports only inclusive totals', () => { + const result = { + generations: [[{ message: { usage_metadata: { input_tokens: 2600, output_tokens: 120, total_tokens: 2720 } } }]], + llmOutput: { usage: rawUsage }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('aggregates normalized usage across batched prompts', () => { + const result = { + generations: [ + [{ message: { usage_metadata: normalizedUsage } }], + [{ message: { usage_metadata: normalizedUsage } }], + ], + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 5200, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 5440, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 4096, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 1024, + }); + }); + + it('counts shared usage totals once for multiple candidates', () => { + const aggregateUsage = { ...normalizedUsage, output_tokens: 240, total_tokens: 2840 }; + const result = { + generations: [[{ message: { usage_metadata: aggregateUsage } }, { message: { usage_metadata: aggregateUsage } }]], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 240, totalTokens: 2840 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + ...expectedUsage, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2840, + }); + }); + + it.each([ + { + name: 'raw Anthropic usage', + result: { + generations: [], + llmOutput: { + usage: { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }, + }, + }, + { + name: 'normalized usage', + result: { + generations: [ + [ + { + message: { + usage_metadata: { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + input_token_details: { cache_read: 0, cache_creation: 0 }, + }, + }, + }, + ], + ], + }, + }, + ])('preserves zero counts in $name', ({ result }) => { + expect(extractLlmResponseAttributes(result as LangChainLLMResult, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 0, + [GEN_AI_USAGE_TOTAL_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 0, + }); + }); + + it('does not invent cache counts for legacy OpenAI usage', () => { + const result = { + generations: [], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + }); + }); + + it('preserves legacy message totals over final stream chunk counters', () => { + const result = { + generations: [ + [ + { + message: { + response_metadata: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }, + }, + ], + ], + llmOutput: { tokenUsage: { promptTokens: 0, completionTokens: 120, totalTokens: 120 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + }); + }); + + it('preserves zero legacy message usage over generic counters', () => { + const result = { + generations: [ + [{ message: { response_metadata: { tokenUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 } } } }], + ], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 0, + [GEN_AI_USAGE_TOTAL_TOKENS]: 0, + }); + }); + + it('omits null cache counts in raw Anthropic usage', () => { + const result = { + generations: [], + llmOutput: { + usage: { + input_tokens: 40, + output_tokens: 120, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 40, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 160, + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts index b0c6e3d16eed..a8038425e158 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts @@ -1,7 +1,10 @@ +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { describe, expect, it, vi } from 'vitest'; import type { LangChainMessage } from '../../../../src/ai/langchain/types'; import { _INTERNAL_mergeLangChainCallbackHandler, + getConversationIdFromMetadata, + getConversationIdMetadataFromConfig, normalizeLangChainMessages, } from '../../../../src/ai/langchain/utils'; @@ -149,3 +152,66 @@ describe('_INTERNAL_mergeLangChainCallbackHandler', () => { expect(_INTERNAL_mergeLangChainCallbackHandler(existing, sentryHandler)).toBe(existing); }); }); + +describe('getConversationIdFromMetadata', () => { + it('reads thread_id', () => { + expect(getConversationIdFromMetadata({ thread_id: 'thread-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'thread-1', + }); + }); + + it('falls back to session_id, then sessionId', () => { + expect(getConversationIdFromMetadata({ session_id: 'session-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-1', + }); + expect(getConversationIdFromMetadata({ sessionId: 'session-2' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-2', + }); + }); + + it('prefers thread_id over the session keys', () => { + expect( + getConversationIdFromMetadata({ sessionId: 'session-1', session_id: 'session-2', thread_id: 'thread-1' }), + ).toEqual({ [GEN_AI_CONVERSATION_ID]: 'thread-1' }); + }); + + it('stringifies a numeric id', () => { + expect(getConversationIdFromMetadata({ thread_id: 42 })).toEqual({ [GEN_AI_CONVERSATION_ID]: '42' }); + }); + + it('ignores empty, non-primitive and missing values', () => { + expect(getConversationIdFromMetadata({ thread_id: '' })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: { id: 'x' } })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: null })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: NaN })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: Infinity })).toEqual({}); + expect(getConversationIdFromMetadata({})).toEqual({}); + expect(getConversationIdFromMetadata(undefined)).toEqual({}); + }); + + it('skips an empty thread_id and uses the next key', () => { + expect(getConversationIdFromMetadata({ thread_id: '', sessionId: 'session-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-1', + }); + }); +}); + +describe('getConversationIdMetadataFromConfig', () => { + it('picks the first conversation id key out of configurable', () => { + expect( + getConversationIdMetadataFromConfig({ + configurable: { thread_id: 'thread-1', sessionId: 42, model: 'gpt-4o' }, + }), + ).toEqual({ thread_id: 'thread-1' }); + }); + + it('skips values that are not a valid id', () => { + expect( + getConversationIdMetadataFromConfig({ configurable: { thread_id: '', session_id: { id: 'x' }, sessionId: 42 } }), + ).toEqual({ sessionId: 42 }); + }); + + it('returns nothing without configurable', () => { + expect(getConversationIdMetadataFromConfig({})).toEqual({}); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts index 1e8a204bd85e..3f894efae3ea 100644 --- a/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts @@ -1,8 +1,17 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import { + conversationIdIntegration, + getCurrentScope, + getMainCarrier, + setCurrentClient, + spanToStaticSpanJSON, + startSpan, +} from '@sentry/core'; import type { Span } from '@sentry/core'; +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { instrumentCompiledGraphInvoke } from '../../../../src/ai/langgraph'; import type { CompiledGraph } from '../../../../src/ai/langgraph/types'; +import { wrapToolsWithSpans } from '../../../../src/ai/langgraph/utils'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; describe('LangGraph invoke_agent span names', () => { @@ -20,6 +29,7 @@ describe('LangGraph invoke_agent span names', () => { dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, traceLifecycle, + integrations: [conversationIdIntegration()], }), ); setCurrentClient(client); @@ -30,14 +40,14 @@ describe('LangGraph invoke_agent span names', () => { return endedSpans; } - async function invokeGraph(compileOptions: Record): Promise { + async function invokeGraph(compileOptions: Record, config?: Record): Promise { const invoke = instrumentCompiledGraphInvoke( async () => ({ messages: [] }), {} as CompiledGraph, compileOptions, {}, ); - await invoke({}); + await invoke({}, config); } it('names the span `{operation} {agent}` when an agent name is present', async () => { @@ -54,3 +64,80 @@ describe('LangGraph invoke_agent span names', () => { expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent'); }); }); + +describe('LangGraph conversation id', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [conversationIdIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + async function invokeGraph(config: Record): Promise { + const endedSpans = setupClient(); + const invoke = instrumentCompiledGraphInvoke(async () => ({ messages: [] }), {} as CompiledGraph, {}, {}); + await invoke({}, config); + return endedSpans[0]!; + } + + it('reads the same metadata keys as the LangChain spans', async () => { + const span = await invokeGraph({ configurable: { sessionId: 'session-1' } }); + + expect(spanToStaticSpanJSON(span).data[GEN_AI_CONVERSATION_ID]).toBe('session-1'); + }); + + it('lets an id set on the scope win over configurable.thread_id', async () => { + getCurrentScope().setConversationId('scope-id'); + const span = await invokeGraph({ configurable: { thread_id: 'thread-1' } }); + + expect(spanToStaticSpanJSON(span).data[GEN_AI_CONVERSATION_ID]).toBe('scope-id'); + }); + + it('copies the conversation id from configurable into the metadata the graph runs with', async () => { + setupClient(); + let receivedConfig: Record | undefined; + const invoke = instrumentCompiledGraphInvoke( + async (_input: unknown, config: unknown) => { + receivedConfig = config as Record; + return { messages: [] }; + }, + {} as CompiledGraph, + {}, + {}, + undefined, + {}, + ); + + await invoke({}, { configurable: { sessionId: 'session-1' }, metadata: { user: 'u' } }); + + expect(receivedConfig?.metadata).toEqual({ sessionId: 'session-1', user: 'u', __sentry_langgraph__: true }); + }); + + it('sets the conversation id on wrapped tool spans from the call metadata', async () => { + const endedSpans = setupClient(); + const tool = { name: 'add', invoke: async () => '3' }; + wrapToolsWithSpans([tool], {}); + + await startSpan({ name: 'root' }, () => tool.invoke({}, { metadata: { thread_id: 'thread-1' } })); + + const toolSpan = endedSpans.find(span => spanToStaticSpanJSON(span).description === 'execute_tool add')!; + expect(spanToStaticSpanJSON(toolSpan).data[GEN_AI_CONVERSATION_ID]).toBe('thread-1'); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts index bc282e0348bc..f2784635c5f2 100644 --- a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts @@ -11,12 +11,12 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, spanToStaticSpanJSON, @@ -69,8 +69,8 @@ describe('instrumentWorkersAiClient', () => { * are on here because the `run` call is the root span in these tests, with no active parent. */ const ALWAYS_RECORDED = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', @@ -187,8 +187,8 @@ describe('instrumentWorkersAiClient', () => { const span = spanToStaticSpanJSON(endedSpans[0]!); expect(span.description).toBe('evaluate typesafe/jev'); expect(span.data).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.evaluate', + [SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', + [SENTRY_OP]: 'gen_ai.evaluate', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', diff --git a/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts b/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts index cba539ae6e3a..a14324871f8a 100644 --- a/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts @@ -1,5 +1,69 @@ +import { + GEN_AI_RESPONSE_TEXT, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; import { describe, expect, it } from 'vitest'; -import { extractAgentNameFromParams, extractLLMFromParams } from '../../../../src/ai/langgraph/utils'; +import type { Span } from '@sentry/core'; +import { + extractAgentNameFromParams, + extractLLMFromParams, + setResponseAttributes, +} from '../../../../src/ai/langgraph/utils'; + +describe('LangGraph cache usage', () => { + it.each([ + { name: 'positive counts', cacheRead: 2048, cacheWrite: 512 }, + { name: 'zero counts', cacheRead: 0, cacheWrite: 0 }, + { name: 'absent counts', cacheRead: undefined, cacheWrite: undefined }, + ])('aggregates $name across new messages without adding caches to input totals', ({ cacheRead, cacheWrite }) => { + const attributes: Record = {}; + const span = { + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + } as unknown as Span; + const inputMessages = [ + { + role: 'assistant', + content: 'Previous answer', + usage_metadata: { + input_tokens: 10000, + output_tokens: 100, + total_tokens: 10100, + input_token_details: { cache_read: 9000, cache_creation: 1000 }, + }, + }, + ]; + const usage = { + input_tokens: 2600, + output_tokens: 120, + total_tokens: 2720, + input_token_details: { cache_read: cacheRead, cache_creation: cacheWrite }, + }; + + setResponseAttributes(span, inputMessages, { + messages: [ + ...inputMessages, + { role: 'assistant', content: 'First answer', usage_metadata: usage }, + { role: 'assistant', content: 'Second answer', usage_metadata: usage }, + ], + }); + + expect(attributes).toEqual({ + [GEN_AI_RESPONSE_TEXT]: + '[{"role":"assistant","content":"First answer"},{"role":"assistant","content":"Second answer"}]', + [GEN_AI_USAGE_INPUT_TOKENS]: 5200, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 5440, + ...(cacheRead === undefined ? {} : { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: cacheRead * 2 }), + ...(cacheWrite === undefined ? {} : { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: cacheWrite * 2 }), + }); + }); +}); describe('extractLLMFromParams', () => { it('returns null for empty or invalid args', () => { diff --git a/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts b/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts new file mode 100644 index 000000000000..131b1043ba77 --- /dev/null +++ b/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; +import { + piAiAssistantMessageToGenAiMessage, + piAiContentToString, + piAiFinishReason, + piAiMessagesToGenAiMessages, +} from '../../../../src/ai/pi-ai/messages'; + +describe('convert pi-ai messages to gen_ai messages', () => { + it('maps user, assistant and tool result messages', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'user', content: 'Weather in Vienna?', timestamp: 1 }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'The user wants the weather.' }, + { type: 'text', text: 'Let me check.' }, + { type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: { city: 'Vienna' } }, + ], + stopReason: 'toolUse', + }, + { + role: 'toolResult', + toolCallId: 'call_1', + toolName: 'get_weather', + content: [{ type: 'text', text: 'Sunny in Vienna.' }], + isError: false, + }, + ]), + ).toStrictEqual([ + { role: 'user', parts: [{ type: 'text', content: 'Weather in Vienna?' }] }, + { + role: 'assistant', + parts: [ + { type: 'reasoning', content: 'The user wants the weather.' }, + { type: 'text', content: 'Let me check.' }, + { type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Vienna"}' }, + ], + }, + { + role: 'tool', + parts: [{ type: 'tool_call_response', id: 'call_1', name: 'get_weather', result: 'Sunny in Vienna.' }], + }, + ]); + }); + + it('reports images by media type and drops their base64 data', () => { + expect( + piAiMessagesToGenAiMessages([ + { + role: 'user', + content: [ + { type: 'text', text: 'What is this?' }, + { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }, + ], + }, + ]), + ).toStrictEqual([ + { + role: 'user', + parts: [ + { type: 'text', content: 'What is this?' }, + { type: 'blob', mime_type: 'image/png' }, + ], + }, + ]); + }); + + it('leaves out system messages, redacted thinking and messages without content', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'system', content: '', sections: { preamble: 'You are helpful.' } }, + { role: 'assistant', content: [{ type: 'thinking', thinking: 'c2VjcmV0', redacted: true }] }, + { role: 'user', content: '' }, + 'not a message', + ]), + ).toStrictEqual([]); + }); + + it('leaves out messages with a role the conventions do not have', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'custom', content: 'Summary of the earlier turns.' }, + { role: 'user', content: 'Weather in Berlin?' }, + ]), + ).toStrictEqual([{ role: 'user', parts: [{ type: 'text', content: 'Weather in Berlin?' }] }]); + }); + + it('drops empty text blocks, which would hide the tool calls next to them', () => { + expect( + piAiMessagesToGenAiMessages([ + { + role: 'assistant', + content: [ + { type: 'text', text: '' }, + { type: 'text', text: ' \n' }, + { type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: {} }, + ], + }, + { role: 'assistant', content: [{ type: 'text', text: '' }] }, + ]), + ).toStrictEqual([ + { role: 'assistant', parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{}' }] }, + ]); + }); + + it('maps the tool-calling stop reason to the conventions and passes the others through', () => { + expect(piAiFinishReason('toolUse')).toBe('tool_call'); + expect(piAiFinishReason('stop')).toBe('stop'); + expect(piAiFinishReason('aborted')).toBe('aborted'); + expect(piAiFinishReason(undefined)).toBeUndefined(); + expect(piAiFinishReason('')).toBeUndefined(); + }); + + it('keeps unknown part kinds as objects', () => { + expect(piAiMessagesToGenAiMessages([{ role: 'user', content: [{ type: 'audio', url: 'a.mp3' }] }])).toStrictEqual([ + { role: 'user', parts: [{ type: 'object', content: { type: 'audio', url: 'a.mp3' } }] }, + ]); + }); + + it('maps an assistant message to an output message with its finish reason', () => { + expect( + piAiAssistantMessageToGenAiMessage( + { + role: 'assistant', + content: [{ type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: { city: 'Vienna' } }], + }, + 'tool_call', + ), + ).toStrictEqual({ + role: 'assistant', + parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Vienna"}' }], + finish_reason: 'tool_call', + }); + expect(piAiAssistantMessageToGenAiMessage({ role: 'assistant', content: [] }, 'stop')).toBeUndefined(); + }); + + it('renders text-only content as text and other content as mapped parts', () => { + expect( + piAiContentToString([ + { type: 'text', text: 'line 1' }, + { type: 'text', text: 'line 2' }, + ]), + ).toBe('line 1\nline 2'); + expect(piAiContentToString([{ type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }])).toBe( + '[{"type":"blob","mime_type":"image/png"}]', + ); + }); +}); diff --git a/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts b/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts new file mode 100644 index 000000000000..6d4b768ba616 --- /dev/null +++ b/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { setPiAiUsageAttributes } from '../../../../src/ai/pi-ai/usage'; + +function spanRecorder(): { span: Span; attributes: Record } { + const attributes: Record = {}; + return { + span: { setAttributes: (values: Record) => Object.assign(attributes, values) } as unknown as Span, + attributes, + }; +} + +describe('setPiAiUsageAttributes', () => { + it('counts the cached tokens into the input tokens and their cost into the input cost', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { + input: 37, + output: 20, + cacheRead: 52, + cacheWrite: 38, + reasoning: 8, + totalTokens: 147, + cost: { input: 0.000111, output: 0.0001, cacheRead: 0.0000156, cacheWrite: 0.0001425, total: 0.0003691 }, + }); + + expect(attributes).toStrictEqual({ + 'gen_ai.usage.input_tokens': 127, + 'gen_ai.usage.output_tokens': 20, + 'gen_ai.usage.total_tokens': 147, + 'gen_ai.usage.cache_read.input_tokens': 52, + 'gen_ai.usage.cache_creation.input_tokens': 38, + 'gen_ai.usage.reasoning.output_tokens': 8, + 'gen_ai.cost.input_tokens': 0.000111 + 0.0000156 + 0.0001425, + 'gen_ai.cost.output_tokens': 0.0001, + 'gen_ai.cost.total_tokens': 0.0003691, + 'gen_ai.cost.cache_read.input_tokens': 0.0000156, + 'gen_ai.cost.cache_creation.input_tokens': 0.0001425, + }); + }); + + it('writes only the counters the response has', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { input: 10, output: 2, totalTokens: 12 }); + + expect(attributes).toStrictEqual({ + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 2, + 'gen_ai.usage.total_tokens': 12, + }); + }); + + it('skips the zero counters of a request that failed before it was billed', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 }, true); + setPiAiUsageAttributes(span, undefined); + + expect(attributes).toStrictEqual({}); + }); +}); diff --git a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts index 8d9f9d15b257..40abfadf8f2f 100644 --- a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { Span } from '@sentry/core'; import { GEN_AI_PROVIDER_NAME, @@ -21,6 +20,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../src/ai/core/gen-ai-attributes'; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from '../../../../src/ai/workers-ai/constants'; @@ -49,29 +49,42 @@ function createMockSpan(): { span: Span; attributes: Record } { describe('workers-ai utils', () => { describe('getOperationName', () => { it('returns "chat" for prompt inputs', () => { - expect(getOperationName({ prompt: 'Hello' })).toBe('chat'); + expect(getOperationName(MODEL, { prompt: 'Hello' })).toBe('chat'); }); it('returns "chat" for messages inputs', () => { - expect(getOperationName({ messages: [{ role: 'user', content: 'Hi' }] })).toBe('chat'); + expect(getOperationName(MODEL, { messages: [{ role: 'user', content: 'Hi' }] })).toBe('chat'); }); it('returns "embeddings" for text inputs', () => { - expect(getOperationName({ text: 'embed me' })).toBe('embeddings'); + expect(getOperationName(MODEL, { text: 'embed me' })).toBe('embeddings'); }); - it('returns "evaluate" for TypeSafe state and questions inputs', () => { - expect(getOperationName({ state: 'Help!', questions: {} })).toBe('evaluate'); + it('returns "evaluate" for TypeSafe Jev models', () => { + expect(getOperationName('typesafe/jev', { state: 'Help!', questions: {} })).toBe('evaluate'); + expect(getOperationName('typesafe/jev-1.13', { state: 'Help!', questions: {} })).toBe('evaluate'); + }); + + it('returns "evaluate" for Cloudflare Clef models', () => { + expect(getOperationName('@cf/cloudflare/clef', { state: 'Help!', questions: {} })).toBe('evaluate'); + }); + + it('does not return "evaluate" for other TypeSafe models', () => { + expect(getOperationName('typesafe/other', { prompt: 'Hello' })).toBe('chat'); + }); + + it('does not return "evaluate" for other models with state and questions inputs', () => { + expect(getOperationName(MODEL, { state: 'Help!', questions: {} })).toBe('chat'); }); it('prefers "chat" when both messages and text are present', () => { - expect(getOperationName({ messages: [{ role: 'user', content: 'Hi' }], text: 'embed me' })).toBe('chat'); + expect(getOperationName(MODEL, { messages: [{ role: 'user', content: 'Hi' }], text: 'embed me' })).toBe('chat'); }); it('falls back to "chat" for null, undefined and empty inputs', () => { - expect(getOperationName(null)).toBe('chat'); - expect(getOperationName(undefined)).toBe('chat'); - expect(getOperationName({})).toBe('chat'); + expect(getOperationName(MODEL, null)).toBe('chat'); + expect(getOperationName(MODEL, undefined)).toBe('chat'); + expect(getOperationName(MODEL, {})).toBe('chat'); }); }); @@ -80,7 +93,7 @@ describe('workers-ai utils', () => { expect(extractRequestAttributes(MODEL, { prompt: 'Hello' }, 'chat')).toEqual({ [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: MODEL, }); }); @@ -104,7 +117,7 @@ describe('workers-ai utils', () => { ).toEqual({ [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: MODEL, [GEN_AI_REQUEST_TEMPERATURE]: 0.5, [GEN_AI_REQUEST_MAX_TOKENS]: 100, diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index 9eea2d8b03b8..4f03a86e835d 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -8,6 +8,7 @@ import { createTransport, getActiveSpan, getAsyncContextStrategy, + getCurrentScope, getDefaultCurrentScope, getDefaultIsolationScope, getMainCarrier, @@ -42,6 +43,7 @@ class TestClient extends Client { } function initTestClient(traceLifecycle: 'static' | 'stream' = 'static'): void { + getCurrentScope().setClient(undefined); initAndBind(TestClient, { dsn: 'https://username@domain/123', integrations: [], diff --git a/packages/server-utils/test/integrations/prisma.test.ts b/packages/server-utils/test/integrations/prisma.test.ts index fba6e1706aa1..2d02718b6d1e 100644 --- a/packages/server-utils/test/integrations/prisma.test.ts +++ b/packages/server-utils/test/integrations/prisma.test.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { Client, createTransport, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; +import { Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; import { afterEach, describe, expect, it } from 'vitest'; import { instrumentPrisma } from '../../src/integrations/prisma'; import type { TracingHelper } from '../../src/integrations/prisma/types'; @@ -23,6 +23,7 @@ class TestClient extends Client { } function initTestClient(): void { + getCurrentScope().setClient(undefined); initAndBind(TestClient, { dsn: 'https://username@domain/123', integrations: [], diff --git a/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts b/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts new file mode 100644 index 000000000000..07b5a35da9f5 --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts @@ -0,0 +1,151 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { GEN_AI_CONVERSATION_ID, GEN_AI_RESPONSE_ID } from '@sentry/conventions/attributes'; +import { + conversationIdIntegration, + getMainCarrier, + setConversationId, + setCurrentClient, + spanToStaticSpanJSON, + withActiveSpan, +} from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { getProviderMetadataAttributes } from '../../../src/ai/vercel-ai'; +import { + clearOperationCallId, + createSpanFromMessage, + enrichSpanOnEnd, +} from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +type Message = Parameters[0]; +const channelOptions = {} as Parameters[1]; + +describe('Vercel AI SDK conversation id', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + // The bare test client has no default integrations; the Node SDK registers this one itself. + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [conversationIdIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + setConversationId(undefined); + clearOperationCallId('call-1'); + getMainCarrier().__SENTRY__ = undefined; + }); + + function run(message: Message): Record { + const span = createSpanFromMessage(message, channelOptions)!; + enrichSpanOnEnd(span, message, {} as Parameters[2]); + span.end(); + return spanToStaticSpanJSON(endedSpans[endedSpans.length - 1]!).data ?? {}; + } + + it('reads the OpenAI Conversations API id from the operation `providerOptions`', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + }); + + it('reads the id from the `azure` provider options', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { azure: { conversation: 'conv_azure' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_azure'); + }); + + it('reads the `azure` id when the `openai` options have no conversation', () => { + const data = run({ + type: 'generateText', + event: { + callId: 'call-1', + providerOptions: { openai: { reasoningEffort: 'low' }, azure: { conversation: 'conv_azure' } }, + }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_azure'); + }); + + it('passes the operation id on to the model-call and tool spans that start under it', () => { + const operationMessage: Message = { + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }; + const operationSpan = createSpanFromMessage(operationMessage, channelOptions)!; + // Child events start while the operation span is the active span (the channel binds it as context). + const [modelCall, toolCall] = withActiveSpan(operationSpan, () => [ + run({ type: 'languageModelCall', event: { callId: 'call-1', modelId: 'gpt-4' } }), + run({ + type: 'executeTool', + event: { callId: 'call-1', toolCall: { toolName: 'echo', toolCallId: 'tc-1', input: {} } }, + }), + ]); + operationSpan.end(); + + expect(spanToStaticSpanJSON(operationSpan).data?.[GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + expect(modelCall![GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + expect(toolCall![GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + }); + + it('does not pass an outer conversation id to a nested operation', () => { + const outer = createSpanFromMessage( + { + type: 'generateText', + event: { callId: 'call-outer', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }, + channelOptions, + )!; + // e.g. a sub-agent `generateText` called from inside a tool's `execute` + const inner = withActiveSpan(outer, () => + run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { previousResponseId: 'resp_1' } } }, + }), + ); + outer.end(); + clearOperationCallId('call-outer'); + + expect(inner[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + }); + + it('sets nothing for `previousResponseId` chaining, which names a response rather than a thread', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { previousResponseId: 'resp_1' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + }); + + it('does not record the provider `responseId` as the conversation id', () => { + const data = run({ + type: 'languageModelCall', + event: { callId: 'call-1', modelId: 'gpt-4' }, + result: { response: { id: 'resp_1' }, providerMetadata: { openai: { responseId: 'resp_1' } } }, + }); + expect(data[GEN_AI_RESPONSE_ID]).toBe('resp_1'); + expect(data[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + expect(getProviderMetadataAttributes({ openai: { responseId: 'resp_1' } })).toEqual({}); + }); + + it('lets an id set via `Sentry.setConversationId()` win over the provider option', () => { + setConversationId('conv-from-scope'); + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv-from-scope'); + }); +}); diff --git a/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts new file mode 100644 index 000000000000..0101b19cf69b --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage, enrichSpanOnEnd } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +type Message = Parameters[0]; + +describe('Vercel AI evaluate spans', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + // `ai` 7.0.128 renamed the channel event from `experimental_evaluate` to `experimental_decide`. + it.each(['experimental_evaluate', 'experimental_decide'] as const)('creates an evaluate span for `%s`', type => { + const message = { + type, + event: { + provider: 'gateway', + modelId: 'typesafe-ai/jev', + state: 'I want a refund', + questions: { wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' } }, + recordInputs: true, + recordOutputs: true, + }, + result: { answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + } as unknown as Message; + + const span = createSpanFromMessage(message, {} as Parameters[1])!; + expect(span).toBeDefined(); + enrichSpanOnEnd(span, message, {} as Parameters[2]); + span.end(); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe('evaluate typesafe-ai/jev'); + expect(json.op).toBe('gen_ai.evaluate'); + expect(JSON.parse(json.data?.['gen_ai.output.messages'] as string)).toEqual([ + { type: 'evaluation', answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + ]); + }); +}); diff --git a/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts b/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts new file mode 100644 index 000000000000..e47c2f3d272a --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +describe('Vercel AI telemetry metadata', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + it('records metadata values as strings and skips null and undefined', () => { + const endedSpans = setupClient(); + const span = createSpanFromMessage( + { + type: 'generateText', + event: { + telemetryMetadata: { + requestId: 'req_1', + attempt: 2, + cached: false, + tags: ['a', 'b'], + empty: null, + missing: undefined, + }, + }, + } as Parameters[0], + {} as Parameters[1], + ); + span?.end(); + + const data = spanToStaticSpanJSON(endedSpans[0]!).data; + expect(data).toMatchObject({ + 'vercel.ai.telemetry.metadata.requestId': 'req_1', + 'vercel.ai.telemetry.metadata.attempt': '2', + 'vercel.ai.telemetry.metadata.cached': 'false', + 'vercel.ai.telemetry.metadata.tags': 'a,b', + }); + expect(data).not.toHaveProperty('vercel.ai.telemetry.metadata.empty'); + expect(data).not.toHaveProperty('vercel.ai.telemetry.metadata.missing'); + }); +}); diff --git a/packages/solid/package.json b/packages/solid/package.json index 4dbba8f7a941..be95545a57db 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -56,7 +56,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index a9e133da3a97..40f91c2323ed 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -14,17 +14,11 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import type { Client, Integration, Span } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - filterCollectedUrl, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, filterCollectedUrl } from '@sentry/core'; import type { BeforeLeaveEventArgs, HashRouter, @@ -71,7 +65,7 @@ function handleNavigation(location: string): void { name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : location, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.${framework}.solidrouter`, + [SENTRY_ORIGIN]: `auto.navigation.${framework}.solidrouter`, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, @@ -153,7 +147,7 @@ function withSentryRouterRoot(Root: Component): Component( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -156,7 +156,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/solid/test/solidrouter.test.tsx b/packages/solid/test/solidrouter.test.tsx index 138cf7e7e868..36d36b11b2c5 100644 --- a/packages/solid/test/solidrouter.test.tsx +++ b/packages/solid/test/solidrouter.test.tsx @@ -1,13 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { spanToJSON } from '@sentry/browser'; import type { Span } from '@sentry/core'; -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import type { MemoryHistory } from '@solidjs/router'; import { createMemoryHistory, MemoryRouter, Navigate, Route } from '@solidjs/router'; import { render, waitFor } from '@solidjs/testing-library'; @@ -74,8 +68,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -105,8 +99,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -150,8 +144,8 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.name).toBe(parametrizedRoute); expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }); for (const [key, value] of Object.entries(expectedParams as Record)) { @@ -186,8 +180,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/about', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }), }), ); diff --git a/packages/solid/test/tanstackrouter.test.ts b/packages/solid/test/tanstackrouter.test.ts index de3a2e9496d7..92908d5b1aa0 100644 --- a/packages/solid/test/tanstackrouter.test.ts +++ b/packages/solid/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -91,7 +90,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/posts/$postId', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/posts/$postId', 'url.path.parameter.postId': '999', diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 7562af9e552c..464889441685 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -87,7 +87,7 @@ "@sentry/server-utils": "11.4.0", "@sentry/solid": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "0.25.0" + "@sentry/conventions": "0.26.0" }, "devDependencies": { "@solidjs/router": "^1.0.0", diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index bc01f6bb9906..8c8576aeb7ae 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -88,7 +88,9 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/solidstart/src/server/withServerActionInstrumentation.ts b/packages/solidstart/src/server/withServerActionInstrumentation.ts index 2c572a4f0e2a..785ab1432f91 100644 --- a/packages/solidstart/src/server/withServerActionInstrumentation.ts +++ b/packages/solidstart/src/server/withServerActionInstrumentation.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR } from '@sentry/core'; +import { handleCallbackErrors, SPAN_STATUS_ERROR } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import { captureException, getActiveSpan, spanToJSON, startSpan } from '@sentry/node'; import { isRedirect } from './utils'; @@ -9,6 +9,7 @@ import { HTTP_TARGET, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; @@ -48,7 +49,7 @@ export async function withServerActionInstrumentation { beforeEach(() => { vi.clearAllMocks(); + SentrySolid.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { @@ -39,6 +40,10 @@ describe('Initialize Solid Start SDK', () => { }); describe('browserTracingIntegration', () => { + beforeEach(() => { + SentrySolid.getCurrentScope().setClient(undefined); + }); + it('adds the `browserTracingIntegration` when `__SENTRY_TRACING__` is not set', () => { const client = solidStartInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/packages/solidstart/test/client/solidrouter.test.tsx b/packages/solidstart/test/client/solidrouter.test.tsx index 226868db256d..0b5d32947a6c 100644 --- a/packages/solidstart/test/client/solidrouter.test.tsx +++ b/packages/solidstart/test/client/solidrouter.test.tsx @@ -1,13 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { spanToJSON } from '@sentry/browser'; import type { Span } from '@sentry/core'; -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import type { MemoryHistory } from '@solidjs/router'; import { createMemoryHistory, MemoryRouter, Navigate, Route } from '@solidjs/router'; import { render, waitFor } from '@solidjs/testing-library'; @@ -74,8 +68,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -105,8 +99,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -150,8 +144,8 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.name).toBe(parametrizedRoute); expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }); for (const [key, value] of Object.entries(expectedParams as Record)) { @@ -186,8 +180,8 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/about', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', + [SENTRY_OP]: 'navigation', + [SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }), }), ); diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts index c5df698ed307..067e7a6de818 100644 --- a/packages/solidstart/test/server/sdk.test.ts +++ b/packages/solidstart/test/server/sdk.test.ts @@ -10,6 +10,7 @@ const browserInit = vi.spyOn(SentryNode, 'init'); describe('Initialize Solid Start SDK', () => { beforeEach(() => { vi.clearAllMocks(); + SentryNode.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { diff --git a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts index b196f7a78a2a..1a908c0908b9 100644 --- a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts +++ b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts @@ -1,15 +1,13 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + CODE_FUNCTION_NAME, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import * as SentryCoreServer from '@sentry/core/server'; import * as SentryNode from '@sentry/node'; -import { - createTransport, - NodeClient, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, - spanToJSON, -} from '@sentry/node'; +import { createTransport, NodeClient, setCurrentClient, spanToJSON } from '@sentry/node'; import { redirect } from '@solidjs/router'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { withServerActionInstrumentation } from '../../src/server'; @@ -101,10 +99,10 @@ describe('withServerActionInstrumentation', () => { expect.objectContaining({ name: 'getPrefecture', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'getPrefecture', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_ORIGIN]: 'auto.function.solidstart', }), }), ); @@ -128,7 +126,7 @@ describe('withServerActionInstrumentation', () => { const span = new SentryCore.SentrySpan({ attributes: { 'http.target': '/_server', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockGetActiveSpan.mockReturnValue(span); @@ -153,7 +151,7 @@ describe('withServerActionInstrumentation', () => { const span = new SentryCore.SentrySpan({ attributes: { 'url.path': '/_server', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockGetActiveSpan.mockReturnValue(span); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c402014f45b1..edc7ac86dcd1 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "magic-string": "~0.30.0" }, diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index 62fbc3a144d0..59f7c1b8174a 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { debug, @@ -8,7 +7,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core/browser'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_UPDATE } from '@sentry/conventions/op'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; import { DEBUG_BUILD } from './debug_build'; @@ -65,7 +64,7 @@ function recordInitSpan(componentName: string | undefined, description: string): name: hasSpanStreaming ? componentName || UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte', + [SENTRY_ORIGIN]: 'auto.ui.svelte', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -87,7 +86,7 @@ function recordUpdateSpans(componentName: string | undefined, description: strin name: hasSpanStreaming ? componentName || UI_UPDATE_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_UPDATE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte', + [SENTRY_ORIGIN]: 'auto.ui.svelte', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index f151719bc4a2..140e969eea2d 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -12,6 +12,7 @@ const browserInit = vi.spyOn(SentryBrowser, 'init'); describe('Initialize Svelte SDk', () => { beforeEach(() => { vi.clearAllMocks(); + SentryBrowser.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 76e8c91f196f..bd5c9caa36bf 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -73,7 +73,7 @@ "@opentelemetry/api": "^1.9.1", "@sentry/cloudflare": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 4ddeaad8ffef..a1a78995ce0e 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -4,7 +4,6 @@ import { handleCallbackErrors, hasSpanStreamingEnabled, objectify, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { startSpan } from '@sentry/core/browser'; import { @@ -14,6 +13,7 @@ import { SENTRY_OP, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { captureException } from '@sentry/svelte'; @@ -95,7 +95,7 @@ export function wrapLoadWithSentry any>(origLoad: T) attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [URL_PATH]: event.url.pathname, ...(routeId && { [URL_TEMPLATE]: routeId }), diff --git a/packages/sveltekit/src/client/svelte4BrowserTracing.ts b/packages/sveltekit/src/client/svelte4BrowserTracing.ts index b80651ad0734..19c0f3600b98 100644 --- a/packages/sveltekit/src/client/svelte4BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte4BrowserTracing.ts @@ -4,7 +4,6 @@ import { NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { getCurrentScope, @@ -13,7 +12,7 @@ import { startInactiveSpan, WINDOW, } from '@sentry/svelte'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD, ROUTER } from '@sentry/conventions/op'; import type { Navigation, Page } from '@sveltejs/kit'; // eslint-disable-next-line typescript/no-deprecated @@ -49,7 +48,7 @@ function _instrumentPageload(client: Client, pageStore: Readable): void { name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -127,7 +126,7 @@ function _instrumentNavigations(client: Client, navigatingStore: Readable any>(origLoad: T) attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [URL_PATH]: event.url.pathname, [HTTP_ROUTE]: routeId, @@ -130,7 +125,7 @@ export function wrapServerLoadWithSentry any>(origSe attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [HTTP_REQUEST_METHOD]: event.request.method, [URL_PATH]: event.url.pathname, diff --git a/packages/sveltekit/src/server-common/serverRoute.ts b/packages/sveltekit/src/server-common/serverRoute.ts index 97f06bcbaf5c..4a62f5268820 100644 --- a/packages/sveltekit/src/server-common/serverRoute.ts +++ b/packages/sveltekit/src/server-common/serverRoute.ts @@ -3,7 +3,6 @@ import { FUNCTION_SPAN_NAME_FALLBACK, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; @@ -13,6 +12,7 @@ import { HTTP_ROUTE, SENTRY_DESCRIPTION, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import type { RequestEvent } from '@sveltejs/kit'; @@ -71,7 +71,7 @@ export function wrapServerRouteWithSentry( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: httpMethod, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [HTTP_REQUEST_METHOD]: httpMethod, [HTTP_ROUTE]: routeId, // Relay infers the description from `code.function.name`, which would drop the route. diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index cff338b62ed0..cb0730b4c513 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -87,7 +87,9 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 22927842682c..1ab26de90d38 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -26,6 +26,7 @@ export { captureFeedback, captureMessage, close, + consoleLoggingIntegration, continueTrace, createTransport, dedupeIntegration, @@ -53,7 +54,9 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index 587af53544af..c27d9db8ddb6 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -4,13 +4,12 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; import { writable } from 'svelte/store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { navigating, page } from '$app/stores'; import { browserTracingIntegration } from '../../src/client'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the // `navigating` store for each test. @@ -115,7 +114,7 @@ describe('browserTracingIntegration', () => { name: '/', attributes: { 'sentry.op': 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -219,7 +218,7 @@ describe('browserTracingIntegration', () => { name: '/users/[id]', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/[id]', 'sentry.sveltekit.navigation.from': '/users', @@ -234,7 +233,7 @@ describe('browserTracingIntegration', () => { name: 'SvelteKit Route Change', attributes: { [SENTRY_OP]: 'router', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + [SENTRY_ORIGIN]: 'auto.ui.sveltekit', 'sentry.sveltekit.navigation.from': '/users', 'sentry.sveltekit.navigation.to': '/users/[id]', 'sentry.sveltekit.navigation.type': 'link', @@ -364,7 +363,7 @@ describe('browserTracingIntegration', () => { attributes: { 'sentry.op': 'navigation', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [URL_TEMPLATE]: '/users/[id]', 'sentry.sveltekit.navigation.from': '/users/[id]', 'sentry.sveltekit.navigation.to': '/users/[id]', @@ -377,7 +376,7 @@ describe('browserTracingIntegration', () => { name: 'SvelteKit Route Change', attributes: { [SENTRY_OP]: 'router', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + [SENTRY_ORIGIN]: 'auto.ui.sveltekit', 'sentry.sveltekit.navigation.from': '/users/[id]', 'sentry.sveltekit.navigation.to': '/users/[id]', }, diff --git a/packages/sveltekit/test/client/fetch.test.ts b/packages/sveltekit/test/client/fetch.test.ts index 2819fbbbdeb1..03aa56637899 100644 --- a/packages/sveltekit/test/client/fetch.test.ts +++ b/packages/sveltekit/test/client/fetch.test.ts @@ -3,10 +3,12 @@ */ import { beforeEach, describe, expect, it } from 'vitest'; -import { init } from '../../src/client/index'; +import { getCurrentScope, init } from '../../src/client/index'; describe('instruments fetch', () => { beforeEach(() => { + getCurrentScope().setClient(undefined); + // For the happy path, we can assume that both fetch and the fetch proxy are set // We test the edge cases in the other tests below diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index 616326565f46..5978a2bffc01 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -1,6 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; import type { Load } from '@sveltejs/kit'; @@ -106,9 +105,9 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'url.template': '/users/[id]', @@ -136,9 +135,9 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', }, @@ -202,9 +201,9 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'url.template': '/users/[id]', @@ -223,9 +222,9 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', 'sentry.description': '/users/123', diff --git a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts index f77d2c0f8195..eca41882dd76 100644 --- a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts +++ b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts @@ -4,9 +4,8 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentSvelteKitTracing } from '../../src/client/svelte5BrowserTracing'; @@ -83,7 +82,7 @@ describe('svelte5 browser tracing', () => { name: '/', attributes: { 'sentry.op': 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -124,7 +123,7 @@ describe('svelte5 browser tracing', () => { name: '/users/[id]', attributes: expect.objectContaining({ 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/[id]', }), diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index 3412e9376b25..2c53de829a3e 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -1,5 +1,5 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { SpanJSON, StreamedSpanJSON, TransactionEvent } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { _enhanceKitSpan, @@ -47,8 +47,8 @@ describe('svelteKitSpansIntegration', () => { expect(event.spans).toHaveLength(1); expect(event.spans?.[0]?.op).toBe('function'); expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit'); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('function'); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(event.spans?.[0]?.data[SENTRY_OP]).toBe('function'); + expect(event.spans?.[0]?.data[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); describe('_enhanceKitSpan', () => { @@ -74,8 +74,8 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe(op); expect(span.origin).toBe(origin); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + expect(span.data[SENTRY_OP]).toBe(op); + expect(span.data[SENTRY_ORIGIN]).toBe(origin); }); it("doesn't change spans from other origins", () => { @@ -88,8 +88,8 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBeUndefined(); expect(span.origin).toBeUndefined(); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(span.data[SENTRY_OP]).toBeUndefined(); + expect(span.data[SENTRY_ORIGIN]).toBeUndefined(); }); it("doesn't overwrite the sveltekit.handle.root span", () => { @@ -98,8 +98,8 @@ describe('svelteKitSpansIntegration', () => { op: 'http.server', origin: 'auto.http.sveltekit', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SENTRY_OP]: 'http.server', + [SENTRY_ORIGIN]: 'auto.http.sveltekit', }, span_id: '123', trace_id: 'abc', @@ -108,8 +108,8 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(rootHandleSpan); - expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); - expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(rootHandleSpan.data[SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.data[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); expect(rootHandleSpan.op).toBe('http.server'); expect(rootHandleSpan.origin).toBe('auto.http.sveltekit'); }); @@ -118,8 +118,8 @@ describe('svelteKitSpansIntegration', () => { const span = { description: 'someOtherSpan', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + [SENTRY_OP]: 'db', + [SENTRY_ORIGIN]: 'auto.db.pg', }, op: 'db', origin: 'auto.db.pg', @@ -132,8 +132,8 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe('db'); expect(span.origin).toBe('auto.db.pg'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + expect(span.data[SENTRY_OP]).toBe('db'); + expect(span.data[SENTRY_ORIGIN]).toBe('auto.db.pg'); }); it("doesn't overwrite already set ops or origins on sveltekit spans", () => { @@ -142,7 +142,7 @@ describe('svelteKitSpansIntegration', () => { description: 'sveltekit.resolve', origin: 'auto.custom.origin', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', }, span_id: '123', trace_id: 'abc', @@ -152,7 +152,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(span); expect(span.origin).toBe('auto.custom.origin'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.data[SENTRY_OP]).toBe('custom.op'); }); it('overwrites previously set "manual" origins on sveltekit spans', () => { @@ -161,7 +161,7 @@ describe('svelteKitSpansIntegration', () => { description: 'sveltekit.resolve', origin: 'manual', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', }, span_id: '123', trace_id: 'abc', @@ -171,7 +171,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(span); expect(span.origin).toBe('auto.http.sveltekit'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.data[SENTRY_OP]).toBe('custom.op'); }); }); @@ -202,8 +202,8 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + expect(span.attributes?.[SENTRY_OP]).toBe(op); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe(origin); expect(span.attributes?.['sentry.description']).toBe(spanName); }); @@ -212,8 +212,8 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(span.attributes?.[SENTRY_OP]).toBeUndefined(); + expect(span.attributes?.[SENTRY_ORIGIN]).toBeUndefined(); expect(span.attributes?.['sentry.description']).toBeUndefined(); }); @@ -221,30 +221,30 @@ describe('svelteKitSpansIntegration', () => { const rootHandleSpan = makeStreamedSpan({ name: 'sveltekit.handle.root', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SENTRY_OP]: 'http.server', + [SENTRY_ORIGIN]: 'auto.http.sveltekit', }, }); _enhanceKitSpanStreamed(rootHandleSpan); - expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); - expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(rootHandleSpan.attributes?.[SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.attributes?.[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); it("doesn't enhance unrelated spans", () => { const span = makeStreamedSpan({ name: 'someOtherSpan', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + [SENTRY_OP]: 'db', + [SENTRY_ORIGIN]: 'auto.db.pg', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + expect(span.attributes?.[SENTRY_OP]).toBe('db'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.db.pg'); }); it("doesn't overwrite already set ops or origins on sveltekit spans", () => { @@ -252,15 +252,15 @@ describe('svelteKitSpansIntegration', () => { const span = makeStreamedSpan({ name: 'sveltekit.resolve', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.custom.origin', + [SENTRY_OP]: 'custom.op', + [SENTRY_ORIGIN]: 'auto.custom.origin', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.custom.origin'); + expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.custom.origin'); }); it("doesn't overwrite an already set description", () => { @@ -278,15 +278,15 @@ describe('svelteKitSpansIntegration', () => { const span = makeStreamedSpan({ name: 'sveltekit.resolve', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_OP]: 'custom.op', + [SENTRY_ORIGIN]: 'manual', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); }); }); diff --git a/packages/sveltekit/test/server-common/load.test.ts b/packages/sveltekit/test/server-common/load.test.ts index 4dc513b778c7..97b3a6390c08 100644 --- a/packages/sveltekit/test/server-common/load.test.ts +++ b/packages/sveltekit/test/server-common/load.test.ts @@ -1,6 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client, Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { NodeClient, setCurrentClient } from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; @@ -166,9 +165,9 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'http.route': '/users/[id]', @@ -187,9 +186,9 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', }, @@ -257,9 +256,9 @@ describe('wrapServerLoadWithSentry calls `startSpan`', () => { expect(transaction.contexts?.trace).toEqual({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', 'http.request.method': 'GET', 'url.path': '/users/123', @@ -301,7 +300,7 @@ describe('wrapServerLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', }), name: '/users/[id]', // <-- this shows that the route was still accessed }), @@ -356,9 +355,9 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'http.route': '/users/[id]', @@ -377,9 +376,9 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', 'sentry.description': '/users/123', @@ -397,9 +396,9 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'http.request.method': 'GET', 'url.path': '/users/123', diff --git a/packages/sveltekit/test/server-common/serverRoute.test.ts b/packages/sveltekit/test/server-common/serverRoute.test.ts index 72efa31c1a7e..ef984fbb8af9 100644 --- a/packages/sveltekit/test/server-common/serverRoute.test.ts +++ b/packages/sveltekit/test/server-common/serverRoute.test.ts @@ -1,13 +1,10 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import type { NumericRange, RequestEvent } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - wrapServerRouteWithSentry, -} from '../../src/server'; +import { wrapServerRouteWithSentry } from '../../src/server'; describe('wrapServerRouteWithSentry', () => { const originalRouteHandler = vi.fn(); @@ -37,9 +34,9 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'http.route': '/api/users/:id', }, @@ -60,9 +57,9 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', }, name: 'GET Server Route', @@ -93,9 +90,9 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'http.route': '/api/users/:id', 'sentry.description': 'GET /api/users/:id', @@ -115,9 +112,9 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'sentry.description': 'GET Server Route', }, diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 32a5d8b318d8..8e576bd282fc 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -53,7 +53,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser-utils": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/tanstackstart-react/src/server/routeParametrization.ts b/packages/tanstackstart-react/src/server/routeParametrization.ts index 8e39a05fcc57..c47b5ddd3e32 100644 --- a/packages/tanstackstart-react/src/server/routeParametrization.ts +++ b/packages/tanstackstart-react/src/server/routeParametrization.ts @@ -1,40 +1,175 @@ import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { - escapeStringForRegex, - getActiveSpan, - getCurrentScope, - getRootSpan, - spanToJSON, - updateSpanName, -} from '@sentry/core'; - -function patternToRegex(pattern: string): RegExp { - const segments = pattern - .split('/') - .map(segment => { - if (segment.startsWith('$')) { - return '[^/]+'; +import { getActiveSpan, getCurrentScope, getRootSpan, spanToJSON, updateSpanName } from '@sentry/core'; + +type RouteSegment = + | { kind: 'static'; value: string } + | { kind: 'param' | 'optional' | 'splat'; prefix: string; suffix: string }; + +interface ParsedRoutePattern { + segments: RouteSegment[]; + isIndex: boolean; + affixLength: number; +} + +interface MatchScore { + statics: number; + dynamics: number; + optionals: number; +} + +// e.g. `@{$groupSlug}`, `sitemap-{$page}.xml`, `{-$locale}`, `{$}` +const BRACED_SEGMENT_REGEX = /^(.*?)\{(-?)\$([^}]*)\}(.*)$/; + +const parsedRoutePatterns = new Map(); + +function parseRoutePattern(pattern: string): ParsedRoutePattern { + const cached = parsedRoutePatterns.get(pattern); + if (cached) { + return cached; + } + + const parts = pattern.split('/').filter(Boolean); + const segments: RouteSegment[] = []; + let affixLength = 0; + for (let i = 0; i < parts.length; i++) { + const part = parts[i] as string; + const braced = part.match(BRACED_SEGMENT_REGEX); + if (part === '$' || (braced && !braced[3] && !braced[2])) { + // A splat consumes the rest of the path, so anything after it in the pattern is part of its suffix. + const rest = parts.slice(i + 1).join('/'); + const prefix = braced?.[1] ?? ''; + const suffix = (braced?.[4] ?? '') + (rest ? `/${rest}` : ''); + segments.push({ kind: 'splat', prefix: prefix.toLowerCase(), suffix: suffix.toLowerCase() }); + affixLength += prefix.length + suffix.length; + break; + } + if (part.startsWith('$')) { + segments.push({ kind: 'param', prefix: '', suffix: '' }); + } else if (braced?.[3]) { + const [, prefix = '', optional, , suffix = ''] = braced; + segments.push({ + kind: optional ? 'optional' : 'param', + prefix: prefix.toLowerCase(), + suffix: suffix.toLowerCase(), + }); + affixLength += prefix.length + suffix.length; + } else { + segments.push({ kind: 'static', value: part.toLowerCase() }); + } + } + + const parsed = { segments, isIndex: pattern.endsWith('/'), affixLength }; + parsedRoutePatterns.set(pattern, parsed); + return parsed; +} + +function hasAffixes(value: string, prefix: string, suffix: string): boolean { + return value.startsWith(prefix) && value.endsWith(suffix) && value.length >= prefix.length + suffix.length; +} + +function isBetterScore(a: MatchScore, b: MatchScore): boolean { + if (a.statics !== b.statics) { + return a.statics > b.statics; + } + if (a.dynamics !== b.dynamics) { + return a.dynamics > b.dynamics; + } + return a.optionals > b.optionals; +} + +/** + * Returns the best score with which `segments` match the remaining path `parts`, or `undefined` if they don't match. + * Like TanStack Router, a path part matched by a static, param or optional segment adds to the respective score, + * weighted so that earlier parts outweigh all later ones. + */ +function scoreMatch( + segments: RouteSegment[], + parts: string[], + si: number, + pi: number, + score: MatchScore, +): MatchScore | undefined { + const segment = segments[si]; + if (!segment) { + return pi === parts.length ? score : undefined; + } + + const part = parts[pi]; + const weight = 2 ** (parts.length - pi - 1); + + switch (segment.kind) { + case 'static': + return part === segment.value + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, statics: score.statics + weight }) + : undefined; + case 'param': + return part !== undefined && hasAffixes(part, segment.prefix, segment.suffix) + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, dynamics: score.dynamics + weight }) + : undefined; + case 'optional': { + const skipped = scoreMatch(segments, parts, si + 1, pi, score); + const consumed = + part !== undefined && hasAffixes(part, segment.prefix, segment.suffix) + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, optionals: score.optionals + weight }) + : undefined; + if (!skipped || !consumed) { + return skipped || consumed; + } + return isBetterScore(consumed, skipped) ? consumed : skipped; + } + case 'splat': { + if (!segment.prefix && !segment.suffix) { + return score; } - return escapeStringForRegex(segment); - }) - .join('/'); - return new RegExp(`^${segments}$`); + return part !== undefined && hasAffixes(parts.slice(pi).join('/'), segment.prefix, segment.suffix) + ? score + : undefined; + } + } +} + +function isMoreSpecific( + a: { score: MatchScore; route: ParsedRoutePattern }, + b: { score: MatchScore; route: ParsedRoutePattern }, +): boolean { + if ( + a.score.statics !== b.score.statics || + a.score.dynamics !== b.score.dynamics || + a.score.optionals !== b.score.optionals + ) { + return isBetterScore(a.score, b.score); + } + if (a.route.isIndex !== b.route.isIndex) { + return a.route.isIndex; + } + if (a.route.segments.length !== b.route.segments.length) { + return a.route.segments.length > b.route.segments.length; + } + return a.route.affixLength > b.route.affixLength; } /** - * Matches a URL pathname against a list of TanStack Start route patterns. - * Patterns use `$param` syntax for dynamic segments (e.g., `/users/$id`). - * - * Patterns are expected to be pre-sorted by specificity (more segments first, static before dynamic). + * Matches a URL pathname against a list of TanStack Start route patterns (the `fullPaths` of the generated route tree). + * Supports `$param`, `prefix{$param}suffix`, optional `{-$param}` and splat (`$`, `{$}`) segments, as well as + * index routes (trailing slash). If several patterns match, the most specific one is picked the way TanStack Router + * picks it. */ export function matchUrlToRoutePattern(pathname: string, patterns: string[]): string | undefined { - const normalizedPathname = pathname.length > 1 ? pathname.replace(/\/$/, '') : pathname; + if (pathname === '/' && patterns.includes('/')) { + return '/'; + } + + // TanStack Router matches case-insensitively by default. + const parts = pathname.toLowerCase().split('/').filter(Boolean); + let best: { pattern: string; score: MatchScore; route: ParsedRoutePattern } | undefined; for (const pattern of patterns) { - if (patternToRegex(pattern).test(normalizedPathname)) { - return pattern; + const route = parseRoutePattern(pattern); + const score = scoreMatch(route.segments, parts, 0, 0, { statics: 0, dynamics: 0, optionals: 0 }); + if (score && (!best || isMoreSpecific({ score, route }, best))) { + best = { pattern, score, route }; } } - return undefined; + return best?.pattern; } /** diff --git a/packages/tanstackstart-react/src/server/utils.ts b/packages/tanstackstart-react/src/server/utils.ts index 5d58be067658..7f6793fc806b 100644 --- a/packages/tanstackstart-react/src/server/utils.ts +++ b/packages/tanstackstart-react/src/server/utils.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import type { StartSpanOptions } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; /** * Returns span options for TanStack Start middleware spans. @@ -10,7 +9,7 @@ export function getMiddlewareSpanOptions(name: string): StartSpanOptions { return { name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.tanstackstart', + [SENTRY_ORIGIN]: 'auto.middleware.tanstackstart', [SENTRY_OP]: MIDDLEWARE, }, }; diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 77029ebbf907..83f148a77771 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -1,7 +1,13 @@ import { getClient, getTraceMetaTags, hasSpanStreamingEnabled } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/node'; -import { CODE_FUNCTION_NAME, HTTP_REQUEST_METHOD, SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { captureException, startSpan } from '@sentry/node'; +import { + CODE_FUNCTION_NAME, + HTTP_REQUEST_METHOD, + SENTRY_DESCRIPTION, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { updateSpanWithRouteParametrization } from './routeParametrization'; @@ -154,7 +160,7 @@ export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry { // request path carries the generated server function id, which is high cardinality. name: hasSpanStreaming ? 'serverFn' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.tanstackstart.server', + [SENTRY_ORIGIN]: 'auto.function.tanstackstart.server', [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'serverFn', // The global function middleware renames this span and needs the method, which it diff --git a/packages/tanstackstart-react/src/vite/routePatterns.ts b/packages/tanstackstart-react/src/vite/routePatterns.ts index 9c90b863931c..5ffbdaa83c52 100644 --- a/packages/tanstackstart-react/src/vite/routePatterns.ts +++ b/packages/tanstackstart-react/src/vite/routePatterns.ts @@ -3,6 +3,8 @@ import * as path from 'node:path'; import { uniq } from '@sentry/core'; import type { Plugin } from 'vite'; +const ROUTE_TREE_FILE_NAME = 'routeTree.gen.ts'; + /** * Extracts route patterns from TanStack Start's generated routeTree.gen.ts * and replaces `__SENTRY_ROUTE_PATTERNS__` references with the extracted patterns. @@ -28,11 +30,10 @@ export function makeRoutePatternPlugin(): Plugin { return null; } - // extract the patterns from the route tree file - const routeTreePath = path.resolve(resolvedRoot, 'src/routeTree.gen.ts'); let patterns: string[] = []; try { - if (fs.existsSync(routeTreePath)) { + const routeTreePath = findRouteTreeFile(resolvedRoot); + if (routeTreePath) { patterns = extractRoutePatterns(fs.readFileSync(routeTreePath, 'utf-8')); } } catch { @@ -47,6 +48,31 @@ export function makeRoutePatternPlugin(): Plugin { }; } +/** + * Finds the generated route tree. TanStack Start writes it to `/routeTree.gen.ts`, and the + * `srcDirectory` option (default `src`) is not exposed by its Vite plugin, so we check `src` first and then + * every other top-level directory. + */ +export function findRouteTreeFile(root: string): string | undefined { + const defaultPath = path.join(root, 'src', ROUTE_TREE_FILE_NAME); + if (fs.existsSync(defaultPath)) { + return defaultPath; + } + + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) { + continue; + } + const candidate = path.join(root, entry.name, ROUTE_TREE_FILE_NAME); + if (fs.existsSync(candidate)) { + return candidate; + } + } + + const rootPath = path.join(root, ROUTE_TREE_FILE_NAME); + return fs.existsSync(rootPath) ? rootPath : undefined; +} + /** * Extracts full route path patterns from the content of routeTree.gen.ts. * @@ -69,14 +95,5 @@ export function extractRoutePatterns(content: string): string[] { } } - return uniq(patterns).sort((a, b) => { - const aSegments = a.split('/'); - const bSegments = b.split('/'); - if (bSegments.length !== aSegments.length) { - return bSegments.length - aSegments.length; - } - const aDynamic = aSegments.filter(s => s.startsWith('$')).length; - const bDynamic = bSegments.filter(s => s.startsWith('$')).length; - return aDynamic - bDynamic; - }); + return uniq(patterns); } diff --git a/packages/tanstackstart-react/test/client/sdk.test.ts b/packages/tanstackstart-react/test/client/sdk.test.ts index 0364ee45a6aa..2cb77601eae8 100644 --- a/packages/tanstackstart-react/test/client/sdk.test.ts +++ b/packages/tanstackstart-react/test/client/sdk.test.ts @@ -10,6 +10,7 @@ describe('TanStack Start React Client SDK', () => { beforeEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); + SentryReact.getCurrentScope().setClient(undefined); }); it('Adds TanStack Start React client metadata to the SDK options', () => { diff --git a/packages/tanstackstart-react/test/server/routeParametrization.test.ts b/packages/tanstackstart-react/test/server/routeParametrization.test.ts index 88fd2643dc9b..d5079cb1bc8c 100644 --- a/packages/tanstackstart-react/test/server/routeParametrization.test.ts +++ b/packages/tanstackstart-react/test/server/routeParametrization.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { matchUrlToRoutePattern } from '../../src/server/routeParametrization'; describe('matchUrlToRoutePattern', () => { - // Pre-sorted by specificity: more segments first, static before dynamic const patterns = ['/users/$userId/posts/$postId', '/api/health', '/page-a', '/page-b/$id', '/']; it('matches the root route', () => { @@ -29,13 +28,13 @@ describe('matchUrlToRoutePattern', () => { expect(matchUrlToRoutePattern('/page-b', patterns)).toBeUndefined(); }); - it('prefers static over dynamic matches when pre-sorted', () => { - const patternsWithOverlap = ['/page-b/special', '/page-b/$id']; + it('prefers static over dynamic matches', () => { + const patternsWithOverlap = ['/page-b/$id', '/page-b/special']; expect(matchUrlToRoutePattern('/page-b/special', patternsWithOverlap)).toBe('/page-b/special'); }); - it('prefers more specific routes when pre-sorted', () => { - const patternsNested = ['/users/$id/profile', '/users/$id']; + it('prefers more specific routes', () => { + const patternsNested = ['/users/$id', '/users/$id/profile']; expect(matchUrlToRoutePattern('/users/123/profile', patternsNested)).toBe('/users/$id/profile'); }); @@ -43,4 +42,96 @@ describe('matchUrlToRoutePattern', () => { expect(matchUrlToRoutePattern('/page-a/', patterns)).toBe('/page-a'); expect(matchUrlToRoutePattern('/page-b/42/', patterns)).toBe('/page-b/$id'); }); + + describe('TanStack Router segment grammar', () => { + // `fullPaths` of a route tree generated by @tanstack/router-generator, in generator order. + // Expected matches were checked against `router.matchRoutes()` of @tanstack/react-router. + const routeTreePatterns = [ + '/', + '/$user', + '/@{$groupSlug}', + '/about', + '/blog/latest', + '/blog/{-$tag}', + '/files', + '/posts', + '/pre-{$id}-post', + '/shop/items', + '/sitemap-{$page}.xml', + '/dashboard', + '/docs/', + '/docs/{$}', + '/files/$', + '/files/special', + '/groups/$groupId', + '/posts/$postId', + '/{-$locale}/about', + '/artists/', + '/@{$groupSlug}/covers/$coverId', + '/a/$x/c', + '/a/b/$y', + '/shop/{-$cat}/items', + '/groups/$groupId/', + ]; + + it.each([ + ['/', '/'], + ['/artists', '/artists/'], + ['/artists/', '/artists/'], + ['/ARTISTS', '/artists/'], + ['/groups/abc', '/groups/$groupId/'], + ['/groups/abc/', '/groups/$groupId/'], + ['/@some-group', '/@{$groupSlug}'], + ['/@some-group/covers/c1', '/@{$groupSlug}/covers/$coverId'], + ['/sitemap-3.xml', '/sitemap-{$page}.xml'], + ['/sitemap-.xml', '/sitemap-{$page}.xml'], + ['/pre-1-post', '/pre-{$id}-post'], + ['/about', '/{-$locale}/about'], + ['/en/about', '/{-$locale}/about'], + ['/shop/items', '/shop/{-$cat}/items'], + ['/shop/toys/items', '/shop/{-$cat}/items'], + ['/blog/latest', '/blog/latest'], + ['/blog/foo', '/blog/{-$tag}'], + ['/files', '/files/$'], + ['/files/a/b/c', '/files/$'], + ['/files/special', '/files/special'], + ['/docs', '/docs/'], + ['/docs/x/y', '/docs/{$}'], + ['/a/b/c', '/a/b/$y'], + ['/a/z/c', '/a/$x/c'], + ['/john', '/$user'], + ['/posts/1', '/posts/$postId'], + ['/dashboard', '/dashboard'], + ])('matches %s to %s', (pathname, expected) => { + expect(matchUrlToRoutePattern(pathname, routeTreePatterns)).toBe(expected); + }); + + it('does not match paths no route covers', () => { + expect(matchUrlToRoutePattern('/nope/nope/nope', routeTreePatterns)).toBeUndefined(); + }); + + it.each([ + ['/', '/'], + ['/about', '/about'], + ['/pricing', '/pricing'], + ['/en', '/{-$locale}/$slug'], + ['/hello', '/{-$locale}/$slug'], + ['/en/hello', '/{-$locale}/$slug'], + ['/posts', '/posts/'], + ['/posts/new', '/posts/new'], + ['/posts/1', '/posts/$postId'], + ])('prefers static over param over optional segments when matching %s', (pathname, expected) => { + const patterns = [ + '/', + '/about', + '/pricing', + '/posts/$postId', + '/posts/new', + '/{-$locale}/$slug', + '/posts/', + '/{-$locale}/', + ]; + expect(matchUrlToRoutePattern(pathname, patterns)).toBe(expected); + }); + }); }); diff --git a/packages/tanstackstart-react/test/server/sdk.test.ts b/packages/tanstackstart-react/test/server/sdk.test.ts index 889c43fff513..286058bbf512 100644 --- a/packages/tanstackstart-react/test/server/sdk.test.ts +++ b/packages/tanstackstart-react/test/server/sdk.test.ts @@ -9,6 +9,7 @@ describe('TanStack Start React Server SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + SentryNode.getCurrentScope().setClient(undefined); }); it('Adds TanStack Start React server metadata to the SDK options', () => { diff --git a/packages/tanstackstart-react/test/vite/routePatterns.test.ts b/packages/tanstackstart-react/test/vite/routePatterns.test.ts index 07b1c2803bff..413b29adafe1 100644 --- a/packages/tanstackstart-react/test/vite/routePatterns.test.ts +++ b/packages/tanstackstart-react/test/vite/routePatterns.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, it } from 'vitest'; -import { extractRoutePatterns } from '../../src/vite/routePatterns'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { extractRoutePatterns, findRouteTreeFile } from '../../src/vite/routePatterns'; describe('extractRoutePatterns', () => { it('extracts full path patterns from single-line fullPaths union', () => { @@ -57,17 +60,6 @@ export interface FileRouteTypes { expect(patterns).toContain('/users/$userId/posts/$postId'); }); - it('sorts patterns by specificity: more segments first, static before dynamic', () => { - const content = ` -export interface FileRouteTypes { - fullPaths: '/' | '/page-b/$id' | '/page-b/special' | '/users/$id/profile' | '/users/$id' - fileRoutesByTo: FileRoutesByTo -} -`; - const patterns = extractRoutePatterns(content); - expect(patterns).toEqual(['/users/$id/profile', '/page-b/special', '/page-b/$id', '/users/$id', '/']); - }); - it('handles double-quoted paths (quoteStyle: "double")', () => { const content = ` export interface FileRouteTypes { @@ -92,3 +84,45 @@ export interface FileRouteTypes { expect(patterns.filter(p => p === '/page-a')).toHaveLength(1); }); }); + +describe('findRouteTreeFile', () => { + let root: string; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-tanstackstart-route-tree-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + function writeRouteTree(dir: string): string { + const filePath = path.join(root, dir, 'routeTree.gen.ts'); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, ''); + return filePath; + } + + it('finds the route tree in the default `src` directory', () => { + const expected = writeRouteTree('src'); + writeRouteTree('app'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('finds the route tree in a custom `srcDirectory`', () => { + fs.mkdirSync(path.join(root, 'src')); + const expected = writeRouteTree('app'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('finds the route tree in the project root', () => { + const expected = writeRouteTree('.'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('ignores node_modules and dot directories', () => { + writeRouteTree('node_modules'); + writeRouteTree('.output'); + expect(findRouteTreeFile(root)).toBeUndefined(); + }); +}); diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 30d4a428eefb..abe436f67705 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -83,7 +83,9 @@ export { instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 70503ad97164..ba3c4ce71830 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,6 +1,7 @@ import { diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; import type { Client, Integration } from '@sentry/core'; import { + _INTERNAL_warnIfClientIsActive, consoleIntegration, conversationIdIntegration, createStackParser, @@ -80,6 +81,8 @@ export function init(options: VercelEdgeOptions = {}): Client { options.traceLifecycle = options.traceLifecycle ?? getTraceLifecycleFromEnv(process.env.SENTRY_TRACE_LIFECYCLE); + _INTERNAL_warnIfClientIsActive(); + const client = new VercelEdgeClient({ ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), diff --git a/packages/vercel-edge/test/sdk.test.ts b/packages/vercel-edge/test/sdk.test.ts index dbb5fd9e02a4..f9c69cdac777 100644 --- a/packages/vercel-edge/test/sdk.test.ts +++ b/packages/vercel-edge/test/sdk.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, it } from 'vitest'; -import { getDefaultIntegrations, init, spanStreamingIntegration } from '../src'; -import { type Event, type Integration } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { close, getClient, getCurrentScope, getDefaultIntegrations, init, spanStreamingIntegration } from '../src'; +import { type Event, type Integration, originalConsoleMethods } from '@sentry/core'; + +afterEach(() => { + getCurrentScope().setClient(undefined); +}); describe('getDefaultIntegrations', () => { it('includes request data collection by default', () => { @@ -74,4 +78,43 @@ describe('init', () => { expect(integrations?.length).toBe(1); expect((integrations?.[0] as MarkedIntegration)?._custom).toBe(true); }); + + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + it('warns and replaces the active client', () => { + const first = init({ enableOpenTelemetrySetup: false }); + const second = init({ enableOpenTelemetrySetup: false }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + it('does not warn after close()', async () => { + init({ enableOpenTelemetrySetup: false }); + await close(); + const second = init({ enableOpenTelemetrySetup: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + }); }); diff --git a/packages/vue/package.json b/packages/vue/package.json index 9ebf49932e7b..612745d9fb0b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -46,7 +46,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "@tanstack/vue-router": "^1.64.0", diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 6e8db5868f85..3f28ec5259b4 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -6,6 +6,7 @@ import { SENTRY_OP, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core'; @@ -17,7 +18,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; @@ -134,7 +134,7 @@ export function instrumentVueRouter( // This will override the origin, and add params & query attributes activePageLoadSpan.setAttributes({ ...attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', }); hasHandledFirstPageLoad = true; @@ -153,7 +153,7 @@ export function instrumentVueRouter( attributes: { ...attributes, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: transactionSource, }, }, diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts index ed2c659c76f9..f485c6cb3fd3 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -13,6 +13,7 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Integration } from '@sentry/core'; @@ -20,7 +21,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; import type { AnyRouter } from '@tanstack/vue-router'; @@ -103,7 +103,7 @@ export function tanstackRouterBrowserTracingIntegration( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -163,7 +163,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index a6084cd58ff0..822ef2909a45 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,4 @@ -import { getActiveSpan, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; +import { getActiveSpan, getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { debug, @@ -10,7 +10,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, uniq, } from '@sentry/core'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_UNMOUNT, UI_UPDATE } from '@sentry/conventions/op'; import { DEFAULT_HOOKS, DEFAULT_ROOT_SPAN_TIMEOUT } from './constants'; import { DEBUG_BUILD } from './debug-build'; @@ -145,7 +145,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi name: hasSpanStreaming ? conventionComponentName || UI_RENDER_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + [SENTRY_ORIGIN]: 'auto.ui.vue', ...(conventionComponentName && { [UI_COMPONENT_NAME]: conventionComponentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -196,7 +196,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi : description, attributes: { [SENTRY_OP]: VUE_OPERATION_TO_SPAN_OP[operation], - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + [SENTRY_ORIGIN]: 'auto.ui.vue', ...(conventionComponentName && { [UI_COMPONENT_NAME]: conventionComponentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/vue/test/integration/VueIntegration.test.ts b/packages/vue/test/integration/VueIntegration.test.ts index 6197beeb2d67..3b8180b7a93c 100644 --- a/packages/vue/test/integration/VueIntegration.test.ts +++ b/packages/vue/test/integration/VueIntegration.test.ts @@ -45,6 +45,7 @@ describe('Sentry.VueIntegration', () => { }); afterEach(() => { + Sentry.getCurrentScope().setClient(undefined); vi.resetAllMocks(); }); diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index 6a596588335f..20793b05c5fa 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -21,6 +21,7 @@ describe('Sentry.init', () => { }); afterEach(() => { + Sentry.getCurrentScope().setClient(undefined); vi.clearAllMocks(); }); diff --git a/packages/vue/test/integration/mixinRegistration.test.ts b/packages/vue/test/integration/mixinRegistration.test.ts index 90abef41e74b..c9dcf4942c02 100644 --- a/packages/vue/test/integration/mixinRegistration.test.ts +++ b/packages/vue/test/integration/mixinRegistration.test.ts @@ -130,6 +130,7 @@ const it = baseIt.extend({ } }); }); + Sentry.getCurrentScope().setClient(undefined); }, }); /* oxlint-enable no-empty-pattern */ diff --git a/packages/vue/test/integration/normalize.test.ts b/packages/vue/test/integration/normalize.test.ts index 3fdedb5433a9..90764e810960 100644 --- a/packages/vue/test/integration/normalize.test.ts +++ b/packages/vue/test/integration/normalize.test.ts @@ -26,6 +26,7 @@ describe('@sentry/vue init() normalize stringifier', () => { afterEach(() => { // Reset the registered stringifier so subsequent test files start clean. setNormalizeStringifier(undefined); + Sentry.getCurrentScope().setClient(undefined); }); it("collapses Vue 3 component instances (`__isVue`) to '[VueViewModel]'", () => { diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 94aed649b7e8..bd0972bfcd09 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,8 +1,12 @@ import * as SentryBrowser from '@sentry/browser'; import type { Span, SpanAttributes } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, ROUTER_NAVIGATION_ROUTE_ID, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + ROUTER_NAVIGATION_ROUTE_ID, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Route } from '../src/router'; import { instrumentVueRouter } from '../src/router'; @@ -127,7 +131,7 @@ describe('instrumentVueRouter()', () => { name: transactionName, attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: transactionSource, ...getAttributesForRoute(to, transactionSource === 'route' ? transactionName : undefined), }, @@ -179,7 +183,7 @@ describe('instrumentVueRouter()', () => { expect(mockRootSpan.updateName).toHaveBeenCalledWith(transactionName); expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SENTRY_SEGMENT_NAME_SOURCE, transactionSource); expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', ...getAttributesForRoute(to, transactionSource === 'route' ? transactionName : undefined), }); }, @@ -207,7 +211,7 @@ describe('instrumentVueRouter()', () => { name: '/login', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', ...getAttributesForRoute(to, '/login'), }, @@ -238,7 +242,7 @@ describe('instrumentVueRouter()', () => { name: 'login-screen', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', ...getAttributesForRoute(to), }, @@ -298,7 +302,7 @@ describe('instrumentVueRouter()', () => { expect(mockRootSpan.updateName).not.toHaveBeenCalled(); expect(mockRootSpan.setAttribute).not.toHaveBeenCalled(); expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', ...getAttributesForRoute(to, '/books/:bookId/chapter/:chapterId'), }); expect(mockRootSpan.name).toEqual('customTxnName'); @@ -471,7 +475,7 @@ describe('instrumentVueRouter()', () => { name: 'Navigation', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', ...getAttributesForRoute(to), }, diff --git a/packages/vue/test/tanstackrouter.test.ts b/packages/vue/test/tanstackrouter.test.ts index 593d4da842ef..54384e81908c 100644 --- a/packages/vue/test/tanstackrouter.test.ts +++ b/packages/vue/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { AnyRouter } from '@tanstack/vue-router'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -119,7 +118,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/test/:id', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path.parameter.id': '123', 'params.id': '123', @@ -292,7 +291,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { { name: '/test/:id', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }), }, diff --git a/yarn.lock b/yarn.lock index ef776d7290d0..c07d28325713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6132,55 +6132,55 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@nx/nx-darwin-arm64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.7.tgz#50576085d00470762e0fc40f51d1ba6fd56c6d24" - integrity sha512-HGu8Bc3DzmIFeKHzIEDBDScgI8/hOjVIj/EzJBE5289dUf+CQ5OSXc6kw2NL9SiCMAPKa0clqcJgLJLqP5t/DQ== - -"@nx/nx-darwin-x64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.7.tgz#05255685f6b7e80d4b490a1e207c150413b275d0" - integrity sha512-J7z/Hh+bEKB9xvHdmtO5sK7YytFx8Ry6YpvYI2PoSl2/xDBca7q69fzHKTtm4S/8FvA1Qoi/m//JTIMQUB7cKA== - -"@nx/nx-freebsd-x64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.7.tgz#0917685ae21512ddcdb26bd7f7107ffbd7964622" - integrity sha512-9H4bbI3fBr8Ct+6xmF6dqDKEvER60XV3tanXTSVtUBxyAVw3cPmRh7aMAu5PNyN+ewzRAJvV6KI4Rx1kA+s9jw== - -"@nx/nx-linux-arm-gnueabihf@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.7.tgz#6f925157c87f4bfcc83d62dac2c09ad1eec0c676" - integrity sha512-QsGaaZ7PI18c7rFgvUe0F0UYa5oErSKY6Yff6HVXy2zAAWluM4F/TXIRYbtXlvSq++CHhFXxk29Xb3+/zGR/rg== - -"@nx/nx-linux-arm64-gnu@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.7.tgz#4955e8b86b6dab0e7f3bf25647408ea2a5b4079f" - integrity sha512-CfzWaS+b32lI/inlYPv1ZAK9CWTWFHzT7TPThvOHML65nrgFl8cQ4Z4FeGdyCbHu2NoG3vR1E36O2tPI2/DLGg== - -"@nx/nx-linux-arm64-musl@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.7.tgz#c22c05e1822a3389e11b14967acfa92871634f3a" - integrity sha512-53QFuODMEHc1gobCT2WOmYz89DZtEIuLphirdIgnXkkPBTdrP77LPbJUXMRXCMKr90BQaSWhTX+J/ubANRE8og== - -"@nx/nx-linux-x64-gnu@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.7.tgz#d5887573e7c6c0f9a51010d75001b31607644bc6" - integrity sha512-unzmqGIDXGzujo1ZtbrMj2e5D5ldQ1feZmqDPhvO4casK2d96jpT8LtgIILpVDUfhLjl+By185gb0ArrWTsyKw== - -"@nx/nx-linux-x64-musl@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.7.tgz#2f1c357f5c479a0785178469955ffac231b8cd6f" - integrity sha512-oTjMGuM105ok8aH5rlg2YP0Kgkjg0VfUj1exwp49S70gGBJ0r8v5rEtJwOSdCf1WTHuEh0xi66Y/b1S0khF8dg== - -"@nx/nx-win32-arm64-msvc@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.7.tgz#4740d3151fa52dcb55eb4c1b3bbed88c78b9eb96" - integrity sha512-K741meG/l48TPPeDqnhYTV7pP+1ljjZ+0DRJBxMrPFIu8qKE3Abko/7NKWmWRjcSXJvtxvYkgG3wZds4N2Bhow== - -"@nx/nx-win32-x64-msvc@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.7.tgz#87cc09dc269e3582065d8cdd78741f53af345f0e" - integrity sha512-AHVjXkreXjVKAKpMNCsF5nQC/et6lpQcERRm8AYOCncI59hTjYOLpdCMBLrIHA3hgc6SUYU8n5+tMAbv7zXXiQ== +"@nx/nx-darwin-arm64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.10.tgz#f3ade8546f98faf83b05d92f0dedfa7f831658ab" + integrity sha512-gy0ASfsnFsMmTLQaGTQyf8l95kAgawBpdMrgtxsnXzyKAasqzfIEhEbb74/6+DmsmcnNyfBvAKvVvFBiNs3ZYw== + +"@nx/nx-darwin-x64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.10.tgz#8c4129e946d15919ee93106c87658f7ee850b2d2" + integrity sha512-IGNSVU85ktrNm9cH9yLgSaQjmRVCcEldaZwtAqNaMcNU+Go5xY4zYv8tDQ2MJ3hXpi6pNdcFzZp8tZIJliHyPA== + +"@nx/nx-freebsd-x64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.10.tgz#d2e6bf8adc3e156e0a5a98d3fe22aeb40e378a5d" + integrity sha512-KlmBmvD8uE7Saah3DO61D7ywn08FtYJE0I0AwZGs+0cisY1bUawtZZNYvbPTprHaCOWvbU2GeIVstK34vmbVXg== + +"@nx/nx-linux-arm-gnueabihf@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.10.tgz#e395493ffff3e1702b8b14b99d84e92842fb7539" + integrity sha512-xwCv9gR968hMeeWUqBneLR9nBqP+ECPVFumdWCxMBh116ftCjMMik8E3BdUKLdqreVYFJdwvJRPortHj/YYikA== + +"@nx/nx-linux-arm64-gnu@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.10.tgz#e55aceb9352e32eda3c1adfb8c89172a8b463000" + integrity sha512-AiwBgerLicRzcyEevqwDuP22Nj5nFFrjJsCeZ+D4rC2HmS4LdBzXb+BBuHAZTysCo9QClk1t9VuVwwKyorU7ZA== + +"@nx/nx-linux-arm64-musl@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.10.tgz#6f3cbefbe733a09cbec4d72e6f924e56c66ff1ed" + integrity sha512-8GNi8v6Ej/TyAaVBiFRAwFYrqdje/dyFbPzpq895m3ow8zbPrYmoz/qizrgkRzBFulgkSxu33LXi388reVLi2w== + +"@nx/nx-linux-x64-gnu@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.10.tgz#b2eb03ee33fcc53a05cecda7cd77c745def42f92" + integrity sha512-spcDvoQK/mrSvnHFdophlwgzUoa8lYXAZTk15+w/gLpaowP5hR4mInhUWsU9VuEoe41J3x+FcFwmw01Djj6KNQ== + +"@nx/nx-linux-x64-musl@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.10.tgz#50a2da0722cb641d76bf812a2079c0fa1716660a" + integrity sha512-76PdpGIQ1PVA77sYM4Ons779Yx7y3H+Hlsax2HhifAv8dKhAz9/Lmhst9ZqS2zU5v03lGEnN77W4KZQHJBiltA== + +"@nx/nx-win32-arm64-msvc@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.10.tgz#92802c879a43bda1c986c74136a54653675d8ab8" + integrity sha512-dARAojqVyvE88Mt1ms6+UTb3TIAoo4xzkPaX05Qcmh/c8+SQgA24JuqiRp+4kRz1voYwFexRpWpcrfpKJ0Nr+Q== + +"@nx/nx-win32-x64-msvc@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.10.tgz#378b6f7b2f078ccc91d83573af14e65722dcae06" + integrity sha512-qlH70TBMrRw/5pMg6MtPXkuM3LzDHszpqWcJ4NPdipLsyD6T+B+NteanlPtxZrdfqi+8Hd/7fv2OkllA8iY1fg== "@octokit/auth-token@^2.4.4": version "2.5.0" @@ -7988,10 +7988,10 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry/conventions@0.25.0", "@sentry/conventions@^0.25.0": - version "0.25.0" - resolved "https://sfw.security.sentry.io/npm/@sentry/conventions/-/conventions-0.25.0.tgz#62c276fe8c608e533a42f0dba2ebc58c62b97b14" - integrity sha512-7SouCAX3qx+AzLsxb6gbl3dKVWNhp2wFjdYQ/T90sGwqPPQUoZVA9YwsWwvx07hqcCiSqFW9mUAzyUftASnuhQ== +"@sentry/conventions@0.26.0", "@sentry/conventions@^0.26.0": + version "0.26.0" + resolved "https://sfw.security.sentry.io/npm/@sentry/conventions/-/conventions-0.26.0.tgz#3da6c91e1c4e570914b998f8b4f97ab7e5144a23" + integrity sha512-ttSRcoLf2kIENDIFzH12aJaVMdxnidMYLvVNwEX/JjXfXaQot8u5M47QmgAFOOhVJ4nQ2uU8wTCh+fwSPrrCfg== "@sentry/node-cpu-profiler@^2.4.4": version "2.4.4" @@ -8009,38 +8009,38 @@ detect-libc "^2.0.4" node-abi "^3.89.0" -"@sentry/rrdom@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrdom/-/rrdom-2.43.2.tgz#24e20c5f9ffb085af9527fb5dbbcf30536322e8b" - integrity sha512-Tvb7TEGvGSmNEufxbMh1vyYq9HFC6kGhsTtdHJh6uBK1YEi09ybNqx7lPLk5PmJWsEsvdFu/Ef2Io+FSyXPq7Q== +"@sentry/rrdom@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrdom/-/rrdom-2.44.1.tgz#b7d9281ecc14a90e6dc0fc49bf75df31d7b41319" + integrity sha512-1w4FmGe1ddSOl7Qsu30f+SI2YdQSyEJ5Oo2zR8ymzCRJ0gaC8ZpkusGQe6tSLX3IwKL9DNLrEM4zO031+9BLtA== dependencies: - "@sentry/rrweb-snapshot" "2.43.2" + "@sentry/rrweb-snapshot" "2.44.1" -"@sentry/rrweb-snapshot@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb-snapshot/-/rrweb-snapshot-2.43.2.tgz#d0869a70d287f6adb20bc2838eb0e8a8229db39e" - integrity sha512-O8sC8pxiSo4QIEcPSYKyvNHBenlkNvitGUrku1yesVUhLAlF00WBf/9iHjB5EeAGFdVv9st3ObJpiF5q/+AKVw== +"@sentry/rrweb-snapshot@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb-snapshot/-/rrweb-snapshot-2.44.1.tgz#d1ca7d1af8e7cc4be7fbbb973c0b3a1f02bb0181" + integrity sha512-pMrOrncMfbN2gLe8IIGK6ReMThbNeCcnAxVff1EolxGHW6ry8qhVgFYhURZpDZuWLjiDOVAEcCFz/j3XTzMBGQ== -"@sentry/rrweb-types@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb-types/-/rrweb-types-2.43.2.tgz#e5de7070377f9da612204c1b633c93ea4505ef4e" - integrity sha512-xp5ogZmC+YnodPj/F9GqWRG+B1MiwfphNXjLYNE7nCr7lZpP1Iaoom7ctmu75SOP6UkR3YSS4jCW91EJ3UEjiw== +"@sentry/rrweb-types@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb-types/-/rrweb-types-2.44.1.tgz#9b79811dc22850f484f327764a26ed4e554dfc07" + integrity sha512-qsDPN5pfR1lswCrEpGKoQp5MNC6bpta/Fi/WpS41vpV6hCI7IWB9GYzD4PDgfERslGihY3S35v+wnanktsFgaw== dependencies: - "@sentry/rrweb-snapshot" "2.43.2" + "@sentry/rrweb-snapshot" "2.44.1" "@types/css-font-loading-module" "0.0.7" -"@sentry/rrweb@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb/-/rrweb-2.43.2.tgz#cb92f3a30888834a2fb4ae84b93475814efa7050" - integrity sha512-/xJWPOO0zQoQe7+tUshv0OHCA0aGLcaQsS/qWsxsnmJzI9zOYfFHp22/xakmzaY6XLnAhCITe0wcI4HljItoVA== +"@sentry/rrweb@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb/-/rrweb-2.44.1.tgz#8c7a1ba0606e81c5892779f976c336d5a6073221" + integrity sha512-DsxmVOlrsvSUgdbYELTtgP9zSan96NuZBM+kx85itij+/0ADBe6i2Mx2MLKQCMExenhnoZhTzf0ksZY0fkEymg== dependencies: - "@sentry/rrdom" "2.43.2" - "@sentry/rrweb-snapshot" "2.43.2" - "@sentry/rrweb-types" "2.43.2" + "@sentry/rrdom" "2.44.1" + "@sentry/rrweb-snapshot" "2.44.1" + "@sentry/rrweb-types" "2.44.1" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" - fflate "^0.4.4" + fflate "^0.4.9" mitt "^3.0.0" "@shikijs/core@1.29.2": @@ -10874,7 +10874,7 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -agent-base@6, agent-base@^6.0.2: +agent-base@6, agent-base@6.0.2, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -11546,13 +11546,14 @@ aws-ssl-profiles@^1.1.2: resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== -axios@1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.0.tgz#f8e5dd931cef2a5f8c32216d5784eda2f8750eb7" - integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== +axios@1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe" + integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g== dependencies: follow-redirects "^1.16.0" form-data "^4.0.5" + https-proxy-agent "^5.0.1" proxy-from-env "^2.1.0" axios@1.20.0: @@ -12010,10 +12011,10 @@ boxen@8.0.1, boxen@^8.0.1: widest-line "^5.0.0" wrap-ansi "^9.0.0" -brace-expansion@5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" - integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== +brace-expansion@5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.8.tgz#135ad0d8d808eb18eb5e0ec9a21f3a0b92ef18cf" + integrity sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg== dependencies: balanced-match "^4.0.2" @@ -13005,13 +13006,14 @@ compressible@~2.0.18: mime-db ">= 1.43.0 < 2" compression@^1.7.4: - version "1.8.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" - integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + version "1.8.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.2.tgz#781397127ae2da00c0c5f7c76bda5b2a9b32a49f" + integrity sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ== dependencies: bytes "3.1.2" compressible "~2.0.18" debug "2.6.9" + destroy "1.2.0" negotiator "~0.6.4" on-headers "~1.1.0" safe-buffer "5.2.1" @@ -13362,16 +13364,16 @@ css-select@^4.1.3, css-select@^4.2.0: domutils "^2.8.0" nth-check "^2.0.1" -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== +css-select@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-6.0.0.tgz#7e63f09881ad118084091048ed543786dad96644" + integrity sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw== dependencies: boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" + css-what "^7.0.0" + domhandler "^5.0.3" + domutils "^3.2.2" + nth-check "^2.1.1" css-tree@^2.3.1: version "2.3.1" @@ -13397,11 +13399,16 @@ css-tree@~2.2.0: mdn-data "2.0.28" source-map-js "^1.0.1" -css-what@^6.0.1, css-what@^6.1.0: +css-what@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css-what@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-7.0.0.tgz#5796fbebd43571d73c60ba0dd7a6e75dd0d22fe4" + integrity sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ== + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -14054,10 +14061,10 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== +domutils@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -15823,10 +15830,10 @@ fflate@0.8.3: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== -fflate@^0.4.4: - version "0.4.8" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" - integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== +fflate@^0.4.9: + version "0.4.9" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.9.tgz#b5d1fd9776e4d3cbcd1a83a1f163b984a0972e21" + integrity sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw== figures@3.2.0, figures@^3.0.0: version "3.2.0" @@ -18237,17 +18244,17 @@ js-tokens@^10.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0" - integrity sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag== + version "3.15.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.2.tgz#3f83823ac6be17f570f23b2ecdef3777ff5ea364" + integrity sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w== dependencies: argparse "^1.0.7" esprima "^4.0.0" js-yaml@^4.1.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848" - integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== dependencies: argparse "^2.0.1" @@ -20962,7 +20969,7 @@ npmlog@^6.0.0: gauge "^4.0.3" set-blocking "^2.0.0" -nth-check@^2.0.1: +nth-check@^2.0.1, nth-check@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== @@ -21042,10 +21049,10 @@ nwsapi@^2.2.12, nwsapi@^2.2.16, nwsapi@^2.2.4: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.24.tgz#f8927043d4c9b516abdebe804a32c8d1f9484d1f" integrity sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A== -nx@22.7.7: - version "22.7.7" - resolved "https://registry.yarnpkg.com/nx/-/nx-22.7.7.tgz#b93dd707a3f0d966012aa62ad0323e43de2c47e6" - integrity sha512-T4hq5PMAPXeqAmm0Y6Gr5ApeI2I+T8MflWEKN1cZ12R1Rnq2uEpfOgiwAZ74bHONmNgv6LRG9P9+x8SX2tDIlw== +nx@22.7.10: + version "22.7.10" + resolved "https://registry.yarnpkg.com/nx/-/nx-22.7.10.tgz#246bfc3c23741314636ae3f6dea6c092f8dcbfbe" + integrity sha512-T22WdfbkRi9cLH8a+FkKsNrt540DpNkzkFv1StSsphfu1povyqoVQw9Rpl/poNvnAICNKPwZcpGsy/QXMYkECw== dependencies: "@emnapi/core" "1.4.5" "@emnapi/runtime" "1.4.5" @@ -21055,16 +21062,17 @@ nx@22.7.7: "@tybys/wasm-util" "0.9.0" "@yarnpkg/lockfile" "1.1.0" "@zkochan/js-yaml" "0.0.7" + agent-base "6.0.2" ansi-colors "4.1.3" ansi-regex "5.0.1" ansi-styles "4.3.0" argparse "2.0.1" asynckit "0.4.0" - axios "1.16.0" + axios "1.18.1" balanced-match "4.0.3" base64-js "1.5.1" bl "4.1.0" - brace-expansion "5.0.6" + brace-expansion "5.0.8" buffer "5.7.1" call-bind-apply-helpers "1.0.2" chalk "4.1.2" @@ -21075,6 +21083,7 @@ nx@22.7.7: color-convert "2.0.1" color-name "1.1.4" combined-stream "1.0.8" + debug "4.4.3" defaults "1.0.4" define-lazy-prop "2.0.0" delayed-stream "1.0.0" @@ -21105,6 +21114,7 @@ nx@22.7.7: has-symbols "1.1.0" has-tostringtag "1.0.2" hasown "2.0.4" + https-proxy-agent "5.0.1" ieee754 "1.2.1" ignore "7.0.5" inherits "2.0.4" @@ -21123,6 +21133,7 @@ nx@22.7.7: mimic-fn "2.1.0" minimatch "10.2.5" minimist "1.2.8" + ms "2.1.3" npm-run-path "4.0.1" once "1.4.0" onetime "5.1.2" @@ -21158,16 +21169,16 @@ nx@22.7.7: yargs "17.7.2" yargs-parser "21.1.1" optionalDependencies: - "@nx/nx-darwin-arm64" "22.7.7" - "@nx/nx-darwin-x64" "22.7.7" - "@nx/nx-freebsd-x64" "22.7.7" - "@nx/nx-linux-arm-gnueabihf" "22.7.7" - "@nx/nx-linux-arm64-gnu" "22.7.7" - "@nx/nx-linux-arm64-musl" "22.7.7" - "@nx/nx-linux-x64-gnu" "22.7.7" - "@nx/nx-linux-x64-musl" "22.7.7" - "@nx/nx-win32-arm64-msvc" "22.7.7" - "@nx/nx-win32-x64-msvc" "22.7.7" + "@nx/nx-darwin-arm64" "22.7.10" + "@nx/nx-darwin-x64" "22.7.10" + "@nx/nx-freebsd-x64" "22.7.10" + "@nx/nx-linux-arm-gnueabihf" "22.7.10" + "@nx/nx-linux-arm64-gnu" "22.7.10" + "@nx/nx-linux-arm64-musl" "22.7.10" + "@nx/nx-linux-x64-gnu" "22.7.10" + "@nx/nx-linux-x64-musl" "22.7.10" + "@nx/nx-win32-arm64-msvc" "22.7.10" + "@nx/nx-win32-x64-msvc" "22.7.10" nypm@^0.6.0, nypm@^0.6.8, nypm@^0.6.9: version "0.6.9" @@ -23208,9 +23219,9 @@ property-information@^7.0.0: integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== proxy-addr@^2.0.7, proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932" + integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ== dependencies: forwarded "0.2.0" ipaddr.js "1.9.1" @@ -24568,10 +24579,10 @@ sass@^1.49.9: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sax@^1.2.4, sax@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.5.0.tgz#b5549b671069b7aa392df55ec7574cf411179eb8" - integrity sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA== +sax@1.6.1, sax@^1.2.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.1.tgz#4c23cf608c0b693ab54b4b5888e92cfe977b9843" + integrity sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q== sax@~1.2.4: version "1.2.4" @@ -24761,9 +24772,9 @@ serialize-javascript@^7.0.3: integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw== seroval-plugins@^1.4.0, seroval-plugins@^1.5.0, seroval-plugins@^1.5.4: - version "1.6.2" - resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.6.2.tgz#8e49b47fd585c2025f74f84f78272ea1753a8daa" - integrity sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ== + version "1.6.8" + resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.6.8.tgz#3187b90331153472bdba83da5be63912e14cd2ac" + integrity sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g== seroval-plugins@~1.5.0: version "1.5.4" @@ -24771,9 +24782,9 @@ seroval-plugins@~1.5.0: integrity sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw== seroval@^1.4.0, seroval@^1.5.0, seroval@^1.5.4, seroval@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.6.2.tgz#93ecff62ca1312a565e37146b2a332b9d625194d" - integrity sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ== + version "1.6.8" + resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.6.8.tgz#916c4a293cfadcd9e0efdb73a8bfbe061f81866b" + integrity sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg== seroval@~1.5.0: version "1.5.4" @@ -25003,9 +25014,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3, shell-quote@^1.8.4: - version "1.10.0" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.10.0.tgz#482033e192e4f5c07151521ffa03400ec71b1b0f" - integrity sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA== + version "1.12.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.12.0.tgz#e98fdfd2e921d8a58ae2820f4fc0f75c39f09675" + integrity sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q== shiki@^1.22.0, shiki@^1.23.1, shiki@^1.26.1: version "1.29.2" @@ -25254,9 +25265,9 @@ sorted-array-functions@^1.3.0: integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab" + integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== source-map-loader@4.0.0: version "4.0.0" @@ -25901,17 +25912,17 @@ svelte@^4.2.8: periscopic "^3.1.0" svgo@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.0.2.tgz#a62246f0a9d671c0314d04f3cc15f78b1bd0667f" - integrity sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng== + version "4.1.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.1.0.tgz#c977eb69640ef232a5e8236a5a327ed8c9d52853" + integrity sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q== dependencies: commander "^11.1.0" - css-select "^5.1.0" + css-select "^6.0.0" css-tree "^3.0.1" - css-what "^6.1.0" + css-what "^7.0.0" csso "^5.0.5" picocolors "^1.1.1" - sax "^1.5.0" + sax "1.6.1" swr@^2.2.5: version "2.2.5" @@ -27743,10 +27754,10 @@ web-streams-polyfill@^3.1.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== -web-vitals@^6.1.1: - version "6.1.1" - resolved "https://sfw.security.sentry.io/npm/web-vitals/-/web-vitals-6.1.1.tgz#855ba63ed40847c55e061ac13f55ee5f6435b723" - integrity sha512-r93gKnR6ZC2O8F3JrS0AAGIQ0v0OhXuyg4DAj2oG7K+GPkwXfUSc3ZcWeu50AsV5yIc6xxnTjERipJ6EvSw2vg== +web-vitals@^6.2.3: + version "6.2.3" + resolved "https://sfw.security.sentry.io/npm/web-vitals/-/web-vitals-6.2.3.tgz#8581188b2a46875d04b9536ab06b352e3a0468b8" + integrity sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A== webidl-conversions@^3.0.0: version "3.0.1"