diff --git a/.agents/skills/e2e/SKILL.md b/.agents/skills/e2e/SKILL.md
index 8c45d939a8cf..bc9f3efe42f4 100644
--- a/.agents/skills/e2e/SKILL.md
+++ b/.agents/skills/e2e/SKILL.md
@@ -116,7 +116,7 @@ All tests completed successfully. Your SDK changes work correctly with this test
- **No tarballs found**: Run `yarn build && yarn build:tarball` at repository root
- **Test app not found**: List available apps and ask user to clarify
-- **Verdaccio not running**: Tests should start Verdaccio automatically, but if issues occur, check Docker
+- **Packed tarballs missing**: Run `yarn build:tarball` at the repo root, then `yarn test:prepare` in `dev-packages/e2e-tests`
- **Build failures**: Fix build errors before running tests
## Common Test Applications
diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md
index f4b4bd287271..c1a7811e5971 100644
--- a/.cursor/BUGBOT.md
+++ b/.cursor/BUGBOT.md
@@ -63,6 +63,7 @@ Do not flag the issues below if they appear in tests.
- 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.
- Timeouts or sleeps in tests. Instead suggest concrete events or other signals to wait on.
- Flag usage of `getFirstEnvelope*`, `getMultipleEnvelope*` or related test helpers. These are NOT reliable anymore. Instead suggest helpers like `waitForTransaction`, `waitForError`, `waitForSpans`, etc.
+- Flag any new or modified `docker-compose.yml` under `dev-packages/node-integration-tests/suites/` or `dev-packages/node-core-integration-tests/suites/` where a service does not define a `healthcheck:`. The runner uses `docker compose up --wait` and relies on healthchecks to know when services are actually ready; without one the test will race the service's startup.
## Platform-safe code
diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml
index cfa664b1d219..a862cd355af2 100644
--- a/.github/actions/install-dependencies/action.yml
+++ b/.github/actions/install-dependencies/action.yml
@@ -15,7 +15,7 @@ runs:
shell: bash
- name: Check dependency cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
id: cache_dependencies
with:
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 544bb7900008..add193a29d3b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -930,10 +930,12 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Bun
- if: contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "elysia-bun"]'), matrix.test-application)
+ if:
+ contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "elysia-bun", "hono-4"]'),
+ matrix.test-application)
uses: oven-sh/setup-bun@v2
- name: Set up AWS SAM
- if: matrix.test-application == 'aws-serverless'
+ if: matrix.test-application == 'aws-serverless' || matrix.test-application == 'aws-serverless-layer'
uses: aws-actions/setup-sam@v2
with:
use-installer: true
@@ -959,18 +961,24 @@ jobs:
if: steps.restore-tarball-cache.outputs.cache-hit != 'true'
run: yarn build:tarball
- - name: Validate Verdaccio
- run: yarn test:validate
+ - name: Prepare e2e tests
+ run: yarn test:prepare
working-directory: dev-packages/e2e-tests
- - name: Prepare Verdaccio
- run: yarn test:prepare
+ - name: Validate e2e tests setup
+ run: yarn test:validate
working-directory: dev-packages/e2e-tests
- name: Copy to temp
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
working-directory: dev-packages/e2e-tests
+ - name: Add pnpm overrides
+ run:
+ yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
+ }}/dev-packages/e2e-tests/packed
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
@@ -1069,18 +1077,24 @@ jobs:
if: steps.restore-tarball-cache.outputs.cache-hit != 'true'
run: yarn build:tarball
- - name: Validate Verdaccio
- run: yarn test:validate
+ - name: Prepare E2E tests
+ run: yarn test:prepare
working-directory: dev-packages/e2e-tests
- - name: Prepare Verdaccio
- run: yarn test:prepare
+ - name: Validate test setup
+ run: yarn test:validate
working-directory: dev-packages/e2e-tests
- name: Copy to temp
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
working-directory: dev-packages/e2e-tests
+ - name: Add pnpm overrides
+ run:
+ yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
+ }}/dev-packages/e2e-tests/packed
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index ac4e1df08841..e28d6988d9a1 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -140,18 +140,24 @@ jobs:
path: ${{ env.CACHED_BUILD_PATHS }}
key: canary-${{ env.HEAD_COMMIT }}
- - name: Validate Verdaccio
- run: yarn test:validate
+ - name: Prepare e2e tests
+ run: yarn test:prepare
working-directory: dev-packages/e2e-tests
- - name: Prepare Verdaccio
- run: yarn test:prepare
+ - name: Validate test setup
+ run: yarn test:validate
working-directory: dev-packages/e2e-tests
- name: Copy to temp
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
working-directory: dev-packages/e2e-tests
+ - name: Add pnpm overrides
+ run:
+ yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
+ }}/dev-packages/e2e-tests/packed
+ working-directory: dev-packages/e2e-tests
+
- name: Build E2E app
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
diff --git a/.gitignore b/.gitignore
index 464a09a9980e..741055ba1671 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ packages/**/*.junit.xml
# Local Claude Code settings that should not be committed
.claude/settings.local.json
+.claude/worktrees
# Triage report
**/triage_report.md
diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json
index 96122ee95b3e..91ba709d0e7f 100644
--- a/.oxlintrc.base.json
+++ b/.oxlintrc.base.json
@@ -117,6 +117,19 @@
"max-lines": "off"
}
},
+ {
+ "files": ["**/integrations/node-fetch/vendored/**/*.ts"],
+ "rules": {
+ "typescript/consistent-type-imports": "off",
+ "typescript/no-unnecessary-type-assertion": "off",
+ "typescript/no-unsafe-member-access": "off",
+ "typescript/no-explicit-any": "off",
+ "typescript/prefer-for-of": "off",
+ "max-lines": "off",
+ "complexity": "off",
+ "no-param-reassign": "off"
+ }
+ },
{
"files": [
"**/scenarios/**",
diff --git a/.size-limit.js b/.size-limit.js
index 86f3ef5ed87d..cad516a0a49a 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -155,7 +155,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
- limit: '46 KB',
+ limit: '47 KB',
},
// Vue SDK (ESM)
{
@@ -191,7 +191,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
- limit: '45 KB',
+ limit: '46.5 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics)',
@@ -203,7 +203,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: true,
- limit: '46 KB',
+ limit: '47.5 KB',
},
{
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
@@ -215,25 +215,25 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay)',
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: true,
- limit: '82 KB',
+ limit: '83.5 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: true,
- limit: '83 KB',
+ limit: '84.5 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
gzip: true,
- limit: '88 KB',
+ limit: '89 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
- limit: '89 KB',
+ limit: '90 KB',
},
// browser CDN bundles (non-gzipped)
{
@@ -241,14 +241,14 @@ module.exports = [
path: createCDNPath('bundle.min.js'),
gzip: false,
brotli: false,
- limit: '83.5 KB',
+ limit: '84 KB',
},
{
name: 'CDN Bundle (incl. Tracing) - uncompressed',
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
- limit: '134 KB',
+ limit: '138 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed',
@@ -262,42 +262,42 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '138 KB',
+ limit: '141.5 KB',
},
{
name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '211 KB',
+ limit: '212 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: false,
brotli: false,
- limit: '251 KB',
+ limit: '255.5 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '255 KB',
+ limit: '258.5 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
gzip: false,
brotli: false,
- limit: '264 KB',
+ limit: '268 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '268 KB',
+ limit: '271.5 KB',
},
// Next.js SDK (ESM)
{
@@ -324,7 +324,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '59 KB',
+ limit: '60 KB',
},
// Node SDK (ESM)
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ba7c69b14dd..dfcace55cda1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,68 @@
## Unreleased
+## 10.50.0
+
+### Important Changes
+
+- **feat(effect): Support v4 beta ([#20394](https://github.com/getsentry/sentry-javascript/pull/20394))**
+
+ The `@sentry/effect` integration now supports Effect v4 beta, enabling Sentry instrumentation for the latest Effect framework version.
+ Read more in the [Effect SDK readme](https://github.com/getsentry/sentry-javascript/blob/39740da9e46de76f4b03bb7ae11849ea761dac14/packages/effect/README.md).
+
+- **feat(hono): Add `@sentry/hono/bun` for Bun runtime ([#20355](https://github.com/getsentry/sentry-javascript/pull/20355))**
+
+ A new `@sentry/hono/bun` entry point adds first-class support for running Hono applications instrumented with Sentry on the Bun runtime.
+ Read more in the [Hono SDK readme](https://github.com/getsentry/sentry-javascript/blob/39740da9e46de76f4b03bb7ae11849ea761dac14/packages/hono/README.md).
+
+- **feat(replay): Add replayStart/replayEnd client lifecycle hooks ([#20369](https://github.com/getsentry/sentry-javascript/pull/20369))**
+
+ New `replayStart` and `replayEnd` client lifecycle hooks let you react to replay session start and end events in your application.
+
+### Other Changes
+
+- feat(core): Emit `no_parent_span` client outcomes for discarded spans requiring a parent ([#20350](https://github.com/getsentry/sentry-javascript/pull/20350))
+- feat(deps): Bump protobufjs from 7.5.4 to 7.5.5 ([#20372](https://github.com/getsentry/sentry-javascript/pull/20372))
+- feat(hono): Add runtime packages as optional peer dependencies ([#20423](https://github.com/getsentry/sentry-javascript/pull/20423))
+- feat(opentelemetry): Add tracingChannel utility for context propagation ([#20358](https://github.com/getsentry/sentry-javascript/pull/20358))
+- fix(browser): Enrich graphqlClient spans for relative URLs ([#20370](https://github.com/getsentry/sentry-javascript/pull/20370))
+- fix(browser): Filter implausible LCP values ([#20338](https://github.com/getsentry/sentry-javascript/pull/20338))
+- fix(cloudflare): Use TransformStream to keep track of streams ([#20452](https://github.com/getsentry/sentry-javascript/pull/20452))
+- fix(console): Re-patch console in AWS Lambda runtimes ([#20337](https://github.com/getsentry/sentry-javascript/pull/20337))
+- fix(core): Correct `GoogleGenAIIstrumentedMethod` typo in type name
+- fix(core): Handle stateless MCP wrapper transport correlation ([#20293](https://github.com/getsentry/sentry-javascript/pull/20293))
+- fix(hono): Remove undefined from options type ([#20419](https://github.com/getsentry/sentry-javascript/pull/20419))
+- fix(node): Guard against null `httpVersion` in outgoing request span attributes ([#20430](https://github.com/getsentry/sentry-javascript/pull/20430))
+- fix(node-core): Pass rejection reason instead of Promise as originalException ([#20366](https://github.com/getsentry/sentry-javascript/pull/20366))
+
+
+ Internal Changes
+
+- chore: Ignore claude worktrees ([#20440](https://github.com/getsentry/sentry-javascript/pull/20440))
+- chore: Prevent test from creating zombie process ([#20392](https://github.com/getsentry/sentry-javascript/pull/20392))
+- chore: Update size-limit ([#20412](https://github.com/getsentry/sentry-javascript/pull/20412))
+- chore(dev-deps): Bump nx from 22.5.0 to 22.6.5 ([#20458](https://github.com/getsentry/sentry-javascript/pull/20458))
+- chore(e2e-tests): Use tarball symlinks for E2E tests instead of verdaccio ([#20386](https://github.com/getsentry/sentry-javascript/pull/20386))
+- chore(lint): Remove lint warnings ([#20413](https://github.com/getsentry/sentry-javascript/pull/20413))
+- chore(test): Remove empty variant tests ([#20443](https://github.com/getsentry/sentry-javascript/pull/20443))
+- chore(tests): Use verdaccio as node process instead of docker image ([#20336](https://github.com/getsentry/sentry-javascript/pull/20336))
+- docs(readme): Update usage instructions for binary scripts ([#20426](https://github.com/getsentry/sentry-javascript/pull/20426))
+- ref(node): Vendor undici instrumentation ([#20190](https://github.com/getsentry/sentry-javascript/pull/20190))
+- test(aws-serverless): Ensure aws-serverless E2E tests run locally ([#20441](https://github.com/getsentry/sentry-javascript/pull/20441))
+- test(aws-serverless): Split npm & layer tests ([#20442](https://github.com/getsentry/sentry-javascript/pull/20442))
+- test(browser): Fix flaky sessions route-lifecycle test + upgrade axios ([#20197](https://github.com/getsentry/sentry-javascript/pull/20197))
+- test(cloudflare): Use `.makeRequestAndWaitForEnvelope` to wait for envelopes ([#20208](https://github.com/getsentry/sentry-javascript/pull/20208))
+- test(effect): Rename effect e2e tests to a versioned folder ([#20390](https://github.com/getsentry/sentry-javascript/pull/20390))
+- test(hono): Add E2E test for Hono on Cloudflare, Node and Bun ([#20406](https://github.com/getsentry/sentry-javascript/pull/20406))
+- test(hono): Add E2E tests for middleware spans ([#20451](https://github.com/getsentry/sentry-javascript/pull/20451))
+- test(nextjs): Unskip blocked cf tests ([#20356](https://github.com/getsentry/sentry-javascript/pull/20356))
+- test(node): Refactor integration tests for `honoIntegration` ([#20397](https://github.com/getsentry/sentry-javascript/pull/20397))
+- test(node): Use docker-compose healthchecks for service readiness ([#20429](https://github.com/getsentry/sentry-javascript/pull/20429))
+- test(node-core): Fix minute-boundary race in session-aggregate tests ([#20437](https://github.com/getsentry/sentry-javascript/pull/20437))
+- test(nuxt): Fix flaky database error test ([#20447](https://github.com/getsentry/sentry-javascript/pull/20447))
+
+
+
## 10.49.0
### Important Changes
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
new file mode 100644
index 000000000000..c69a872adc77
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 000000000000..731ce2db6146
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts
@@ -0,0 +1,21 @@
+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/public-api/beforeSendSpan-streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts
index ce07297c0a04..be5d0feee840 100644
--- a/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/public-api/beforeSendSpan-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils';
sentryTest('beforeSendSpan applies changes to streamed span', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
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 b5f8f41ab4b4..7a70c832558f 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
@@ -11,13 +11,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
sentryTest(
'sends a streamed span envelope if spanStreamingIntegration is enabled',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const spanEnvelopePromise = waitForStreamedSpanEnvelope(page);
@@ -167,6 +167,18 @@ sentryTest(
},
{
attributes: {
+ 'culture.calendar': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.locale': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.timezone': {
+ type: 'string',
+ value: expect.any(String),
+ },
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
type: 'string',
value: 'test',
diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts
index 1ccee9cd2728..05f10146f3f5 100644
--- a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts
+++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/test.ts
@@ -1,27 +1,30 @@
import { expect } from '@playwright/test';
-import type { SerializedSession } from '@sentry/core/src';
import { sentryTest } from '../../../utils/fixtures';
-import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers';
+import { waitForSession } from '../../../utils/helpers';
sentryTest(
'should start new sessions on pushState navigation with route lifecycle (default).',
async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });
- const sessionsPromise = getMultipleSentryEnvelopeRequests(page, 10, {
- url,
- envelopeType: 'session',
- timeout: 4000,
- });
+ const initSessionPromise = waitForSession(page, s => !!s.init && s.status === 'ok');
+ await page.goto(url);
+ const initSession = await initSessionPromise;
- await page.waitForSelector('#navigate');
-
- await page.locator('#navigate').click();
+ const session1Promise = waitForSession(page, s => !!s.init && s.status === 'ok' && s.sid !== initSession.sid);
await page.locator('#navigate').click();
+ const session1 = await session1Promise;
+
+ const session2Promise = waitForSession(page, s => !!s.init && s.status === 'ok' && s.sid !== session1.sid);
await page.locator('#navigate').click();
+ const session2 = await session2Promise;
- const startedSessions = (await sessionsPromise).filter(session => session.init);
+ const session3Promise = waitForSession(page, s => !!s.init && s.status === 'ok' && s.sid !== session2.sid);
+ await page.locator('#navigate').click();
+ const session3 = await session3Promise;
- expect(startedSessions.length).toBe(4);
+ // Verify we got 4 distinct init sessions (1 initial + 3 navigations)
+ const sids = new Set([initSession.sid, session1.sid, session2.sid, session3.sid]);
+ expect(sids.size).toBe(4);
},
);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts
index 10e58acb81ad..a851918b5438 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest('finishes streamed pageload span when the page goes background', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts
index 25d4ac497992..30e32621edbd 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings-streamed/test.ts
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
@@ -8,7 +8,7 @@ sentryTest(
async ({ browserName, getLocalTestUrl, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];
- sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName) || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName));
await page.route('http://sentry-test-site.example/*', async route => {
const request = route.request();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts
index fd384d0d3ff9..f1b0882d2325 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts
@@ -11,13 +11,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest('captures streamed interaction span tree. @firefox', async ({ browserName, getLocalTestUrl, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];
- sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName) || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName));
const url = await getLocalTestUrl({ testDir: __dirname });
const interactionSpansPromise = waitForStreamedSpans(page, spans =>
@@ -40,6 +40,18 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
expect(interactionSegmentSpan).toEqual({
attributes: {
+ 'culture.calendar': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.locale': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.timezone': {
+ type: 'string',
+ value: expect.any(String),
+ },
[SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: {
type: 'string',
value: 'idleTimeout',
@@ -87,7 +99,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
});
const loAFSpans = interactionSpanTree.filter(span => getSpanOp(span)?.startsWith('ui.long-animation-frame'));
- expect(loAFSpans).toHaveLength(1);
+ expect(loAFSpans).toHaveLength(browserName === 'chromium' ? 1 : 0);
const interactionSpan = interactionSpanTree.find(span => getSpanOp(span) === 'ui.interaction.click');
expect(interactionSpan).toEqual({
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
index a97e13a4890a..61c8fc3303dd 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
@@ -1,12 +1,12 @@
import { expect } from '@playwright/test';
import { extractTraceparentData, parseBaggageHeader, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
+import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils';
sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
sentryTest('continues sampling decision from initial pageload span', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -80,7 +80,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
});
sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts
index ea50f09f2361..79cabe19b927 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-negative/test.ts
@@ -7,7 +7,6 @@ import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
- testingCdnBundle,
waitForClientReportRequest,
waitForTracingHeadersOnUrl,
} from '../../../../../../utils/helpers';
@@ -21,7 +20,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
sentryTest(
'Continues negative sampling decision from meta tag across all traces and downstream propagations',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts
index 367b48e70eda..cd64596f9bfd 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta-precedence/test.ts
@@ -6,7 +6,6 @@ import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
- testingCdnBundle,
waitForClientReportRequest,
waitForTracingHeadersOnUrl,
} from '../../../../../../utils/helpers';
@@ -21,7 +20,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
sentryTest(
'meta tag decision has precedence over sampling decision from previous trace in session storage',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts
index 08cee9111b8a..154fe167a4a1 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/meta/test.ts
@@ -5,7 +5,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE,
} from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
+import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils';
const metaTagSampleRand = 0.051121;
@@ -13,7 +13,7 @@ const metaTagSampleRate = 0.2;
sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => {
sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -96,7 +96,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true` and page contains
sentryTest(
'Propagates continued tag sampling decision to outgoing requests',
async ({ page, getLocalTestUrl }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
index d661a4548e94..cd319e614c71 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
@@ -7,7 +7,6 @@ import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
- testingCdnBundle,
waitForClientReportRequest,
} from '../../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils';
@@ -19,7 +18,7 @@ import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/
*/
sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
sentryTest('explicit sampling decisions in `tracesSampler` have precedence', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
index d6e45901f959..571f4a6d8b5e 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
index 80e500437f79..fd63b6358bdf 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -71,7 +71,7 @@ sentryTest("navigation spans link back to previous trace's root span", async ({
});
sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
index c34aba99dbdd..1e5666e116e8 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
@@ -1,7 +1,7 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
/*
@@ -13,7 +13,7 @@ import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest(
'only the first root spans in the trace link back to the previous trace',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
index cbcc231593ea..141e5a9505dc 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
@@ -1,13 +1,13 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest(
"links back to previous trace's local root span if continued from meta tags",
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
index 06366eb9921a..0ff22e58a405 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest('includes a span link to a previously negatively sampled span', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
index 96a5bbeacc6d..5ed1c8021d4a 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest('adds link between hard page reloads when opting into sessionStorage', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts
index 3054c1c84bcb..f20fe774264b 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-before-navigation-streamed/test.ts
@@ -1,13 +1,13 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
"doesn't capture long animation frame that starts before a navigation.",
async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts
index 7ba1dddd0c90..e03474070bb0 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-disabled-streamed/test.ts
@@ -1,14 +1,14 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
'does not capture long animation frame when flag is disabled.',
async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) =>
route.fulfill({ path: `${__dirname}/assets/script.js` }),
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 c1e7efa5e8d8..040b78a89e9d 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
@@ -3,14 +3,14 @@ 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, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
'captures long animation frame span for top-level script.',
async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) =>
route.fulfill({ path: `${__dirname}/assets/script.js` }),
@@ -62,7 +62,7 @@ sentryTest(
sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
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 4f9207fa1e34..2529c6e2f66d 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
@@ -3,14 +3,14 @@ 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, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
'captures long animation frame span for top-level script.',
async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
// Long animation frame should take priority over long tasks
@@ -64,7 +64,7 @@ sentryTest(
sentryTest('captures long animation frame span for event listener.', async ({ browserName, getLocalTestUrl, page }) => {
// Long animation frames only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts
index 74ce32706584..3d8658cb9065 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-before-navigation-streamed/test.ts
@@ -1,13 +1,13 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
"doesn't capture long task spans starting before a navigation in the navigation transaction",
async ({ browserName, getLocalTestUrl, page }) => {
// Long tasks only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
const url = await getLocalTestUrl({ testDir: __dirname });
await page.route('**/path/to/script.js', route => route.fulfill({ path: `${__dirname}/assets/script.js` }));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts
index 83600f5d4a6a..da7f44f05df7 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled-streamed/test.ts
@@ -1,12 +1,12 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest("doesn't capture long task spans when flag is disabled.", async ({ browserName, getLocalTestUrl, page }) => {
// Long tasks only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts
index 8b73aa91dff6..e312078254d8 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled-streamed/test.ts
@@ -1,12 +1,12 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest('captures long task.', async ({ browserName, getLocalTestUrl, page }) => {
// Long tasks only work on chrome
- sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium' || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
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 403fdd4fdc0a..520a3d330bb9 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
@@ -7,7 +7,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import {
getSpanOp,
getSpansFromEnvelope,
@@ -15,8 +15,8 @@ import {
waitForStreamedSpanEnvelope,
} from '../../../../utils/spanUtils';
-sentryTest('starts a streamed navigation span on page navigation', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+sentryTest('starts a streamed navigation span on page navigation', async ({ browserName, getLocalTestUrl, page }) => {
+ sentryTest.skip(shouldSkipTracingTest());
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope(
@@ -69,18 +69,32 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ getL
expect(navigationSpan).toEqual({
attributes: {
- 'network.connection.effective_type': {
+ 'culture.calendar': {
type: 'string',
value: expect.any(String),
},
- 'device.processor_count': {
- type: expect.stringMatching(/^(integer)|(double)$/),
- value: expect.any(Number),
+ 'culture.locale': {
+ type: 'string',
+ value: expect.any(String),
},
- 'network.connection.rtt': {
+ 'culture.timezone': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'device.processor_count': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
+ ...(browserName !== 'webkit' && {
+ 'network.connection.effective_type': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'network.connection.rtt': {
+ type: expect.stringMatching(/^(integer)|(double)$/),
+ value: expect.any(Number),
+ },
+ }),
'sentry.idle_span_finish_reason': {
type: 'string',
value: 'idleTimeout',
@@ -150,7 +164,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ getL
});
sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
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 86882134cab4..6b09fcd0097d 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
@@ -11,13 +11,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
sentryTest(
'creates a pageload streamed span envelope with url as pageload span name source',
- async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ async ({ browserName, getLocalTestUrl, page }) => {
+ sentryTest.skip(shouldSkipTracingTest());
const spanEnvelopePromise = waitForStreamedSpanEnvelope(
page,
@@ -62,8 +62,15 @@ sentryTest(
expect(pageloadSpan).toEqual({
attributes: {
- // formerly known as 'effectiveConnectionType'
- 'network.connection.effective_type': {
+ 'culture.calendar': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.locale': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'culture.timezone': {
type: 'string',
value: expect.any(String),
},
@@ -80,18 +87,25 @@ sentryTest(
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
- 'network.connection.rtt': {
- type: expect.stringMatching(/^(integer)|(double)$/),
- value: expect.any(Number),
- },
'browser.web_vital.ttfb.request_time': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
- 'browser.web_vital.ttfb.value': {
- type: expect.stringMatching(/^(integer)|(double)$/),
- value: expect.any(Number),
- },
+ ...(browserName !== 'webkit' && {
+ // formerly known as 'effectiveConnectionType'
+ 'network.connection.effective_type': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'network.connection.rtt': {
+ type: expect.stringMatching(/^(integer)|(double)$/),
+ value: expect.any(Number),
+ },
+ 'browser.web_vital.ttfb.value': {
+ type: expect.stringMatching(/^(integer)|(double)$/),
+ value: expect.any(Number),
+ },
+ }),
'sentry.idle_span_finish_reason': {
type: 'string',
value: '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 fb6fa3ab2393..25ea24e6cbfe 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
@@ -7,13 +7,13 @@ import {
} from '@sentry/browser';
import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest(
'waits for Sentry.reportPageLoaded() to be called when `enableReportPageLoaded` is true',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
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 79df6a902e45..74ac5aedf101 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
@@ -6,13 +6,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/browser';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest(
'final timeout cancels the pageload span even if `enableReportPageLoaded` is true',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
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 77f138f34053..001e87ad31fc 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
@@ -6,13 +6,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/browser';
import { sentryTest } from '../../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
sentryTest(
'starting a navigation span cancels the pageload span even if `enableReportPageLoaded` is true',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts
index 967ef101092d..02e24fc409d3 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/child/test.ts
@@ -4,14 +4,13 @@ import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
- testingCdnBundle,
waitForClientReportRequest,
} from '../../../../utils/helpers';
import { waitForStreamedSpans } from '../../../../utils/spanUtils';
import type { ClientReport } from '@sentry/core';
sentryTest('ignored child spans are dropped and their children are reparented', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const spansPromise = waitForStreamedSpans(page, spans => !!spans?.find(s => s.name === 'parent-span'));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts
index 93042cc5469e..99f87d647d89 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/ignoreSpans-streamed/segment/test.ts
@@ -4,14 +4,13 @@ import {
envelopeRequestParser,
hidePage,
shouldSkipTracingTest,
- testingCdnBundle,
waitForClientReportRequest,
} from '../../../../utils/helpers';
import { observeStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils';
import type { ClientReport } from '@sentry/core';
sentryTest('ignored segment span drops entire trace', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts
index dc35f0c8fcf1..bafa95aee03f 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
import { waitForStreamedSpan, waitForStreamedSpans } from '../../../utils/spanUtils';
sentryTest('links spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment);
const rootSpan2Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan2' && !!s.is_segment);
@@ -29,7 +29,7 @@ sentryTest('links spans with addLink() in trace context', async ({ getLocalTestU
});
sentryTest('links spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const rootSpan1Promise = waitForStreamedSpan(page, s => s.name === 'rootSpan1' && !!s.is_segment);
const rootSpan3SpansPromise = waitForStreamedSpans(page, spans =>
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts
index 31ddd09977cb..409d79327a91 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts
@@ -1,11 +1,11 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest.beforeEach(async ({ browserName, page }) => {
- if (shouldSkipTracingTest() || testingCdnBundle() || browserName !== 'chromium') {
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
sentryTest.skip();
}
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts
index 30dd4f92dbfc..893be918553d 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest.beforeEach(async ({ browserName }) => {
- if (shouldSkipTracingTest() || testingCdnBundle() || browserName !== 'chromium') {
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
sentryTest.skip();
}
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts
index 1f71cb8d76a7..8cff98edfcd0 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts
@@ -1,11 +1,11 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest.beforeEach(async ({ browserName, page }) => {
- if (shouldSkipTracingTest() || testingCdnBundle() || browserName !== 'chromium') {
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
sentryTest.skip();
}
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts
index 73f37f07a291..d410b1eaa356 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest.beforeEach(async ({ page }) => {
- if (shouldSkipTracingTest() || testingCdnBundle()) {
+ if (shouldSkipTracingTest()) {
sentryTest.skip();
}
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/init.js
new file mode 100644
index 000000000000..94db849f5fde
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/init.js
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false }),
+ Sentry.spanStreamingIntegration(),
+ ],
+ tracesSampleRate: 1,
+ sendClientReports: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/subject.js
new file mode 100644
index 000000000000..6ba8011d77ac
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/subject.js
@@ -0,0 +1 @@
+fetch('http://sentry-test-site.example/api/test');
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts
new file mode 100644
index 000000000000..609df6f551a3
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts
@@ -0,0 +1,44 @@
+import { expect } from '@playwright/test';
+import type { ClientReport } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ hidePage,
+ shouldSkipTracingTest,
+ waitForClientReportRequest,
+} from '../../../utils/helpers';
+
+sentryTest(
+ 'records no_parent_span client report for fetch requests without an active span',
+ async ({ getLocalTestUrl, page }) => {
+ sentryTest.skip(shouldSkipTracingTest());
+
+ await page.route('http://sentry-test-site.example/api/test', route => {
+ route.fulfill({
+ status: 200,
+ body: 'ok',
+ headers: { 'Content-Type': 'text/plain' },
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const clientReportPromise = waitForClientReportRequest(page, report => {
+ return report.discarded_events.some(e => e.reason === 'no_parent_span');
+ });
+
+ await page.goto(url);
+
+ await hidePage(page);
+
+ const clientReport = envelopeRequestParser(await clientReportPromise);
+
+ expect(clientReport.discarded_events).toEqual([
+ {
+ category: 'span',
+ quantity: 1,
+ reason: 'no_parent_span',
+ },
+ ]);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/init.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/init.js
new file mode 100644
index 000000000000..10e5ac1b84eb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false })],
+ tracesSampleRate: 1,
+ sendClientReports: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/subject.js b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/subject.js
new file mode 100644
index 000000000000..6ba8011d77ac
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/subject.js
@@ -0,0 +1 @@
+fetch('http://sentry-test-site.example/api/test');
diff --git a/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/test.ts b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/test.ts
new file mode 100644
index 000000000000..2672d41c17fb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/no-parent-span-client-report/test.ts
@@ -0,0 +1,45 @@
+import { expect } from '@playwright/test';
+import type { ClientReport } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ hidePage,
+ shouldSkipTracingTest,
+ testingCdnBundle,
+ waitForClientReportRequest,
+} from '../../../utils/helpers';
+
+sentryTest(
+ 'records no_parent_span client report for fetch requests without an active span',
+ async ({ getLocalTestUrl, page }) => {
+ sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+
+ await page.route('http://sentry-test-site.example/api/test', route => {
+ route.fulfill({
+ status: 200,
+ body: 'ok',
+ headers: { 'Content-Type': 'text/plain' },
+ });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const clientReportPromise = waitForClientReportRequest(page, report => {
+ return report.discarded_events.some(e => e.reason === 'no_parent_span');
+ });
+
+ await page.goto(url);
+
+ await hidePage(page);
+
+ const clientReport = envelopeRequestParser(await clientReportPromise);
+
+ expect(clientReport.discarded_events).toEqual([
+ {
+ category: 'span',
+ quantity: 1,
+ reason: 'no_parent_span',
+ },
+ ]);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts
index 201c3e4979f2..49865da46140 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
@@ -19,7 +19,11 @@ sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page })
const allSpans = await spansPromise;
const pageloadSpan = allSpans.find(s => getSpanOp(s) === 'pageload');
- const requestSpans = allSpans.filter(s => getSpanOp(s) === 'http.client');
+ const requestSpans = allSpans
+ .filter(s => getSpanOp(s) === 'http.client')
+ .sort((a, b) =>
+ (a.attributes!['http.url']!.value as string).localeCompare(b.attributes!['http.url']!.value as string),
+ );
expect(requestSpans).toHaveLength(3);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts
index d3f20fd36453..50442d6840ce 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest('creates spans for XHR requests', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts
index a144e171a93a..c9f48a4ebefd 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/default/test.ts
@@ -1,10 +1,10 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest('sets an inactive span active and adds child spans to it', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => s.name === 'checkout-flow' && s.is_segment));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts
index 8f5e54e1fba0..f0ce77d4e00c 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested-parentAlwaysRoot/test.ts
@@ -1,12 +1,12 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
'nested calls to setActiveSpanInBrowser with parentSpanIsAlwaysRootSpan=false result in correct parenting',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const checkoutSpansPromise = waitForStreamedSpans(page, spans =>
spans.some(s => s.name === 'checkout-flow' && s.is_segment),
diff --git a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts
index 1b04553090bc..37a76f81152d 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/setSpanActive-streamed/nested/test.ts
@@ -1,12 +1,12 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { waitForStreamedSpans } from '../../../../utils/spanUtils';
sentryTest(
'nested calls to setActiveSpanInBrowser still parent to root span by default',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const checkoutSpansPromise = waitForStreamedSpans(page, spans =>
spans.some(s => s.name === 'checkout-flow' && s.is_segment),
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts
index 28f3e5039910..3c2cafaa0430 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation-streamed/test.ts
@@ -7,12 +7,11 @@ import {
getFirstSentryEnvelopeRequest,
shouldSkipFeedbackTest,
shouldSkipTracingTest,
- testingCdnBundle,
} from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -76,7 +75,7 @@ sentryTest('creates a new trace and sample_rand on each navigation', async ({ ge
});
sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -138,7 +137,7 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes
});
sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -188,7 +187,7 @@ sentryTest('error during navigation has new navigation traceId', async ({ getLoc
sentryTest(
'outgoing fetch request during navigation has navigation traceId in headers',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -232,7 +231,7 @@ sentryTest(
sentryTest(
'outgoing XHR request during navigation has navigation traceId in headers',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -276,7 +275,7 @@ sentryTest(
sentryTest(
'user feedback event after navigation has navigation traceId in headers',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest());
const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts
index 1b4458991559..646d4be6fa56 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-streamed/test.ts
@@ -7,12 +7,11 @@ import {
getFirstSentryEnvelopeRequest,
shouldSkipFeedbackTest,
shouldSkipTracingTest,
- testingCdnBundle,
} from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
sentryTest('creates a new trace for a navigation after the initial pageload', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation');
@@ -41,7 +40,7 @@ sentryTest('creates a new trace for a navigation after the initial pageload', as
});
sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
@@ -83,7 +82,7 @@ sentryTest('error after pageload has pageload traceId', async ({ getLocalTestUrl
});
sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -126,7 +125,7 @@ sentryTest('error during pageload has pageload traceId', async ({ getLocalTestUr
sentryTest(
'outgoing fetch request during pageload has pageload traceId in headers',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -165,7 +164,7 @@ sentryTest(
sentryTest(
'outgoing XHR request during pageload has pageload traceId in headers',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -202,7 +201,7 @@ sentryTest(
);
sentryTest('user feedback event after pageload has pageload traceId in headers', async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest());
const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true });
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts
index d294efcd2e3b..07ac4b0c8cd3 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTrace-streamed/test.ts
@@ -1,12 +1,12 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
-import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
sentryTest(
'creates a new trace if `startNewTrace` is called and leaves old trace valid outside the callback',
async ({ getLocalTestUrl, page }) => {
- sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
+ sentryTest.skip(shouldSkipTracingTest());
const url = await getLocalTestUrl({ testDir: __dirname });
diff --git a/dev-packages/bun-integration-tests/expect.ts b/dev-packages/bun-integration-tests/expect.ts
index 599caaa9e5be..6f1add5ffeae 100644
--- a/dev-packages/bun-integration-tests/expect.ts
+++ b/dev-packages/bun-integration-tests/expect.ts
@@ -68,7 +68,11 @@ export function expectedEvent(event: Event, { sdk }: { sdk: 'bun' | 'hono' }): E
export function eventEnvelope(
event: Event,
- { includeSampleRand = false, sdk = 'bun' }: { includeSampleRand?: boolean; sdk?: 'bun' | 'hono' } = {},
+ {
+ includeSampleRand = false,
+ includeTransaction = true,
+ sdk = 'bun',
+ }: { includeSampleRand?: boolean; includeTransaction?: boolean; sdk?: 'bun' | 'hono' } = {},
): Envelope {
return [
{
@@ -79,11 +83,13 @@ export function eventEnvelope(
environment: event.environment || 'production',
public_key: 'public',
trace_id: UUID_MATCHER,
+
sample_rate: expect.any(String),
sampled: expect.any(String),
// release is auto-detected from GitHub CI env vars, so only expect it if we know it will be there
...(process.env.GITHUB_SHA ? { release: expect.any(String) } : {}),
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
+ ...(includeTransaction && { transaction: expect.any(String) }),
},
},
[[{ type: 'event' }, expectedEvent(event, { sdk })]],
diff --git a/dev-packages/bun-integration-tests/suites/basic/test.ts b/dev-packages/bun-integration-tests/suites/basic/test.ts
index 673464f0c81a..c03a09535702 100644
--- a/dev-packages/bun-integration-tests/suites/basic/test.ts
+++ b/dev-packages/bun-integration-tests/suites/basic/test.ts
@@ -25,7 +25,7 @@ it('captures an error thrown in Bun.serve fetch handler', async ({ signal }) =>
url: expect.stringContaining('/error'),
}),
},
- { includeSampleRand: true },
+ { includeSampleRand: true, includeTransaction: false },
),
)
.ignore('transaction')
diff --git a/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts b/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts
new file mode 100644
index 000000000000..075fc896618b
--- /dev/null
+++ b/dev-packages/bun-integration-tests/suites/hono-sdk/index.ts
@@ -0,0 +1,31 @@
+import { sentry } from '@sentry/hono/bun';
+import { Hono } from 'hono';
+
+const app = new Hono();
+
+app.use(
+ sentry(app, {
+ dsn: process.env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+ }),
+);
+
+app.get('/', c => {
+ return c.text('Hello from Hono on Bun!');
+});
+
+app.get('/hello/:name', c => {
+ const name = c.req.param('name');
+ return c.text(`Hello, ${name}!`);
+});
+
+app.get('/error/:param', () => {
+ throw new Error('Test error from Hono app');
+});
+
+const server = Bun.serve({
+ port: 0,
+ fetch: app.fetch,
+});
+
+process.send?.(JSON.stringify({ event: 'READY', port: server.port }));
diff --git a/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts b/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts
new file mode 100644
index 000000000000..62d5021fddb9
--- /dev/null
+++ b/dev-packages/bun-integration-tests/suites/hono-sdk/test.ts
@@ -0,0 +1,131 @@
+import { expect, it } from 'vitest';
+import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect';
+import { createRunner } from '../../runner';
+
+it('Hono app captures parametrized errors (Hono SDK on Bun)', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .expect(envelope => {
+ const [, envelopeItems] = envelope;
+ const [itemHeader, itemPayload] = envelopeItems[0];
+
+ expect(itemHeader.type).toBe('transaction');
+
+ expect(itemPayload).toMatchObject({
+ type: 'transaction',
+ platform: 'node',
+ transaction: 'GET /error/:param',
+ transaction_info: {
+ source: 'route',
+ },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ op: 'http.server',
+ status: 'internal_error',
+ origin: 'auto.http.bun.serve',
+ },
+ response: {
+ status_code: 500,
+ },
+ },
+ request: expect.objectContaining({
+ method: 'GET',
+ url: expect.stringContaining('/error/param-123'),
+ }),
+ breadcrumbs: [
+ {
+ timestamp: expect.any(Number),
+ category: 'console',
+ level: 'error',
+ message: 'Error: Test error from Hono app',
+ data: expect.objectContaining({
+ logger: 'console',
+ arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
+ }),
+ },
+ ],
+ });
+ })
+
+ .expect(
+ eventEnvelope(
+ {
+ level: 'error',
+ transaction: 'GET /error/:param',
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Test error from Hono app',
+ stacktrace: {
+ frames: expect.any(Array),
+ },
+ mechanism: { type: 'auto.http.hono.context_error', handled: false },
+ },
+ ],
+ },
+ request: {
+ cookies: {},
+ headers: expect.any(Object),
+ method: 'GET',
+ url: expect.stringContaining('/error/param-123'),
+ },
+ breadcrumbs: [
+ {
+ timestamp: expect.any(Number),
+ category: 'console',
+ level: 'error',
+ message: 'Error: Test error from Hono app',
+ data: expect.objectContaining({
+ logger: 'console',
+ arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }],
+ }),
+ },
+ ],
+ },
+ { sdk: 'hono', includeSampleRand: true, includeTransaction: true },
+ ),
+ )
+ .unordered()
+ .start(signal);
+
+ await runner.makeRequest('get', '/error/param-123', { expectError: true });
+ await runner.completed();
+});
+
+it('Hono app captures parametrized route names on Bun', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .expect(envelope => {
+ const [, envelopeItems] = envelope;
+ const [itemHeader, itemPayload] = envelopeItems[0];
+
+ expect(itemHeader.type).toBe('transaction');
+
+ expect(itemPayload).toMatchObject({
+ type: 'transaction',
+ platform: 'node',
+ transaction: 'GET /hello/:name',
+ transaction_info: {
+ source: 'route',
+ },
+ contexts: {
+ trace: {
+ span_id: SHORT_UUID_MATCHER,
+ trace_id: UUID_MATCHER,
+ op: 'http.server',
+ status: 'ok',
+ origin: 'auto.http.bun.serve',
+ },
+ },
+ request: expect.objectContaining({
+ method: 'GET',
+ url: expect.stringContaining('/hello/world'),
+ }),
+ });
+ })
+ .start(signal);
+
+ await runner.makeRequest('get', '/hello/world');
+ await runner.completed();
+});
diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts
index e0a48dd33ff5..542ffe82b802 100644
--- a/dev-packages/cloudflare-integration-tests/runner.ts
+++ b/dev-packages/cloudflare-integration-tests/runner.ts
@@ -50,6 +50,12 @@ type StartResult = {
path: string,
options?: { headers?: Record; data?: BodyInit; expectError?: boolean },
): Promise;
+ makeRequestAndWaitForEnvelope(
+ method: 'get' | 'post',
+ path: string,
+ expected: Expected | Expected[],
+ options?: { headers?: Record; data?: BodyInit; expectError?: boolean },
+ ): Promise;
};
/** Creates a test runner */
@@ -108,6 +114,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;
let envelopeCount = 0;
+ const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const { resolve: setWorkerPort, promise: workerPortPromise } = deferredPromise();
let child: ReturnType | undefined;
let childSubWorker: ReturnType | undefined;
@@ -120,6 +127,12 @@ export function createRunner(...paths: string[]) {
}
}
+ function waitForEnvelope(expected: Expected): Promise {
+ return new Promise((resolveWaiter, rejectWaiter) => {
+ envelopeWaiters.push({ expected, resolve: resolveWaiter, reject: rejectWaiter });
+ });
+ }
+
function assertEnvelopeMatches(expected: Expected, envelope: Envelope): void {
if (typeof expected === 'function') {
expected(envelope);
@@ -137,6 +150,18 @@ export function createRunner(...paths: string[]) {
return;
}
+ // Check per-request waiters first (FIFO order)
+ if (envelopeWaiters.length > 0) {
+ const waiter = envelopeWaiters.shift()!;
+ try {
+ assertEnvelopeMatches(waiter.expected, envelope);
+ waiter.resolve();
+ } catch (e) {
+ waiter.reject(e);
+ }
+ return;
+ }
+
try {
if (unordered) {
// find any matching expected envelope
@@ -242,6 +267,10 @@ export function createRunner(...paths: string[]) {
`SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`,
'--var',
`SERVER_URL:${serverUrl}`,
+ '--port',
+ '0',
+ '--inspector-port',
+ '0',
...extraWranglerArgs,
],
{ stdio, signal },
@@ -304,6 +333,18 @@ export function createRunner(...paths: string[]) {
return;
}
},
+ makeRequestAndWaitForEnvelope: async function (
+ method: 'get' | 'post',
+ path: string,
+ expected: Expected | Expected[],
+ options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {},
+ ): Promise {
+ const expectations = Array.isArray(expected) ? expected : [expected];
+ const envelopePromises = expectations.map(e => waitForEnvelope(e));
+ const result = await this.makeRequest(method, path, options);
+ await Promise.all(envelopePromises);
+ return result;
+ },
};
},
};
diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts
index 090142714d5b..d9c18431202b 100644
--- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts
@@ -197,7 +197,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa
},
'url.port': {
type: 'string',
- value: '8787',
+ value: expect.stringMatching(/^\d{4,5}$/),
},
'url.scheme': {
type: 'string',
@@ -221,7 +221,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa
},
'http.request.header.cf_connecting_ip': {
type: 'string',
- value: '::1',
+ value: expect.stringMatching(/^(::1|127\.0\.0\.1)$/),
},
'http.request.header.host': {
type: 'string',
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/test.ts
index 1415950208cc..483b170936e4 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/test.ts
@@ -25,15 +25,13 @@ it.skip('sends child spans on repeated Durable Object calls', async ({ signal })
// All 5 child spans should be present
expect(transactionEvent.spans).toHaveLength(5);
- expect(transactionEvent.spans).toEqual(
- expect.arrayContaining([
- expect.objectContaining({ description: 'task-1', op: 'task' }),
- expect.objectContaining({ description: 'task-2', op: 'task' }),
- expect.objectContaining({ description: 'task-3', op: 'task' }),
- expect.objectContaining({ description: 'task-4', op: 'task' }),
- expect.objectContaining({ description: 'task-5', op: 'task' }),
- ]),
- );
+ expect(transactionEvent.spans).toEqual([
+ expect.objectContaining({ description: 'task-1', op: 'task' }),
+ expect.objectContaining({ description: 'task-2', op: 'task' }),
+ expect.objectContaining({ description: 'task-3', op: 'task' }),
+ expect.objectContaining({ description: 'task-4', op: 'task' }),
+ expect.objectContaining({ description: 'task-5', op: 'task' }),
+ ]);
// All child spans share the root trace_id
const rootTraceId = transactionEvent.contexts?.trace?.trace_id;
@@ -43,13 +41,12 @@ it.skip('sends child spans on repeated Durable Object calls', async ({ signal })
}
}
- // Expect 5 transaction envelopes — one per call.
- const runner = createRunner(__dirname).expectN(5, assertDoWorkEnvelope).start(signal);
+ const runner = createRunner(__dirname).start(signal);
- await runner.makeRequest('get', '/');
- await runner.makeRequest('get', '/');
- await runner.makeRequest('get', '/');
- await runner.makeRequest('get', '/');
- await runner.makeRequest('get', '/');
- await runner.completed();
+ // Each request waits for its envelope to be received and validated before proceeding.
+ await runner.makeRequestAndWaitForEnvelope('get', '/', assertDoWorkEnvelope);
+ await runner.makeRequestAndWaitForEnvelope('get', '/', assertDoWorkEnvelope);
+ await runner.makeRequestAndWaitForEnvelope('get', '/', assertDoWorkEnvelope);
+ await runner.makeRequestAndWaitForEnvelope('get', '/', assertDoWorkEnvelope);
+ await runner.makeRequestAndWaitForEnvelope('get', '/', assertDoWorkEnvelope);
});
diff --git a/dev-packages/cloudflare-integration-tests/vite.config.mts b/dev-packages/cloudflare-integration-tests/vite.config.mts
index a80bbbf63f32..0fdb560b8f11 100644
--- a/dev-packages/cloudflare-integration-tests/vite.config.mts
+++ b/dev-packages/cloudflare-integration-tests/vite.config.mts
@@ -28,6 +28,9 @@ export default defineConfig({
singleThread: true,
},
},
+ sequence: {
+ shuffle: true,
+ },
reporters: process.env.DEBUG
? ['default', { summary: false }]
: process.env.GITHUB_ACTIONS
diff --git a/dev-packages/e2e-tests/.gitignore b/dev-packages/e2e-tests/.gitignore
index 2ce9dc2100a5..21181cb54143 100644
--- a/dev-packages/e2e-tests/.gitignore
+++ b/dev-packages/e2e-tests/.gitignore
@@ -4,3 +4,4 @@ tmp
.tmp_build_stderr
pnpm-lock.yaml
.last-run.json
+packed
diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts
index 0ecd3999db4d..24c3629d8dce 100644
--- a/dev-packages/e2e-tests/ciCopyToTemp.ts
+++ b/dev-packages/e2e-tests/ciCopyToTemp.ts
@@ -15,5 +15,7 @@ async function run(): Promise {
await copyToTemp(originalPath, tmpDirPath);
}
-// eslint-disable-next-line @typescript-eslint/no-floating-promises
-run();
+run().catch(error => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/dev-packages/e2e-tests/ciPnpmOverrides.ts b/dev-packages/e2e-tests/ciPnpmOverrides.ts
new file mode 100644
index 000000000000..909a59fa2e9d
--- /dev/null
+++ b/dev-packages/e2e-tests/ciPnpmOverrides.ts
@@ -0,0 +1,20 @@
+/* eslint-disable no-console */
+
+import { addPnpmOverrides } from './lib/pnpmOverrides';
+import * as path from 'path';
+
+async function run(): Promise {
+ const tmpDirPath = process.argv[2];
+ const packedDirPath = process.argv[3];
+
+ if (!tmpDirPath || !packedDirPath) {
+ throw new Error('Tmp dir path and packed dir path are required');
+ }
+
+ await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath));
+}
+
+run().catch(error => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/dev-packages/e2e-tests/lib/constants.ts b/dev-packages/e2e-tests/lib/constants.ts
deleted file mode 100644
index b5476ba4614d..000000000000
--- a/dev-packages/e2e-tests/lib/constants.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const TEST_REGISTRY_CONTAINER_NAME = 'verdaccio-e2e-test-registry';
-export const DEFAULT_BUILD_TIMEOUT_SECONDS = 60 * 5;
-export const DEFAULT_TEST_TIMEOUT_SECONDS = 60 * 2;
-export const VERDACCIO_VERSION = '5.22.1';
diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.ts
index 830ff76f6077..c45a5793e2fc 100644
--- a/dev-packages/e2e-tests/lib/copyToTemp.ts
+++ b/dev-packages/e2e-tests/lib/copyToTemp.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import { readFileSync, writeFileSync } from 'fs';
import { cp } from 'fs/promises';
-import { join } from 'path';
+import { isAbsolute, join, resolve } from 'path';
export async function copyToTemp(originalPath: string, tmpDirPath: string): Promise {
// copy files to tmp dir
@@ -35,7 +35,7 @@ function fixPackageJson(cwd: string): void {
const extendsPath = packageJson.volta.extends;
// We add a virtual dir to ensure that the relative depth is consistent
// dirPath is relative to ./../test-applications/xxx
- const newPath = join(__dirname, 'virtual-dir/', extendsPath);
+ const newPath = resolve(__dirname, 'virtual-dir/', extendsPath);
packageJson.volta.extends = newPath;
console.log(`Fixed volta.extends to ${newPath}`);
} else {
@@ -45,17 +45,24 @@ function fixPackageJson(cwd: string): void {
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
-function fixFileLinkDependencies(dependencyObj: Record): void {
+// Exported for pnpmOverrides as well
+export function fixFileLinkDependencies(dependencyObj: Record): void {
for (const [key, value] of Object.entries(dependencyObj)) {
- if (value.startsWith('link:')) {
- const dirPath = value.replace('link:', '');
-
- // We add a virtual dir to ensure that the relative depth is consistent
- // dirPath is relative to ./../test-applications/xxx
- const newPath = join(__dirname, 'virtual-dir/', dirPath);
+ const prefix = value.startsWith('link:') ? 'link:' : value.startsWith('file:') ? 'file:' : null;
+ if (!prefix) {
+ continue;
+ }
- dependencyObj[key] = `link:${newPath}`;
- console.log(`Fixed ${key} dependency to ${newPath}`);
+ const dirPath = value.slice(prefix.length);
+ if (isAbsolute(dirPath)) {
+ continue;
}
+
+ // We add a virtual dir to ensure that the relative depth is consistent
+ // dirPath is relative to ./../test-applications/xxx
+ const absPath = resolve(__dirname, 'virtual-dir', dirPath);
+
+ dependencyObj[key] = `${prefix}${absPath}`;
+ console.log(`Fixed ${key} dependency to ${absPath}`);
}
}
diff --git a/dev-packages/e2e-tests/lib/packedTarballUtils.ts b/dev-packages/e2e-tests/lib/packedTarballUtils.ts
new file mode 100644
index 000000000000..7469eb12f4a2
--- /dev/null
+++ b/dev-packages/e2e-tests/lib/packedTarballUtils.ts
@@ -0,0 +1,60 @@
+import * as path from 'path';
+import * as fs from 'fs';
+import { sync as globSync } from 'glob';
+
+const E2E_TESTS_ROOT = path.resolve(__dirname, '..');
+const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..');
+
+/**
+ * Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version.
+ * @returns The names of the published Sentry tarball packages.
+ */
+export function getPublishedSentryTarballPackageNames(): string[] {
+ const version = getE2eTestsPackageVersion();
+ const names: string[] = [];
+
+ for (const packageJsonPath of globSync('packages/*/package.json', {
+ cwd: REPOSITORY_ROOT,
+ absolute: true,
+ })) {
+ const pkg = readJson<{ name?: string }>(packageJsonPath);
+ const name = pkg.name;
+ if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) {
+ continue;
+ }
+ const packageDir = path.dirname(packageJsonPath);
+ const tarball = path.join(packageDir, versionedTarballFilename(name, version));
+ if (fs.existsSync(tarball)) {
+ names.push(name);
+ }
+ }
+
+ return names.sort();
+}
+
+/** Stable symlink name in `packed/` (no version segment). */
+export function packedSymlinkFilename(packageName: string): string {
+ return `${npmPackBasename(packageName)}-packed.tgz`;
+}
+
+/**
+ * Versioned tarball filename produced by `npm pack` in a package directory.
+ */
+export function versionedTarballFilename(packageName: string, version: string): string {
+ return `${npmPackBasename(packageName)}-${version}.tgz`;
+}
+
+/**
+ * npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`.
+ */
+function npmPackBasename(packageName: string): string {
+ return packageName.replace(/^@/, '').replace(/\//g, '-');
+}
+
+function readJson(filePath: string): T {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
+}
+
+function getE2eTestsPackageVersion(): string {
+ return readJson<{ version: string }>(path.join(E2E_TESTS_ROOT, 'package.json')).version;
+}
diff --git a/dev-packages/e2e-tests/lib/pnpmOverrides.ts b/dev-packages/e2e-tests/lib/pnpmOverrides.ts
new file mode 100644
index 000000000000..cf88b6c48f09
--- /dev/null
+++ b/dev-packages/e2e-tests/lib/pnpmOverrides.ts
@@ -0,0 +1,43 @@
+import { readFile, writeFile } from 'fs/promises';
+import * as path from 'path';
+import { getPublishedSentryTarballPackageNames, packedSymlinkFilename } from './packedTarballUtils';
+import { fixFileLinkDependencies } from './copyToTemp';
+
+/**
+ * For a given temp test application directory, add pnpm.overrides to pin the internal Sentry packages to the packed tarballs.
+ * This is used to ensure that the test application uses the correct version of the internal Sentry packages.
+ * @param tmpDirPath - The temporary directory path of the test application.
+ * @param packedDirPath - The path to the packed tarballs.
+ * @param packageNames - The names of the internal Sentry packages to pin to the packed tarballs.
+ * @returns
+ */
+export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string): Promise {
+ const packageJsonPath = path.join(tmpDirPath, 'package.json');
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as {
+ pnpm?: { overrides?: Record };
+ };
+
+ const overrides: Record = {};
+
+ const packageNames = getPublishedSentryTarballPackageNames();
+
+ for (const packageName of packageNames) {
+ overrides[packageName] = `file:${packedDirPath}/${packedSymlinkFilename(packageName)}`;
+ }
+
+ const fixedOverrides = packageJson.pnpm?.overrides ?? {};
+ fixFileLinkDependencies(fixedOverrides);
+
+ packageJson.pnpm = {
+ ...packageJson.pnpm,
+ overrides: {
+ ...overrides,
+ ...fixedOverrides,
+ },
+ };
+
+ // oxlint-disable-next-line no-console
+ console.log(`Added ${packageNames.length} internal Sentry packages to pnpm.overrides`);
+
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
+}
diff --git a/dev-packages/e2e-tests/lib/publishPackages.ts b/dev-packages/e2e-tests/lib/publishPackages.ts
deleted file mode 100644
index 5aed3a76e77a..000000000000
--- a/dev-packages/e2e-tests/lib/publishPackages.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/* eslint-disable no-console */
-import * as childProcess from 'child_process';
-import { readFileSync } from 'fs';
-import { globSync } from 'glob';
-import * as path from 'path';
-
-const repositoryRoot = path.resolve(__dirname, '../../..');
-
-/**
- * Publishes all built Sentry package tarballs to the local Verdaccio test registry.
- */
-export function publishPackages(): void {
- const version = (JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8')) as { version: string })
- .version;
-
- // Get absolute paths of all the packages we want to publish to the fake registry
- // Only include the current versions, to avoid getting old tarballs published as well
- const packageTarballPaths = globSync(`packages/*/sentry-*-${version}.tgz`, {
- cwd: repositoryRoot,
- absolute: true,
- });
-
- if (packageTarballPaths.length === 0) {
- throw new Error(`No packages to publish for version ${version}, did you run "yarn build:tarballs"?`);
- }
-
- const npmrc = path.join(__dirname, '../test-registry.npmrc');
-
- for (const tarballPath of packageTarballPaths) {
- console.log(`Publishing tarball ${tarballPath} ...`);
- const result = childProcess.spawnSync('npm', ['--userconfig', npmrc, 'publish', tarballPath], {
- cwd: repositoryRoot,
- encoding: 'utf8',
- stdio: 'inherit',
- });
-
- if (result.status !== 0) {
- throw new Error(`Error publishing tarball ${tarballPath}`);
- }
- }
-}
diff --git a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts
new file mode 100644
index 000000000000..ac6110ed7afd
--- /dev/null
+++ b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts
@@ -0,0 +1,65 @@
+/* eslint-disable no-console */
+import * as fs from 'fs';
+import * as path from 'path';
+import { sync as globSync } from 'glob';
+import { packedSymlinkFilename, versionedTarballFilename } from './packedTarballUtils';
+
+const e2eTestsRoot = path.resolve(__dirname, '..');
+const repositoryRoot = path.resolve(e2eTestsRoot, '../..');
+const packedDir = path.join(e2eTestsRoot, 'packed');
+
+function readJson(filePath: string): T {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
+}
+
+/**
+ * Ensures `packed/-packed.tgz` symlinks point at the current versioned tarballs under `packages/*`.
+ * Run after `yarn build:tarball` at the repo root (or from CI after restoring the tarball cache).
+ */
+export function syncPackedTarballSymlinks(): void {
+ const { version } = readJson<{ version: string }>(path.join(e2eTestsRoot, 'package.json'));
+
+ fs.mkdirSync(packedDir, { recursive: true });
+
+ for (const entry of fs.readdirSync(packedDir, { withFileTypes: true })) {
+ if (!entry.name.endsWith('-packed.tgz')) {
+ continue;
+ }
+ fs.rmSync(path.join(packedDir, entry.name), { recursive: true, force: true });
+ }
+
+ const packageJsonPaths = globSync('packages/*/package.json', {
+ cwd: repositoryRoot,
+ absolute: true,
+ });
+
+ let linked = 0;
+ for (const packageJsonPath of packageJsonPaths) {
+ const pkg = readJson<{ name?: string }>(packageJsonPath);
+ const name = pkg.name;
+ if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) {
+ continue;
+ }
+
+ const packageDir = path.dirname(packageJsonPath);
+ const expectedTarball = path.join(packageDir, versionedTarballFilename(name, version));
+
+ if (!fs.existsSync(expectedTarball)) {
+ continue;
+ }
+
+ const linkName = packedSymlinkFilename(name);
+ const linkPath = path.join(packedDir, linkName);
+
+ fs.symlinkSync(expectedTarball, linkPath);
+ linked++;
+ }
+
+ if (linked === 0) {
+ throw new Error(
+ `No packed tarballs found for version ${version} under packages/*/. Run "yarn build:tarball" at the repository root.`,
+ );
+ }
+
+ console.log(`Linked ${linked} tarball symlinks in ${path.relative(repositoryRoot, packedDir) || 'packed'}.`);
+}
diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json
index 827dde43679f..25d87ad10a6e 100644
--- a/dev-packages/e2e-tests/package.json
+++ b/dev-packages/e2e-tests/package.json
@@ -7,16 +7,15 @@
"lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware",
"lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware",
"lint:ts": "tsc --noEmit",
- "test:e2e": "run-s test:validate-configuration test:validate-test-app-setups test:run",
+ "test:e2e": "run-s test:prepare test:validate test:run",
"test:run": "ts-node run.ts",
- "test:validate-configuration": "ts-node validate-verdaccio-configuration.ts",
- "test:validate-test-app-setups": "ts-node validate-test-app-setups.ts",
"test:prepare": "ts-node prepare.ts",
- "test:validate": "run-s test:validate-configuration test:validate-test-app-setups",
- "clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm",
+ "test:validate": "ts-node validate-packed-tarball-setup.ts",
+ "clean": "rimraf tmp node_modules packed && yarn clean:test-applications && yarn clean:pnpm",
"ci:build-matrix": "ts-node ./lib/getTestMatrix.ts",
"ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true",
"ci:copy-to-temp": "ts-node ./ciCopyToTemp.ts",
+ "ci:pnpm-overrides": "ts-node ./ciPnpmOverrides.ts",
"clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular,event-dumps}",
"clean:pnpm": "pnpm store prune"
},
@@ -27,8 +26,7 @@
"eslint-plugin-regexp": "^1.15.0",
"glob": "^13.0.6",
"rimraf": "^6.1.3",
- "ts-node": "10.9.2",
- "yaml": "2.8.3"
+ "ts-node": "10.9.2"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/prepare.ts b/dev-packages/e2e-tests/prepare.ts
index d887ef310502..049b727b1b21 100644
--- a/dev-packages/e2e-tests/prepare.ts
+++ b/dev-packages/e2e-tests/prepare.ts
@@ -1,18 +1,15 @@
/* eslint-disable no-console */
import * as dotenv from 'dotenv';
-import { registrySetup } from './registrySetup';
+import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks';
async function run(): Promise {
// Load environment variables from .env file locally
dotenv.config();
- try {
- registrySetup();
- } catch (error) {
- console.error(error);
- process.exit(1);
- }
+ syncPackedTarballSymlinks();
}
-// eslint-disable-next-line @typescript-eslint/no-floating-promises
-run();
+run().catch(error => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/dev-packages/e2e-tests/registrySetup.ts b/dev-packages/e2e-tests/registrySetup.ts
deleted file mode 100644
index 6c521e619f76..000000000000
--- a/dev-packages/e2e-tests/registrySetup.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/* eslint-disable no-console */
-import * as childProcess from 'child_process';
-import { TEST_REGISTRY_CONTAINER_NAME, VERDACCIO_VERSION } from './lib/constants';
-import { publishPackages } from './lib/publishPackages';
-
-// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines
-function groupCIOutput(groupTitle: string, fn: () => void): void {
- if (process.env.CI) {
- console.log(`::group::${groupTitle}`);
- fn();
- console.log('::endgroup::');
- } else {
- fn();
- }
-}
-
-export function registrySetup(): void {
- groupCIOutput('Test Registry Setup', () => {
- // Stop test registry container (Verdaccio) if it was already running
- childProcess.spawnSync('docker', ['stop', TEST_REGISTRY_CONTAINER_NAME], { stdio: 'ignore' });
- console.log('Stopped previously running test registry');
-
- // Start test registry (Verdaccio)
- const startRegistryProcessResult = childProcess.spawnSync(
- 'docker',
- [
- 'run',
- '--detach',
- '--rm',
- '--name',
- TEST_REGISTRY_CONTAINER_NAME,
- '-p',
- '4873:4873',
- '-v',
- `${__dirname}/verdaccio-config:/verdaccio/conf`,
- `verdaccio/verdaccio:${VERDACCIO_VERSION}`,
- ],
- { encoding: 'utf8', stdio: 'inherit' },
- );
-
- if (startRegistryProcessResult.status !== 0) {
- throw new Error('Start Registry Process failed.');
- }
-
- publishPackages();
- });
-
- console.log('');
- console.log('');
-}
diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts
index 443ccf806b73..c10934a81b8d 100644
--- a/dev-packages/e2e-tests/run.ts
+++ b/dev-packages/e2e-tests/run.ts
@@ -6,7 +6,8 @@ import { sync as globSync } from 'glob';
import { tmpdir } from 'os';
import { join, resolve } from 'path';
import { copyToTemp } from './lib/copyToTemp';
-import { registrySetup } from './registrySetup';
+import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks';
+import { addPnpmOverrides } from './lib/pnpmOverrides';
interface SentryTestVariant {
'build-command': string;
@@ -184,74 +185,70 @@ async function run(): Promise {
...envVarsToInject,
};
- try {
- console.log('Cleaning test-applications...');
- console.log('');
+ console.log('Syncing packed tarball symlinks...');
+ console.log('');
- if (!process.env.SKIP_REGISTRY) {
- registrySetup();
- }
+ syncPackedTarballSymlinks();
- await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname });
- await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname });
+ console.log('Cleaning test-applications...');
+ console.log('');
- const testAppPaths = appName ? [appName.trim()] : globSync('*', { cwd: `${__dirname}/test-applications/` });
+ await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname });
+ await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname });
- console.log(`Runnings tests for: ${testAppPaths.join(', ')}`);
- console.log('');
+ const testAppPaths = appName ? [appName.trim()] : globSync('*', { cwd: `${__dirname}/test-applications/` });
- for (const testAppPath of testAppPaths) {
- const originalPath = resolve('test-applications', testAppPath);
- const tmpDirPath = await mkdtemp(join(tmpdir(), `sentry-e2e-tests-${appName}-`));
+ console.log(`Runnings tests for: ${testAppPaths.join(', ')}`);
+ console.log('');
- await copyToTemp(originalPath, tmpDirPath);
- const cwd = tmpDirPath;
- // Resolve variant if needed
- const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel
- ? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath)
- : {
- buildCommand: 'pnpm test:build',
- assertCommand: 'pnpm test:assert',
- testLabel: testAppPath,
- };
+ const packedDirPath = resolve(__dirname, 'packed');
- // Print which variant we're using if found
- if (matchedVariantLabel) {
- console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`);
- }
+ for (const testAppPath of testAppPaths) {
+ const originalPath = resolve('test-applications', testAppPath);
+ const tmpDirPath = await mkdtemp(join(tmpdir(), `sentry-e2e-tests-${appName}-`));
- console.log(`Building ${testLabel} in ${tmpDirPath}...`);
- await asyncExec(`volta run ${buildCommand}`, { env, cwd });
-
- console.log(`Testing ${testLabel}...`);
- // Pass command as a string to support shell features (env vars, operators like &&)
- // This matches how buildCommand is handled for consistency
- // Properly quote test flags to preserve spaces and special characters
- const quotedTestFlags = testFlags.map(flag => {
- // If flag contains spaces or special shell characters, quote it
- if (
- flag.includes(' ') ||
- flag.includes('"') ||
- flag.includes("'") ||
- flag.includes('$') ||
- flag.includes('`')
- ) {
- // Escape single quotes and wrap in single quotes (safest for shell)
- return `'${flag.replace(/'/g, "'\\''")}'`;
- }
- return flag;
- });
- const testCommand = `volta run ${assertCommand}${quotedTestFlags.length > 0 ? ` ${quotedTestFlags.join(' ')}` : ''}`;
- await asyncExec(testCommand, { env, cwd });
+ await copyToTemp(originalPath, tmpDirPath);
+ await addPnpmOverrides(tmpDirPath, packedDirPath);
- // clean up (although this is tmp, still nice to do)
- await rm(tmpDirPath, { recursive: true });
+ const cwd = tmpDirPath;
+ // Resolve variant if needed
+ const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel
+ ? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath)
+ : {
+ buildCommand: 'pnpm test:build',
+ assertCommand: 'pnpm test:assert',
+ testLabel: testAppPath,
+ };
+
+ // Print which variant we're using if found
+ if (matchedVariantLabel) {
+ console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`);
}
- } catch (error) {
- console.error(error);
- process.exit(1);
+
+ console.log(`Building ${testLabel} in ${tmpDirPath}...`);
+ await asyncExec(`volta run ${buildCommand}`, { env, cwd });
+
+ console.log(`Testing ${testLabel}...`);
+ // Pass command as a string to support shell features (env vars, operators like &&)
+ // This matches how buildCommand is handled for consistency
+ // Properly quote test flags to preserve spaces and special characters
+ const quotedTestFlags = testFlags.map(flag => {
+ // If flag contains spaces or special shell characters, quote it
+ if (flag.includes(' ') || flag.includes('"') || flag.includes("'") || flag.includes('$') || flag.includes('`')) {
+ // Escape single quotes and wrap in single quotes (safest for shell)
+ return `'${flag.replace(/'/g, "'\\''")}'`;
+ }
+ return flag;
+ });
+ const testCommand = `volta run ${assertCommand}${quotedTestFlags.length > 0 ? ` ${quotedTestFlags.join(' ')}` : ''}`;
+ await asyncExec(testCommand, { env, cwd });
+
+ // clean up (although this is tmp, still nice to do)
+ await rm(tmpDirPath, { recursive: true });
}
}
-// eslint-disable-next-line @typescript-eslint/no-floating-promises
-run();
+run().catch(error => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json
index 0d27f73f94e8..8cd141645d15 100644
--- a/dev-packages/e2e-tests/test-applications/angular-17/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
- "@sentry/angular": "* || latest",
+ "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
@@ -31,7 +31,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@angular-devkit/build-angular": "^17.1.1",
"@angular/cli": "^17.1.1",
"@angular/compiler-cli": "^17.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-18/package.json b/dev-packages/e2e-tests/test-applications/angular-18/package.json
index a32d3f5de99f..06cd30bc5ac5 100644
--- a/dev-packages/e2e-tests/test-applications/angular-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
- "@sentry/angular": "* || latest",
+ "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
@@ -31,7 +31,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@angular-devkit/build-angular": "^18.0.0",
"@angular/cli": "^18.0.0",
"@angular/compiler-cli": "^18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json
index 1e02f440b0a9..2b6c81fb7a20 100644
--- a/dev-packages/e2e-tests/test-applications/angular-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
- "@sentry/angular": "* || latest",
+ "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -34,7 +34,7 @@
"@angular/compiler-cli": "^19.0.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.4.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.npmrc b/dev-packages/e2e-tests/test-applications/angular-20/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/angular-20/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json
index fbf27580ed67..922964eed0c1 100644
--- a/dev-packages/e2e-tests/test-applications/angular-20/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "^20.0.0",
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/router": "^20.0.0",
- "@sentry/angular": "* || latest",
+ "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -34,7 +34,7 @@
"@angular/compiler-cli": "^20.0.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.4.0",
diff --git a/dev-packages/e2e-tests/test-applications/angular-21/.npmrc b/dev-packages/e2e-tests/test-applications/angular-21/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/angular-21/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/angular-21/package.json b/dev-packages/e2e-tests/test-applications/angular-21/package.json
index 0558d370e2c5..b7401593849f 100644
--- a/dev-packages/e2e-tests/test-applications/angular-21/package.json
+++ b/dev-packages/e2e-tests/test-applications/angular-21/package.json
@@ -24,7 +24,7 @@
"@angular/platform-browser": "^21.0.0",
"@angular/platform-browser-dynamic": "^21.0.0",
"@angular/router": "^21.0.0",
- "@sentry/angular": "* || latest",
+ "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -35,7 +35,7 @@
"@angular/compiler-cli": "^21.0.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@types/jasmine": "~5.1.0",
"http-server": "^14.1.1",
"jasmine-core": "~5.4.0",
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/.npmrc b/dev-packages/e2e-tests/test-applications/astro-4/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/astro-4/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json
index 339f5fd18c7d..0afd444f6d3d 100644
--- a/dev-packages/e2e-tests/test-applications/astro-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json
@@ -15,7 +15,7 @@
"@astrojs/check": "0.9.2",
"@astrojs/node": "8.3.4",
"@playwright/test": "~1.56.0",
- "@sentry/astro": "* || latest",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@spotlightjs/astro": "2.1.6",
"astro": "4.16.19",
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json
index 400ab6144248..fabe0ce2333b 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/package.json
@@ -13,8 +13,8 @@
"@astrojs/cloudflare": "^12.6.12",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/astro": "latest || *",
- "@sentry/cloudflare": "latest || *",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
"astro": "^5.17.1"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/.npmrc b/dev-packages/e2e-tests/test-applications/astro-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/astro-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json
index f42b22b3d07f..268ce9ed82ca 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json
@@ -16,7 +16,7 @@
"@astrojs/node": "^9.0.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/astro": "latest || *",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
"astro": "^5.0.3"
},
"pnpm": {
diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/.npmrc b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json
index 2e7cfa8e62dd..4869975f7519 100644
--- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/package.json
@@ -16,8 +16,8 @@
"@astrojs/cloudflare": "^13.0.2",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/astro": "latest || *",
- "@sentry/cloudflare": "latest || *",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
"astro": "^6.0.0",
"wrangler": "^4.72.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/astro-6/.npmrc b/dev-packages/e2e-tests/test-applications/astro-6/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/astro-6/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/astro-6/package.json b/dev-packages/e2e-tests/test-applications/astro-6/package.json
index 050ea8980e06..37f61a774e32 100644
--- a/dev-packages/e2e-tests/test-applications/astro-6/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-6/package.json
@@ -15,7 +15,7 @@
"@astrojs/node": "^10.0.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/astro": "latest || *",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
"astro": "^6.0.6"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json
new file mode 100644
index 000000000000..16acf393e5d4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "aws-serverless-layer",
+ "version": "1.0.0",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:pull-sam-image": "./pull-sam-image.sh",
+ "test:build": "pnpm test:pull-sam-image && pnpm install && npx rimraf node_modules/@sentry/aws-serverless/nodejs",
+ "test:assert": "pnpm test"
+ },
+ "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package",
+ "devDependencies": {
+ "@aws-sdk/client-lambda": "^3.863.0",
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/",
+ "aws-cdk-lib": "^2.210.0",
+ "constructs": "^10.4.2",
+ "glob": "^11.0.3",
+ "rimraf": "^5.0.10"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "//": "We need to override this here again to ensure this is not overwritten by the test runner",
+ "pnpm": {
+ "overrides": {
+ "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/"
+ }
+ },
+ "sentryTest": {
+ "variants": [
+ {
+ "build-command": "NODE_VERSION=22 pnpm test:build",
+ "assert-command": "NODE_VERSION=22 pnpm test:assert",
+ "label": "aws-serverless-layer (Node 22)"
+ },
+ {
+ "build-command": "NODE_VERSION=18 pnpm test:build",
+ "assert-command": "NODE_VERSION=18 pnpm test:assert",
+ "label": "aws-serverless-layer (Node 18)"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts
new file mode 100644
index 000000000000..e47333c66e76
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts
@@ -0,0 +1,5 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+export default getPlaywrightConfig(undefined, {
+ timeout: 60 * 1000 * 3, // 3 minutes
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh
new file mode 100755
index 000000000000..c9659547a4ea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# Pull the Lambda Node docker image for SAM local. NODE_VERSION should be the major only (e.g. 20).
+# Defaults to 20 to match the repo's Volta Node major (see root package.json "volta.node").
+
+set -e
+
+NODE_VERSION="${NODE_VERSION:-20}"
+
+echo "Pulling Lambda Node $NODE_VERSION docker image..."
+docker pull "public.ecr.aws/lambda/nodejs:${NODE_VERSION}"
+
+echo "Successfully pulled Lambda Node $NODE_VERSION docker image"
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/samconfig.toml b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/samconfig.toml
new file mode 100644
index 000000000000..26f5a51c7a77
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/samconfig.toml
@@ -0,0 +1,10 @@
+# SAM CLI expects this file in the project root; without it, `sam local start-lambda` logs
+# OSError / missing file errors when run from the e2e temp directory.
+# These values are placeholders — this app only uses `sam local`, not deploy.
+version = 0.1
+
+[default.deploy.parameters]
+stack_name = "sentry-e2e-aws-serverless-layer-local"
+region = "us-east-1"
+confirm_changeset = false
+capabilities = "CAPABILITY_IAM"
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Error/index.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Error/index.js
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/ErrorEsm/index.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/ErrorEsm/index.mjs
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Streaming/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Streaming/index.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Streaming/index.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Streaming/index.mjs
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingCjs/index.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingCjs/index.js
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingEsm/index.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingEsm/index.mjs
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts
new file mode 100644
index 000000000000..8475ee0a328a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts
@@ -0,0 +1,124 @@
+import { Stack, CfnResource, StackProps } from 'aws-cdk-lib';
+import { Construct } from 'constructs';
+import * as path from 'node:path';
+import * as fs from 'node:fs';
+import * as os from 'node:os';
+import * as dns from 'node:dns/promises';
+import { arch, platform } from 'node:process';
+import { globSync } from 'glob';
+
+const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-layer';
+const LAMBDA_FUNCTION_TIMEOUT = 10;
+const LAYER_DIR = './node_modules/@sentry/aws-serverless/';
+export const SAM_PORT = 3001;
+
+/** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */
+function samLambdaArchitecture(): 'arm64' | 'x86_64' {
+ return arch === 'arm64' ? 'arm64' : 'x86_64';
+}
+
+export class LocalLambdaStack extends Stack {
+ sentryLayer: CfnResource;
+
+ constructor(scope: Construct, id: string, props: StackProps, hostIp: string) {
+ console.log('[LocalLambdaStack] Creating local SAM Lambda Stack');
+ super(scope, id, props);
+
+ this.templateOptions.templateFormatVersion = '2010-09-09';
+ this.templateOptions.transforms = ['AWS::Serverless-2016-10-31'];
+
+ console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack');
+
+ const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR });
+
+ if (!layerZipFile) {
+ throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`);
+ }
+
+ this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', {
+ type: 'AWS::Serverless::LayerVersion',
+ properties: {
+ ContentUri: path.join(LAYER_DIR, layerZipFile),
+ CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'],
+ CompatibleArchitectures: [samLambdaArchitecture()],
+ },
+ });
+
+ const dsn = `http://public@${hostIp}:3031/1337`;
+ console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`);
+
+ this.addLambdaFunctions(dsn);
+ }
+
+ private addLambdaFunctions(dsn: string) {
+ console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${LAMBDA_FUNCTIONS_DIR} to the SAM stack`);
+
+ const lambdaDirs = fs
+ .readdirSync(LAMBDA_FUNCTIONS_DIR)
+ .filter(dir => fs.statSync(path.join(LAMBDA_FUNCTIONS_DIR, dir)).isDirectory());
+
+ for (const lambdaDir of lambdaDirs) {
+ const functionName = `Layer${lambdaDir}`;
+
+ if (!process.env.NODE_VERSION) {
+ throw new Error('[LocalLambdaStack] NODE_VERSION is not set');
+ }
+
+ new CfnResource(this, functionName, {
+ type: 'AWS::Serverless::Function',
+ properties: {
+ Architectures: [samLambdaArchitecture()],
+ CodeUri: path.join(LAMBDA_FUNCTIONS_DIR, lambdaDir),
+ Handler: 'index.handler',
+ Runtime: `nodejs${process.env.NODE_VERSION}.x`,
+ Timeout: LAMBDA_FUNCTION_TIMEOUT,
+ Layers: [{ Ref: this.sentryLayer.logicalId }],
+ Environment: {
+ Variables: {
+ SENTRY_DSN: dsn,
+ SENTRY_TRACES_SAMPLE_RATE: 1.0,
+ SENTRY_DEBUG: true,
+ NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`,
+ },
+ },
+ },
+ });
+
+ console.log(`[LocalLambdaStack] Added Lambda function: ${functionName}`);
+ }
+ }
+
+ static async waitForStack(timeout = 60000, port = SAM_PORT) {
+ const startTime = Date.now();
+ const maxWaitTime = timeout;
+
+ while (Date.now() - startTime < maxWaitTime) {
+ try {
+ const response = await fetch(`http://127.0.0.1:${port}/`);
+
+ if (response.ok || response.status === 404) {
+ console.log(`[LocalLambdaStack] SAM stack is ready`);
+ return;
+ }
+ } catch {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ }
+
+ throw new Error(`[LocalLambdaStack] Failed to start SAM stack after ${timeout}ms`);
+ }
+}
+
+export async function getHostIp() {
+ if (process.env.GITHUB_ACTIONS) {
+ const host = await dns.lookup(os.hostname());
+ return host.address;
+ }
+
+ if (platform === 'darwin' || platform === 'win32') {
+ return 'host.docker.internal';
+ }
+
+ const host = await dns.lookup(os.hostname());
+ return host.address;
+}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs
new file mode 100644
index 000000000000..dfc97cb7f3f3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'aws-serverless-layer',
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts
new file mode 100644
index 000000000000..4df52b322d26
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts
@@ -0,0 +1,137 @@
+import { test as base, expect } from '@playwright/test';
+import { App } from 'aws-cdk-lib';
+import { LocalLambdaStack, SAM_PORT, getHostIp } from '../src/stack';
+import { writeFileSync } from 'node:fs';
+import { execSync, spawn } from 'node:child_process';
+import { LambdaClient } from '@aws-sdk/client-lambda';
+
+const DOCKER_NETWORK_NAME = 'lambda-test-network';
+const SAM_TEMPLATE_FILE = 'sam.template.yml';
+
+/** Major Node for SAM `--invoke-image`; default matches root `package.json` `volta.node` and `pull-sam-image.sh`. */
+const DEFAULT_NODE_VERSION_MAJOR = '20';
+
+const SAM_INSTALL_ERROR =
+ 'You need to install sam, e.g. run `brew install aws-sam-cli`. Ensure `sam` is on your PATH when running tests.';
+
+export { expect };
+
+export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClient: LambdaClient }>({
+ testEnvironment: [
+ async ({}, use) => {
+ console.log('[testEnvironment fixture] Setting up AWS Lambda test infrastructure');
+
+ const nodeVersionMajor = process.env.NODE_VERSION?.trim() || DEFAULT_NODE_VERSION_MAJOR;
+ process.env.NODE_VERSION = nodeVersionMajor;
+
+ assertSamOnPath();
+
+ execSync('docker network prune -f');
+ createDockerNetwork();
+
+ const hostIp = await getHostIp();
+ const app = new App();
+
+ const stack = new LocalLambdaStack(app, 'LocalLambdaStack', {}, hostIp);
+ const template = app.synth().getStackByName('LocalLambdaStack').template;
+ writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2));
+
+ const args = [
+ 'local',
+ 'start-lambda',
+ '--debug',
+ '--template',
+ SAM_TEMPLATE_FILE,
+ '--warm-containers',
+ 'EAGER',
+ '--docker-network',
+ DOCKER_NETWORK_NAME,
+ '--skip-pull-image',
+ '--invoke-image',
+ `public.ecr.aws/lambda/nodejs:${nodeVersionMajor}`,
+ ];
+
+ console.log(`[testEnvironment fixture] Running SAM with args: ${args.join(' ')}`);
+
+ const samProcess = spawn('sam', args, {
+ stdio: process.env.DEBUG ? 'inherit' : 'ignore',
+ env: envForSamChild(),
+ });
+
+ try {
+ await LocalLambdaStack.waitForStack();
+
+ await use(stack);
+ } finally {
+ console.log('[testEnvironment fixture] Tearing down AWS Lambda test infrastructure');
+
+ samProcess.kill('SIGTERM');
+ await new Promise(resolve => {
+ samProcess.once('exit', resolve);
+ setTimeout(() => {
+ if (!samProcess.killed) {
+ samProcess.kill('SIGKILL');
+ }
+ resolve(void 0);
+ }, 5000);
+ });
+
+ removeDockerNetwork();
+ }
+ },
+ { scope: 'worker', auto: true },
+ ],
+ lambdaClient: async ({}, use) => {
+ const lambdaClient = new LambdaClient({
+ endpoint: `http://127.0.0.1:${SAM_PORT}`,
+ region: 'us-east-1',
+ credentials: {
+ accessKeyId: 'dummy',
+ secretAccessKey: 'dummy',
+ },
+ });
+
+ await use(lambdaClient);
+ },
+});
+
+/** Avoid forcing linux/amd64 on Apple Silicon when `DOCKER_DEFAULT_PLATFORM` is set globally. */
+function envForSamChild(): NodeJS.ProcessEnv {
+ const env = { ...process.env };
+ if (process.arch === 'arm64') {
+ delete env.DOCKER_DEFAULT_PLATFORM;
+ }
+ return env;
+}
+
+function assertSamOnPath(): void {
+ try {
+ execSync('sam --version', { encoding: 'utf-8', stdio: 'pipe' });
+ } catch {
+ throw new Error(SAM_INSTALL_ERROR);
+ }
+}
+
+function createDockerNetwork() {
+ try {
+ execSync(`docker network create --driver bridge ${DOCKER_NETWORK_NAME}`);
+ } catch (error) {
+ const stderr = (error as { stderr?: Buffer }).stderr?.toString() ?? '';
+ if (stderr.includes('already exists')) {
+ console.log(`[testEnvironment fixture] Reusing existing docker network ${DOCKER_NETWORK_NAME}`);
+ return;
+ }
+ throw error;
+ }
+}
+
+function removeDockerNetwork() {
+ try {
+ execSync(`docker network rm ${DOCKER_NETWORK_NAME}`);
+ } catch (error) {
+ const stderr = (error as { stderr?: Buffer }).stderr?.toString() ?? '';
+ if (!stderr.includes('No such network')) {
+ console.warn(`[testEnvironment fixture] Failed to remove docker network ${DOCKER_NETWORK_NAME}: ${stderr}`);
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts
similarity index 96%
rename from dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts
index 966ddf032218..c32dbfea7435 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts
@@ -4,7 +4,7 @@ import { test, expect } from './lambda-fixtures';
test.describe('Lambda layer', () => {
test('tracing in CJS works', async ({ lambdaClient }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-layer', transactionEvent => {
return transactionEvent?.transaction === 'LayerTracingCjs';
});
@@ -72,7 +72,7 @@ test.describe('Lambda layer', () => {
});
test('tracing in ESM works', async ({ lambdaClient }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-layer', transactionEvent => {
return transactionEvent?.transaction === 'LayerTracingEsm';
});
@@ -140,7 +140,7 @@ test.describe('Lambda layer', () => {
});
test('capturing errors works', async ({ lambdaClient }) => {
- const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => {
+ const errorEventPromise = waitForError('aws-serverless-layer', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'test';
});
@@ -168,7 +168,7 @@ test.describe('Lambda layer', () => {
});
test('capturing errors works in ESM', async ({ lambdaClient }) => {
- const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => {
+ const errorEventPromise = waitForError('aws-serverless-layer', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'test esm';
});
@@ -196,7 +196,7 @@ test.describe('Lambda layer', () => {
});
test('streaming handlers work', async ({ lambdaClient }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-layer', transactionEvent => {
return transactionEvent?.transaction === 'LayerStreaming';
});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/.npmrc b/dev-packages/e2e-tests/test-applications/aws-serverless/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json
index a3d164e15813..f74e7a670c50 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json
@@ -1,42 +1,25 @@
{
- "name": "aws-lambda-sam",
+ "name": "aws-serverless",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
- "test:build": "pnpm install && npx rimraf node_modules/@sentry/aws-serverless/nodejs",
+ "test:pull-sam-image": "./pull-sam-image.sh",
+ "test:build": "pnpm test:pull-sam-image && pnpm install",
"test:assert": "pnpm test"
},
- "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package",
"devDependencies": {
"@aws-sdk/client-lambda": "^3.863.0",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/",
- "@types/tmp": "^0.2.6",
+ "@sentry/aws-serverless": "file:../../packed/sentry-aws-serverless-packed.tgz",
"aws-cdk-lib": "^2.210.0",
"constructs": "^10.4.2",
- "glob": "^11.0.3",
- "rimraf": "^5.0.10",
- "tmp": "^0.2.5"
+ "rimraf": "^5.0.10"
},
"volta": {
"extends": "../../package.json"
- },
- "sentryTest": {
- "variants": [
- {
- "build-command": "NODE_VERSION=20 ./pull-sam-image.sh && pnpm test:build",
- "assert-command": "NODE_VERSION=20 pnpm test:assert",
- "label": "aws-serverless (Node 20)"
- },
- {
- "build-command": "NODE_VERSION=18 ./pull-sam-image.sh && pnpm test:build",
- "assert-command": "NODE_VERSION=18 pnpm test:assert",
- "label": "aws-serverless (Node 18)"
- }
- ]
}
}
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh b/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh
index d6790c2c2c49..c9659547a4ea 100755
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh
@@ -1,13 +1,11 @@
#!/bin/bash
-# Script to pull the correct Lambda docker image based on the NODE_VERSION environment variable.
+# Pull the Lambda Node docker image for SAM local. NODE_VERSION should be the major only (e.g. 20).
+# Defaults to 20 to match the repo's Volta Node major (see root package.json "volta.node").
set -e
-if [[ -z "$NODE_VERSION" ]]; then
- echo "Error: NODE_VERSION not set"
- exit 1
-fi
+NODE_VERSION="${NODE_VERSION:-20}"
echo "Pulling Lambda Node $NODE_VERSION docker image..."
docker pull "public.ecr.aws/lambda/nodejs:${NODE_VERSION}"
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml b/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml
new file mode 100644
index 000000000000..6771bf7d7900
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml
@@ -0,0 +1,10 @@
+# SAM CLI expects this file in the project root; without it, `sam local start-lambda` logs
+# OSError / missing file errors when run from the e2e temp directory.
+# These values are placeholders — this app only uses `sam local`, not deploy.
+version = 0.1
+
+[default.deploy.parameters]
+stack_name = "sentry-e2e-aws-serverless-local"
+region = "us-east-1"
+confirm_changeset = false
+capabilities = "CAPABILITY_IAM"
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts
index 63463c914e1d..63ab8a533e70 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts
@@ -4,17 +4,18 @@ import * as path from 'node:path';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as dns from 'node:dns/promises';
-import { platform } from 'node:process';
-import { globSync } from 'glob';
+import { arch, platform } from 'node:process';
import { execFileSync } from 'node:child_process';
-const LAMBDA_FUNCTIONS_WITH_LAYER_DIR = './src/lambda-functions-layer';
-const LAMBDA_FUNCTIONS_WITH_NPM_DIR = './src/lambda-functions-npm';
+const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-npm';
const LAMBDA_FUNCTION_TIMEOUT = 10;
-const LAYER_DIR = './node_modules/@sentry/aws-serverless/';
-const DEFAULT_NODE_VERSION = '22';
export const SAM_PORT = 3001;
+/** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */
+function samLambdaArchitecture(): 'arm64' | 'x86_64' {
+ return arch === 'arm64' ? 'arm64' : 'x86_64';
+}
+
function resolvePackagesDir(): string {
// When running via the e2e test runner, tests are copied to a temp directory
// so we need the workspace root passed via env var
@@ -27,8 +28,6 @@ function resolvePackagesDir(): string {
}
export class LocalLambdaStack extends Stack {
- sentryLayer: CfnResource;
-
constructor(scope: Construct, id: string, props: StackProps, hostIp: string) {
console.log('[LocalLambdaStack] Creating local SAM Lambda Stack');
super(scope, id, props);
@@ -36,100 +35,70 @@ export class LocalLambdaStack extends Stack {
this.templateOptions.templateFormatVersion = '2010-09-09';
this.templateOptions.transforms = ['AWS::Serverless-2016-10-31'];
- console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack');
-
- const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR });
-
- if (!layerZipFile) {
- throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`);
- }
-
- this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', {
- type: 'AWS::Serverless::LayerVersion',
- properties: {
- ContentUri: path.join(LAYER_DIR, layerZipFile),
- CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'],
- },
- });
-
const dsn = `http://public@${hostIp}:3031/1337`;
console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`);
- this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_LAYER_DIR, dsn, addLayer: true });
- this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_NPM_DIR, dsn, addLayer: false });
+ this.addLambdaFunctions(dsn);
}
- private addLambdaFunctions({
- functionsDir,
- dsn,
- addLayer,
- }: {
- functionsDir: string;
- dsn: string;
- addLayer: boolean;
- }) {
- console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${functionsDir} to the SAM stack`);
+ private addLambdaFunctions(dsn: string) {
+ console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${LAMBDA_FUNCTIONS_DIR} to the SAM stack`);
const lambdaDirs = fs
- .readdirSync(functionsDir)
- .filter(dir => fs.statSync(path.join(functionsDir, dir)).isDirectory());
+ .readdirSync(LAMBDA_FUNCTIONS_DIR)
+ .filter(dir => fs.statSync(path.join(LAMBDA_FUNCTIONS_DIR, dir)).isDirectory());
for (const lambdaDir of lambdaDirs) {
- const functionName = `${addLayer ? 'Layer' : 'Npm'}${lambdaDir}`;
-
- if (!addLayer) {
- const lambdaPath = path.resolve(functionsDir, lambdaDir);
- const packageLockPath = path.join(lambdaPath, 'package-lock.json');
- const nodeModulesPath = path.join(lambdaPath, 'node_modules');
-
- // Point the dependency at the locally built packages so tests use the current workspace bits
- // We need to link all @sentry/* packages that are dependencies of aws-serverless
- // because otherwise npm will try to install them from the registry, where the current version is not yet published
- const packagesToLink = ['aws-serverless', 'node', 'core', 'node-core', 'opentelemetry'];
- const dependencies: Record = {};
-
- const packagesDir = resolvePackagesDir();
- for (const pkgName of packagesToLink) {
- const pkgDir = path.join(packagesDir, pkgName);
- if (!fs.existsSync(pkgDir)) {
- throw new Error(
- `[LocalLambdaStack] Workspace package ${pkgName} not found at ${pkgDir}. Did you run the build?`,
- );
- }
- const relativePath = path.relative(lambdaPath, pkgDir);
- dependencies[`@sentry/${pkgName}`] = `file:${relativePath.replace(/\\/g, '/')}`;
+ const functionName = `Npm${lambdaDir}`;
+
+ const lambdaPath = path.resolve(LAMBDA_FUNCTIONS_DIR, lambdaDir);
+ const packageLockPath = path.join(lambdaPath, 'package-lock.json');
+ const nodeModulesPath = path.join(lambdaPath, 'node_modules');
+
+ const packagesToLink = ['aws-serverless', 'node', 'core', 'node-core', 'opentelemetry'];
+ const dependencies: Record = {};
+
+ const packagesDir = resolvePackagesDir();
+ for (const pkgName of packagesToLink) {
+ const pkgDir = path.join(packagesDir, pkgName);
+ if (!fs.existsSync(pkgDir)) {
+ throw new Error(
+ `[LocalLambdaStack] Workspace package ${pkgName} not found at ${pkgDir}. Did you run the build?`,
+ );
}
+ const relativePath = path.relative(lambdaPath, pkgDir);
+ dependencies[`@sentry/${pkgName}`] = `file:${relativePath.replace(/\\/g, '/')}`;
+ }
- console.log(`[LocalLambdaStack] Install dependencies for ${functionName}`);
+ console.log(`[LocalLambdaStack] Install dependencies for ${functionName}`);
- if (fs.existsSync(packageLockPath)) {
- // Prevent stale lock files from pinning the published package version
- fs.rmSync(packageLockPath);
- }
+ if (fs.existsSync(packageLockPath)) {
+ fs.rmSync(packageLockPath);
+ }
- if (fs.existsSync(nodeModulesPath)) {
- // Ensure we reinstall from the workspace instead of reusing cached dependencies
- fs.rmSync(nodeModulesPath, { recursive: true, force: true });
- }
+ if (fs.existsSync(nodeModulesPath)) {
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
+ }
+
+ const packageJson = {
+ dependencies,
+ };
- const packageJson = {
- dependencies,
- };
+ fs.writeFileSync(path.join(lambdaPath, 'package.json'), JSON.stringify(packageJson, null, 2));
+ execFileSync('npm', ['install', '--install-links', '--prefix', lambdaPath], { stdio: 'inherit' });
- fs.writeFileSync(path.join(lambdaPath, 'package.json'), JSON.stringify(packageJson, null, 2));
- // Use --install-links to copy files instead of creating symlinks for file: dependencies.
- // Symlinks don't work inside the Docker container because the target paths don't exist there.
- execFileSync('npm', ['install', '--install-links', '--prefix', lambdaPath], { stdio: 'inherit' });
+ if (!process.env.NODE_VERSION) {
+ throw new Error('[LocalLambdaStack] NODE_VERSION is not set');
}
new CfnResource(this, functionName, {
type: 'AWS::Serverless::Function',
properties: {
- CodeUri: path.join(functionsDir, lambdaDir),
+ Architectures: [samLambdaArchitecture()],
+ CodeUri: path.join(LAMBDA_FUNCTIONS_DIR, lambdaDir),
Handler: 'index.handler',
- Runtime: `nodejs${process.env.NODE_VERSION ?? DEFAULT_NODE_VERSION}.x`,
+ Runtime: `nodejs${process.env.NODE_VERSION}.x`,
Timeout: LAMBDA_FUNCTION_TIMEOUT,
- Layers: addLayer ? [{ Ref: this.sentryLayer.logicalId }] : undefined,
Environment: {
Variables: {
SENTRY_DSN: dsn,
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
index 196ae2471c69..4eff620fa17f 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'aws-serverless-lambda-sam',
+ proxyServerName: 'aws-serverless',
});
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts
index 23aab3a7d683..4df52b322d26 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts
@@ -1,14 +1,19 @@
import { test as base, expect } from '@playwright/test';
import { App } from 'aws-cdk-lib';
-import * as tmp from 'tmp';
import { LocalLambdaStack, SAM_PORT, getHostIp } from '../src/stack';
import { writeFileSync } from 'node:fs';
-import { spawn, execSync } from 'node:child_process';
+import { execSync, spawn } from 'node:child_process';
import { LambdaClient } from '@aws-sdk/client-lambda';
const DOCKER_NETWORK_NAME = 'lambda-test-network';
const SAM_TEMPLATE_FILE = 'sam.template.yml';
+/** Major Node for SAM `--invoke-image`; default matches root `package.json` `volta.node` and `pull-sam-image.sh`. */
+const DEFAULT_NODE_VERSION_MAJOR = '20';
+
+const SAM_INSTALL_ERROR =
+ 'You need to install sam, e.g. run `brew install aws-sam-cli`. Ensure `sam` is on your PATH when running tests.';
+
export { expect };
export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClient: LambdaClient }>({
@@ -16,6 +21,11 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
async ({}, use) => {
console.log('[testEnvironment fixture] Setting up AWS Lambda test infrastructure');
+ const nodeVersionMajor = process.env.NODE_VERSION?.trim() || DEFAULT_NODE_VERSION_MAJOR;
+ process.env.NODE_VERSION = nodeVersionMajor;
+
+ assertSamOnPath();
+
execSync('docker network prune -f');
createDockerNetwork();
@@ -26,11 +36,6 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
const template = app.synth().getStackByName('LocalLambdaStack').template;
writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2));
- const debugLog = tmp.fileSync({ prefix: 'sentry_aws_lambda_tests_sam_debug', postfix: '.log' });
- if (!process.env.CI) {
- console.log(`[test_environment fixture] Writing SAM debug log to: ${debugLog.name}`);
- }
-
const args = [
'local',
'start-lambda',
@@ -42,16 +47,15 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
'--docker-network',
DOCKER_NETWORK_NAME,
'--skip-pull-image',
+ '--invoke-image',
+ `public.ecr.aws/lambda/nodejs:${nodeVersionMajor}`,
];
- if (process.env.NODE_VERSION) {
- args.push('--invoke-image', `public.ecr.aws/lambda/nodejs:${process.env.NODE_VERSION}`);
- }
-
console.log(`[testEnvironment fixture] Running SAM with args: ${args.join(' ')}`);
const samProcess = spawn('sam', args, {
- stdio: process.env.CI ? 'inherit' : ['ignore', debugLog.fd, debugLog.fd],
+ stdio: process.env.DEBUG ? 'inherit' : 'ignore',
+ env: envForSamChild(),
});
try {
@@ -91,6 +95,23 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
},
});
+/** Avoid forcing linux/amd64 on Apple Silicon when `DOCKER_DEFAULT_PLATFORM` is set globally. */
+function envForSamChild(): NodeJS.ProcessEnv {
+ const env = { ...process.env };
+ if (process.arch === 'arm64') {
+ delete env.DOCKER_DEFAULT_PLATFORM;
+ }
+ return env;
+}
+
+function assertSamOnPath(): void {
+ try {
+ execSync('sam --version', { encoding: 'utf-8', stdio: 'pipe' });
+ } catch {
+ throw new Error(SAM_INSTALL_ERROR);
+ }
+}
+
function createDockerNetwork() {
try {
execSync(`docker network create --driver bridge ${DOCKER_NETWORK_NAME}`);
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts
index e5b6ee1b9f32..943d5a2ab0f3 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts
@@ -4,7 +4,7 @@ import { test, expect } from './lambda-fixtures';
test.describe('NPM package', () => {
test('tracing in CJS works', async ({ lambdaClient }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => {
return transactionEvent?.transaction === 'NpmTracingCjs';
});
@@ -72,7 +72,7 @@ test.describe('NPM package', () => {
});
test('tracing in ESM works', async ({ lambdaClient }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => {
return transactionEvent?.transaction === 'NpmTracingEsm';
});
diff --git a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/.npmrc b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-header/package.json b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-header/package.json
index 04bac1dafd66..34ffe98a592f 100644
--- a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-header/package.json
+++ b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-header/package.json
@@ -8,7 +8,7 @@
"preview": "vite preview --port 3032"
},
"dependencies": {
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../../../packed/sentry-browser-packed.tgz",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-one/package.json b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-one/package.json
index 6db9d42e97ac..b286b158b47c 100644
--- a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-one/package.json
+++ b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/mfe-one/package.json
@@ -8,7 +8,7 @@
"preview": "vite preview --port 3033"
},
"dependencies": {
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../../../packed/sentry-browser-packed.tgz",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/shell/package.json b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/shell/package.json
index a3437e079f7f..778b8439a635 100644
--- a/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/shell/package.json
+++ b/dev-packages/e2e-tests/test-applications/browser-mfe-vite/apps/shell/package.json
@@ -8,8 +8,8 @@
"preview": "vite preview --port 3030"
},
"dependencies": {
- "@sentry/browser": "latest || *",
- "@sentry/react": "latest || *",
+ "@sentry/browser": "file:../../../../packed/sentry-browser-packed.tgz",
+ "@sentry/react": "file:../../../../packed/sentry-react-packed.tgz",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.npmrc b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json
index f6aae0e57df3..1c391eb7cf5e 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
@@ -18,7 +18,7 @@
"vite": "^7.0.4"
},
"dependencies": {
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
"@sentry/vite-plugin": "^5.2.0"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-hono/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
index 3d536e6fbabe..b3b8695148fb 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm typecheck && vitest run ."
},
"dependencies": {
- "@sentry/cloudflare": "latest || *",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
"hono": "4.12.12"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json
index 160b8a9cdc03..7433244fc417 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json
@@ -15,7 +15,7 @@
"test:dev": "TEST_ENV=development playwright test"
},
"dependencies": {
- "@sentry/cloudflare": "latest || *"
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json
index 37b3352bdcfc..bae4b4ffd272 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.24.0",
- "@sentry/cloudflare": "latest || *",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
"agents": "0.3.10",
"zod": "^3.25.76"
},
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
index 344337612165..b8b028797805 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
@@ -15,7 +15,7 @@
"test:dev": "TEST_ENV=development playwright test"
},
"dependencies": {
- "@sentry/cloudflare": "latest || *"
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json
index a0a1b020df86..83ac7bce286d 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/package.json
@@ -15,7 +15,7 @@
"test:dev": "TEST_ENV=development playwright test"
},
"dependencies": {
- "@sentry/cloudflare": "latest || *"
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/.npmrc b/dev-packages/e2e-tests/test-applications/create-react-app/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-react-app/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/package.json b/dev-packages/e2e-tests/test-applications/create-react-app/package.json
index 0c2bc337d396..30f745d60bc7 100644
--- a/dev-packages/e2e-tests/test-applications/create-react-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-react-app/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
index d495d14019b6..a413c5c31ba0 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json
@@ -16,7 +16,7 @@
"@remix-run/express": "^2.17.4",
"@remix-run/node": "^2.17.4",
"@remix-run/react": "^2.17.4",
- "@sentry/remix": "latest || *",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"compression": "^1.7.4",
"express": "^4.21.2",
"isbot": "^4.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-express/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
index 20788a102bc1..2dc4f42dd34d 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json
@@ -17,7 +17,7 @@
"@remix-run/express": "^2.17.4",
"@remix-run/node": "^2.17.4",
"@remix-run/react": "^2.17.4",
- "@sentry/remix": "latest || *",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json
index 185099a6adfc..38a7e231ddf1 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm playwright test"
},
"dependencies": {
- "@sentry/remix": "latest || *",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@remix-run/css-bundle": "2.17.4",
"@remix-run/node": "2.17.4",
"@remix-run/react": "2.17.4",
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
index d31e86ff0cdc..fd57d2920d5a 100644
--- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm playwright test"
},
"dependencies": {
- "@sentry/remix": "latest || *",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@remix-run/css-bundle": "2.17.4",
"@remix-run/node": "2.17.4",
"@remix-run/react": "2.17.4",
diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json
index 4a0d90942367..d7599f0e332d 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
@@ -10,7 +10,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node": "latest || *"
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz"
},
"devDependencies": {
"rollup": "^4.35.0",
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/.npmrc b/dev-packages/e2e-tests/test-applications/default-browser/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/default-browser/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/package.json b/dev-packages/e2e-tests/test-applications/default-browser/package.json
index f181008e0427..6f3298f53285 100644
--- a/dev-packages/e2e-tests/test-applications/default-browser/package.json
+++ b/dev-packages/e2e-tests/test-applications/default-browser/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
"@types/node": "^18.19.1",
"typescript": "~5.0.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/deno-streamed/.npmrc b/dev-packages/e2e-tests/test-applications/deno-streamed/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/deno-streamed/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/deno-streamed/package.json b/dev-packages/e2e-tests/test-applications/deno-streamed/package.json
index 70a20db2de05..7bbaeaf631f8 100644
--- a/dev-packages/e2e-tests/test-applications/deno-streamed/package.json
+++ b/dev-packages/e2e-tests/test-applications/deno-streamed/package.json
@@ -10,7 +10,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/deno": "latest || *",
+ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
diff --git a/dev-packages/e2e-tests/test-applications/deno/.npmrc b/dev-packages/e2e-tests/test-applications/deno/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/deno/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/deno/package.json b/dev-packages/e2e-tests/test-applications/deno/package.json
index ff30a9304e53..d46cc08530c5 100644
--- a/dev-packages/e2e-tests/test-applications/deno/package.json
+++ b/dev-packages/e2e-tests/test-applications/deno/package.json
@@ -10,7 +10,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/deno": "latest || *",
+ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/.gitignore b/dev-packages/e2e-tests/test-applications/effect-3-browser/.gitignore
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/.gitignore
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/.gitignore
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/build.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/build.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/build.mjs
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/build.mjs
diff --git a/dev-packages/e2e-tests/test-applications/effect-3-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-3-browser/package.json
new file mode 100644
index 000000000000..284c8f5ef9b2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "effect-3-browser-test-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "start": "serve -s build",
+ "build": "node build.mjs",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/effect": "file:../../packed/sentry-effect-packed.tgz",
+ "@types/node": "^18.19.1",
+ "effect": "^3.19.19",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "webpack": "^5.91.0",
+ "serve": "14.0.1",
+ "terser-webpack-plugin": "^5.3.10",
+ "html-webpack-plugin": "^5.6.0"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/playwright.config.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/playwright.config.mjs
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/playwright.config.mjs
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/public/index.html b/dev-packages/e2e-tests/test-applications/effect-3-browser/public/index.html
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/public/index.html
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/public/index.html
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/src/index.js b/dev-packages/e2e-tests/test-applications/effect-3-browser/src/index.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/src/index.js
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/src/index.js
diff --git a/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs
new file mode 100644
index 000000000000..6da20fa0890e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'effect-3-browser',
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts
similarity index 87%
rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts
index 80589f683c28..bca922963ee1 100644
--- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/errors.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('captures an error', async ({ page }) => {
- const errorEventPromise = waitForError('effect-browser', event => {
+ const errorEventPromise = waitForError('effect-3-browser', event => {
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
});
@@ -29,11 +29,11 @@ test('captures an error', async ({ page }) => {
});
test('sets correct transactionName', async ({ page }) => {
- const transactionPromise = waitForTransaction('effect-browser', async transactionEvent => {
+ const transactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
- const errorEventPromise = waitForError('effect-browser', event => {
+ const errorEventPromise = waitForError('effect-3-browser', event => {
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
});
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts
similarity index 90%
rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts
index f81bc249cbd8..7857b7f9a156 100644
--- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/logs.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/logs.test.ts
@@ -3,7 +3,7 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
import type { SerializedLogContainer } from '@sentry/core';
test('should send Effect debug logs', async ({ page }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -26,7 +26,7 @@ test('should send Effect debug logs', async ({ page }) => {
});
test('should send Effect info logs', async ({ page }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -49,7 +49,7 @@ test('should send Effect info logs', async ({ page }) => {
});
test('should send Effect warning logs', async ({ page }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -72,7 +72,7 @@ test('should send Effect warning logs', async ({ page }) => {
});
test('should send Effect error logs', async ({ page }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -95,7 +95,7 @@ test('should send Effect error logs', async ({ page }) => {
});
test('should send Effect logs with context attributes', async ({ page }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-browser', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-browser', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context')
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts
similarity index 90%
rename from dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts
index b7c60b488403..db2a1dc352a8 100644
--- a/dev-packages/e2e-tests/test-applications/effect-browser/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-browser/tests/transactions.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
test('captures a pageload transaction', async ({ page }) => {
- const transactionPromise = waitForTransaction('effect-browser', async transactionEvent => {
+ const transactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
@@ -49,11 +49,11 @@ test('captures a pageload transaction', async ({ page }) => {
});
test('captures a navigation transaction', async ({ page }) => {
- const pageLoadTransactionPromise = waitForTransaction('effect-browser', async transactionEvent => {
+ const pageLoadTransactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
- const navigationTransactionPromise = waitForTransaction('effect-browser', async transactionEvent => {
+ const navigationTransactionPromise = waitForTransaction('effect-3-browser', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -80,11 +80,11 @@ test('captures a navigation transaction', async ({ page }) => {
});
test('captures Effect spans with correct parent-child structure', async ({ page }) => {
- const pageloadPromise = waitForTransaction('effect-browser', transactionEvent => {
+ const pageloadPromise = waitForTransaction('effect-3-browser', transactionEvent => {
return transactionEvent?.contexts?.trace?.op === 'pageload';
});
- const transactionPromise = waitForTransaction('effect-browser', transactionEvent => {
+ const transactionPromise = waitForTransaction('effect-3-browser', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'ui.action.click' &&
transactionEvent.spans?.some(span => span.description === 'custom-effect-span')
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-3-browser/tsconfig.json
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-browser/tsconfig.json
rename to dev-packages/e2e-tests/test-applications/effect-3-browser/tsconfig.json
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/.gitignore b/dev-packages/e2e-tests/test-applications/effect-3-node/.gitignore
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-node/.gitignore
rename to dev-packages/e2e-tests/test-applications/effect-3-node/.gitignore
diff --git a/dev-packages/e2e-tests/test-applications/angular-17/.npmrc b/dev-packages/e2e-tests/test-applications/effect-3-node/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/angular-17/.npmrc
rename to dev-packages/e2e-tests/test-applications/effect-3-node/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/package.json b/dev-packages/e2e-tests/test-applications/effect-3-node/package.json
similarity index 95%
rename from dev-packages/e2e-tests/test-applications/effect-node/package.json
rename to dev-packages/e2e-tests/test-applications/effect-3-node/package.json
index 621a017d3020..43f9bee85306 100644
--- a/dev-packages/e2e-tests/test-applications/effect-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/package.json
@@ -1,5 +1,5 @@
{
- "name": "effect-node-app",
+ "name": "effect-3-node-app",
"version": "1.0.0",
"private": true,
"type": "module",
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-3-node/playwright.config.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-node/playwright.config.mjs
rename to dev-packages/e2e-tests/test-applications/effect-3-node/playwright.config.mjs
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-node/src/app.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs
similarity index 75%
rename from dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs
rename to dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs
index a86a1bd91404..d74e61dc653b 100644
--- a/dev-packages/e2e-tests/test-applications/effect-browser/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'effect-browser',
+ proxyServerName: 'effect-3-node',
});
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts
similarity index 86%
rename from dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts
index 3b7da230c0e0..848ffcfb8117 100644
--- a/dev-packages/e2e-tests/test-applications/effect-node/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/errors.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
test('Captures manually reported error', async ({ baseURL }) => {
- const errorEventPromise = waitForError('effect-node', event => {
+ const errorEventPromise = waitForError('effect-3-node', event => {
return !event.type && event.exception?.values?.[0]?.value === 'This is an error';
});
@@ -17,7 +17,7 @@ test('Captures manually reported error', async ({ baseURL }) => {
});
test('Captures thrown exception', async ({ baseURL }) => {
- const errorEventPromise = waitForError('effect-node', event => {
+ const errorEventPromise = waitForError('effect-3-node', event => {
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
});
@@ -30,7 +30,7 @@ test('Captures thrown exception', async ({ baseURL }) => {
});
test('Captures Effect.fail as error', async ({ baseURL }) => {
- const errorEventPromise = waitForError('effect-node', event => {
+ const errorEventPromise = waitForError('effect-3-node', event => {
return !event.type && event.exception?.values?.[0]?.value === 'Effect failure';
});
@@ -43,7 +43,7 @@ test('Captures Effect.fail as error', async ({ baseURL }) => {
});
test('Captures Effect.die as error', async ({ baseURL }) => {
- const errorEventPromise = waitForError('effect-node', event => {
+ const errorEventPromise = waitForError('effect-3-node', event => {
return !event.type && event.exception?.values?.[0]?.value?.includes('Effect defect');
});
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts
similarity index 88%
rename from dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts
index 85f5840e14a8..2519f18722fd 100644
--- a/dev-packages/e2e-tests/test-applications/effect-node/tests/logs.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/logs.test.ts
@@ -3,7 +3,7 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
import type { SerializedLogContainer } from '@sentry/core';
test('should send Effect debug logs', async ({ baseURL }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -22,7 +22,7 @@ test('should send Effect debug logs', async ({ baseURL }) => {
});
test('should send Effect info logs', async ({ baseURL }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -41,7 +41,7 @@ test('should send Effect info logs', async ({ baseURL }) => {
});
test('should send Effect warning logs', async ({ baseURL }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -60,7 +60,7 @@ test('should send Effect warning logs', async ({ baseURL }) => {
});
test('should send Effect error logs', async ({ baseURL }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(
@@ -79,7 +79,7 @@ test('should send Effect error logs', async ({ baseURL }) => {
});
test('should send Effect logs with context attributes', async ({ baseURL }) => {
- const logEnvelopePromise = waitForEnvelopeItem('effect-node', envelope => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-3-node', envelope => {
return (
envelope[0].type === 'log' &&
(envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context')
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts
similarity index 88%
rename from dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts
rename to dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts
index ed7a58fa28df..b9693b2af6df 100644
--- a/dev-packages/e2e-tests/test-applications/effect-node/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/transactions.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
test('Sends an HTTP transaction', async ({ baseURL }) => {
- const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => {
return transactionEvent?.transaction === 'http.server GET';
});
@@ -14,7 +14,7 @@ test('Sends an HTTP transaction', async ({ baseURL }) => {
});
test('Sends transaction with manual Effect span', async ({ baseURL }) => {
- const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => {
return (
transactionEvent?.transaction === 'http.server GET' &&
transactionEvent?.spans?.some(span => span.description === 'test-span')
@@ -36,7 +36,7 @@ test('Sends transaction with manual Effect span', async ({ baseURL }) => {
});
test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => {
- const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => {
return (
transactionEvent?.transaction === 'http.server GET' &&
transactionEvent?.spans?.some(span => span.description === 'custom-effect-span')
@@ -87,7 +87,7 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
});
test('Sends transaction for error route', async ({ baseURL }) => {
- const transactionEventPromise = waitForTransaction('effect-node', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('effect-3-node', transactionEvent => {
return transactionEvent?.transaction === 'http.server GET';
});
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-3-node/tsconfig.json
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/effect-node/tsconfig.json
rename to dev-packages/e2e-tests/test-applications/effect-3-node/tsconfig.json
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore b/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore
new file mode 100644
index 000000000000..bd66327c3b4a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/.gitignore
@@ -0,0 +1,28 @@
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+/dist
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/angular-18/.npmrc b/dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/angular-18/.npmrc
rename to dev-packages/e2e-tests/test-applications/effect-4-browser/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs
new file mode 100644
index 000000000000..63c63597d4fe
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/build.mjs
@@ -0,0 +1,52 @@
+import * as path from 'path';
+import * as url from 'url';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import TerserPlugin from 'terser-webpack-plugin';
+import webpack from 'webpack';
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+webpack(
+ {
+ entry: path.join(__dirname, 'src/index.js'),
+ output: {
+ path: path.join(__dirname, 'build'),
+ filename: 'app.js',
+ },
+ optimization: {
+ minimize: true,
+ minimizer: [new TerserPlugin()],
+ },
+ plugins: [
+ new webpack.EnvironmentPlugin(['E2E_TEST_DSN']),
+ new HtmlWebpackPlugin({
+ template: path.join(__dirname, 'public/index.html'),
+ }),
+ ],
+ performance: {
+ hints: false,
+ },
+ mode: 'production',
+ },
+ (err, stats) => {
+ if (err) {
+ console.error(err.stack || err);
+ if (err.details) {
+ console.error(err.details);
+ }
+ return;
+ }
+
+ const info = stats.toJson();
+
+ if (stats.hasErrors()) {
+ console.error(info.errors);
+ process.exit(1);
+ }
+
+ if (stats.hasWarnings()) {
+ console.warn(info.warnings);
+ process.exit(1);
+ }
+ },
+);
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
similarity index 90%
rename from dev-packages/e2e-tests/test-applications/effect-browser/package.json
rename to dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
index 6c2e7e63ced8..4baf797b1019 100644
--- a/dev-packages/e2e-tests/test-applications/effect-browser/package.json
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
@@ -1,5 +1,5 @@
{
- "name": "effect-browser-test-app",
+ "name": "effect-4-browser-test-app",
"version": "1.0.0",
"private": true,
"scripts": {
@@ -13,7 +13,7 @@
"dependencies": {
"@sentry/effect": "latest || *",
"@types/node": "^18.19.1",
- "effect": "^3.19.19",
+ "effect": "^4.0.0-beta.50",
"typescript": "~5.0.0"
},
"devDependencies": {
@@ -37,6 +37,7 @@
]
},
"volta": {
+ "node": "22.15.0",
"extends": "../../package.json"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html b/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html
new file mode 100644
index 000000000000..19d5c3d99a2f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/public/index.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Effect Browser App
+
+
+ Effect Browser E2E Test
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js b/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js
new file mode 100644
index 000000000000..1748b4200ce1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/src/index.js
@@ -0,0 +1,96 @@
+// @ts-check
+import * as Sentry from '@sentry/effect';
+import * as Logger from 'effect/Logger';
+import * as Layer from 'effect/Layer';
+import * as ManagedRuntime from 'effect/ManagedRuntime';
+import * as Tracer from 'effect/Tracer';
+import * as References from 'effect/References';
+import * as Effect from 'effect/Effect';
+
+const AppLayer = Layer.mergeAll(
+ Sentry.effectLayer({
+ dsn: process.env.E2E_TEST_DSN,
+ integrations: [
+ Sentry.browserTracingIntegration({
+ _experiments: { enableInteractions: true },
+ }),
+ ],
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+ environment: 'qa',
+ tunnel: 'http://localhost:3031',
+ enableLogs: true,
+ }),
+ Logger.layer([Sentry.SentryEffectLogger]),
+ Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer),
+ Layer.succeed(References.MinimumLogLevel, 'Debug'),
+);
+
+// v4 pattern: ManagedRuntime creates a long-lived runtime from the layer
+const runtime = ManagedRuntime.make(AppLayer);
+
+// Force layer to build immediately (synchronously) so Sentry initializes at page load
+Effect.runSync(runtime.contextEffect);
+
+const runEffect = fn => runtime.runPromise(fn());
+
+document.getElementById('exception-button')?.addEventListener('click', () => {
+ throw new Error('I am an error!');
+});
+
+document.getElementById('effect-span-button')?.addEventListener('click', async () => {
+ await runEffect(() =>
+ Effect.gen(function* () {
+ yield* Effect.sleep('50 millis');
+ yield* Effect.sleep('25 millis').pipe(Effect.withSpan('nested-span'));
+ }).pipe(Effect.withSpan('custom-effect-span', { kind: 'internal' })),
+ );
+ const el = document.getElementById('effect-span-result');
+ if (el) el.textContent = 'Span sent!';
+});
+
+document.getElementById('effect-fail-button')?.addEventListener('click', async () => {
+ try {
+ await runEffect(() => Effect.fail(new Error('Effect failure')));
+ } catch {
+ const el = document.getElementById('effect-fail-result');
+ if (el) el.textContent = 'Effect failed (expected)';
+ }
+});
+
+document.getElementById('effect-die-button')?.addEventListener('click', async () => {
+ try {
+ await runEffect(() => Effect.die('Effect defect'));
+ } catch {
+ const el = document.getElementById('effect-die-result');
+ if (el) el.textContent = 'Effect died (expected)';
+ }
+});
+
+document.getElementById('log-button')?.addEventListener('click', async () => {
+ await runEffect(() =>
+ Effect.gen(function* () {
+ yield* Effect.logDebug('Debug log from Effect');
+ yield* Effect.logInfo('Info log from Effect');
+ yield* Effect.logWarning('Warning log from Effect');
+ yield* Effect.logError('Error log from Effect');
+ }),
+ );
+ const el = document.getElementById('log-result');
+ if (el) el.textContent = 'Logs sent!';
+});
+
+document.getElementById('log-context-button')?.addEventListener('click', async () => {
+ await runEffect(() =>
+ Effect.logInfo('Log with context').pipe(
+ Effect.annotateLogs('userId', '12345'),
+ Effect.annotateLogs('action', 'test'),
+ ),
+ );
+ const el = document.getElementById('log-context-result');
+ if (el) el.textContent = 'Log with context sent!';
+});
+
+document.getElementById('navigation-link')?.addEventListener('click', () => {
+ document.getElementById('navigation-target')?.scrollIntoView({ behavior: 'smooth' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs
new file mode 100644
index 000000000000..04374ed614c6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'effect-4-browser',
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts
new file mode 100644
index 000000000000..25b5762390ad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/errors.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('captures an error', async ({ page }) => {
+ const errorEventPromise = waitForError('effect-4-browser', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+ expect(errorEvent.transaction).toBe('/');
+
+ expect(errorEvent.request).toEqual({
+ url: 'http://localhost:3030/',
+ headers: expect.any(Object),
+ });
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
+
+test('sets correct transactionName', async ({ page }) => {
+ const transactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const errorEventPromise = waitForError('effect-4-browser', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+ const transactionEvent = await transactionPromise;
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: transactionEvent.contexts?.trace?.trace_id,
+ span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts
new file mode 100644
index 000000000000..1026ed4ceeca
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/logs.test.ts
@@ -0,0 +1,116 @@
+import { expect, test } from '@playwright/test';
+import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
+import type { SerializedLogContainer } from '@sentry/core';
+
+test('should send Effect debug logs', async ({ page }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'debug' && item.body === 'Debug log from Effect',
+ )
+ );
+ });
+
+ await page.goto('/');
+ const logButton = page.locator('id=log-button');
+ await logButton.click();
+
+ await expect(page.locator('id=log-result')).toHaveText('Logs sent!');
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const debugLog = logs.find(log => log.level === 'debug' && log.body === 'Debug log from Effect');
+ expect(debugLog).toBeDefined();
+ expect(debugLog?.level).toBe('debug');
+});
+
+test('should send Effect info logs', async ({ page }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'info' && item.body === 'Info log from Effect',
+ )
+ );
+ });
+
+ await page.goto('/');
+ const logButton = page.locator('id=log-button');
+ await logButton.click();
+
+ await expect(page.locator('id=log-result')).toHaveText('Logs sent!');
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const infoLog = logs.find(log => log.level === 'info' && log.body === 'Info log from Effect');
+ expect(infoLog).toBeDefined();
+ expect(infoLog?.level).toBe('info');
+});
+
+test('should send Effect warning logs', async ({ page }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'warn' && item.body === 'Warning log from Effect',
+ )
+ );
+ });
+
+ await page.goto('/');
+ const logButton = page.locator('id=log-button');
+ await logButton.click();
+
+ await expect(page.locator('id=log-result')).toHaveText('Logs sent!');
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const warnLog = logs.find(log => log.level === 'warn' && log.body === 'Warning log from Effect');
+ expect(warnLog).toBeDefined();
+ expect(warnLog?.level).toBe('warn');
+});
+
+test('should send Effect error logs', async ({ page }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'error' && item.body === 'Error log from Effect',
+ )
+ );
+ });
+
+ await page.goto('/');
+ const logButton = page.locator('id=log-button');
+ await logButton.click();
+
+ await expect(page.locator('id=log-result')).toHaveText('Logs sent!');
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const errorLog = logs.find(log => log.level === 'error' && log.body === 'Error log from Effect');
+ expect(errorLog).toBeDefined();
+ expect(errorLog?.level).toBe('error');
+});
+
+test('should send Effect logs with context attributes', async ({ page }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-browser', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context')
+ );
+ });
+
+ await page.goto('/');
+ const logContextButton = page.locator('id=log-context-button');
+ await logContextButton.click();
+
+ await expect(page.locator('id=log-context-result')).toHaveText('Log with context sent!');
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const contextLog = logs.find(log => log.body === 'Log with context');
+ expect(contextLog).toBeDefined();
+ expect(contextLog?.level).toBe('info');
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts
new file mode 100644
index 000000000000..6bec97ca4d79
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tests/transactions.test.ts
@@ -0,0 +1,120 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('captures a pageload transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const pageLoadTransaction = await transactionPromise;
+
+ expect(pageLoadTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.browser',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'url',
+ }),
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ event_id: expect.stringMatching(/[a-f0-9]{32}/),
+ measurements: expect.any(Object),
+ platform: 'javascript',
+ release: 'e2e-test',
+ request: {
+ headers: {
+ 'User-Agent': expect.any(String),
+ },
+ url: 'http://localhost:3030/',
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ type: 'transaction',
+ });
+});
+
+test('captures a navigation transaction', async ({ page }) => {
+ const pageLoadTransactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTransactionPromise = waitForTransaction('effect-4-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto('/');
+ await pageLoadTransactionPromise;
+
+ const linkElement = page.locator('id=navigation-link');
+ await linkElement.click();
+
+ const navigationTransaction = await navigationTransactionPromise;
+
+ expect(navigationTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.browser',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
+
+test('captures Effect spans with correct parent-child structure', async ({ page }) => {
+ const pageloadPromise = waitForTransaction('effect-4-browser', transactionEvent => {
+ return transactionEvent?.contexts?.trace?.op === 'pageload';
+ });
+
+ const transactionPromise = waitForTransaction('effect-4-browser', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'ui.action.click' &&
+ transactionEvent.spans?.some(span => span.description === 'custom-effect-span')
+ );
+ });
+
+ await page.goto('/');
+ await pageloadPromise;
+
+ const effectSpanButton = page.locator('id=effect-span-button');
+ await effectSpanButton.click();
+
+ await expect(page.locator('id=effect-span-result')).toHaveText('Span sent!');
+
+ const transactionEvent = await transactionPromise;
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ description: 'custom-effect-span',
+ }),
+ );
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ description: 'nested-span',
+ }),
+ );
+
+ const parentSpan = spans.find(s => s.description === 'custom-effect-span');
+ const nestedSpan = spans.find(s => s.description === 'nested-span');
+ expect(nestedSpan?.parent_span_id).toBe(parentSpan?.span_id);
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json
new file mode 100644
index 000000000000..cb69f25b8d50
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src", "tests"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore b/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore
new file mode 100644
index 000000000000..f06235c460c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/package.json b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json
new file mode 100644
index 000000000000..528b5cc77339
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "effect-4-node-app",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@effect/platform-node": "^4.0.0-beta.50",
+ "@sentry/effect": "file:../../packed/sentry-effect-packed.tgz",
+ "@types/node": "^18.19.1",
+ "effect": "^4.0.0-beta.50",
+ "typescript": "~5.0.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "volta": {
+ "node": "22.15.0",
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
new file mode 100644
index 000000000000..5ebfef33be77
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
@@ -0,0 +1,146 @@
+import * as Sentry from '@sentry/effect';
+import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
+import * as Effect from 'effect/Effect';
+import * as Cause from 'effect/Cause';
+import * as Layer from 'effect/Layer';
+import * as Logger from 'effect/Logger';
+import * as Tracer from 'effect/Tracer';
+import * as References from 'effect/References';
+import { HttpRouter, HttpServerResponse } from 'effect/unstable/http';
+import { createServer } from 'http';
+
+const SentryLive = Layer.mergeAll(
+ Sentry.effectLayer({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ debug: !!process.env.DEBUG,
+ tunnel: 'http://localhost:3031/',
+ tracesSampleRate: 1,
+ enableLogs: true,
+ }),
+ Logger.layer([Sentry.SentryEffectLogger]),
+ Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer),
+ Layer.succeed(References.MinimumLogLevel, 'Debug'),
+);
+
+const Routes = Layer.mergeAll(
+ HttpRouter.add('GET', '/test-success', HttpServerResponse.json({ version: 'v1' })),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-transaction',
+ Effect.gen(function* () {
+ yield* Effect.void.pipe(Effect.withSpan('test-span'));
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-effect-span',
+ Effect.gen(function* () {
+ yield* Effect.gen(function* () {
+ yield* Effect.sleep('50 millis');
+ yield* Effect.sleep('25 millis').pipe(Effect.withSpan('nested-span'));
+ }).pipe(Effect.withSpan('custom-effect-span', { kind: 'internal' }));
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-error',
+ Effect.gen(function* () {
+ const exceptionId = Sentry.captureException(new Error('This is an error'));
+ yield* Effect.promise(() => Sentry.flush(2000));
+ return yield* HttpServerResponse.json({ exceptionId });
+ }),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-exception/:id',
+ Effect.gen(function* () {
+ yield* Effect.sync(() => {
+ throw new Error('This is an exception with id 123');
+ });
+ return HttpServerResponse.empty();
+ }).pipe(
+ Effect.catchCause(cause => {
+ const error = Cause.squash(cause);
+ Sentry.captureException(error);
+ return Effect.gen(function* () {
+ yield* Effect.promise(() => Sentry.flush(2000));
+ return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 });
+ });
+ }),
+ ),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-effect-fail',
+ Effect.gen(function* () {
+ yield* Effect.fail(new Error('Effect failure'));
+ return HttpServerResponse.empty();
+ }).pipe(
+ Effect.catchCause(cause => {
+ const error = Cause.squash(cause);
+ Sentry.captureException(error);
+ return Effect.gen(function* () {
+ yield* Effect.promise(() => Sentry.flush(2000));
+ return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 });
+ });
+ }),
+ ),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-effect-die',
+ Effect.gen(function* () {
+ yield* Effect.die('Effect defect');
+ return HttpServerResponse.empty();
+ }).pipe(
+ Effect.catchCause(cause => {
+ const error = Cause.squash(cause);
+ Sentry.captureException(error);
+ return Effect.gen(function* () {
+ yield* Effect.promise(() => Sentry.flush(2000));
+ return yield* HttpServerResponse.json({ error: String(error) }, { status: 500 });
+ });
+ }),
+ ),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-log',
+ Effect.gen(function* () {
+ yield* Effect.logDebug('Debug log from Effect');
+ yield* Effect.logInfo('Info log from Effect');
+ yield* Effect.logWarning('Warning log from Effect');
+ yield* Effect.logError('Error log from Effect');
+ return yield* HttpServerResponse.json({ message: 'Logs sent' });
+ }),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-log-with-context',
+ Effect.gen(function* () {
+ yield* Effect.logInfo('Log with context').pipe(
+ Effect.annotateLogs('userId', '12345'),
+ Effect.annotateLogs('action', 'test'),
+ );
+ return yield* HttpServerResponse.json({ message: 'Log with context sent' });
+ }),
+ ),
+);
+
+const HttpLive = HttpRouter.serve(Routes).pipe(
+ Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3030 })),
+ Layer.provide(SentryLive),
+);
+
+NodeRuntime.runMain(Layer.launch(HttpLive));
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs
new file mode 100644
index 000000000000..6874b711993a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'effect-4-node',
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts
new file mode 100644
index 000000000000..f4d01534e60f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/errors.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Captures manually reported error', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('effect-4-node', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an error';
+ });
+
+ const response = await fetch(`${baseURL}/test-error`);
+ const body = await response.json();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(body.exceptionId).toBeDefined();
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an error');
+});
+
+test('Captures thrown exception', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('effect-4-node', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ await fetch(`${baseURL}/test-exception/123`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
+});
+
+test('Captures Effect.fail as error', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('effect-4-node', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'Effect failure';
+ });
+
+ await fetch(`${baseURL}/test-effect-fail`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Effect failure');
+});
+
+test('Captures Effect.die as error', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('effect-4-node', event => {
+ return !event.type && event.exception?.values?.[0]?.value?.includes('Effect defect');
+ });
+
+ await fetch(`${baseURL}/test-effect-die`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toContain('Effect defect');
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts
new file mode 100644
index 000000000000..f7563576ad75
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/logs.test.ts
@@ -0,0 +1,96 @@
+import { expect, test } from '@playwright/test';
+import { waitForEnvelopeItem } from '@sentry-internal/test-utils';
+import type { SerializedLogContainer } from '@sentry/core';
+
+test('should send Effect debug logs', async ({ baseURL }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'debug' && item.body === 'Debug log from Effect',
+ )
+ );
+ });
+
+ await fetch(`${baseURL}/test-log`);
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const debugLog = logs.find(log => log.level === 'debug' && log.body === 'Debug log from Effect');
+ expect(debugLog).toBeDefined();
+ expect(debugLog?.level).toBe('debug');
+});
+
+test('should send Effect info logs', async ({ baseURL }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'info' && item.body === 'Info log from Effect',
+ )
+ );
+ });
+
+ await fetch(`${baseURL}/test-log`);
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const infoLog = logs.find(log => log.level === 'info' && log.body === 'Info log from Effect');
+ expect(infoLog).toBeDefined();
+ expect(infoLog?.level).toBe('info');
+});
+
+test('should send Effect warning logs', async ({ baseURL }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'warn' && item.body === 'Warning log from Effect',
+ )
+ );
+ });
+
+ await fetch(`${baseURL}/test-log`);
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const warnLog = logs.find(log => log.level === 'warn' && log.body === 'Warning log from Effect');
+ expect(warnLog).toBeDefined();
+ expect(warnLog?.level).toBe('warn');
+});
+
+test('should send Effect error logs', async ({ baseURL }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(
+ item => item.level === 'error' && item.body === 'Error log from Effect',
+ )
+ );
+ });
+
+ await fetch(`${baseURL}/test-log`);
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const errorLog = logs.find(log => log.level === 'error' && log.body === 'Error log from Effect');
+ expect(errorLog).toBeDefined();
+ expect(errorLog?.level).toBe('error');
+});
+
+test('should send Effect logs with context attributes', async ({ baseURL }) => {
+ const logEnvelopePromise = waitForEnvelopeItem('effect-4-node', envelope => {
+ return (
+ envelope[0].type === 'log' &&
+ (envelope[1] as SerializedLogContainer).items.some(item => item.body === 'Log with context')
+ );
+ });
+
+ await fetch(`${baseURL}/test-log-with-context`);
+
+ const logEnvelope = await logEnvelopePromise;
+ const logs = (logEnvelope[1] as SerializedLogContainer).items;
+ const contextLog = logs.find(log => log.body === 'Log with context');
+ expect(contextLog).toBeDefined();
+ expect(contextLog?.level).toBe('info');
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts
new file mode 100644
index 000000000000..5aeaf9b2a8ba
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/transactions.test.ts
@@ -0,0 +1,99 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends an HTTP transaction', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
+ return transactionEvent?.transaction === 'http.server GET';
+ });
+
+ await fetch(`${baseURL}/test-success`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.transaction).toBe('http.server GET');
+});
+
+test('Sends transaction with manual Effect span', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'http.server GET' &&
+ transactionEvent?.spans?.some(span => span.description === 'test-span')
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.transaction).toBe('http.server GET');
+
+ const spans = transactionEvent.spans || [];
+ expect(spans).toEqual([
+ expect.objectContaining({
+ description: 'test-span',
+ }),
+ ]);
+});
+
+test('Sends Effect spans with correct parent-child structure', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'http.server GET' &&
+ transactionEvent?.spans?.some(span => span.description === 'custom-effect-span')
+ );
+ });
+
+ await fetch(`${baseURL}/test-effect-span`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.transaction).toBe('http.server GET');
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ contexts: expect.objectContaining({
+ trace: expect.objectContaining({
+ origin: 'auto.http.effect',
+ }),
+ }),
+ spans: [
+ expect.objectContaining({
+ description: 'custom-effect-span',
+ origin: 'auto.function.effect',
+ }),
+ expect.objectContaining({
+ description: 'nested-span',
+ origin: 'auto.function.effect',
+ }),
+ ],
+ sdk: expect.objectContaining({
+ name: 'sentry.javascript.effect',
+ packages: [
+ expect.objectContaining({
+ name: 'npm:@sentry/effect',
+ }),
+ expect.objectContaining({
+ name: 'npm:@sentry/node-light',
+ }),
+ ],
+ }),
+ }),
+ );
+
+ const parentSpan = transactionEvent.spans?.[0]?.span_id;
+ const nestedSpan = transactionEvent.spans?.[1]?.parent_span_id;
+
+ expect(nestedSpan).toBe(parentSpan);
+});
+
+test('Sends transaction for error route', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('effect-4-node', transactionEvent => {
+ return transactionEvent?.transaction === 'http.server GET';
+ });
+
+ await fetch(`${baseURL}/test-error`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.transaction).toBe('http.server GET');
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json b/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json
new file mode 100644
index 000000000000..2cc9aca23e0e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "declaration": false
+ },
+ "include": ["src"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-browser/.npmrc b/dev-packages/e2e-tests/test-applications/effect-browser/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/effect-browser/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/.npmrc b/dev-packages/e2e-tests/test-applications/effect-node/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/effect-node/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/elysia-bun/.npmrc b/dev-packages/e2e-tests/test-applications/elysia-bun/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/elysia-bun/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/elysia-bun/package.json b/dev-packages/e2e-tests/test-applications/elysia-bun/package.json
index 73689db97994..98cf84595882 100644
--- a/dev-packages/e2e-tests/test-applications/elysia-bun/package.json
+++ b/dev-packages/e2e-tests/test-applications/elysia-bun/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/elysia": "latest || *",
+ "@sentry/elysia": "file:../../packed/sentry-elysia-packed.tgz",
"elysia": "^1.4.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/elysia-node/.npmrc b/dev-packages/e2e-tests/test-applications/elysia-node/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/elysia-node/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/elysia-node/package.json b/dev-packages/e2e-tests/test-applications/elysia-node/package.json
index dda646fab480..fcd2a4e8f71b 100644
--- a/dev-packages/e2e-tests/test-applications/elysia-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/elysia-node/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/elysia": "latest || *",
+ "@sentry/elysia": "file:../../packed/sentry-elysia-packed.tgz",
"elysia": "latest",
"@elysiajs/node": "^1.4.5"
},
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/.npmrc b/dev-packages/e2e-tests/test-applications/ember-classic/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/ember-classic/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/package.json b/dev-packages/e2e-tests/test-applications/ember-classic/package.json
index 3cb6a5c9d836..9f96912ce555 100644
--- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json
+++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json
@@ -27,7 +27,7 @@
"@playwright/test": "~1.56.0",
"@ember/string": "~3.1.1",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/ember": "latest || *",
+ "@sentry/ember": "file:../../packed/sentry-ember-packed.tgz",
"@tsconfig/ember": "~3.0.6",
"@tsconfig/node18": "18.2.4",
"@types/ember": "~4.0.11",
diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc b/dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
index e451a5da9db7..7f0b611b2bc1 100644
--- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
+++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json
@@ -51,7 +51,7 @@
"tracked-built-ins": "^3.3.0",
"webpack": "^5.91.0",
"@playwright/test": "~1.56.0",
- "@sentry/ember": "latest || *",
+ "@sentry/ember": "file:../../packed/sentry-ember-packed.tgz",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@tsconfig/ember": "^3.0.6",
"@types/node": "^18.19.1",
diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/.npmrc b/dev-packages/e2e-tests/test-applications/generic-ts3.8/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
index fe0e0f6ec5f0..f27c55af0a0a 100644
--- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
+++ b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json
@@ -14,11 +14,11 @@
"@types/node": "^14.0.0"
},
"dependencies": {
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/wasm": "latest || *"
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry-internal/replay": "file:../../packed/sentry-internal-replay-packed.tgz",
+ "@sentry/wasm": "file:../../packed/sentry-wasm-packed.tgz"
},
"pnpm": {
"overrides": {
diff --git a/dev-packages/e2e-tests/test-applications/generic-ts5.0/.npmrc b/dev-packages/e2e-tests/test-applications/generic-ts5.0/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/generic-ts5.0/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/generic-ts5.0/package.json b/dev-packages/e2e-tests/test-applications/generic-ts5.0/package.json
index 1079d8f4c793..e60d1b19489b 100644
--- a/dev-packages/e2e-tests/test-applications/generic-ts5.0/package.json
+++ b/dev-packages/e2e-tests/test-applications/generic-ts5.0/package.json
@@ -14,11 +14,11 @@
"@types/node": "^18.19.1"
},
"dependencies": {
- "@sentry/browser": "latest || *",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry-internal/replay": "latest || *",
- "@sentry/wasm": "latest || *"
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry-internal/replay": "file:../../packed/sentry-internal-replay-packed.tgz",
+ "@sentry/wasm": "file:../../packed/sentry-wasm-packed.tgz"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/.gitignore b/dev-packages/e2e-tests/test-applications/hono-4/.gitignore
new file mode 100644
index 000000000000..534f51704346
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/.gitignore
@@ -0,0 +1,36 @@
+# prod
+dist/
+
+# dev
+.yarn/
+!.yarn/releases
+.vscode/*
+!.vscode/launch.json
+!.vscode/*.code-snippets
+.idea/workspace.xml
+.idea/usage.statistics.xml
+.idea/shelf
+
+# deps
+node_modules/
+.wrangler
+
+# env
+.env
+.env.production
+.dev.vars
+
+# logs
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# test
+test-results
+
+# misc
+.DS_Store
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/.npmrc b/dev-packages/e2e-tests/test-applications/hono-4/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/angular-19/.npmrc
rename to dev-packages/e2e-tests/test-applications/hono-4/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json
new file mode 100644
index 000000000000..53519a1bd80c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "hono-4",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev:cf": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')",
+ "dev:node": "node --import tsx/esm --import @sentry/node/preload src/entry.node.ts",
+ "dev:bun": "bun src/entry.bun.ts",
+ "build": "wrangler deploy --dry-run",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "TEST_ENV=production playwright test"
+ },
+ "dependencies": {
+ "@sentry/bun": "latest || *",
+ "@sentry/cloudflare": "latest || *",
+ "@sentry/hono": "latest || *",
+ "@sentry/node": "latest || *",
+ "@hono/node-server": "^1.19.10",
+ "hono": "^4.12.14"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@cloudflare/workers-types": "^4.20240725.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "tsx": "^4.20.3",
+ "typescript": "^5.5.2",
+ "wrangler": "^4.61.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "variants": [
+ {
+ "assert-command": "RUNTIME=node pnpm test:assert",
+ "label": "hono-4 (node)"
+ },
+ {
+ "assert-command": "RUNTIME=bun pnpm test:assert",
+ "label": "hono-4 (bun)"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts
new file mode 100644
index 000000000000..74a21e10a349
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts
@@ -0,0 +1,32 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+type Runtime = 'cloudflare' | 'node' | 'bun';
+
+const RUNTIME = (process.env.RUNTIME || 'cloudflare') as Runtime;
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const APP_PORT = 38787;
+
+const startCommands: Record = {
+ cloudflare: `pnpm dev:cf --port ${APP_PORT}`,
+ node: `pnpm dev:node`,
+ bun: `pnpm dev:bun`,
+};
+
+const config = getPlaywrightConfig(
+ {
+ startCommand: startCommands[RUNTIME],
+ port: APP_PORT,
+ },
+ {
+ workers: '100%',
+ retries: 0,
+ },
+);
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts
new file mode 100644
index 000000000000..e057eb78d4c5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts
@@ -0,0 +1,23 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/bun';
+import { addRoutes } from './routes';
+
+const app = new Hono();
+
+app.use(
+ sentry(app, {
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ }),
+);
+
+addRoutes(app);
+
+const port = Number(process.env.PORT || 38787);
+
+export default {
+ port,
+ fetch: app.fetch,
+};
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts
new file mode 100644
index 000000000000..e348dde56226
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts
@@ -0,0 +1,18 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/cloudflare';
+import { addRoutes } from './routes';
+
+const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>();
+
+app.use(
+ sentry(app, env => ({
+ dsn: env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ })),
+);
+
+addRoutes(app);
+
+export default app;
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts
new file mode 100644
index 000000000000..eb2c669c6806
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts
@@ -0,0 +1,24 @@
+import { Hono } from 'hono';
+import { sentry } from '@sentry/hono/node';
+import { serve } from '@hono/node-server';
+import { addRoutes } from './routes';
+
+const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>();
+
+app.use(
+ // @ts-expect-error - Env is not yet in type
+ sentry(app, {
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa',
+ tracesSampleRate: 1.0,
+ tunnel: 'http://localhost:3031/',
+ }),
+);
+
+addRoutes(app);
+
+const port = Number(process.env.PORT || 38787);
+
+serve({ fetch: app.fetch, port }, () => {
+ console.log(`Hono (Node) listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts
new file mode 100644
index 000000000000..cc7bfae9896d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/middleware.ts
@@ -0,0 +1,17 @@
+import type { MiddlewareHandler } from 'hono';
+
+export const middlewareA: MiddlewareHandler = async function middlewareA(c, next) {
+ // Add some delay
+ await new Promise(resolve => setTimeout(resolve, 50));
+ await next();
+};
+
+export const middlewareB: MiddlewareHandler = async function middlewareB(_c, next) {
+ // Add some delay
+ await new Promise(resolve => setTimeout(resolve, 60));
+ await next();
+};
+
+export const failingMiddleware: MiddlewareHandler = async function failingMiddleware(_c, _next) {
+ throw new Error('Middleware error');
+};
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts
new file mode 100644
index 000000000000..656fea319579
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts
@@ -0,0 +1,10 @@
+import { Hono } from 'hono';
+
+const testMiddleware = new Hono();
+
+testMiddleware.get('/named', c => c.json({ middleware: 'named' }));
+testMiddleware.get('/anonymous', c => c.json({ middleware: 'anonymous' }));
+testMiddleware.get('/multi', c => c.json({ middleware: 'multi' }));
+testMiddleware.get('/error', c => c.text('should not reach'));
+
+export { testMiddleware };
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts
new file mode 100644
index 000000000000..65d30787de64
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts
@@ -0,0 +1,39 @@
+import type { Hono } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+import { testMiddleware } from './route-groups/test-middleware';
+import { middlewareA, middlewareB, failingMiddleware } from './middleware';
+
+export function addRoutes(app: Hono<{ Bindings?: { E2E_TEST_DSN: string } }>): void {
+ app.get('/', c => {
+ return c.text('Hello Hono!');
+ });
+
+ app.get('/test-param/:paramId', c => {
+ return c.json({ paramId: c.req.param('paramId') });
+ });
+
+ app.get('/error/:cause', c => {
+ throw new Error('This is a test error for Sentry!', {
+ cause: c.req.param('cause'),
+ });
+ });
+
+ app.get('/http-exception/:code', c => {
+ // oxlint-disable-next-line typescript/no-explicit-any
+ const code = Number(c.req.param('code')) as any;
+ throw new HTTPException(code, { message: `HTTPException ${code}` });
+ });
+
+ // === Middleware ===
+ // Middleware is registered on the main app (the patched instance) via `app.use()`
+ // TODO: In the future, we may want to support middleware registration on sub-apps (route groups)
+ app.use('/test-middleware/named/*', middlewareA);
+ app.use('/test-middleware/anonymous/*', async (c, next) => {
+ c.header('X-Custom', 'anonymous');
+ await next();
+ });
+ app.use('/test-middleware/multi/*', middlewareA, middlewareB);
+ app.use('/test-middleware/error/*', failingMiddleware);
+
+ app.route('/test-middleware', testMiddleware);
+}
diff --git a/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs
similarity index 76%
rename from dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs
rename to dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs
index 41eb647958b7..cd6f91b3455d 100644
--- a/dev-packages/e2e-tests/test-applications/effect-node/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'effect-node',
+ proxyServerName: 'hono-4',
});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts
new file mode 100644
index 000000000000..e85958e8328b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts
@@ -0,0 +1,54 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+const APP_NAME = 'hono-4';
+
+test('captures error thrown in route handler', async ({ baseURL }) => {
+ const errorWaiter = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'This is a test error for Sentry!';
+ });
+
+ const response = await fetch(`${baseURL}/error/test-cause`);
+ expect(response.status).toBe(500);
+
+ const event = await errorWaiter;
+ expect(event.exception?.values?.[0]?.value).toBe('This is a test error for Sentry!');
+});
+
+test('captures HTTPException with 502 status', async ({ baseURL }) => {
+ const errorWaiter = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'HTTPException 502';
+ });
+
+ const response = await fetch(`${baseURL}/http-exception/502`);
+ expect(response.status).toBe(502);
+
+ const event = await errorWaiter;
+ expect(event.exception?.values?.[0]?.value).toBe('HTTPException 502');
+});
+
+// TODO: 401 and 404 HTTPExceptions should not be captured by Sentry by default,
+// but currently they are. Fix the filtering and update these tests accordingly.
+test('captures HTTPException with 401 status', async ({ baseURL }) => {
+ const errorWaiter = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'HTTPException 401';
+ });
+
+ const response = await fetch(`${baseURL}/http-exception/401`);
+ expect(response.status).toBe(401);
+
+ const event = await errorWaiter;
+ expect(event.exception?.values?.[0]?.value).toBe('HTTPException 401');
+});
+
+test('captures HTTPException with 404 status', async ({ baseURL }) => {
+ const errorWaiter = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'HTTPException 404';
+ });
+
+ const response = await fetch(`${baseURL}/http-exception/404`);
+ expect(response.status).toBe(404);
+
+ const event = await errorWaiter;
+ expect(event.exception?.values?.[0]?.value).toBe('HTTPException 404');
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts
new file mode 100644
index 000000000000..a03398798756
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts
@@ -0,0 +1,143 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+import { type SpanJSON } from '@sentry/core';
+
+const APP_NAME = 'hono-4';
+
+test('creates a span for named middleware', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction(APP_NAME, event => {
+ return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/named';
+ });
+
+ const response = await fetch(`${baseURL}/test-middleware/named`);
+ expect(response.status).toBe(200);
+
+ const transaction = await transactionPromise;
+ const spans = transaction.spans || [];
+
+ const middlewareSpan = spans.find(
+ (span: { description?: string; op?: string }) =>
+ span.op === 'middleware.hono' && span.description === 'middlewareA',
+ );
+
+ expect(middlewareSpan).toEqual(
+ expect.objectContaining({
+ description: 'middlewareA',
+ op: 'middleware.hono',
+ origin: 'auto.middleware.hono',
+ status: 'ok',
+ }),
+ );
+
+ // The middleware has a 50ms delay, so the span duration should be at least 50ms (0.05s)
+ // @ts-expect-error timestamp is defined
+ const durationMs = (middlewareSpan?.timestamp - middlewareSpan?.start_timestamp) * 1000;
+ expect(durationMs).toBeGreaterThanOrEqual(49);
+});
+
+test('creates a span for anonymous middleware', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction(APP_NAME, event => {
+ return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/anonymous';
+ });
+
+ const response = await fetch(`${baseURL}/test-middleware/anonymous`);
+ expect(response.status).toBe(200);
+
+ const transaction = await transactionPromise;
+ const spans = transaction.spans || [];
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ description: '',
+ op: 'middleware.hono',
+ origin: 'auto.middleware.hono',
+ status: 'ok',
+ }),
+ );
+});
+
+test('multiple middleware are sibling spans under the same parent', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction(APP_NAME, event => {
+ return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/multi';
+ });
+
+ const response = await fetch(`${baseURL}/test-middleware/multi`);
+ expect(response.status).toBe(200);
+
+ const transaction = await transactionPromise;
+ const spans = transaction.spans || [];
+
+ // Sort spans because they are in a different order in Node/Bun (OTel-based)
+ const middlewareSpans = spans
+ .filter((span: SpanJSON) => span.op === 'middleware.hono' && span.origin === 'auto.middleware.hono')
+ .sort((a, b) => (a.start_timestamp ?? 0) - (b.start_timestamp ?? 0));
+
+ expect(middlewareSpans).toHaveLength(2);
+ expect(middlewareSpans[0]?.description).toBe('middlewareA');
+ expect(middlewareSpans[1]?.description).toBe('middlewareB');
+
+ // Both middleware spans share the same parent (siblings, not nested)
+ expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id);
+
+ // middlewareA has a 50ms delay, middlewareB has a 60ms delay
+ // @ts-expect-error timestamp is defined
+ const middlewareADuration = (middlewareSpans[0]?.timestamp - middlewareSpans[0]?.start_timestamp) * 1000;
+ // @ts-expect-error timestamp is defined
+ const middlewareBDuration = (middlewareSpans[1]?.timestamp - middlewareSpans[1]?.start_timestamp) * 1000;
+ expect(middlewareADuration).toBeGreaterThanOrEqual(49);
+ expect(middlewareBDuration).toBeGreaterThanOrEqual(59);
+});
+
+test('captures error thrown in middleware', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Middleware error';
+ });
+
+ const response = await fetch(`${baseURL}/test-middleware/error`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Middleware error');
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
+ expect.objectContaining({
+ handled: false,
+ type: 'auto.middleware.hono',
+ }),
+ );
+});
+
+test('sets error status on middleware span when middleware throws', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction(APP_NAME, event => {
+ return event.contexts?.trace?.op === 'http.server' && event.transaction === 'GET /test-middleware/error/*';
+ });
+
+ await fetch(`${baseURL}/test-middleware/error`);
+
+ const transaction = await transactionPromise;
+ const spans = transaction.spans || [];
+
+ const failingSpan = spans.find(
+ (span: { description?: string; op?: string }) =>
+ span.op === 'middleware.hono' && span.description === 'failingMiddleware',
+ );
+
+ expect(failingSpan).toBeDefined();
+ expect(failingSpan?.status).toBe('internal_error');
+ expect(failingSpan?.origin).toBe('auto.middleware.hono');
+});
+
+test('includes request data on error events from middleware', async ({ baseURL }) => {
+ const errorPromise = waitForError(APP_NAME, event => {
+ return event.exception?.values?.[0]?.value === 'Middleware error';
+ });
+
+ await fetch(`${baseURL}/test-middleware/error`);
+
+ const errorEvent = await errorPromise;
+ expect(errorEvent.request).toEqual(
+ expect.objectContaining({
+ method: 'GET',
+ url: expect.stringContaining('/test-middleware/error'),
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts
new file mode 100644
index 000000000000..58c73c6a8369
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const APP_NAME = 'hono-4';
+
+test('sends a transaction for the index route', async ({ baseURL }) => {
+ const transactionWaiter = waitForTransaction(APP_NAME, event => {
+ return event.transaction === 'GET /';
+ });
+
+ const response = await fetch(`${baseURL}/`);
+ expect(response.status).toBe(200);
+
+ const transaction = await transactionWaiter;
+ expect(transaction.contexts?.trace?.op).toBe('http.server');
+});
+
+test('sends a transaction for a parameterized route', async ({ baseURL }) => {
+ const transactionWaiter = waitForTransaction(APP_NAME, event => {
+ return event.transaction === 'GET /test-param/:paramId';
+ });
+
+ const response = await fetch(`${baseURL}/test-param/123`);
+ expect(response.status).toBe(200);
+
+ const transaction = await transactionWaiter;
+ expect(transaction.contexts?.trace?.op).toBe('http.server');
+ expect(transaction.transaction).toBe('GET /test-param/:paramId');
+});
+
+test('sends a transaction for a route that throws', async ({ baseURL }) => {
+ const transactionWaiter = waitForTransaction(APP_NAME, event => {
+ return event.transaction === 'GET /error/:cause';
+ });
+
+ await fetch(`${baseURL}/error/test-cause`);
+
+ const transaction = await transactionWaiter;
+ expect(transaction.contexts?.trace?.op).toBe('http.server');
+ expect(transaction.contexts?.trace?.status).toBe('internal_error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json
new file mode 100644
index 000000000000..3c4abeff44d6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+ "types": ["@cloudflare/workers-types"]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc
new file mode 100644
index 000000000000..d4344dfa198a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc
@@ -0,0 +1,7 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "hono-4",
+ "main": "src/entry.cloudflare.ts",
+ "compatibility_date": "2026-04-20",
+ "compatibility_flags": ["nodejs_compat"],
+}
diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/.npmrc b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json
index 87c67b61f195..56789ec7cedb 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
@@ -14,8 +14,8 @@
"test:assert": "pnpm playwright test"
},
"dependencies": {
- "@sentry/cloudflare": "latest || *",
- "@sentry/react-router": "latest || *",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"@sentry/vite-plugin": "^5.2.0",
"@shopify/hydrogen": "2025.5.0",
"@shopify/remix-oxygen": "^3.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-11/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
index 48e2525de321..2a230d9d5a68 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json
@@ -20,7 +20,7 @@
"@nestjs/microservices": "^11.0.0",
"@nestjs/schedule": "^5.0.0",
"@nestjs/platform-express": "^11.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
index 4a21f67e908a..3bd765774d2e 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json
@@ -19,7 +19,7 @@
"@nestjs/microservices": "^8.0.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/platform-express": "^8.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
index 3128d2f7ae51..e429f8cbb328 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json
@@ -20,7 +20,7 @@
"@nestjs/core": "^10.3.10",
"@nestjs/graphql": "^12.2.0",
"@nestjs/platform-express": "^10.3.10",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"graphql": "^16.9.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-basic/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
index 6917e546a383..ebf0244bc276 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json
@@ -19,7 +19,7 @@
"@nestjs/microservices": "^10.0.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/platform-express": "^10.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"axios": "1.15.0",
"rxjs": "^7.8.1"
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json
index 77d8c024e021..c4cfcd118f53 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-bullmq/package.json
@@ -18,7 +18,7 @@
"@nestjs/platform-express": "^11.0.0",
"@nestjs/bullmq": "^11.0.0",
"bullmq": "^5.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
index d15679556bad..c8fe82cff563 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json
@@ -18,7 +18,7 @@
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/event-emitter": "^2.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
index d5cecac78725..720cfe158eae 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
@@ -19,7 +19,7 @@
"@nestjs/microservices": "^10.0.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/platform-fastify": "^10.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"fastify": "^4.28.1"
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-graphql/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
index be8ed9d58533..05a38d691807 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json
@@ -20,7 +20,7 @@
"@nestjs/core": "^10.3.10",
"@nestjs/graphql": "^12.2.0",
"@nestjs/platform-express": "^10.3.10",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"graphql": "^16.9.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json
index ee3ca5ebf816..4bfc4eee7710 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-microservices/package.json
@@ -16,7 +16,7 @@
"@nestjs/core": "^11.0.0",
"@nestjs/microservices": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json
index 6356b48b322f..c859d4e49791 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json
@@ -16,7 +16,7 @@
"@nestjs/platform-express": "^11.0.0",
"@nestjs/websockets": "^11.0.0",
"@nestjs/platform-socket.io": "^11.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
index f19782b24a7d..35ce0bc009e1 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json
@@ -17,7 +17,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
index 297555d6802f..e9da4c97ae26 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json
@@ -17,7 +17,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
- "@sentry/nestjs": "latest || *",
+ "@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
index 29270d71da6a..f6137db6843c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
@@ -12,7 +12,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-14/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
index 2388110ad775..09a928eddfa0 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
@@ -25,7 +25,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
index 01d3747009c7..ce63c62aeefa 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc
deleted file mode 100644
index c6b3ef9b3eaa..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://localhost:4873
-@sentry-internal:registry=http://localhost:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
index ca609897ff4c..b0c7f2852e01 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
index 380e2ce0f66f..24fde175baea 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
@@ -14,7 +14,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.50.0",
"@trpc/client": "~11.8.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index 9263605b5672..9e453cf0edf5 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json
index 75e51867a38c..deb955b58daf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json
@@ -12,8 +12,8 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"import-in-the-middle": "^2",
"next": "16.1.7",
"react": "19.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
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 bc306ef7dab7..3f3907a77bed 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
@@ -23,8 +23,8 @@
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"import-in-the-middle": "^1",
"next": "16.1.7",
"react": "19.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json
index 5be3e1b9a9d2..14334483d116 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
@@ -18,8 +18,8 @@
},
"dependencies": {
"@opennextjs/cloudflare": "^1.14.9",
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"next": "16.2.3",
"react": "19.1.0",
"react-dom": "19.1.0"
@@ -43,6 +43,10 @@
{
"build-command": "pnpm test:build-latest",
"label": "nextjs-16-cf-workers (latest)"
+ },
+ {
+ "build-command": "pnpm test:build-canary",
+ "label": "nextjs-16-cf-workers (canary)"
}
]
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts
index 8b6349a97e5f..cba53fa1970d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.describe.skip('Cloudflare Runtime', () => {
+test.describe('Cloudflare Runtime', () => {
test('Should report cloudflare as the runtime in API route error events', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
return !!errorEvent?.exception?.values?.some(value =>
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
index 1ff2d2b1cabb..b42d2cd61b93 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => {
+test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => {
// Navigate to ISR page
await page.goto('/isr-test/laptop');
@@ -14,8 +13,7 @@ test.skip('should remove sentry-trace and baggage meta tags on ISR dynamic route
await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => {
+test('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => {
// Navigate to ISR static page
await page.goto('/isr-test/static');
@@ -27,8 +25,7 @@ test.skip('should remove sentry-trace and baggage meta tags on ISR static route'
await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should remove meta tags for different ISR dynamic route values', async ({ page }) => {
+test('should remove meta tags for different ISR dynamic route values', async ({ page }) => {
// Test with 'phone' (one of the pre-generated static params)
await page.goto('/isr-test/phone');
await expect(page.locator('#isr-product-id')).toHaveText('phone');
@@ -44,8 +41,7 @@ test.skip('should remove meta tags for different ISR dynamic route values', asyn
await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should create unique transactions for ISR pages on each visit', async ({ page }) => {
+test('should create unique transactions for ISR pages on each visit', async ({ page }) => {
const traceIds: string[] = [];
// Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed
@@ -75,8 +71,7 @@ test.skip('should create unique transactions for ISR pages on each visit', async
expect(uniqueTraceIds.size).toBe(5);
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('ISR route should be identified correctly in the route manifest', async ({ page }) => {
+test('ISR route should be identified correctly in the route manifest', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
index 30faebe69548..b3ba64bb55c8 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should create a parameterized transaction when the `app` directory is used', async ({ page }) => {
+test('should create a parameterized transaction when the `app` directory is used', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
return (
transactionEvent.transaction === '/parameterized/:one' && transactionEvent.contexts?.trace?.op === 'pageload'
@@ -41,8 +40,7 @@ test.skip('should create a parameterized transaction when the `app` directory is
});
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({
+test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({
page,
}) => {
const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
@@ -83,8 +81,7 @@ test.skip('should create a static transaction when the `app` directory is used a
});
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should create a partially parameterized transaction when the `app` directory is used', async ({ page }) => {
+test('should create a partially parameterized transaction when the `app` directory is used', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
return (
transactionEvent.transaction === '/parameterized/:one/beep' && transactionEvent.contexts?.trace?.op === 'pageload'
@@ -123,8 +120,7 @@ test.skip('should create a partially parameterized transaction when the `app` di
});
});
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('should create a nested parameterized transaction when the `app` directory is used.', async ({ page }) => {
+test('should create a nested parameterized transaction when the `app` directory is used.', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
return (
transactionEvent.transaction === '/parameterized/:one/beep/:two' &&
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
index 59ec6d504382..f48158a54697 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
@@ -2,8 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { isDevMode } from './isDevMode';
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => {
+test('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => {
test.skip(isDevMode, "Prefetch requests don't have the prefetch header in dev mode");
const pageloadTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
index 38cb628cb9ce..ba42d9fadbb9 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
@@ -1,8 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
-// TODO(https://github.com/opennextjs/opennextjs-cloudflare/issues/1141): Unskip once opennext supports prefetch-hints.json
-test.skip('Should capture errors for crashing streaming promises in server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
+test('Should capture errors for crashing streaming promises in server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
page,
}) => {
const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json
index ea0475e5ed61..03035b9ddb33 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json
@@ -13,8 +13,8 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"import-in-the-middle": "^2",
"next": "16.1.7",
"react": "19.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json
index 5a1fed010500..ee74ff6e9259 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json
@@ -23,8 +23,8 @@
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"ai": "^3.0.0",
"import-in-the-middle": "^1",
"next": "16.1.7",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json
index 24101853350b..b30636cd3576 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-userfeedback/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc
deleted file mode 100644
index a3160f4de175..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc
+++ /dev/null
@@ -1,4 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-public-hoist-pattern[]=*import-in-the-middle*
-public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
index 4f90b2bc9fe8..1e417a48fd1f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
@@ -23,8 +23,8 @@
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@vercel/queue": "^0.1.3",
"ai": "^3.0.0",
"import-in-the-middle": "^2",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
index 83c0fac47610..cb7927e9b0d8 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
@@ -14,8 +14,8 @@
"test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-orpc/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
index 7e32f562916a..21f835e2ecd4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json
@@ -16,7 +16,7 @@
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@orpc/server": "latest",
"@orpc/client": "latest",
"next": "14.2.35",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json
index eda574954224..f677e02dd954 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json
@@ -15,8 +15,8 @@
"test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
index 16d2ef6d6050..9667f17865f1 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
@@ -9,7 +9,7 @@
"test:assert": "pnpm ts-node --script-mode assert-build.ts"
},
"dependencies": {
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"next": "16.1.7",
"react": "19.1.0",
"react-dom": "19.1.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-connect/.npmrc b/dev-packages/e2e-tests/test-applications/node-connect/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-connect/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-connect/package.json b/dev-packages/e2e-tests/test-applications/node-connect/package.json
index db3979ac7a94..729cfbe6c095 100644
--- a/dev-packages/e2e-tests/test-applications/node-connect/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-connect/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/node": "^18.19.1",
"@types/connect": "3.4.38",
"connect": "3.7.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json
index 6f379575019b..160edce67c56 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json
@@ -18,8 +18,8 @@
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json
index b7d9b06647b3..0ac871787ede 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json
@@ -20,8 +20,8 @@
"@opentelemetry/semantic-conventions": "^1.30.0",
"@opentelemetry/sdk-node": "^0.57.2",
"@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json
index 28d17064a5ff..7cfea6cc7052 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json
@@ -11,8 +11,8 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^1.30.1",
"@opentelemetry/instrumentation": "^0.57.1",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json
index f79c0894bfc9..b44b3a62911e 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json
@@ -18,8 +18,8 @@
"@opentelemetry/resources": "^2.6.0",
"@opentelemetry/sdk-trace-node": "^2.6.0",
"@opentelemetry/semantic-conventions": "^1.40.0",
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json
index dd294c205b32..8552a7990a2d 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json
@@ -20,8 +20,8 @@
"@opentelemetry/semantic-conventions": "^1.40.0",
"@opentelemetry/sdk-node": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json
index 7c1ea4377070..dc57fb2568f8 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json
@@ -11,8 +11,8 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node-core": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.6.0",
"@opentelemetry/instrumentation": "^0.214.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-core-light-express/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-light-express/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-light-express/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-light-express/package.json b/dev-packages/e2e-tests/test-applications/node-core-light-express/package.json
index d1902f528561..83ca28556782 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-light-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-light-express/package.json
@@ -12,7 +12,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node-core": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"express": "^4.21.2",
@@ -21,16 +21,9 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"volta": {
"node": "22.18.0"
- },
- "sentryTest": {
- "variants": [
- {
- "label": "node 22 (light mode, requires Node 22+ for diagnostics_channel)"
- }
- ]
}
}
diff --git a/dev-packages/e2e-tests/test-applications/node-core-light-otlp/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-light-otlp/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-core-light-otlp/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-core-light-otlp/package.json b/dev-packages/e2e-tests/test-applications/node-core-light-otlp/package.json
index fcf388cfaa89..9a1f27147639 100644
--- a/dev-packages/e2e-tests/test-applications/node-core-light-otlp/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-core-light-otlp/package.json
@@ -16,7 +16,7 @@
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
"@opentelemetry/sdk-trace-base": "^2.5.1",
"@opentelemetry/sdk-trace-node": "^2.5.1",
- "@sentry/node-core": "latest || *",
+ "@sentry/node-core": "file:../../packed/sentry-node-core-packed.tgz",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"express": "^4.21.2",
@@ -25,16 +25,9 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"volta": {
"node": "22.18.0"
- },
- "sentryTest": {
- "variants": [
- {
- "label": "node 22 (light mode + OTLP integration)"
- }
- ]
}
}
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
index adb4f189fe85..a836b2b618ba 100644
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
@@ -12,14 +12,14 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node": "latest || *",
- "@sentry/sveltekit": "latest || *",
- "@sentry/remix": "latest || *",
- "@sentry/astro": "latest || *",
- "@sentry/nextjs": "latest || *",
- "@sentry/aws-serverless": "latest || *",
- "@sentry/google-cloud-serverless": "latest || *",
- "@sentry/bun": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
+ "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/aws-serverless": "file:../../packed/sentry-aws-serverless-packed.tgz",
+ "@sentry/google-cloud-serverless": "file:../../packed/sentry-google-cloud-serverless-packed.tgz",
+ "@sentry/bun": "file:../../packed/sentry-bun-packed.tgz",
"@types/node": "^18.19.1",
"typescript": "~5.0.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
index 532e28769675..125372c4501a 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-cjs-preload/package.json
@@ -9,8 +9,8 @@
"test:assert": "playwright test"
},
"dependencies": {
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"express": "^4.21.2"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
index b54247ea1292..a2d2c720e92a 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json
@@ -9,8 +9,8 @@
"test:assert": "playwright test"
},
"dependencies": {
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"express": "^4.21.2"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
index 0ce974f5aa43..4a602b6bd304 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/package.json
@@ -9,8 +9,8 @@
"test:assert": "playwright test"
},
"dependencies": {
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"express": "^4.21.2"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
index a389962d4deb..f6fc9adf6108 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json
@@ -9,8 +9,8 @@
"test:assert": "playwright test"
},
"dependencies": {
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"express": "^4.21.2"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
index 1036f91cf4e9..84afe281c642 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@trpc/server": "10.45.4",
"@trpc/client": "10.45.4",
"@types/express": "4.17.17",
diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json
index 6a5a293b956d..4460adbd034c 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json
@@ -14,7 +14,7 @@
"@cfworker/json-schema": "^4.0.0",
"@modelcontextprotocol/server": "2.0.0-alpha.2",
"@modelcontextprotocol/node": "2.0.0-alpha.2",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "^4.17.21",
"@types/node": "^18.19.1",
"express": "^4.21.2",
@@ -25,7 +25,7 @@
"@modelcontextprotocol/client": "2.0.0-alpha.2",
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"type": "module",
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
index cca96b3e49cb..e5ec85096dce 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-v5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express-v5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json
index a6dd5fab1fbf..cf33b86e8669 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-v5/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express-v5/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@trpc/server": "10.45.4",
"@trpc/client": "10.45.4",
"@types/express": "^4.17.21",
@@ -24,7 +24,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"resolutions": {
"@types/qs": "6.9.17"
diff --git a/dev-packages/e2e-tests/test-applications/node-express/.npmrc b/dev-packages/e2e-tests/test-applications/node-express/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-express/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json
index 6ab9eb2047b9..4d2ad1833a58 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-express/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@trpc/server": "10.45.4",
"@trpc/client": "10.45.4",
"@types/express": "^4.17.21",
@@ -24,7 +24,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/core": "latest || *"
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"resolutions": {
"@types/qs": "6.9.17"
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-3/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
index 663268c466a9..3fa36adbbbd5 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test && pnpm test:override"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/node": "^18.19.1",
"fastify": "3.29.5",
"typescript": "~5.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
index 389aa9ff677b..086ec85fac7a 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test && pnpm test:override"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/node": "^18.19.1",
"fastify": "4.29.1",
"typescript": "5.6.3",
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
index a7a754d3f1a6..dc0fa7770c70 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test && pnpm test:override"
},
"dependencies": {
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/node": "^18.19.1",
"fastify": "^5.7.0",
"typescript": "5.6.3",
diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc b/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-firebase/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json
index e0e0614a23f8..0961acb488d2 100644
--- a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/package.json
@@ -8,7 +8,7 @@
},
"dependencies": {
"@firebase/app": "^0.13.1",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../../packed/sentry-node-packed.tgz",
"express": "^4.21.2",
"firebase": "^12.0.0"
},
diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json
index c3be318b8c38..f179461322dd 100644
--- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1",
- "@sentry/node": "latest || *"
+ "@sentry/node": "file:../../../packed/sentry-node-packed.tgz"
},
"devDependencies": {
"typescript": "5.9.3"
diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/.npmrc b/dev-packages/e2e-tests/test-applications/node-hapi/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-hapi/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json
index b735268d901d..ae87544644bf 100644
--- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@hapi/boom": "10.0.1",
"@hapi/hapi": "21.3.10",
- "@sentry/node": "latest || *"
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/.npmrc b/dev-packages/e2e-tests/test-applications/node-koa/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-koa/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json
index 0d993990730b..f4ef47cd0940 100644
--- a/dev-packages/e2e-tests/test-applications/node-koa/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json
@@ -12,7 +12,7 @@
"dependencies": {
"@koa/bodyparser": "^5.1.1",
"@koa/router": "^12.0.1",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/node": "^18.19.1",
"koa": "^2.15.2",
"typescript": "~5.0.0"
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
index 76fcf398380d..b26e3981e028 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/package.json
@@ -13,8 +13,8 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-node": "^2.6.0",
- "@sentry/node": "latest || *",
- "@sentry/opentelemetry": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
index f695309f00d7..38badec93335 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@opentelemetry/sdk-node": "0.213.0",
"@opentelemetry/exporter-trace-otlp-http": "0.213.0",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
index 8e5563fdb4ec..04a6a80a500c 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json
@@ -17,7 +17,7 @@
"@opentelemetry/instrumentation-undici": "0.24.0",
"@opentelemetry/instrumentation-http": "0.214.0",
"@opentelemetry/instrumentation": "0.214.0",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-otel/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json
index ef7b17112108..274b141ff2a7 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@opentelemetry/sdk-node": "0.213.0",
"@opentelemetry/exporter-trace-otlp-http": "0.213.0",
- "@sentry/node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "4.17.17",
"@types/node": "^18.19.1",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/.npmrc b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/.npmrc
deleted file mode 100644
index 949fbddc2343..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-# @sentry:registry=http://127.0.0.1:4873
-# @sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
index d217090e80fb..b136ea49dd4c 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json
@@ -11,8 +11,8 @@
},
"dependencies": {
"@playwright/test": "~1.56.0",
- "@sentry/node": "latest || *",
- "@sentry/profiling-node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/profiling-node": "file:../../packed/sentry-profiling-node-packed.tgz",
"@types/node": "^18.19.1",
"esbuild": "0.25.0",
"typescript": "^5.7.3"
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-electron/.npmrc b/dev-packages/e2e-tests/test-applications/node-profiling-electron/.npmrc
deleted file mode 100644
index 949fbddc2343..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-profiling-electron/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-# @sentry:registry=http://127.0.0.1:4873
-# @sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json
index 5e87a3ca8002..c8b6c167396b 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling-electron/package.json
@@ -11,8 +11,8 @@
"@electron/rebuild": "^3.7.0",
"@playwright/test": "~1.56.0",
"@sentry/electron": "latest || *",
- "@sentry/node": "latest || *",
- "@sentry/profiling-node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/profiling-node": "file:../../packed/sentry-profiling-node-packed.tgz",
"electron": "^33.2.0"
},
"volta": {
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/.npmrc b/dev-packages/e2e-tests/test-applications/node-profiling-esm/.npmrc
deleted file mode 100644
index 949fbddc2343..000000000000
--- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-# @sentry:registry=http://127.0.0.1:4873
-# @sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
index fb1f3bf055b8..c7e5c39b9807 100644
--- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
+++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json
@@ -11,8 +11,8 @@
},
"dependencies": {
"@playwright/test": "~1.56.0",
- "@sentry/node": "latest || *",
- "@sentry/profiling-node": "latest || *",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "@sentry/profiling-node": "file:../../packed/sentry-profiling-node-packed.tgz",
"@types/node": "^18.19.1",
"esbuild": "0.25.0",
"typescript": "^5.7.3"
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
index e146587fd08e..a61e1da1bdcd 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json
@@ -14,7 +14,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/nuxt": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"nuxt": "^3.14.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
index 8160b472f57e..73b0c59e8a24 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json
@@ -16,7 +16,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/nuxt": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"nuxt": "3.7.0",
"vue": "3.3.4",
"vue-router": "4.2.4"
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
index 9e6fedb17838..21acb5644735 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json
@@ -14,8 +14,8 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/nuxt": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"nuxt": "^3.14.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
index 2a2ab10334f1..b7481e044b3e 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
@@ -16,8 +16,8 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/nuxt": "latest || *",
- "@sentry/core": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"nuxt": "^3.14.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts
index ecb0e32133db..f7635e6e06c9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/database.test.ts
@@ -128,7 +128,9 @@ test.describe('database integration', () => {
test('captures database error and marks span as failed', async ({ request }) => {
const errorPromise = waitForError('nuxt-3', errorEvent => {
- return !!errorEvent?.exception?.values?.[0]?.value?.includes('no such table');
+ return !!errorEvent?.exception?.values?.some(
+ value => value.mechanism?.type === 'auto.db.nuxt' && value.value?.includes('no such table'),
+ );
});
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
@@ -141,9 +143,11 @@ test.describe('database integration', () => {
const [error, transaction] = await Promise.all([errorPromise, transactionPromise]);
- expect(error).toBeDefined();
- expect(error.exception?.values?.[0]?.value).toContain('no such table');
- expect(error.exception?.values?.[0]?.mechanism).toEqual({
+ const dbException = error.exception?.values?.find(value => value.mechanism?.type === 'auto.db.nuxt');
+
+ expect(dbException).toBeDefined();
+ expect(dbException?.value).toContain('no such table');
+ expect(dbException?.mechanism).toEqual({
handled: false,
type: 'auto.db.nuxt',
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-4/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index 3f25ef7df0e4..02477111483d 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -19,7 +19,7 @@
},
"dependencies": {
"@pinia/nuxt": "^0.5.5",
- "@sentry/nuxt": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"nuxt": "^4.1.2"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
index 9b9fdd892563..a74df938bc32 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/database.test.ts
@@ -128,7 +128,9 @@ test.describe('database integration', () => {
test('captures database error and marks span as failed', async ({ request }) => {
const errorPromise = waitForError('nuxt-4', errorEvent => {
- return !!errorEvent?.exception?.values?.[0]?.value?.includes('no such table');
+ return !!errorEvent?.exception?.values?.some(
+ value => value.mechanism?.type === 'auto.db.nuxt' && value.value?.includes('no such table'),
+ );
});
const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => {
@@ -141,9 +143,11 @@ test.describe('database integration', () => {
const [error, transaction] = await Promise.all([errorPromise, transactionPromise]);
- expect(error).toBeDefined();
- expect(error.exception?.values?.[0]?.value).toContain('no such table');
- expect(error.exception?.values?.[0]?.mechanism).toEqual({
+ const dbException = error.exception?.values?.find(value => value.mechanism?.type === 'auto.db.nuxt');
+
+ expect(dbException).toBeDefined();
+ expect(dbException?.value).toContain('no such table');
+ expect(dbException?.mechanism).toEqual({
handled: false,
type: 'auto.db.nuxt',
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
index aa8296bc3314..bff128f66ed9 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/package.json
@@ -23,7 +23,7 @@
],
"dependencies": {
"@pinia/nuxt": "^0.11.3",
- "@sentry/nuxt": "latest || *",
+ "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
"nitro": "latest",
"nuxt": "npm:nuxt-nightly@5x"
},
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts
index 331b41d90ccf..ec7102449617 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/database.test.ts
@@ -128,7 +128,9 @@ test.describe('database integration', () => {
test('captures database error and marks span as failed', async ({ request }) => {
const errorPromise = waitForError('nuxt-5', errorEvent => {
- return !!errorEvent?.exception?.values?.[0]?.value?.includes('no such table');
+ return !!errorEvent?.exception?.values?.some(
+ value => value.mechanism?.type === 'auto.db.nuxt' && value.value?.includes('no such table'),
+ );
});
const transactionPromise = waitForTransaction('nuxt-5', transactionEvent => {
@@ -141,9 +143,11 @@ test.describe('database integration', () => {
const [error, transaction] = await Promise.all([errorPromise, transactionPromise]);
- expect(error).toBeDefined();
- expect(error.exception?.values?.[0]?.value).toContain('no such table');
- expect(error.exception?.values?.[0]?.mechanism).toEqual({
+ const dbException = error.exception?.values?.find(value => value.mechanism?.type === 'auto.db.nuxt');
+
+ expect(dbException).toBeDefined();
+ expect(dbException?.value).toContain('no such table');
+ expect(dbException?.mechanism).toEqual({
handled: false,
type: 'auto.db.nuxt',
});
diff --git a/dev-packages/e2e-tests/test-applications/react-17/.npmrc b/dev-packages/e2e-tests/test-applications/react-17/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-17/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-17/package.json b/dev-packages/e2e-tests/test-applications/react-17/package.json
index 7c11ea63d039..1982fffe7a53 100644
--- a/dev-packages/e2e-tests/test-applications/react-17/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-17/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2",
"react": "17.0.2",
diff --git a/dev-packages/e2e-tests/test-applications/react-19/.npmrc b/dev-packages/e2e-tests/test-applications/react-19/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-19/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json
index 08ef823e2bfe..f3a776a03924 100644
--- a/dev-packages/e2e-tests/test-applications/react-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-19/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"history": "4.9.0",
"@types/history": "4.7.11",
"@types/node": "^18.19.1",
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
index 8bb20583f5aa..5a0b7d847162 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-hash-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
index acbf8d00ef2d..dbe32bdcf506 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
index 119fde33b8e8..c4415f3433ed 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
index 973d87e057e5..8dcb32a24721 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"history": "4.9.0",
"@types/history": "4.7.11",
"@types/node": "^18.19.1",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
index 73b0eb2e0d8b..1457efba945e 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
index fe4494775753..128c4967554c 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"react": "18.2.0",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-6/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-6/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json
index 228705bb6493..2d84c95d58f1 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-6/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json
index 0c312ea52926..586fbccee112 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
index 8a9d6ac5656d..20fdccf46f4c 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
@@ -9,7 +9,7 @@
"react-router": "^7.13.0",
"@react-router/node": "^7.13.0",
"@react-router/serve": "^7.13.0",
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json
index 9666bf218893..b7e2fd8de655 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json
@@ -6,7 +6,7 @@
"dependencies": {
"@react-router/node": "latest",
"@react-router/serve": "latest",
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"isbot": "^5.1.17",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
index e69f23c0630a..65f4a96b0165 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-node-20-18/package.json
@@ -9,7 +9,7 @@
"react-router": "7.13.0",
"@react-router/node": "7.13.0",
"@react-router/serve": "7.13.0",
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
index 663e85a53963..56c4b7d052d7 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa-node-20-18/package.json
@@ -17,7 +17,7 @@
"test:assert": "pnpm test:ts &&pnpm test:prod"
},
"dependencies": {
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"@react-router/node": "7.13.0",
"@react-router/serve": "7.13.0",
"isbot": "^5.1.27",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
index e5c375174793..28f189bcd1f3 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json
@@ -17,7 +17,7 @@
"test:assert": "pnpm test:ts &&pnpm test:prod"
},
"dependencies": {
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"@react-router/node": "^7.13.0",
"@react-router/serve": "^7.13.0",
"isbot": "^5.1.27",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
index fbd49881f521..fde0e1699d6a 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json
@@ -9,7 +9,7 @@
"react-router": "^7.13.0",
"@react-router/node": "^7.13.0",
"@react-router/serve": "^7.13.0",
- "@sentry/react-router": "latest || *",
+ "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"isbot": "^5.1.17"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json
index afd2a32834aa..9e649c11afbe 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"express": "^4.21.2",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-spa/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
index 0cb3c988dfc2..c792359c5a3f 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",
"react": "18.3.1",
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/.npmrc b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
index a256cd9f29ae..b5958cefd6f7 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@types/node": "^18.19.1",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/.npmrc b/dev-packages/e2e-tests/test-applications/remix-hydrogen/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
index 8a35edaca3fb..b51c2868f415 100644
--- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
+++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
@@ -17,8 +17,8 @@
"@remix-run/react": "^2.17.4",
"@remix-run/server-runtime": "^2.17.4",
"@remix-run/cloudflare-pages": "^2.17.4",
- "@sentry/cloudflare": "latest || *",
- "@sentry/remix": "latest || *",
+ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@sentry/vite-plugin": "^5.2.0",
"@shopify/hydrogen": "2025.4.0",
"@shopify/remix-oxygen": "2.0.10",
diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc b/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/remix-server-timing/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json
index d31e86ff0cdc..fd57d2920d5a 100644
--- a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json
+++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json
@@ -11,7 +11,7 @@
"test:assert": "pnpm playwright test"
},
"dependencies": {
- "@sentry/remix": "latest || *",
+ "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@remix-run/css-bundle": "2.17.4",
"@remix-run/node": "2.17.4",
"@remix-run/react": "2.17.4",
diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/.npmrc b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json
index 8082add342d1..04a2fd2adeec 100644
--- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/solid": "latest || *",
+ "@sentry/solid": "file:../../packed/sentry-solid-packed.tgz",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/solid-router": "^1.141.8",
"@tanstack/solid-router-devtools": "^1.132.25",
diff --git a/dev-packages/e2e-tests/test-applications/solid/.npmrc b/dev-packages/e2e-tests/test-applications/solid/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solid/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json
index 3551001d5dd1..aa0506110761 100644
--- a/dev-packages/e2e-tests/test-applications/solid/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid/package.json
@@ -25,7 +25,7 @@
},
"dependencies": {
"solid-js": "^1.8.18",
- "@sentry/solid": "latest || *"
+ "@sentry/solid": "file:../../packed/sentry-solid-packed.tgz"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
index 1dbca26fc50c..747162d0bd75 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json
@@ -12,7 +12,7 @@
},
"type": "module",
"dependencies": {
- "@sentry/solidstart": "latest || *"
+ "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-spa/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
index cc4549c12c47..a9d1d6b91da3 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json
@@ -12,7 +12,7 @@
},
"type": "module",
"dependencies": {
- "@sentry/solidstart": "latest || *"
+ "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
index 35a20e4e64a7..c97a130c92b1 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json
@@ -12,7 +12,7 @@
},
"type": "module",
"dependencies": {
- "@sentry/solidstart": "latest || *"
+ "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/solidstart/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json
index bb2f5b85134c..7e382b6dc54b 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/package.json
+++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json
@@ -12,7 +12,7 @@
},
"type": "module",
"dependencies": {
- "@sentry/solidstart": "latest || *"
+ "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc b/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json
index cb84814fd29a..4b9d9062dca3 100644
--- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@next/font": "14.2.15",
- "@sentry/nextjs": "latest || *",
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@supabase/auth-helpers-react": "0.5.0",
"@supabase/auth-ui-react": "0.4.7",
"@supabase/supabase-js": "2.49.1",
diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/.npmrc b/dev-packages/e2e-tests/test-applications/svelte-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/svelte-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
index 1cfa4f510219..48039e81d8bf 100644
--- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json
@@ -24,7 +24,7 @@
"vite": "^5.4.11"
},
"dependencies": {
- "@sentry/svelte": "latest || *"
+ "@sentry/svelte": "file:../../packed/sentry-svelte-packed.tgz"
},
"volta": {
"extends": "../../package.json"
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json
index abfd19bfa4ae..162c148d3a86 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/sveltekit": "latest || *",
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz",
"@spotlightjs/spotlight": "2.0.0-alpha.1"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
index 6b183ea3ca54..50fd974e98b9 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/sveltekit": "latest || *",
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz",
"@spotlightjs/spotlight": "2.0.0-alpha.1"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
index fc691d4acebf..23f059eaee43 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/sveltekit": "latest || *"
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
index 667371981e5f..388ea1f26f35 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/sveltekit": "latest || *"
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
index e17a9b2bdabd..8081e11fe19f 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json
@@ -15,7 +15,7 @@
"test:assert": "pnpm run test:e2e"
},
"dependencies": {
- "@sentry/sveltekit": "latest || *"
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/.npmrc b/dev-packages/e2e-tests/test-applications/tanstack-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/tanstack-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
index edb4a6cd6707..65086e5b4953 100644
--- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json
@@ -12,7 +12,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/react": "latest || *",
+ "@sentry/react": "file:../../packed/sentry-react-packed.tgz",
"@tanstack/react-router": "^1.64.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/.npmrc b/dev-packages/e2e-tests/test-applications/tanstackstart-react/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
index bcfb3279f684..6d431226dbfc 100644
--- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@sentry/tanstackstart-react": "latest || *",
+ "@sentry/tanstackstart-react": "file:../../packed/sentry-tanstackstart-react-packed.tgz",
"@tanstack/react-start": "^1.136.0",
"@tanstack/react-router": "^1.136.0",
"react": "^19.2.0",
diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/.npmrc b/dev-packages/e2e-tests/test-applications/tsx-express/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/tsx-express/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/package.json b/dev-packages/e2e-tests/test-applications/tsx-express/package.json
index 3c8cedfca04e..7794d2c7ac52 100644
--- a/dev-packages/e2e-tests/test-applications/tsx-express/package.json
+++ b/dev-packages/e2e-tests/test-applications/tsx-express/package.json
@@ -11,8 +11,8 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
- "@sentry/core": "latest || *",
- "@sentry/node": "latest || *",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@trpc/server": "10.45.4",
"@trpc/client": "10.45.4",
"@types/express": "^4.17.21",
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/.npmrc b/dev-packages/e2e-tests/test-applications/vue-3/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/vue-3/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json
index 603f2f0ffc31..243f4875add6 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json
@@ -19,7 +19,7 @@
"test:print-version": "node -p \"'Vue version: ' + require('vue/package.json').version\""
},
"dependencies": {
- "@sentry/vue": "latest || *",
+ "@sentry/vue": "file:../../packed/sentry-vue-packed.tgz",
"pinia": "^3.0.0",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json
index 448876ec6d2b..8f764e682cd9 100644
--- a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json
@@ -13,7 +13,7 @@
"test:assert": "pnpm test:prod"
},
"dependencies": {
- "@sentry/vue": "latest || *",
+ "@sentry/vue": "file:../../packed/sentry-vue-packed.tgz",
"@tanstack/vue-router": "^1.141.8",
"vue": "^3.4.15"
},
diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc b/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/webpack-4/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/package.json b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
index d4f59a0d0511..367804adc700 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json
@@ -10,7 +10,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
"babel-loader": "^8.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc b/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc
deleted file mode 100644
index 070f80f05092..000000000000
--- a/dev-packages/e2e-tests/test-applications/webpack-5/.npmrc
+++ /dev/null
@@ -1,2 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/webpack-5/package.json b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
index 4378532be1b9..8d7f63c08f27 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json
@@ -10,7 +10,7 @@
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
- "@sentry/browser": "latest || *",
+ "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",
"webpack": "^5.91.0",
"terser-webpack-plugin": "^5.3.10",
"html-webpack-plugin": "^5.6.0",
diff --git a/dev-packages/e2e-tests/test-registry.npmrc b/dev-packages/e2e-tests/test-registry.npmrc
deleted file mode 100644
index 97b9627a1642..000000000000
--- a/dev-packages/e2e-tests/test-registry.npmrc
+++ /dev/null
@@ -1,6 +0,0 @@
-@sentry:registry=http://127.0.0.1:4873
-@sentry-internal:registry=http://127.0.0.1:4873
-//127.0.0.1:4873/:_authToken=some-token
-
-# Do not notify about npm updates
-update-notifier=false
diff --git a/dev-packages/e2e-tests/validate-packed-tarball-setup.ts b/dev-packages/e2e-tests/validate-packed-tarball-setup.ts
new file mode 100644
index 000000000000..a6cc966d56f0
--- /dev/null
+++ b/dev-packages/e2e-tests/validate-packed-tarball-setup.ts
@@ -0,0 +1,42 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import { sync as globSync } from 'glob';
+import * as path from 'path';
+
+const repositoryRoot = path.resolve(__dirname, '../..');
+
+const e2ePkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')) as { version: string };
+const version = e2ePkg.version;
+
+const tarballPaths = globSync(`packages/*/sentry-*-${version}.tgz`, {
+ cwd: repositoryRoot,
+ absolute: true,
+});
+
+assert.ok(
+ tarballPaths.length > 0,
+ `No tarballs found for version ${version}. Run "yarn build:tarball" at the repository root.`,
+);
+
+const symlinkPaths = globSync('packed/*-packed.tgz', {
+ cwd: __dirname,
+ absolute: true,
+});
+
+assert.ok(
+ symlinkPaths.length > 0,
+ 'No packed tarball symlinks found. Run "yarn test:prepare" in dev-packages/e2e-tests.',
+);
+
+assert.strictEqual(
+ symlinkPaths.length,
+ tarballPaths.length,
+ `Tarball count (${tarballPaths.length}) does not match packed symlink count (${symlinkPaths.length}). Re-run "yarn sync:packed-tarballs".`,
+);
+
+for (const symlinkPath of symlinkPaths) {
+ const st = fs.lstatSync(symlinkPath);
+ assert.ok(st.isSymbolicLink(), `Expected ${symlinkPath} to be a symlink.`);
+ const target = path.resolve(path.dirname(symlinkPath), fs.readlinkSync(symlinkPath));
+ assert.ok(fs.existsSync(target), `Symlink ${symlinkPath} points to missing file: ${target}`);
+}
diff --git a/dev-packages/e2e-tests/validate-test-app-setups.ts b/dev-packages/e2e-tests/validate-test-app-setups.ts
deleted file mode 100644
index edbbe047417f..000000000000
--- a/dev-packages/e2e-tests/validate-test-app-setups.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable no-console */
-import * as fs from 'fs';
-import { globSync } from 'glob';
-import * as path from 'path';
-
-const testRecipePaths = globSync('test-applications/*/test-recipe.json', {
- cwd: __dirname,
- absolute: true,
-});
-
-testRecipePaths.forEach(testRecipePath => {
- const testAppPath = path.dirname(testRecipePath);
- const npmrcPath = path.resolve(testAppPath, '.npmrc');
-
- if (!fs.existsSync(npmrcPath)) {
- console.log(
- `No .npmrc found in test application "${testAppPath}". Please add a .npmrc to this test application that uses the fake test registry. (More info in dev-packages/e2e-tests/README.md)`,
- );
- process.exit(1);
- }
-
- const npmrcContents = fs.readFileSync(npmrcPath, 'utf-8');
- if (!npmrcContents.includes('http://localhost:4873')) {
- console.log(
- `.npmrc in test application "${testAppPath} doesn't contain a reference to the fake test registry at "http://localhost:4873". Please add a .npmrc to this test application that uses the fake test registry. (More info in dev-packages/e2e-tests/README.md)`,
- );
- process.exit(1);
- }
-});
diff --git a/dev-packages/e2e-tests/validate-verdaccio-configuration.ts b/dev-packages/e2e-tests/validate-verdaccio-configuration.ts
deleted file mode 100644
index 7bef179bd5a6..000000000000
--- a/dev-packages/e2e-tests/validate-verdaccio-configuration.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as assert from 'assert';
-import * as fs from 'fs';
-import { globSync } from 'glob';
-import * as path from 'path';
-import * as YAML from 'yaml';
-
-/*
- * This file is a quick automatic check to confirm that the packages in the Verdaccio configuration always match the
- * packages we defined in our monorepo. This is to ensure that the E2E tests do not use the packages that live on NPM
- * but the local ones instead.
- */
-
-const repositoryRoot = path.resolve(__dirname, '../..');
-
-const verdaccioConfigContent = fs.readFileSync('./verdaccio-config/config.yaml', { encoding: 'utf8' });
-const verdaccioConfig = YAML.parse(verdaccioConfigContent);
-// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
-const sentryScopedPackagesInVerdaccioConfig = Object.keys(verdaccioConfig.packages).filter(packageName =>
- packageName.startsWith('@sentry/'),
-);
-
-const packageJsonPaths = globSync('packages/*/package.json', {
- cwd: repositoryRoot,
- absolute: true,
-});
-const packageJsons = packageJsonPaths.map(packageJsonPath => require(packageJsonPath));
-const sentryScopedPackageNames = packageJsons
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- .filter(packageJson => packageJson.name.startsWith('@sentry/'))
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- .map(packageJson => packageJson.name);
-
-const extraPackagesInVerdaccioConfig = sentryScopedPackagesInVerdaccioConfig.filter(
- x => !sentryScopedPackageNames.includes(x),
-);
-const extraPackagesInMonoRepo = sentryScopedPackageNames.filter(
- x => !sentryScopedPackagesInVerdaccioConfig.includes(x),
-);
-
-assert.ok(
- extraPackagesInVerdaccioConfig.length === 0 && extraPackagesInMonoRepo.length === 0,
- `Packages in Verdaccio configuration do not match the "@sentry"-scoped packages in monorepo. Make sure they match!\nPackages missing in Verdaccio configuration: ${JSON.stringify(
- extraPackagesInMonoRepo,
- )}\nPackages missing in monorepo: ${JSON.stringify(extraPackagesInVerdaccioConfig)}`,
-);
diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml
deleted file mode 100644
index 9d726fdf772f..000000000000
--- a/dev-packages/e2e-tests/verdaccio-config/config.yaml
+++ /dev/null
@@ -1,288 +0,0 @@
-# Taken from https://github.com/babel/babel/blob/624c78d99e8f42b2543b8943ab1b62bd71cf12d8/scripts/integration-tests/verdaccio-config.yml
-
-#
-# This is the default config file. It allows all users to do anything,
-# so don't use it on production systems.
-#
-# Look here for more config file examples:
-# https://github.com/verdaccio/verdaccio/tree/master/conf
-#
-
-# path to a directory with all packages
-storage: /verdaccio/storage/data
-
-# https://verdaccio.org/docs/configuration#authentication
-auth:
- htpasswd:
- file: /verdaccio/storage/htpasswd
-
-# https://verdaccio.org/docs/configuration#uplinks
-# a list of other known repositories we can talk to
-uplinks:
- npmjs:
- url: https://registry.npmjs.org/
-
-# Learn how to protect your packages
-# https://verdaccio.org/docs/protect-your-dependencies/
-# https://verdaccio.org/docs/configuration#packages
-packages:
- # To not use a proxy (e.g. npm) but instead use verdaccio for package hosting we need to define rules here without the
- # `proxy` field. Sadly we can't use a wildcard like "@sentry/*" because we have some dependencies (@sentry/cli,
- # @sentry/webpack-plugin) that fall under that wildcard but don't live in this repository. If we were to use that
- # wildcard, we would get a 404 when attempting to install them, since they weren't uploaded to verdaccio, and also
- # don't have a proxy configuration.
-
- '@sentry/angular':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/astro':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/browser':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/bun':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/core':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/cloudflare':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/deno':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/effect':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/elysia':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/ember':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/gatsby':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/hono':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/nestjs':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/nextjs':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/node':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/node-core':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/node-native':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/opentelemetry':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/profiling-node':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/react':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/react-router':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/remix':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/aws-serverless':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/google-cloud-serverless':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/solid':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/solidstart':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/svelte':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/sveltekit':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/tanstackstart':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/tanstackstart-react':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/types':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/vercel-edge':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/vue':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/nuxt':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry/wasm':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@sentry-internal/*':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
- '@*/*':
- # scoped packages
- access: $all
- publish: $all
- unpublish: $all
- proxy: npmjs
-
- '**':
- # allow all users (including non-authenticated users) to read and
- # publish all packages
- #
- # you can specify usernames/groupnames (depending on your auth plugin)
- # and three keywords: "$all", "$anonymous", "$authenticated"
- access: $all
-
- # allow all known users to publish/publish packages
- # (anyone can register by default, remember?)
- publish: $all
- unpublish: $all
- proxy: npmjs
-
-# https://verdaccio.org/docs/configuration#server
-# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections.
-# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout.
-# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
-server:
- keepAliveTimeout: 60
-
-middlewares:
- audit:
- enabled: false
-
-# https://verdaccio.org/docs/logger
-# log settings
-log: { type: stdout, format: pretty, level: http }
-#experiments:
-# # support for npm token command
-# token: false
diff --git a/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts
index ba8110a62675..4ff4dea6d5cd 100644
--- a/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts
+++ b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts
@@ -10,14 +10,17 @@ test('should aggregate successful, crashed and erroneous sessions', async () =>
.ignore('transaction', 'event')
.unignore('sessions')
.expect({
- sessions: {
- aggregates: [
- {
- started: expect.any(String),
- exited: 2,
- errored: 1,
- },
- ],
+ sessions: agg => {
+ // Sessions are bucketed by minute; tolerate splits across a minute boundary by summing.
+ const totals = agg.aggregates.reduce(
+ (acc, b) => ({
+ exited: acc.exited + (b.exited ?? 0),
+ errored: acc.errored + (b.errored ?? 0),
+ crashed: acc.crashed + (b.crashed ?? 0),
+ }),
+ { exited: 0, errored: 0, crashed: 0 },
+ );
+ expect(totals).toEqual({ exited: 2, errored: 1, crashed: 0 });
},
})
.start();
diff --git a/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts
index 228ee9a98643..152861e87765 100644
--- a/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts
+++ b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts
@@ -10,13 +10,17 @@ test('should aggregate successful sessions', async () => {
.ignore('transaction', 'event')
.unignore('sessions')
.expect({
- sessions: {
- aggregates: [
- {
- started: expect.any(String),
- exited: 3,
- },
- ],
+ sessions: agg => {
+ // Sessions are bucketed by minute; tolerate splits across a minute boundary by summing.
+ const totals = agg.aggregates.reduce(
+ (acc, b) => ({
+ exited: acc.exited + (b.exited ?? 0),
+ errored: acc.errored + (b.errored ?? 0),
+ crashed: acc.crashed + (b.crashed ?? 0),
+ }),
+ { exited: 0, errored: 0, crashed: 0 },
+ );
+ expect(totals).toEqual({ exited: 3, errored: 0, crashed: 0 });
},
})
.start();
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 712eeffcdeb3..ad8166e3163c 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
@@ -10,14 +10,17 @@ test('should aggregate successful and crashed sessions', async () => {
.ignore('transaction', 'event')
.unignore('sessions')
.expect({
- sessions: {
- aggregates: [
- {
- started: expect.any(String),
- exited: 2,
- crashed: 1,
- },
- ],
+ sessions: agg => {
+ // Sessions are bucketed by minute; tolerate splits across a minute boundary by summing.
+ const totals = agg.aggregates.reduce(
+ (acc, b) => ({
+ exited: acc.exited + (b.exited ?? 0),
+ errored: acc.errored + (b.errored ?? 0),
+ crashed: acc.crashed + (b.crashed ?? 0),
+ }),
+ { exited: 0, errored: 0, crashed: 0 },
+ );
+ expect(totals).toEqual({ exited: 2, errored: 0, crashed: 1 });
},
})
.start();
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 4f35e6259697..d2c83f5d30fa 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
@@ -10,15 +10,17 @@ test('should aggregate successful, crashed and erroneous sessions', async () =>
.ignore('transaction', 'event')
.unignore('sessions')
.expect({
- sessions: {
- aggregates: [
- {
- started: expect.any(String),
- exited: 1,
- crashed: 1,
- errored: 1,
- },
- ],
+ sessions: agg => {
+ // Sessions are bucketed by minute; tolerate splits across a minute boundary by summing.
+ const totals = agg.aggregates.reduce(
+ (acc, b) => ({
+ exited: acc.exited + (b.exited ?? 0),
+ errored: acc.errored + (b.errored ?? 0),
+ crashed: acc.crashed + (b.crashed ?? 0),
+ }),
+ { exited: 0, errored: 0, crashed: 0 },
+ );
+ expect(totals).toEqual({ exited: 1, errored: 1, crashed: 1 });
},
})
.start();
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 228ee9a98643..152861e87765 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
@@ -10,13 +10,17 @@ test('should aggregate successful sessions', async () => {
.ignore('transaction', 'event')
.unignore('sessions')
.expect({
- sessions: {
- aggregates: [
- {
- started: expect.any(String),
- exited: 3,
- },
- ],
+ sessions: agg => {
+ // Sessions are bucketed by minute; tolerate splits across a minute boundary by summing.
+ const totals = agg.aggregates.reduce(
+ (acc, b) => ({
+ exited: acc.exited + (b.exited ?? 0),
+ errored: acc.errored + (b.errored ?? 0),
+ crashed: acc.crashed + (b.crashed ?? 0),
+ }),
+ { exited: 0, errored: 0, crashed: 0 },
+ );
+ expect(totals).toEqual({ exited: 3, errored: 0, crashed: 0 });
},
})
.start();
diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/amqplib/docker-compose.yml
index c1127f097dbf..6d1468d3e04c 100644
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/docker-compose.yml
@@ -10,6 +10,12 @@ services:
ports:
- '5672:5672'
- '15672:15672'
+ healthcheck:
+ test: ['CMD-SHELL', 'rabbitmq-diagnostics -q ping']
+ interval: 2s
+ timeout: 10s
+ retries: 30
+ start_period: 15s
networks:
default:
diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
index 0be272187f3f..62f2931daba5 100644
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
@@ -34,7 +34,6 @@ describe('amqplib auto-instrumentation', () => {
await createTestRunner()
.withDockerCompose({
workingDirectory: [__dirname],
- readyMatches: ['Time to start RabbitMQ'],
})
.expect({
transaction: (transaction: TransactionEvent) => {
diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts
index 67d0ff8b56fb..484e7c948407 100644
--- a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts
@@ -1,251 +1,137 @@
-import { afterAll, describe, expect } from 'vitest';
+import { afterAll, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
-describe('hono tracing', () => {
- afterAll(() => {
- cleanupChildProcesses();
+const ROUTES = ['/sync', '/async'] as const;
+const METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
+const PATHS = ['/', '/all', '/on'] as const;
+
+type Method = (typeof METHODS)[number];
+
+function verifyHonoSpan(name: string, type: 'middleware' | 'request_handler') {
+ return expect.objectContaining({
+ data: expect.objectContaining({
+ 'hono.name': name,
+ 'hono.type': type,
+ }),
+ description: name,
+ op: type === 'request_handler' ? 'request_handler.hono' : 'middleware.hono',
+ origin: 'auto.http.otel.hono',
});
+}
- createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
- describe.each(['/sync', '/async'] as const)('when using %s route', route => {
- describe.each(['get', 'post', 'put', 'delete', 'patch'] as const)('when using %s method', method => {
- describe.each(['/', '/all', '/on'])('when using %s path', path => {
- test('should handle transaction', async () => {
- const runner = createRunner()
- .expect({
- transaction: {
- transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}`,
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryRequestMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryRequestMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryErrorMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryErrorMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'global',
- 'hono.type': 'middleware',
- }),
- description: 'global',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'base',
- 'hono.type': 'middleware',
- }),
- description: 'base',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': `${route}${path === '/' ? '' : path}`,
- 'hono.type': 'request_handler',
- }),
- description: `${route}${path === '/' ? '' : path}`,
- op: 'request_handler.hono',
- origin: 'auto.http.otel.hono',
- }),
- ]),
- },
- })
- .start();
- runner.makeRequest(method, `${route}${path === '/' ? '' : path}`);
- await runner.completed();
- });
+function baseSpans() {
+ return [
+ verifyHonoSpan('sentryRequestMiddleware', 'middleware'),
+ verifyHonoSpan('sentryErrorMiddleware', 'middleware'),
+ verifyHonoSpan('global', 'middleware'),
+ verifyHonoSpan('base', 'middleware'),
+ ];
+}
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
- test('should handle transaction with anonymous middleware', async () => {
- const runner = createRunner()
- .expect({
- transaction: {
- transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware`,
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryRequestMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryRequestMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryErrorMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryErrorMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'global',
- 'hono.type': 'middleware',
- }),
- description: 'global',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'base',
- 'hono.type': 'middleware',
- }),
- description: 'base',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'anonymous',
- 'hono.type': 'middleware',
- }),
- description: 'anonymous',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': `${route}${path === '/' ? '' : path}/middleware`,
- 'hono.type': 'request_handler',
- }),
- description: `${route}${path === '/' ? '' : path}/middleware`,
- op: 'request_handler.hono',
- origin: 'auto.http.otel.hono',
- }),
- ]),
- },
- })
- .start();
- runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware`);
- await runner.completed();
+createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('should handle transactions for all route/method/path combinations', async () => {
+ const runner = createRunner();
+ const requests: Array<{ method: Method; url: string }> = [];
+
+ for (const route of ROUTES) {
+ for (const method of METHODS) {
+ for (const path of PATHS) {
+ const pathSuffix = path === '/' ? '' : path;
+ const fullPath = `${route}${pathSuffix}`;
+
+ runner.expect({
+ transaction: {
+ transaction: `${method.toUpperCase()} ${fullPath}`,
+ spans: expect.arrayContaining([...baseSpans(), verifyHonoSpan(fullPath, 'request_handler')]),
+ },
});
+ requests.push({ method, url: fullPath });
- test('should handle transaction with separate middleware', async () => {
- const runner = createRunner()
- .expect({
- transaction: {
- transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware/separately`,
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryRequestMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryRequestMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'sentryErrorMiddleware',
- 'hono.type': 'middleware',
- }),
- description: 'sentryErrorMiddleware',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'global',
- 'hono.type': 'middleware',
- }),
- description: 'global',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'base',
- 'hono.type': 'middleware',
- }),
- description: 'base',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': 'anonymous',
- 'hono.type': 'middleware',
- }),
- description: 'anonymous',
- op: 'middleware.hono',
- origin: 'auto.http.otel.hono',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'hono.name': `${route}${path === '/' ? '' : path}/middleware/separately`,
- 'hono.type': 'request_handler',
- }),
- description: `${route}${path === '/' ? '' : path}/middleware/separately`,
- op: 'request_handler.hono',
- origin: 'auto.http.otel.hono',
- }),
- ]),
- },
- })
- .start();
- runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware/separately`);
- await runner.completed();
+ runner.expect({
+ transaction: {
+ transaction: `${method.toUpperCase()} ${fullPath}/middleware`,
+ spans: expect.arrayContaining([
+ ...baseSpans(),
+ verifyHonoSpan('anonymous', 'middleware'),
+ verifyHonoSpan(`${fullPath}/middleware`, 'request_handler'),
+ ]),
+ },
});
+ requests.push({ method, url: `${fullPath}/middleware` });
- test('should handle returned errors for %s path', async () => {
- const runner = createRunner()
- .ignore('transaction')
- .expect({
- event: {
- exception: {
- values: [
- {
- mechanism: {
- type: 'auto.middleware.hono',
- handled: false,
- },
- type: 'Error',
- value: 'response 500',
- },
- ],
- },
- },
- })
- .start();
- runner.makeRequest(method, `${route}${path === '/' ? '' : path}/500`, { expectError: true });
- await runner.completed();
+ runner.expect({
+ transaction: {
+ transaction: `${method.toUpperCase()} ${fullPath}/middleware/separately`,
+ spans: expect.arrayContaining([
+ ...baseSpans(),
+ verifyHonoSpan('anonymous', 'middleware'),
+ verifyHonoSpan(`${fullPath}/middleware/separately`, 'request_handler'),
+ ]),
+ },
});
+ requests.push({ method, url: `${fullPath}/middleware/separately` });
+ }
+ }
+ }
+
+ const started = runner.start();
+ for (const req of requests) {
+ await started.makeRequest(req.method, req.url);
+ }
+ await started.completed();
+ }, 60_000);
+
+ test('should capture 500 errors for all route/method/path combinations', async () => {
+ const runner = createRunner().ignore('transaction');
+ const requests: Array<{ method: Method; url: string }> = [];
- test.each(['/401', '/402', '/403', '/does-not-exist'])(
- 'should ignores error %s path by default',
- async (subPath: string) => {
- const runner = createRunner()
- .expect({
- transaction: {
- transaction: `${method.toUpperCase()} ${route}`,
+ for (const route of ROUTES) {
+ for (const method of METHODS) {
+ for (const path of PATHS) {
+ const pathSuffix = path === '/' ? '' : path;
+
+ runner.expect({
+ event: {
+ exception: {
+ values: [
+ {
+ mechanism: {
+ type: 'auto.middleware.hono',
+ handled: false,
+ },
+ type: 'Error',
+ value: 'response 500',
},
- })
- .start();
- runner.makeRequest(method, `${route}${path === '/' ? '' : path}${subPath}`, { expectError: true });
- runner.makeRequest(method, route);
- await runner.completed();
+ ],
+ },
},
- );
- });
- });
- });
+ });
+ requests.push({ method, url: `${route}${pathSuffix}/500` });
+ }
+ }
+ }
+
+ const started = runner.start();
+ for (const req of requests) {
+ await started.makeRequest(req.method, req.url, { expectError: true });
+ }
+ await started.completed();
+ }, 60_000);
+
+ test.each(['/401', '/402', '/403', '/does-not-exist'])('should not capture %s errors', async (subPath: string) => {
+ const runner = createRunner()
+ .expect({
+ transaction: {
+ transaction: 'GET /sync',
+ },
+ })
+ .start();
+ runner.makeRequest('get', `/sync${subPath}`, { expectError: true });
+ runner.makeRequest('get', '/sync');
+ await runner.completed();
});
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml
index f744bfe6d50c..ab430a56ad00 100644
--- a/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml
@@ -5,3 +5,9 @@ services:
container_name: integration-tests-kafka
ports:
- '9092:9092'
+ healthcheck:
+ test: ['CMD-SHELL', '/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092']
+ interval: 2s
+ timeout: 5s
+ retries: 30
+ start_period: 15s
diff --git a/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts b/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts
index 84e8d4a5612e..b7a996bddc53 100644
--- a/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts
@@ -16,7 +16,6 @@ describe('kafkajs', () => {
await createRunner()
.withDockerCompose({
workingDirectory: [__dirname],
- readyMatches: ['9092'],
})
.expect({
transaction: (transaction: TransactionEvent) => {
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/docker-compose.yml
index 788311e4e117..a57c8cb840f8 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/docker-compose.yml
@@ -10,3 +10,9 @@ services:
environment:
MYSQL_ROOT_PASSWORD: docker
MYSQL_DATABASE: tests
+ 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/node-integration-tests/suites/tracing/knex/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
index e8116293de09..01db0bf6e88a 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
@@ -63,7 +63,7 @@ describe('knex auto instrumentation', () => {
};
await createRunner()
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/knex/pg/docker-compose.yml
index e3edcd1d8d7b..28a916737317 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: tests
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U test -d tests']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
index 7c38381eb125..63af7b3628b5 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
@@ -61,7 +61,7 @@ describe('knex auto instrumentation', () => {
};
await createRunner()
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml
index 71ea54ad7e70..598c394ace5e 100644
--- a/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/docker-compose.yml
@@ -7,3 +7,9 @@ services:
- '3306:3306'
environment:
MYSQL_ROOT_PASSWORD: password
+ healthcheck:
+ test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -ppassword']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 10s
diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts
index c1d680b9a52e..4c3078b922f7 100644
--- a/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/mysql2/test.ts
@@ -34,7 +34,7 @@ describe('mysql2 auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/instrument.mjs
new file mode 100644
index 000000000000..b56505ef5e2d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/instrument.mjs
@@ -0,0 +1,11 @@
+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,
+ transport: loggingTransport,
+ traceLifecycle: 'stream',
+ clientReportFlushInterval: 1_000,
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/scenario.mjs
new file mode 100644
index 000000000000..18afc6db5113
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/scenario.mjs
@@ -0,0 +1,2 @@
+import http from 'http';
+http.get('http://localhost:9999/external', () => {}).on('error', () => {});
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts
new file mode 100644
index 000000000000..2b987f92d755
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report-streamed/test.ts
@@ -0,0 +1,29 @@
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
+
+describe('no_parent_span client report (streaming)', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('records no_parent_span outcome for http.client span without a local parent', async () => {
+ const runner = createRunner()
+ .unignore('client_report')
+ .expect({
+ client_report: report => {
+ expect(report.discarded_events).toEqual([
+ {
+ category: 'span',
+ quantity: 1,
+ reason: 'no_parent_span',
+ },
+ ]);
+ },
+ })
+ .start();
+
+ await runner.completed();
+ });
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/instrument.mjs
new file mode 100644
index 000000000000..3a69e61ceb90
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/instrument.mjs
@@ -0,0 +1,10 @@
+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,
+ transport: loggingTransport,
+ clientReportFlushInterval: 1_000,
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/scenario.mjs
new file mode 100644
index 000000000000..18afc6db5113
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/scenario.mjs
@@ -0,0 +1,2 @@
+import http from 'http';
+http.get('http://localhost:9999/external', () => {}).on('error', () => {});
diff --git a/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts
new file mode 100644
index 000000000000..699dec65ddcf
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/no-parent-span-client-report/test.ts
@@ -0,0 +1,29 @@
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
+
+describe('no_parent_span client report', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('records no_parent_span outcome for http.client span without a local parent', async () => {
+ const runner = createRunner()
+ .unignore('client_report')
+ .expect({
+ client_report: report => {
+ expect(report.discarded_events).toEqual([
+ {
+ category: 'span',
+ quantity: 1,
+ reason: 'no_parent_span',
+ },
+ ]);
+ },
+ })
+ .start();
+
+ await runner.completed();
+ });
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml
index 51d9b86d028f..44425ae56188 100644
--- a/dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/postgres/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: tests
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U test -d tests']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts
index 1fd03e92d0e2..98c42976498a 100644
--- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts
@@ -49,7 +49,6 @@ describe('postgres auto instrumentation', () => {
await createRunner(__dirname, 'scenario.js')
.withDockerCompose({
workingDirectory: [__dirname],
- readyMatches: ['port 5432'],
setupCommand: 'yarn',
})
.expect({ transaction: EXPECTED_TRANSACTION })
@@ -61,7 +60,6 @@ describe('postgres auto instrumentation', () => {
await createRunner(__dirname, 'scenario-ignoreConnect.js')
.withDockerCompose({
workingDirectory: [__dirname],
- readyMatches: ['port 5432'],
setupCommand: 'yarn',
})
.expect({
@@ -152,7 +150,6 @@ describe('postgres auto instrumentation', () => {
await createRunner(__dirname, 'scenario-native.js')
.withDockerCompose({
workingDirectory: [__dirname],
- readyMatches: ['port 5432'],
setupCommand: 'yarn',
})
.expect({ transaction: EXPECTED_TRANSACTION })
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml
index 301280106faa..f3afd85af9ab 100644
--- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U test -d test_db']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
index 2dfbc020966b..f5f06208812e 100644
--- a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
@@ -218,7 +218,7 @@ describe('postgresjs auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.expect({ event: EXPECTED_ERROR_EVENT })
.start()
@@ -438,7 +438,7 @@ describe('postgresjs auto instrumentation', () => {
await createRunner(__dirname, 'scenario.mjs')
.withFlags('--import', `${__dirname}/instrument.mjs`)
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.expect({ event: EXPECTED_ERROR_EVENT })
.start()
@@ -532,7 +532,7 @@ describe('postgresjs auto instrumentation', () => {
await createRunner(__dirname, 'scenario-requestHook.js')
.withFlags('--require', `${__dirname}/instrument-requestHook.cjs`)
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -625,7 +625,7 @@ describe('postgresjs auto instrumentation', () => {
await createRunner(__dirname, 'scenario-requestHook.mjs')
.withFlags('--import', `${__dirname}/instrument-requestHook.mjs`)
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -706,7 +706,7 @@ describe('postgresjs auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-url.cjs')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -787,7 +787,7 @@ describe('postgresjs auto instrumentation', () => {
await createRunner(__dirname, 'scenario-url.mjs')
.withFlags('--import', `${__dirname}/instrument.mjs`)
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -866,7 +866,7 @@ describe('postgresjs auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-unsafe.cjs')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -946,7 +946,7 @@ describe('postgresjs auto instrumentation', () => {
await createRunner(__dirname, 'scenario-unsafe.mjs')
.withFlags('--import', `${__dirname}/instrument.mjs`)
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
index 37d45547b537..24bc212cac77 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U prisma -d tests']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts
index 74bdf4be4bd2..252ed938bf0d 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts
@@ -15,7 +15,6 @@ describe('Prisma ORM v5 Tests', () => {
await createRunner()
.withDockerCompose({
workingDirectory: [cwd],
- readyMatches: ['port 5432'],
setupCommand: 'yarn prisma generate && yarn prisma migrate dev -n sentry-test',
})
.expect({
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml
index ddab7cb9c563..28f111fe3156 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U prisma -d tests']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts
index 07405a496fd0..b804adb10f71 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts
@@ -16,7 +16,6 @@ describe('Prisma ORM v6 Tests', () => {
await createRunner()
.withDockerCompose({
workingDirectory: [cwd],
- readyMatches: ['port 5432'],
setupCommand: `yarn prisma generate --schema ${cwd}/prisma/schema.prisma && yarn prisma migrate dev -n sentry-test --schema ${cwd}/prisma/schema.prisma`,
})
.expect({
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/docker-compose.yml
index a56fd51cf7d9..117b5ef2c901 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/docker-compose.yml
@@ -11,3 +11,9 @@ services:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests
+ healthcheck:
+ test: ['CMD-SHELL', 'pg_isready -U prisma -d tests']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts
index 5bb0158eee3c..f9fb22606772 100644
--- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v7/test.ts
@@ -17,7 +17,6 @@ conditionalTest({ min: 20 })('Prisma ORM v7 Tests', () => {
await createRunner()
.withDockerCompose({
workingDirectory: [cwd],
- readyMatches: ['port 5432'],
setupCommand: `yarn prisma generate --schema ${cwd}/prisma/schema.prisma && tsc -p ${cwd}/prisma/tsconfig.json && yarn prisma migrate dev -n sentry-test --schema ${cwd}/prisma/schema.prisma`,
})
.expect({
diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/redis-cache/docker-compose.yml
index 164d5977e33d..ded08e8f62e5 100644
--- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/docker-compose.yml
@@ -4,6 +4,12 @@ services:
db:
image: redis:latest
restart: always
- container_name: integration-tests-redis
+ container_name: integration-tests-redis-cache
ports:
- '6379:6379'
+ healthcheck:
+ test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
index e1aa0b9c1494..c27957c37b06 100644
--- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
@@ -38,7 +38,7 @@ describe('redis cache auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-ioredis.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -137,7 +137,7 @@ describe('redis cache auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-ioredis.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
@@ -228,7 +228,7 @@ describe('redis cache auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-redis-4.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_REDIS_CONNECT })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/redis/docker-compose.yml
index 164d5977e33d..356302aa1f93 100644
--- a/dev-packages/node-integration-tests/suites/tracing/redis/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/redis/docker-compose.yml
@@ -7,3 +7,9 @@ services:
container_name: integration-tests-redis
ports:
- '6379:6379'
+ healthcheck:
+ test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
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 ec9cc0d93e84..7add18746f3a 100644
--- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts
@@ -43,7 +43,7 @@ describe('redis auto instrumentation', () => {
};
await createRunner(__dirname, 'scenario-ioredis.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
diff --git a/dev-packages/node-integration-tests/suites/tracing/tedious/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/tedious/docker-compose.yml
index 8e3604dca209..b77d05e2be20 100644
--- a/dev-packages/node-integration-tests/suites/tracing/tedious/docker-compose.yml
+++ b/dev-packages/node-integration-tests/suites/tracing/tedious/docker-compose.yml
@@ -10,3 +10,9 @@ services:
environment:
ACCEPT_EULA: 'Y'
MSSQL_SA_PASSWORD: 'TESTing123'
+ healthcheck:
+ test: ['CMD-SHELL', '/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P "TESTing123" -C -Q "SELECT 1"']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 20s
diff --git a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts
index 4b64611ac8f2..a8d45ad43877 100644
--- a/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/tedious/test.ts
@@ -42,7 +42,7 @@ describe.skip('tedious auto instrumentation', { timeout: 75_000 }, () => {
};
await createRunner(__dirname, 'scenario.js')
- .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['1433'] })
+ .withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
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 51e1b4d09ccf..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
@@ -33,4 +33,7 @@ Sentry.captureMessage('SIGTERM flush message');
console.log('READY');
// Keep the process alive so the integration test can send SIGTERM.
-setInterval(() => undefined, 1_000);
+const interval = setInterval(() => undefined, 10_000);
+
+// allow graceful exit once the SIGTERM arrives.
+process.once('SIGTERM', () => clearInterval(interval));
diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts
index 7690fa40ee8b..89f96974c123 100644
--- a/dev-packages/node-integration-tests/utils/runner.ts
+++ b/dev-packages/node-integration-tests/utils/runner.ts
@@ -68,10 +68,6 @@ interface DockerOptions {
* The working directory to run docker compose in
*/
workingDirectory: string[];
- /**
- * The strings to look for in the output to know that the docker compose is ready for the test to be run
- */
- readyMatches: string[];
/**
* The command to run after docker compose is up
*/
@@ -79,56 +75,51 @@ interface DockerOptions {
}
/**
- * Runs docker compose up and waits for the readyMatches to appear in the output
+ * Runs `docker compose up -d --wait`, which blocks until every service's
+ * healthcheck reports healthy. Each suite defines its healthcheck in its
+ * own docker-compose.yml.
*
* Returns a function that can be called to docker compose down
*/
async function runDockerCompose(options: DockerOptions): Promise {
- return new Promise((resolve, reject) => {
- const cwd = join(...options.workingDirectory);
- const close = (): void => {
- spawnSync('docker', ['compose', 'down', '--volumes'], {
- cwd,
- stdio: process.env.DEBUG ? 'inherit' : undefined,
- });
- };
-
- // ensure we're starting fresh
- close();
-
- const child = spawn('docker', ['compose', 'up'], { cwd });
+ const cwd = join(...options.workingDirectory);
+ const close = (): void => {
+ spawnSync('docker', ['compose', 'down', '--volumes'], {
+ cwd,
+ stdio: process.env.DEBUG ? 'inherit' : undefined,
+ });
+ };
- const timeout = setTimeout(() => {
- close();
- reject(new Error('Timed out waiting for docker-compose'));
- }, 75_000);
+ // ensure we're starting fresh
+ close();
- function newData(data: Buffer): void {
- const text = data.toString('utf8');
+ const result = spawnSync('docker', ['compose', 'up', '-d', '--wait'], {
+ cwd,
+ stdio: process.env.DEBUG ? 'inherit' : 'pipe',
+ });
- if (process.env.DEBUG) log(text);
+ if (result.status !== 0) {
+ const stderr = result.stderr?.toString() ?? '';
+ const stdout = result.stdout?.toString() ?? '';
+ // Surface container logs to make healthcheck failures easier to diagnose in CI
+ const logs = spawnSync('docker', ['compose', 'logs'], { cwd }).stdout?.toString() ?? '';
+ close();
+ throw new Error(
+ `docker compose up --wait failed (exit ${result.status})\n${stderr}${stdout}\n--- container logs ---\n${logs}`,
+ );
+ }
- for (const match of options.readyMatches) {
- if (text.includes(match)) {
- child.stdout.removeAllListeners();
- clearTimeout(timeout);
- if (options.setupCommand) {
- try {
- // Prepend local node_modules/.bin to PATH so additionalDependencies binaries take precedence
- const env = { ...process.env, PATH: `${cwd}/node_modules/.bin:${process.env.PATH}` };
- execSync(options.setupCommand, { cwd, stdio: 'inherit', env });
- } catch (e) {
- log('Error running docker setup command', e);
- }
- }
- resolve(close);
- }
- }
+ if (options.setupCommand) {
+ try {
+ // Prepend local node_modules/.bin to PATH so additionalDependencies binaries take precedence
+ const env = { ...process.env, PATH: `${cwd}/node_modules/.bin:${process.env.PATH}` };
+ execSync(options.setupCommand, { cwd, stdio: 'inherit', env });
+ } catch (e) {
+ log('Error running docker setup command', e);
}
+ }
- child.stdout.on('data', newData);
- child.stderr.on('data', newData);
- });
+ return close;
}
type ExpectedEvent = Partial | ((event: Event) => void);
diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
index 9d6edd3157c0..782e5d36c443 100644
--- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
+++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs
@@ -136,6 +136,8 @@ export function makeTerserPlugin() {
'_resolveFilename',
// Set on e.g. the shim feedbackIntegration to be able to detect it
'_isShim',
+ // Marker set by `withStreamedSpan()` to tag streamed `beforeSendSpan` callbacks
+ '_streamed',
// This is used in metadata integration
'_sentryModuleMetadata',
],
diff --git a/package.json b/package.json
index 3d211ca42067..f455f42e3bbf 100644
--- a/package.json
+++ b/package.json
@@ -130,7 +130,7 @@
"madge": "8.0.0",
"nodemon": "^3.1.10",
"npm-run-all2": "^6.2.0",
- "nx": "22.5.0",
+ "nx": "22.6.5",
"oxfmt": "^0.38.0",
"oxlint": "^1.53.0",
"oxlint-tsgolint": "^0.16.0",
@@ -158,7 +158,9 @@
"wide-align/string-width": "4.2.3",
"cliui/wrap-ansi": "7.0.0",
"sucrase": "getsentry/sucrase#es2020-polyfills",
- "**/express/path-to-regexp": "0.1.12"
+ "**/express/path-to-regexp": "0.1.12",
+ "**/@verdaccio/local-storage-legacy/lodash": "4.17.23",
+ "**/@verdaccio/core/minimatch": "~7.4.9"
},
"version": "0.0.0",
"name": "sentry-javascript"
diff --git a/packages/aws-serverless/scripts/buildLambdaLayer.ts b/packages/aws-serverless/scripts/buildLambdaLayer.ts
index cca3b739bf6b..520a456c63ce 100644
--- a/packages/aws-serverless/scripts/buildLambdaLayer.ts
+++ b/packages/aws-serverless/scripts/buildLambdaLayer.ts
@@ -73,6 +73,9 @@ async function buildLambdaLayer(): Promise {
console.log(`Creating final layer zip file ${zipFilename}.`);
// need to preserve the symlink above with -y
run(`zip -r -y ${zipFilename} ${dirsToZip.join(' ')}`, { cwd: 'build/aws/dist-serverless' });
+
+ // Cleanup temporary installation files
+ fs.rmSync('build/aws/dist-serverless/nodejs/', { recursive: true, force: true });
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
index 9a00ab322e16..76e853eef5d3 100644
--- a/packages/browser-utils/src/metrics/browserMetrics.ts
+++ b/packages/browser-utils/src/metrics/browserMetrics.ts
@@ -22,7 +22,7 @@ import {
addTtfbInstrumentationHandler,
type PerformanceLongAnimationFrameTiming,
} from './instrument';
-import { trackLcpAsStandaloneSpan } from './lcp';
+import { isValidLcpMetric, trackLcpAsStandaloneSpan } from './lcp';
import { resourceTimingToSpanAttributes } from './resourceTiming';
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
import { getActivationStart } from './web-vitals/lib/getActivationStart';
@@ -283,7 +283,7 @@ function _trackCLS(): () => void {
function _trackLCP(): () => void {
return addLcpInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1];
- if (!entry) {
+ if (!entry || !isValidLcpMetric(metric.value)) {
return;
}
diff --git a/packages/browser-utils/src/metrics/lcp.ts b/packages/browser-utils/src/metrics/lcp.ts
index a6410ac08580..c11f6bd63cbc 100644
--- a/packages/browser-utils/src/metrics/lcp.ts
+++ b/packages/browser-utils/src/metrics/lcp.ts
@@ -15,6 +15,15 @@ import { addLcpInstrumentationHandler } from './instrument';
import type { WebVitalReportEvent } from './utils';
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
+/**
+ * 60 seconds is the maximum for a plausible LCP value.
+ */
+export const MAX_PLAUSIBLE_LCP_DURATION = 60_000;
+
+export function isValidLcpMetric(lcpValue: number | undefined): lcpValue is number {
+ return lcpValue != null && lcpValue > 0 && lcpValue <= MAX_PLAUSIBLE_LCP_DURATION;
+}
+
/**
* Starts tracking the Largest Contentful Paint on the current page and collects the value once
*
@@ -34,7 +43,7 @@ export function trackLcpAsStandaloneSpan(client: Client): void {
const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined;
- if (!entry) {
+ if (!entry || !isValidLcpMetric(metric.value)) {
return;
}
standaloneLcpValue = metric.value;
@@ -56,6 +65,10 @@ export function _sendStandaloneLcpSpan(
pageloadSpanId: string,
reportEvent: WebVitalReportEvent,
) {
+ if (!isValidLcpMetric(lcpValue)) {
+ return;
+ }
+
DEBUG_BUILD && debug.log(`Sending LCP span (${lcpValue})`);
const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0));
diff --git a/packages/browser-utils/src/metrics/webVitalSpans.ts b/packages/browser-utils/src/metrics/webVitalSpans.ts
index b342b653df97..6f6d8de3901e 100644
--- a/packages/browser-utils/src/metrics/webVitalSpans.ts
+++ b/packages/browser-utils/src/metrics/webVitalSpans.ts
@@ -18,6 +18,7 @@ import { WINDOW } from '../types';
import { getCachedInteractionContext, INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION } from './inp';
import type { InstrumentationHandlerCallback } from './instrument';
import { addClsInstrumentationHandler, addInpInstrumentationHandler, addLcpInstrumentationHandler } from './instrument';
+import { isValidLcpMetric } from './lcp';
import type { WebVitalReportEvent } from './utils';
import { getBrowserPerformanceAPI, listenForWebVitalReportEvents, msToSec, supportsWebVital } from './utils';
import type { PerformanceEventTiming } from './instrument';
@@ -121,7 +122,7 @@ export function trackLcpAsSpan(client: Client): void {
const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined;
- if (!entry) {
+ if (!entry || !isValidLcpMetric(metric.value)) {
return;
}
lcpValue = metric.value;
@@ -143,6 +144,10 @@ export function _sendLcpSpan(
pageloadSpan?: Span,
reportEvent?: WebVitalReportEvent,
): void {
+ if (!isValidLcpMetric(lcpValue)) {
+ return;
+ }
+
DEBUG_BUILD && debug.log(`Sending LCP span (${lcpValue})`);
const performanceTimeOrigin = browserPerformanceTimeOrigin() || 0;
diff --git a/packages/browser-utils/test/metrics/lcp.test.ts b/packages/browser-utils/test/metrics/lcp.test.ts
new file mode 100644
index 000000000000..634b9652f816
--- /dev/null
+++ b/packages/browser-utils/test/metrics/lcp.test.ts
@@ -0,0 +1,105 @@
+import * as SentryCore from '@sentry/core';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { _sendStandaloneLcpSpan, isValidLcpMetric, MAX_PLAUSIBLE_LCP_DURATION } from '../../src/metrics/lcp';
+import * as WebVitalUtils from '../../src/metrics/utils';
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+ return {
+ ...actual,
+ browserPerformanceTimeOrigin: vi.fn(),
+ getCurrentScope: vi.fn(),
+ htmlTreeAsString: vi.fn(),
+ };
+});
+
+describe('isValidLcpMetric', () => {
+ it('returns true for plausible lcp values', () => {
+ expect(isValidLcpMetric(1)).toBe(true);
+ expect(isValidLcpMetric(2_500)).toBe(true);
+ expect(isValidLcpMetric(MAX_PLAUSIBLE_LCP_DURATION)).toBe(true);
+ });
+
+ it('returns false for implausible lcp values', () => {
+ expect(isValidLcpMetric(undefined)).toBe(false);
+ expect(isValidLcpMetric(0)).toBe(false);
+ expect(isValidLcpMetric(-1)).toBe(false);
+ expect(isValidLcpMetric(MAX_PLAUSIBLE_LCP_DURATION + 1)).toBe(false);
+ });
+});
+
+describe('_sendStandaloneLcpSpan', () => {
+ const mockSpan = {
+ addEvent: vi.fn(),
+ end: vi.fn(),
+ };
+
+ const mockScope = {
+ getScopeData: vi.fn().mockReturnValue({
+ transactionName: 'test-transaction',
+ }),
+ };
+
+ beforeEach(() => {
+ vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any);
+ vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000);
+ vi.mocked(SentryCore.htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`);
+ vi.spyOn(WebVitalUtils, 'startStandaloneWebVitalSpan').mockReturnValue(mockSpan as any);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('sends a standalone lcp span with entry data', () => {
+ const lcpValue = 1_234;
+ const mockEntry: LargestContentfulPaint = {
+ name: 'largest-contentful-paint',
+ entryType: 'largest-contentful-paint',
+ startTime: 100,
+ duration: 0,
+ id: 'image',
+ url: 'https://example.com/image.png',
+ size: 1234,
+ loadTime: 95,
+ renderTime: 100,
+ element: { tagName: 'img' } as Element,
+ toJSON: vi.fn(),
+ };
+
+ _sendStandaloneLcpSpan(lcpValue, mockEntry, '123', 'navigation');
+
+ expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({
+ name: '
',
+ transaction: 'test-transaction',
+ attributes: {
+ 'sentry.origin': 'auto.http.browser.lcp',
+ 'sentry.op': 'ui.webvital.lcp',
+ 'sentry.exclusive_time': 0,
+ 'sentry.pageload.span_id': '123',
+ 'sentry.report_event': 'navigation',
+ 'lcp.element': '
',
+ 'lcp.id': 'image',
+ 'lcp.url': 'https://example.com/image.png',
+ 'lcp.loadTime': 95,
+ 'lcp.renderTime': 100,
+ 'lcp.size': 1234,
+ },
+ startTime: 1.1,
+ });
+
+ expect(mockSpan.addEvent).toHaveBeenCalledWith('lcp', {
+ 'sentry.measurement_unit': 'millisecond',
+ 'sentry.measurement_value': lcpValue,
+ });
+ expect(mockSpan.end).toHaveBeenCalledWith(1.1);
+ });
+
+ it('does not send a standalone lcp span for implausibly large values', () => {
+ _sendStandaloneLcpSpan(MAX_PLAUSIBLE_LCP_DURATION + 1, undefined, '123', 'pagehide');
+
+ expect(WebVitalUtils.startStandaloneWebVitalSpan).not.toHaveBeenCalled();
+ expect(mockSpan.addEvent).not.toHaveBeenCalled();
+ expect(mockSpan.end).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/browser-utils/test/metrics/webVitalSpans.test.ts b/packages/browser-utils/test/metrics/webVitalSpans.test.ts
index 733891370fda..8b9325895e85 100644
--- a/packages/browser-utils/test/metrics/webVitalSpans.test.ts
+++ b/packages/browser-utils/test/metrics/webVitalSpans.test.ts
@@ -1,6 +1,7 @@
import * as SentryCore from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as inpModule from '../../src/metrics/inp';
+import { MAX_PLAUSIBLE_LCP_DURATION } from '../../src/metrics/lcp';
import { _emitWebVitalSpan, _sendClsSpan, _sendInpSpan, _sendLcpSpan } from '../../src/metrics/webVitalSpans';
vi.mock('@sentry/core', async () => {
@@ -262,7 +263,7 @@ describe('_sendLcpSpan', () => {
});
it('sends a streamed LCP span without entry data', () => {
- _sendLcpSpan(0, undefined);
+ _sendLcpSpan(250, undefined);
expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
@@ -271,6 +272,13 @@ describe('_sendLcpSpan', () => {
}),
);
});
+
+ it('drops implausible LCP values', () => {
+ _sendLcpSpan(0, undefined);
+ _sendLcpSpan(MAX_PLAUSIBLE_LCP_DURATION + 1, undefined);
+
+ expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled();
+ });
});
describe('_sendClsSpan', () => {
diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts
index f8d2dfd14014..d722e7c1ea65 100644
--- a/packages/browser/src/index.bundle.feedback.ts
+++ b/packages/browser/src/index.bundle.feedback.ts
@@ -4,6 +4,7 @@ import {
elementTimingIntegrationShim,
loggerShim,
replayIntegrationShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { feedbackAsyncIntegration } from './feedbackAsync';
@@ -20,4 +21,5 @@ export {
feedbackAsyncIntegration as feedbackAsyncIntegration,
feedbackAsyncIntegration as feedbackIntegration,
replayIntegrationShim as replayIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
diff --git a/packages/browser/src/index.bundle.logs.metrics.ts b/packages/browser/src/index.bundle.logs.metrics.ts
index f03371dc40b8..415a56cf7cc6 100644
--- a/packages/browser/src/index.bundle.logs.metrics.ts
+++ b/packages/browser/src/index.bundle.logs.metrics.ts
@@ -2,6 +2,7 @@ import {
browserTracingIntegrationShim,
feedbackIntegrationShim,
replayIntegrationShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
export * from './index.bundle.base';
@@ -16,4 +17,5 @@ export {
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
replayIntegrationShim as replayIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
diff --git a/packages/browser/src/index.bundle.replay.feedback.ts b/packages/browser/src/index.bundle.replay.feedback.ts
index da307df3a951..6ad0bb2bd153 100644
--- a/packages/browser/src/index.bundle.replay.feedback.ts
+++ b/packages/browser/src/index.bundle.replay.feedback.ts
@@ -3,6 +3,7 @@ import {
consoleLoggingIntegrationShim,
elementTimingIntegrationShim,
loggerShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { feedbackAsyncIntegration } from './feedbackAsync';
@@ -18,6 +19,7 @@ export {
elementTimingIntegrationShim as elementTimingIntegration,
feedbackAsyncIntegration as feedbackAsyncIntegration,
feedbackAsyncIntegration as feedbackIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
export { replayIntegration, getReplay } from '@sentry-internal/replay';
diff --git a/packages/browser/src/index.bundle.replay.logs.metrics.ts b/packages/browser/src/index.bundle.replay.logs.metrics.ts
index 6ceb7623d77f..02938d0d7063 100644
--- a/packages/browser/src/index.bundle.replay.logs.metrics.ts
+++ b/packages/browser/src/index.bundle.replay.logs.metrics.ts
@@ -1,4 +1,8 @@
-import { browserTracingIntegrationShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims';
+import {
+ browserTracingIntegrationShim,
+ feedbackIntegrationShim,
+ spanStreamingIntegrationShim,
+} from '@sentry-internal/integration-shims';
export * from './index.bundle.base';
@@ -13,4 +17,5 @@ export {
browserTracingIntegrationShim as browserTracingIntegration,
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts
index e305596f190c..e9ec7e99132e 100644
--- a/packages/browser/src/index.bundle.replay.ts
+++ b/packages/browser/src/index.bundle.replay.ts
@@ -4,6 +4,7 @@ import {
elementTimingIntegrationShim,
feedbackIntegrationShim,
loggerShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
export * from './index.bundle.base';
@@ -18,4 +19,5 @@ export {
elementTimingIntegrationShim as elementTimingIntegration,
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
diff --git a/packages/browser/src/index.bundle.tracing.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.logs.metrics.ts
index 0c5c4c0a81cd..19b8118a5c04 100644
--- a/packages/browser/src/index.bundle.tracing.logs.metrics.ts
+++ b/packages/browser/src/index.bundle.tracing.logs.metrics.ts
@@ -30,6 +30,8 @@ export { elementTimingIntegration } from '@sentry-internal/browser-utils';
export { reportPageLoaded } from './tracing/reportPageLoaded';
export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export {
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts
index 5fb7c306cc87..5a531f6b33a9 100644
--- a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts
@@ -30,6 +30,8 @@ export { elementTimingIntegration } from '@sentry-internal/browser-utils';
export { reportPageLoaded } from './tracing/reportPageLoaded';
export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export { getFeedback, sendFeedback } from '@sentry-internal/feedback';
export { feedbackAsyncIntegration as feedbackAsyncIntegration, feedbackAsyncIntegration as feedbackIntegration };
diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
index 9d9098b5be3d..47b43d48f376 100644
--- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
@@ -36,6 +36,8 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
export { reportPageLoaded } from './tracing/reportPageLoaded';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export { getFeedback, sendFeedback } from '@sentry-internal/feedback';
export { feedbackAsyncIntegration as feedbackAsyncIntegration, feedbackAsyncIntegration as feedbackIntegration };
diff --git a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts
index a000d456360b..45c7299bf436 100644
--- a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts
@@ -30,6 +30,8 @@ export { elementTimingIntegration } from '@sentry-internal/browser-utils';
export { reportPageLoaded } from './tracing/reportPageLoaded';
export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration };
export { replayIntegration, getReplay } from '@sentry-internal/replay';
diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts
index 496aacf348b9..63eb9a81c24a 100644
--- a/packages/browser/src/index.bundle.tracing.replay.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.ts
@@ -35,6 +35,8 @@ export { elementTimingIntegrationShim as elementTimingIntegration };
export { reportPageLoaded } from './tracing/reportPageLoaded';
export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration };
export { replayIntegration, getReplay } from '@sentry-internal/replay';
diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts
index 64126c101189..a385ad4b0792 100644
--- a/packages/browser/src/index.bundle.tracing.ts
+++ b/packages/browser/src/index.bundle.tracing.ts
@@ -37,6 +37,8 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan';
export { reportPageLoaded } from './tracing/reportPageLoaded';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
+
export {
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts
index 7dfcd30ad2ef..b168b7d0189f 100644
--- a/packages/browser/src/index.bundle.ts
+++ b/packages/browser/src/index.bundle.ts
@@ -5,6 +5,7 @@ import {
feedbackIntegrationShim,
loggerShim,
replayIntegrationShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
export * from './index.bundle.base';
@@ -18,4 +19,5 @@ export {
feedbackIntegrationShim as feedbackAsyncIntegration,
feedbackIntegrationShim as feedbackIntegration,
replayIntegrationShim as replayIntegration,
+ spanStreamingIntegrationShim as spanStreamingIntegration,
};
diff --git a/packages/browser/src/integrations/culturecontext.ts b/packages/browser/src/integrations/culturecontext.ts
index 486f5dcd012b..f2b705e3e9a9 100644
--- a/packages/browser/src/integrations/culturecontext.ts
+++ b/packages/browser/src/integrations/culturecontext.ts
@@ -17,6 +17,18 @@ const _cultureContextIntegration = (() => {
};
}
},
+ processSegmentSpan(span) {
+ const culture = getCultureContext();
+
+ if (culture) {
+ span.attributes = {
+ 'culture.locale': culture.locale,
+ 'culture.timezone': culture.timezone,
+ 'culture.calendar': culture.calendar,
+ ...span.attributes,
+ };
+ }
+ },
};
}) satisfies IntegrationFn;
diff --git a/packages/browser/src/integrations/graphqlClient.ts b/packages/browser/src/integrations/graphqlClient.ts
index d256fa6b72e1..51a3fe939f23 100644
--- a/packages/browser/src/integrations/graphqlClient.ts
+++ b/packages/browser/src/integrations/graphqlClient.ts
@@ -67,7 +67,9 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption
return;
}
- const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'];
+ // Fall back to `url` because fetch instrumentation only sets `http.url` for absolute URLs;
+ // relative URLs end up only in `url` (see `getFetchSpanAttributes` in packages/core/src/fetch.ts).
+ const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'] || spanAttributes['url'];
const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method'];
if (!isString(httpUrl) || !isString(httpMethod)) {
diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts
index 6211cf72947a..b393f0585b5b 100644
--- a/packages/browser/src/tracing/request.ts
+++ b/packages/browser/src/tracing/request.ts
@@ -404,6 +404,7 @@ function xhrCallback(
const urlForSpanName = stripDataUrlContent(stripUrlQueryAndFragment(url));
+ const client = getClient();
const hasParent = !!getActiveSpan();
const span =
@@ -424,6 +425,10 @@ function xhrCallback(
})
: new SentryNonRecordingSpan();
+ if (shouldCreateSpanResult && !hasParent) {
+ client?.recordDroppedEvent('no_parent_span', 'span');
+ }
+
xhr.__sentry_xhr_span_id__ = span.spanContext().spanId;
spans[xhr.__sentry_xhr_span_id__] = span;
@@ -438,7 +443,6 @@ function xhrCallback(
);
}
- const client = getClient();
if (client) {
client.emit('beforeOutgoingRequestSpan', span, handlerData as XhrHint);
}
diff --git a/packages/browser/test/index.bundle.feedback.test.ts b/packages/browser/test/index.bundle.feedback.test.ts
index 5b72e0566236..767db76fd3ce 100644
--- a/packages/browser/test/index.bundle.feedback.test.ts
+++ b/packages/browser/test/index.bundle.feedback.test.ts
@@ -3,6 +3,7 @@ import {
consoleLoggingIntegrationShim,
loggerShim,
replayIntegrationShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import { feedbackAsyncIntegration } from '../src';
@@ -14,6 +15,7 @@ describe('index.bundle.feedback', () => {
expect(FeedbackBundle.feedbackAsyncIntegration).toBe(feedbackAsyncIntegration);
expect(FeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration);
expect(FeedbackBundle.replayIntegration).toBe(replayIntegrationShim);
+ expect(FeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
expect(FeedbackBundle.logger).toBe(loggerShim);
expect(FeedbackBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.logs.metrics.test.ts b/packages/browser/test/index.bundle.logs.metrics.test.ts
index 98cf359a7266..7d450dc1ced0 100644
--- a/packages/browser/test/index.bundle.logs.metrics.test.ts
+++ b/packages/browser/test/index.bundle.logs.metrics.test.ts
@@ -1,4 +1,5 @@
import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core';
+import { spanStreamingIntegrationShim } from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import * as LogsMetricsBundle from '../src/index.bundle.logs.metrics';
@@ -6,5 +7,6 @@ describe('index.bundle.logs.metrics', () => {
it('has correct exports', () => {
expect(LogsMetricsBundle.logger).toBe(coreLogger);
expect(LogsMetricsBundle.metrics).toBe(coreMetrics);
+ expect(LogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
});
});
diff --git a/packages/browser/test/index.bundle.replay.feedback.test.ts b/packages/browser/test/index.bundle.replay.feedback.test.ts
index b92c2c41b731..9f65458e5824 100644
--- a/packages/browser/test/index.bundle.replay.feedback.test.ts
+++ b/packages/browser/test/index.bundle.replay.feedback.test.ts
@@ -2,6 +2,7 @@ import {
browserTracingIntegrationShim,
consoleLoggingIntegrationShim,
loggerShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import { feedbackAsyncIntegration, replayIntegration } from '../src';
@@ -13,6 +14,7 @@ describe('index.bundle.replay.feedback', () => {
expect(ReplayFeedbackBundle.feedbackAsyncIntegration).toBe(feedbackAsyncIntegration);
expect(ReplayFeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration);
expect(ReplayFeedbackBundle.replayIntegration).toBe(replayIntegration);
+ expect(ReplayFeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
expect(ReplayFeedbackBundle.logger).toBe(loggerShim);
expect(ReplayFeedbackBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.replay.logs.metrics.test.ts
index b031510282f4..d6bb995fae09 100644
--- a/packages/browser/test/index.bundle.replay.logs.metrics.test.ts
+++ b/packages/browser/test/index.bundle.replay.logs.metrics.test.ts
@@ -1,5 +1,9 @@
import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core';
-import { browserTracingIntegrationShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims';
+import {
+ browserTracingIntegrationShim,
+ feedbackIntegrationShim,
+ spanStreamingIntegrationShim,
+} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import { replayIntegration } from '../src';
import * as ReplayLogsMetricsBundle from '../src/index.bundle.replay.logs.metrics';
@@ -10,6 +14,7 @@ describe('index.bundle.replay.logs.metrics', () => {
expect(ReplayLogsMetricsBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(ReplayLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(ReplayLogsMetricsBundle.replayIntegration).toBe(replayIntegration);
+ expect(ReplayLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
expect(ReplayLogsMetricsBundle.logger).toBe(coreLogger);
expect(ReplayLogsMetricsBundle.metrics).toBe(coreMetrics);
diff --git a/packages/browser/test/index.bundle.replay.test.ts b/packages/browser/test/index.bundle.replay.test.ts
index 2bfc2ffcf7fc..1d5dc86d274d 100644
--- a/packages/browser/test/index.bundle.replay.test.ts
+++ b/packages/browser/test/index.bundle.replay.test.ts
@@ -3,6 +3,7 @@ import {
consoleLoggingIntegrationShim,
feedbackIntegrationShim,
loggerShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import { replayIntegration } from '../src';
@@ -14,6 +15,7 @@ describe('index.bundle.replay', () => {
expect(ReplayBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(ReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(ReplayBundle.replayIntegration).toBe(replayIntegration);
+ expect(ReplayBundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
expect(ReplayBundle.logger).toBe(loggerShim);
expect(ReplayBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.test.ts b/packages/browser/test/index.bundle.test.ts
index 6b29fea23aeb..d099b5545fc7 100644
--- a/packages/browser/test/index.bundle.test.ts
+++ b/packages/browser/test/index.bundle.test.ts
@@ -4,6 +4,7 @@ import {
feedbackIntegrationShim,
loggerShim,
replayIntegrationShim,
+ spanStreamingIntegrationShim,
} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
import * as Bundle from '../src/index.bundle';
@@ -14,6 +15,7 @@ describe('index.bundle', () => {
expect(Bundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(Bundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(Bundle.replayIntegration).toBe(replayIntegrationShim);
+ expect(Bundle.spanStreamingIntegration).toBe(spanStreamingIntegrationShim);
expect(Bundle.logger).toBe(loggerShim);
expect(Bundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts
index 19b3701ebf77..483a4ae8a1f5 100644
--- a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts
+++ b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts
@@ -1,7 +1,7 @@
import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core';
import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration } from '../src';
+import { browserTracingIntegration, spanStreamingIntegration } from '../src';
import * as TracingLogsMetricsBundle from '../src/index.bundle.tracing.logs.metrics';
describe('index.bundle.tracing.logs.metrics', () => {
@@ -10,6 +10,7 @@ describe('index.bundle.tracing.logs.metrics', () => {
expect(TracingLogsMetricsBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(TracingLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(TracingLogsMetricsBundle.replayIntegration).toBe(replayIntegrationShim);
+ expect(TracingLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingLogsMetricsBundle.logger).toBe(coreLogger);
expect(TracingLogsMetricsBundle.metrics).toBe(coreMetrics);
diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts
index 4a7cac9f53d6..0c474b195bc8 100644
--- a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts
+++ b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts
@@ -1,6 +1,11 @@
import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration, feedbackAsyncIntegration, replayIntegration } from '../src';
+import {
+ browserTracingIntegration,
+ feedbackAsyncIntegration,
+ replayIntegration,
+ spanStreamingIntegration,
+} from '../src';
import * as TracingReplayFeedbackLogsMetricsBundle from '../src/index.bundle.tracing.replay.feedback.logs.metrics';
describe('index.bundle.tracing.replay.feedback.logs.metrics', () => {
@@ -9,6 +14,7 @@ describe('index.bundle.tracing.replay.feedback.logs.metrics', () => {
expect(TracingReplayFeedbackLogsMetricsBundle.feedbackAsyncIntegration).toBe(feedbackAsyncIntegration);
expect(TracingReplayFeedbackLogsMetricsBundle.feedbackIntegration).toBe(feedbackAsyncIntegration);
expect(TracingReplayFeedbackLogsMetricsBundle.replayIntegration).toBe(replayIntegration);
+ expect(TracingReplayFeedbackLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingReplayFeedbackLogsMetricsBundle.logger).toBe(coreLogger);
expect(TracingReplayFeedbackLogsMetricsBundle.metrics).toBe(coreMetrics);
diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts
index 1cb7a18fe8bd..fe60d079dc41 100644
--- a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts
+++ b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts
@@ -1,6 +1,11 @@
import { consoleLoggingIntegrationShim, loggerShim } from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration, feedbackAsyncIntegration, replayIntegration } from '../src';
+import {
+ browserTracingIntegration,
+ feedbackAsyncIntegration,
+ replayIntegration,
+ spanStreamingIntegration,
+} from '../src';
import * as TracingReplayFeedbackBundle from '../src/index.bundle.tracing.replay.feedback';
describe('index.bundle.tracing.replay.feedback', () => {
@@ -9,6 +14,7 @@ describe('index.bundle.tracing.replay.feedback', () => {
expect(TracingReplayFeedbackBundle.feedbackAsyncIntegration).toBe(feedbackAsyncIntegration);
expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration);
expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration);
+ expect(TracingReplayFeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingReplayFeedbackBundle.logger).toBe(loggerShim);
expect(TracingReplayFeedbackBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts
index b47c00f4b510..4848de24caea 100644
--- a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts
+++ b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts
@@ -1,7 +1,7 @@
import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core';
import { feedbackIntegrationShim } from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration, replayIntegration } from '../src';
+import { browserTracingIntegration, replayIntegration, spanStreamingIntegration } from '../src';
import * as TracingReplayLogsMetricsBundle from '../src/index.bundle.tracing.replay.logs.metrics';
describe('index.bundle.tracing.replay.logs.metrics', () => {
@@ -10,6 +10,7 @@ describe('index.bundle.tracing.replay.logs.metrics', () => {
expect(TracingReplayLogsMetricsBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(TracingReplayLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(TracingReplayLogsMetricsBundle.replayIntegration).toBe(replayIntegration);
+ expect(TracingReplayLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingReplayLogsMetricsBundle.logger).toBe(coreLogger);
expect(TracingReplayLogsMetricsBundle.metrics).toBe(coreMetrics);
diff --git a/packages/browser/test/index.bundle.tracing.replay.test.ts b/packages/browser/test/index.bundle.tracing.replay.test.ts
index 90c82f32cb6b..1cdae8214a20 100644
--- a/packages/browser/test/index.bundle.tracing.replay.test.ts
+++ b/packages/browser/test/index.bundle.tracing.replay.test.ts
@@ -1,6 +1,6 @@
import { consoleLoggingIntegrationShim, feedbackIntegrationShim, loggerShim } from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration, replayIntegration } from '../src';
+import { browserTracingIntegration, replayIntegration, spanStreamingIntegration } from '../src';
import * as TracingReplayBundle from '../src/index.bundle.tracing.replay';
describe('index.bundle.tracing.replay', () => {
@@ -9,6 +9,7 @@ describe('index.bundle.tracing.replay', () => {
expect(TracingReplayBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration);
+ expect(TracingReplayBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingReplayBundle.logger).toBe(loggerShim);
expect(TracingReplayBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/index.bundle.tracing.test.ts b/packages/browser/test/index.bundle.tracing.test.ts
index 2ec58fcb9e48..75fb658c860d 100644
--- a/packages/browser/test/index.bundle.tracing.test.ts
+++ b/packages/browser/test/index.bundle.tracing.test.ts
@@ -5,7 +5,7 @@ import {
replayIntegrationShim,
} from '@sentry-internal/integration-shims';
import { describe, expect, it } from 'vitest';
-import { browserTracingIntegration } from '../src';
+import { browserTracingIntegration, spanStreamingIntegration } from '../src';
import * as TracingBundle from '../src/index.bundle.tracing';
describe('index.bundle.tracing', () => {
@@ -14,6 +14,7 @@ describe('index.bundle.tracing', () => {
expect(TracingBundle.feedbackAsyncIntegration).toBe(feedbackIntegrationShim);
expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim);
expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim);
+ expect(TracingBundle.spanStreamingIntegration).toBe(spanStreamingIntegration);
expect(TracingBundle.logger).toBe(loggerShim);
expect(TracingBundle.consoleLoggingIntegration).toBe(consoleLoggingIntegrationShim);
diff --git a/packages/browser/test/integrations/graphqlClient.test.ts b/packages/browser/test/integrations/graphqlClient.test.ts
index a90b62ee13d5..1c4ab60d30f2 100644
--- a/packages/browser/test/integrations/graphqlClient.test.ts
+++ b/packages/browser/test/integrations/graphqlClient.test.ts
@@ -2,6 +2,8 @@
* @vitest-environment jsdom
*/
+import type { Client } from '@sentry/core';
+import { SentrySpan, spanToJSON } from '@sentry/core';
import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils';
import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils';
import { describe, expect, test } from 'vitest';
@@ -9,6 +11,7 @@ import {
_getGraphQLOperation,
getGraphQLRequestPayload,
getRequestPayloadXhrOrFetch,
+ graphqlClientIntegration,
parseGraphQLQuery,
} from '../../src/integrations/graphqlClient';
@@ -308,4 +311,114 @@ describe('GraphqlClient', () => {
expect(_getGraphQLOperation(requestBody as any)).toBe('unknown');
});
});
+
+ describe('beforeOutgoingRequestSpan handler', () => {
+ function setupHandler(endpoints: Array): (span: SentrySpan, hint: FetchHint | XhrHint) => void {
+ let capturedListener: ((span: SentrySpan, hint: FetchHint | XhrHint) => void) | undefined;
+ const mockClient = {
+ on: (eventName: string, cb: (span: SentrySpan, hint: FetchHint | XhrHint) => void) => {
+ if (eventName === 'beforeOutgoingRequestSpan') {
+ capturedListener = cb;
+ }
+ },
+ } as unknown as Client;
+
+ const integration = graphqlClientIntegration({ endpoints });
+ integration.setup?.(mockClient);
+
+ if (!capturedListener) {
+ throw new Error('beforeOutgoingRequestSpan listener was not registered');
+ }
+ return capturedListener;
+ }
+
+ function makeFetchHint(url: string, body: unknown): FetchHint {
+ return {
+ input: [url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }],
+ response: new Response(null, { status: 200 }),
+ startTimestamp: Date.now(),
+ endTimestamp: Date.now() + 1,
+ };
+ }
+
+ const requestBody = {
+ query: 'query GetHello { hello }',
+ operationName: 'GetHello',
+ variables: {},
+ extensions: {},
+ };
+
+ test('enriches http.client span for absolute URLs (http.url attribute)', () => {
+ const handler = setupHandler([/\/graphql$/]);
+ const span = new SentrySpan({
+ name: 'POST http://localhost:4000/graphql',
+ op: 'http.client',
+ attributes: {
+ 'http.method': 'POST',
+ 'http.url': 'http://localhost:4000/graphql',
+ url: 'http://localhost:4000/graphql',
+ },
+ });
+
+ handler(span, makeFetchHint('http://localhost:4000/graphql', requestBody));
+
+ const json = spanToJSON(span);
+ expect(json.description).toBe('POST http://localhost:4000/graphql (query GetHello)');
+ expect(json.data['graphql.document']).toBe(requestBody.query);
+ });
+
+ test('enriches http.client span for relative URLs (only url attribute)', () => {
+ const handler = setupHandler([/\/graphql$/]);
+ // Fetch instrumentation does NOT set http.url for relative URLs — only `url`.
+ const span = new SentrySpan({
+ name: 'POST /graphql',
+ op: 'http.client',
+ attributes: {
+ 'http.method': 'POST',
+ url: '/graphql',
+ },
+ });
+
+ handler(span, makeFetchHint('/graphql', requestBody));
+
+ const json = spanToJSON(span);
+ expect(json.description).toBe('POST /graphql (query GetHello)');
+ expect(json.data['graphql.document']).toBe(requestBody.query);
+ });
+
+ test('does nothing when no URL attribute is present', () => {
+ const handler = setupHandler([/\/graphql$/]);
+ const span = new SentrySpan({
+ name: 'POST',
+ op: 'http.client',
+ attributes: {
+ 'http.method': 'POST',
+ },
+ });
+
+ handler(span, makeFetchHint('/graphql', requestBody));
+
+ const json = spanToJSON(span);
+ expect(json.description).toBe('POST');
+ expect(json.data['graphql.document']).toBeUndefined();
+ });
+
+ test('does nothing when span op is not http.client', () => {
+ const handler = setupHandler([/\/graphql$/]);
+ const span = new SentrySpan({
+ name: 'custom span',
+ op: 'custom',
+ attributes: {
+ 'http.method': 'POST',
+ url: '/graphql',
+ },
+ });
+
+ handler(span, makeFetchHint('/graphql', requestBody));
+
+ const json = spanToJSON(span);
+ expect(json.description).toBe('custom span');
+ expect(json.data['graphql.document']).toBeUndefined();
+ });
+ });
});
diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts
index 05e870ff5d81..4fbdd9cf7fb4 100644
--- a/packages/cloudflare/src/request.ts
+++ b/packages/cloudflare/src/request.ts
@@ -129,40 +129,36 @@ export function wrapRequestHandler(
const classification = classifyResponseStreaming(res);
if (classification.isStreaming && res.body) {
- // Streaming response detected - monitor consumption to keep span alive
try {
- const [clientStream, monitorStream] = res.body.tee();
-
- // Monitor stream consumption and end span when complete
- const streamMonitor = (async () => {
- const reader = monitorStream.getReader();
-
- try {
- let done = false;
- while (!done) {
- const result = await reader.read();
- done = result.done;
- }
- } catch {
- // Stream error or cancellation - will end span in finally
- } finally {
- reader.releaseLock();
- span.end();
- waitUntil?.(flushAndDispose(client));
- }
- })();
-
- // Keep worker alive until stream monitoring completes (otherwise span won't end)
- waitUntil?.(streamMonitor);
-
- // Return response with client stream
- return new Response(clientStream, {
+ let ended = false;
+
+ const endSpanOnce = (): void => {
+ if (ended) return;
+
+ ended = true;
+ span.end();
+ waitUntil?.(flushAndDispose(client));
+ };
+
+ const transform = new TransformStream({
+ flush() {
+ // Source stream completed normally.
+ endSpanOnce();
+ },
+ cancel() {
+ // Client disconnected (or downstream cancelled). The `cancel`
+ // is being called while the response is still considered
+ // active, so this is a safe place to end the span.
+ endSpanOnce();
+ },
+ });
+
+ return new Response(res.body.pipeThrough(transform), {
status: res.status,
statusText: res.statusText,
headers: res.headers,
});
- } catch (_e) {
- // tee() failed (e.g stream already locked) - fall back to non-streaming handling
+ } catch {
span.end();
waitUntil?.(flushAndDispose(client));
return res;
diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts
index 5486addb405c..2c8a734890fc 100644
--- a/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts
+++ b/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts
@@ -163,9 +163,11 @@ describe('instrumentFetch', () => {
const wrappedHandler = withSentry(vi.fn(), handler);
const waits: Promise[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
- await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
- waitUntil,
- } as unknown as ExecutionContext);
+ await wrappedHandler
+ .fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, {
+ waitUntil,
+ } as unknown as ExecutionContext)
+ .then(response => response.text());
expect(flush).not.toBeCalled();
expect(waitUntil).toBeCalled();
vi.advanceTimersToNextTimer().runAllTimers();
diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts
index 5160d8976e9b..28733ccfe651 100644
--- a/packages/cloudflare/test/request.test.ts
+++ b/packages/cloudflare/test/request.test.ts
@@ -14,6 +14,8 @@ const MOCK_OPTIONS: CloudflareOptions = {
dsn: 'https://public@dsn.ingest.sentry.io/1337',
};
+const NODE_MAJOR_VERSION = parseInt(process.versions.node.split('.')[0]!);
+
function addDelayedWaitUntil(context: ExecutionContext) {
context.waitUntil(new Promise(resolve => setTimeout(() => resolve())));
}
@@ -44,7 +46,7 @@ describe('withSentry', () => {
await wrapRequestHandler(
{ options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
() => new Response('test'),
- );
+ ).then(response => response.text());
expect(waitUntilSpy).toHaveBeenCalledTimes(1);
expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
@@ -111,11 +113,8 @@ describe('withSentry', () => {
await wrapRequestHandler({ options: MOCK_OPTIONS, request: new Request('https://example.com'), context }, () => {
addDelayedWaitUntil(context);
- const response = new Response('test');
- // Add Content-Length to skip probing
- response.headers.set('content-length', '4');
- return response;
- });
+ return new Response('test');
+ }).then(response => response.text());
expect(waitUntil).toBeCalled();
vi.advanceTimersToNextTimer().runAllTimers();
await Promise.all(waits);
@@ -336,7 +335,7 @@ describe('withSentry', () => {
SentryCore.captureMessage('sentry-trace');
return new Response('test');
},
- );
+ ).then(response => response.text());
// Wait for async span end and transaction capture
await new Promise(resolve => setTimeout(resolve, 50));
@@ -389,10 +388,8 @@ describe('flushAndDispose', () => {
const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);
await wrapRequestHandler({ options: MOCK_OPTIONS, request: new Request('https://example.com'), context }, () => {
- const response = new Response('test');
- response.headers.set('content-length', '4');
- return response;
- });
+ return new Response('test');
+ }).then(response => response.text());
// Wait for all waitUntil promises to resolve
await Promise.all(waits);
@@ -518,6 +515,171 @@ describe('flushAndDispose', () => {
disposeSpy.mockRestore();
});
+ // Regression tests for https://github.com/getsentry/sentry-javascript/issues/20409
+ //
+ // Pre-fix: streaming responses were observed via `body.tee()` + a long-running
+ // `waitUntil(streamMonitor)`. Cloudflare caps `waitUntil` at ~30s after the
+ // handler returns, so any stream taking longer than 30s to fully emit had the
+ // monitor cancelled before `span.end()` / `flushAndDispose()` ran — silently
+ // dropping the root `http.server` span.
+ //
+ // Post-fix: the body is piped through a passthrough `TransformStream`; the
+ // `flush` (normal completion) and `cancel` (client disconnect) callbacks fire
+ // while the response stream is still active (no waitUntil cap), so they can
+ // safely end the span and register `flushAndDispose` via a fresh `waitUntil`
+ // window. The contract guaranteed below: `waitUntil` is NOT called with any
+ // long-running stream-observation promise — only with `flushAndDispose`, and
+ // only after the response stream has finished (either by completion or cancel).
+ describe('regression #20409: streaming responses do not park stream observation in waitUntil', () => {
+ test('waitUntil is not called until streaming response is fully delivered', async () => {
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn((promise: Promise) => waits.push(promise));
+ const context = { waitUntil } as unknown as ExecutionContext;
+
+ const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);
+ const disposeSpy = vi.spyOn(CloudflareClient.prototype, 'dispose');
+
+ // Stream emits chunk1, then waits indefinitely until we open the gate
+ // before emitting chunk2 + closing. Models a long-running upstream
+ // (e.g. SSE / LLM streaming) whose body takes longer than the
+ // handler-return time to fully drain.
+ let releaseLastChunk!: () => void;
+ const lastChunkGate = new Promise(resolve => {
+ releaseLastChunk = resolve;
+ });
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new TextEncoder().encode('chunk1'));
+ await lastChunkGate;
+ controller.enqueue(new TextEncoder().encode('chunk2'));
+ controller.close();
+ },
+ });
+
+ const result = await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
+ () => new Response(stream, { headers: { 'content-type': 'text/event-stream' } }),
+ );
+
+ // Handler has returned, but the source stream has NOT closed yet.
+ // The pre-fix code would have already enqueued a long-running
+ // `waitUntil(streamMonitor)` task at this point. The post-fix code
+ // must not call waitUntil at all here.
+ expect(waitUntil).not.toHaveBeenCalled();
+
+ // Drain the response — Cloudflare would do this when forwarding to the client.
+ const reader = result.body!.getReader();
+ await reader.read(); // chunk1
+ // Source still hasn't closed — still no waitUntil.
+ expect(waitUntil).not.toHaveBeenCalled();
+
+ releaseLastChunk();
+ await reader.read(); // chunk2
+ await reader.read(); // done
+ reader.releaseLock();
+
+ // Stream completed → TransformStream `flush` fired → span ended →
+ // `flushAndDispose(client)` queued via waitUntil exactly once.
+ await Promise.all(waits);
+ expect(waitUntil).toHaveBeenCalledTimes(1);
+ expect(waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(flushSpy).toHaveBeenCalled();
+ expect(disposeSpy).toHaveBeenCalled();
+
+ flushSpy.mockRestore();
+ disposeSpy.mockRestore();
+ });
+
+ // Node 18's TransformStream does not invoke the transformer's `cancel` hook
+ // when the downstream consumer cancels (WHATWG spec addition landed in Node 20).
+ // Cloudflare Workers run modern V8 where this works, so we only skip the
+ // test under Node 18.
+ test.skipIf(NODE_MAJOR_VERSION < 20)(
+ 'waitUntil is called once and dispose runs when client cancels mid-stream',
+ async () => {
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn((promise: Promise) => waits.push(promise));
+ const context = { waitUntil } as unknown as ExecutionContext;
+
+ const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);
+ const disposeSpy = vi.spyOn(CloudflareClient.prototype, 'dispose');
+
+ // Stream emits one chunk and then never closes — models an upstream
+ // that keeps emitting indefinitely. We then cancel the response from
+ // the consumer side to model a client disconnect.
+ let sourceCancelled = false;
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode('chunk1'));
+ // intentionally don't close
+ },
+ cancel() {
+ sourceCancelled = true;
+ },
+ });
+
+ const result = await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
+ () => new Response(stream, { headers: { 'content-type': 'text/event-stream' } }),
+ );
+
+ // Handler returned, source still open — no waitUntil yet.
+ expect(waitUntil).not.toHaveBeenCalled();
+
+ const reader = result.body!.getReader();
+ await reader.read(); // chunk1
+ await reader.cancel('client disconnected'); // simulates client disconnect
+ reader.releaseLock();
+
+ // TransformStream `cancel` fired → span ended → flushAndDispose queued.
+ await Promise.all(waits);
+ expect(waitUntil).toHaveBeenCalledTimes(1);
+ expect(waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(flushSpy).toHaveBeenCalled();
+ expect(disposeSpy).toHaveBeenCalled();
+ // pipeThrough should also propagate the cancel upstream to the source.
+ expect(sourceCancelled).toBe(true);
+
+ flushSpy.mockRestore();
+ disposeSpy.mockRestore();
+ },
+ );
+
+ test('waitUntil is called exactly once even if the response is consumed multiple times', async () => {
+ // Sanity: no matter how the response is drained, the TransformStream's
+ // flush callback must only end the span (and queue flushAndDispose) once.
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn((promise: Promise) => waits.push(promise));
+ const context = { waitUntil } as unknown as ExecutionContext;
+
+ const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);
+ const disposeSpy = vi.spyOn(CloudflareClient.prototype, 'dispose');
+
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode('a'));
+ controller.enqueue(new TextEncoder().encode('b'));
+ controller.close();
+ },
+ });
+
+ const result = await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
+ () => new Response(stream, { headers: { 'content-type': 'text/event-stream' } }),
+ );
+
+ const text = await result.text();
+ expect(text).toBe('ab');
+
+ await Promise.all(waits);
+ expect(waitUntil).toHaveBeenCalledTimes(1);
+
+ flushSpy.mockRestore();
+ disposeSpy.mockRestore();
+ });
+ });
+
test('dispose is NOT called for protocol upgrade responses (status 101)', async () => {
const context = createMockExecutionContext();
const waits: Promise[] = [];
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 6c3ca949f38e..00c12db06855 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -28,6 +28,7 @@ import type { Metric } from './types-hoist/metric';
import type { Primitive } from './types-hoist/misc';
import type { ClientOptions } from './types-hoist/options';
import type { ParameterizedString } from './types-hoist/parameterize';
+import type { ReplayEndEvent, ReplayStartEvent } from './types-hoist/replay';
import type { RequestEventData } from './types-hoist/request';
import type { SdkMetadata } from './types-hoist/sdkmetadata';
import type { Session, SessionAggregates } from './types-hoist/session';
@@ -726,6 +727,19 @@ export abstract class Client {
*/
public on(hook: 'openFeedbackWidget', callback: () => void): () => void;
+ /**
+ * A hook that is called when a replay session starts recording (either session or buffer mode).
+ * @returns {() => void} A function that, when executed, removes the registered callback.
+ */
+ public on(hook: 'replayStart', callback: (event: ReplayStartEvent) => void): () => void;
+
+ /**
+ * A hook that is called when a replay session stops recording, either manually or due to an
+ * internal condition such as `maxReplayDuration` expiry, send failure, or mutation limit.
+ * @returns {() => void} A function that, when executed, removes the registered callback.
+ */
+ public on(hook: 'replayEnd', callback: (event: ReplayEndEvent) => void): () => void;
+
/**
* A hook for the browser tracing integrations to trigger a span start for a page load.
* @returns {() => void} A function that, when executed, removes the registered callback.
@@ -1001,6 +1015,16 @@ export abstract class Client {
*/
public emit(hook: 'openFeedbackWidget'): void;
+ /**
+ * Fire a hook event when a replay session starts recording.
+ */
+ public emit(hook: 'replayStart', event: ReplayStartEvent): void;
+
+ /**
+ * Fire a hook event when a replay session stops recording.
+ */
+ public emit(hook: 'replayEnd', event: ReplayEndEvent): void;
+
/**
* Emit a hook event for browser tracing integrations to trigger a span start for a page load.
*/
diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts
index 21a89b15c825..c65f147613dc 100644
--- a/packages/core/src/fetch.ts
+++ b/packages/core/src/fetch.ts
@@ -108,6 +108,7 @@ export function instrumentFetchRequest(
const { spanOrigin = 'auto.http.browser', propagateTraceparent = false } =
typeof spanOriginOrOptions === 'object' ? spanOriginOrOptions : { spanOrigin: spanOriginOrOptions };
+ const client = getClient();
const hasParent = !!getActiveSpan();
const span =
@@ -115,6 +116,10 @@ export function instrumentFetchRequest(
? startInactiveSpan(getSpanStartOptions(url, method, spanOrigin))
: new SentryNonRecordingSpan();
+ if (shouldCreateSpanResult && !hasParent) {
+ client?.recordDroppedEvent('no_parent_span', 'span');
+ }
+
handlerData.fetchData.__span = span.spanContext().spanId;
spans[span.spanContext().spanId] = span;
@@ -141,8 +146,6 @@ export function instrumentFetchRequest(
}
}
- const client = getClient();
-
if (client) {
const fetchHint = {
input: handlerData.args,
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index f4039244e550..c3f8c454e997 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -192,8 +192,10 @@ export type {
GoogleGenAIClient,
GoogleGenAIChat,
GoogleGenAIOptions,
- GoogleGenAIIstrumentedMethod,
+ GoogleGenAIInstrumentedMethod,
} from './tracing/google-genai/types';
+// eslint-disable-next-line deprecation/deprecation
+export type { GoogleGenAIIstrumentedMethod } from './tracing/google-genai/types';
export { SpanBuffer } from './tracing/spans/spanBuffer';
export { hasSpanStreamingEnabled } from './tracing/spans/hasSpanStreamingEnabled';
@@ -441,7 +443,14 @@ export type {
Profile,
ProfileChunk,
} from './types-hoist/profiling';
-export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './types-hoist/replay';
+export type {
+ ReplayEndEvent,
+ ReplayEvent,
+ ReplayRecordingData,
+ ReplayRecordingMode,
+ ReplayStartEvent,
+ ReplayStopReason,
+} from './types-hoist/replay';
export type {
FeedbackEvent,
FeedbackFormData,
diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts
index e96de345d202..cecf1e5cad8a 100644
--- a/packages/core/src/instrument/console.ts
+++ b/packages/core/src/instrument/console.ts
@@ -32,8 +32,7 @@ function instrumentConsole(): void {
originalConsoleMethods[level] = originalConsoleMethod;
return function (...args: any[]): void {
- const handlerData: HandlerDataConsole = { args, level };
- triggerHandlers('console', handlerData);
+ triggerHandlers('console', { args, level } as HandlerDataConsole);
const log = originalConsoleMethods[level];
log?.apply(GLOBAL_OBJ.console, args);
diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts
index b8e7240cf748..be8e2179bdf0 100644
--- a/packages/core/src/integration.ts
+++ b/packages/core/src/integration.ts
@@ -4,6 +4,7 @@ import { DEBUG_BUILD } from './debug-build';
import type { Event, EventHint } from './types-hoist/event';
import type { Integration, IntegrationFn } from './types-hoist/integration';
import type { CoreOptions } from './types-hoist/options';
+import type { StreamedSpanJSON } from './types-hoist/span';
import { debug } from './utils/debug-logger';
export const installedIntegrations: string[] = [];
@@ -138,6 +139,15 @@ export function setupIntegration(client: Client, integration: Integration, integ
client.addEventProcessor(processor);
}
+ (['processSpan', 'processSegmentSpan'] as const).forEach(hook => {
+ const callback = integration[hook];
+ if (typeof callback === 'function') {
+ // The cast is needed because TS can't resolve overloads when the discriminant is a union type.
+ // Both overloads have the same callback signature so this is safe.
+ client.on(hook as 'processSpan', (span: StreamedSpanJSON) => callback.call(integration, span, client));
+ }
+ });
+
DEBUG_BUILD && debug.log(`Integration installed: ${integration.name}`);
}
diff --git a/packages/core/src/integrations/mcp-server/transport.ts b/packages/core/src/integrations/mcp-server/transport.ts
index 4b04a78f43b0..49b141964fa9 100644
--- a/packages/core/src/integrations/mcp-server/transport.ts
+++ b/packages/core/src/integrations/mcp-server/transport.ts
@@ -43,7 +43,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
if (isInitialize) {
try {
initSessionData = extractSessionDataFromInitializeRequest(message);
- storeSessionDataForTransport(this, initSessionData);
+ storeSessionDataForTransport(transport, initSessionData);
} catch {
// noop
}
@@ -52,7 +52,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
const isolationScope = getIsolationScope().clone();
return withIsolationScope(isolationScope, () => {
- const spanConfig = buildMcpServerSpanConfig(message, this, extra as ExtraHandlerData, options);
+ const spanConfig = buildMcpServerSpanConfig(message, transport, extra as ExtraHandlerData, options);
const span = startInactiveSpan(spanConfig);
// For initialize requests, add client info directly to span (works even for stateless transports)
@@ -65,7 +65,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
});
}
- storeSpanForRequest(this, message.id, span, message.method);
+ storeSpanForRequest(transport, message.id, span, message.method);
return withActiveSpan(span, () => {
return (originalOnMessage as (...args: unknown[]) => unknown).call(this, message, extra);
@@ -74,7 +74,7 @@ export function wrapTransportOnMessage(transport: MCPTransport, options: Resolve
}
if (isJsonRpcNotification(message)) {
- return createMcpNotificationSpan(message, this, extra as ExtraHandlerData, options, () => {
+ return createMcpNotificationSpan(message, transport, extra as ExtraHandlerData, options, () => {
return (originalOnMessage as (...args: unknown[]) => unknown).call(this, message, extra);
});
}
@@ -99,7 +99,7 @@ export function wrapTransportSend(transport: MCPTransport, options: ResolvedMcpO
const [message] = args;
if (isJsonRpcNotification(message)) {
- return createMcpOutgoingNotificationSpan(message, this, options, () => {
+ return createMcpOutgoingNotificationSpan(message, transport, options, () => {
return (originalSend as (...args: unknown[]) => unknown).call(this, ...args);
});
}
@@ -114,14 +114,14 @@ export function wrapTransportSend(transport: MCPTransport, options: ResolvedMcpO
if (message.result.protocolVersion || message.result.serverInfo) {
try {
const serverData = extractSessionDataFromInitializeResponse(message.result);
- updateSessionDataForTransport(this, serverData);
+ updateSessionDataForTransport(transport, serverData);
} catch {
// noop
}
}
}
- completeSpanWithResults(this, message.id, message.result, options, !!message.error);
+ completeSpanWithResults(transport, message.id, message.result, options, !!message.error);
}
}
@@ -139,8 +139,8 @@ export function wrapTransportOnClose(transport: MCPTransport): void {
if (transport.onclose) {
fill(transport, 'onclose', originalOnClose => {
return function (this: MCPTransport, ...args: unknown[]) {
- cleanupPendingSpansForTransport(this);
- cleanupSessionDataForTransport(this);
+ cleanupPendingSpansForTransport(transport);
+ cleanupSessionDataForTransport(transport);
return (originalOnClose as (...args: unknown[]) => unknown).call(this, ...args);
};
});
diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts
index 7fdd8cee1683..a72fbed70d7e 100644
--- a/packages/core/src/integrations/requestdata.ts
+++ b/packages/core/src/integrations/requestdata.ts
@@ -1,5 +1,4 @@
import { defineIntegration } from '../integration';
-import { hasSpanStreamingEnabled } from '../tracing/spans/hasSpanStreamingEnabled';
import type { Event } from '../types-hoist/event';
import type { IntegrationFn } from '../types-hoist/integration';
import type { RequestEventData } from '../types-hoist/request';
diff --git a/packages/core/src/tracing/google-genai/types.ts b/packages/core/src/tracing/google-genai/types.ts
index 69f1e279fbd0..35ca728a4a60 100644
--- a/packages/core/src/tracing/google-genai/types.ts
+++ b/packages/core/src/tracing/google-genai/types.ts
@@ -186,10 +186,14 @@ export interface GoogleGenAIChat {
sendMessageStream: (...args: unknown[]) => Promise>;
}
+export type GoogleGenAIInstrumentedMethod = keyof typeof GOOGLE_GENAI_METHOD_REGISTRY;
+
/**
- * @deprecated This type is no longer used and will be removed in the next major version.
+ * @deprecated Use {@link GoogleGenAIInstrumentedMethod} instead. This alias
+ * preserves backwards compatibility with the misspelled name and will be
+ * removed in the next major version.
*/
-export type GoogleGenAIIstrumentedMethod = keyof typeof GOOGLE_GENAI_METHOD_REGISTRY;
+export type GoogleGenAIIstrumentedMethod = GoogleGenAIInstrumentedMethod;
// Export the response type for use in instrumentation
export type GoogleGenAIResponse = GenerateContentResponse;
diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts
index 979c7b460af1..fe8bc31fcae7 100644
--- a/packages/core/src/tracing/spans/captureSpan.ts
+++ b/packages/core/src/tracing/spans/captureSpan.ts
@@ -54,10 +54,12 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW
if (spanJSON.is_segment) {
applyScopeToSegmentSpan(spanJSON, finalScopeData);
// Allow hook subscribers to mutate the segment span JSON
+ // This also invokes the `processSegmentSpan` hook of all integrations
client.emit('processSegmentSpan', spanJSON);
}
- // Allow hook subscribers to mutate the span JSON
+ // This allows hook subscribers to mutate the span JSON
+ // This also invokes the `processSpan` hook of all integrations
client.emit('processSpan', spanJSON);
const { beforeSendSpan } = client.getOptions();
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index 5d40a0a30ebe..08411722cedf 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -68,9 +68,10 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) =
return wrapper(() => {
const scope = getCurrentScope();
const parentSpan = getParentSpan(scope, customParentSpan);
+ const client = getClient();
- const shouldSkipSpan = options.onlyIfParent && !parentSpan;
- const activeSpan = shouldSkipSpan
+ const missingRequiredParent = options.onlyIfParent && !parentSpan;
+ const activeSpan = missingRequiredParent
? new SentryNonRecordingSpan()
: createChildOrRootSpan({
parentSpan,
@@ -79,6 +80,10 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) =
scope,
});
+ if (missingRequiredParent) {
+ client?.recordDroppedEvent('no_parent_span', 'span');
+ }
+
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
@@ -132,8 +137,8 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S
const scope = getCurrentScope();
const parentSpan = getParentSpan(scope, customParentSpan);
- const shouldSkipSpan = options.onlyIfParent && !parentSpan;
- const activeSpan = shouldSkipSpan
+ const missingRequiredParent = options.onlyIfParent && !parentSpan;
+ const activeSpan = missingRequiredParent
? new SentryNonRecordingSpan()
: createChildOrRootSpan({
parentSpan,
@@ -142,6 +147,10 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S
scope,
});
+ if (missingRequiredParent) {
+ getClient()?.recordDroppedEvent('no_parent_span', 'span');
+ }
+
// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
@@ -195,10 +204,12 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return wrapper(() => {
const scope = getCurrentScope();
const parentSpan = getParentSpan(scope, customParentSpan);
+ const client = getClient();
- const shouldSkipSpan = options.onlyIfParent && !parentSpan;
+ const missingRequiredParent = options.onlyIfParent && !parentSpan;
- if (shouldSkipSpan) {
+ if (missingRequiredParent) {
+ client?.recordDroppedEvent('no_parent_span', 'span');
return new SentryNonRecordingSpan();
}
diff --git a/packages/core/src/types-hoist/clientreport.ts b/packages/core/src/types-hoist/clientreport.ts
index 4c07f1956014..154b58c5705e 100644
--- a/packages/core/src/types-hoist/clientreport.ts
+++ b/packages/core/src/types-hoist/clientreport.ts
@@ -11,7 +11,8 @@ export type EventDropReason =
| 'internal_sdk_error'
| 'buffer_overflow'
| 'ignored'
- | 'invalid';
+ | 'invalid'
+ | 'no_parent_span';
export type Outcome = {
reason: EventDropReason;
diff --git a/packages/core/src/types-hoist/integration.ts b/packages/core/src/types-hoist/integration.ts
index fc80cf3f524a..2e3cb5d45723 100644
--- a/packages/core/src/types-hoist/integration.ts
+++ b/packages/core/src/types-hoist/integration.ts
@@ -1,5 +1,6 @@
import type { Client } from '../client';
import type { Event, EventHint } from './event';
+import type { StreamedSpanJSON } from './span';
/** Integration interface */
export interface Integration {
@@ -50,6 +51,20 @@ export interface Integration {
* This receives the client that the integration was installed for as third argument.
*/
processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike;
+
+ /**
+ * An optional hook that allows modifications to a span. This hook runs after the span is ended,
+ * during `captureSpan` and before the span is passed to users' `beforeSendSpan` callback.
+ * Use this hook to modify a span in-place.
+ */
+ processSpan?(span: StreamedSpanJSON, client: Client): void;
+
+ /**
+ * An optional hook that allows modifications to a segment span. This hook runs after the segment span is ended,
+ * during `captureSpan` and before the segment span is passed to users' `beforeSendSpan` callback.
+ * Use this hook to modify a segment span in-place.
+ */
+ processSegmentSpan?(span: StreamedSpanJSON, client: Client): void;
}
/**
diff --git a/packages/core/src/types-hoist/replay.ts b/packages/core/src/types-hoist/replay.ts
index 65641ce011bd..a23f548aa357 100644
--- a/packages/core/src/types-hoist/replay.ts
+++ b/packages/core/src/types-hoist/replay.ts
@@ -25,3 +25,37 @@ export type ReplayRecordingData = string | Uint8Array;
* @hidden
*/
export type ReplayRecordingMode = 'session' | 'buffer';
+
+/**
+ * Reason a replay recording stopped, passed to the `replayEnd` client hook.
+ *
+ * - `manual`: user called `replay.stop()`.
+ * - `sessionExpired`: session hit `maxReplayDuration` or the idle-expiry threshold.
+ * - `sendError`: a replay segment failed to send after retries.
+ * - `mutationLimit`: DOM mutation budget for the session was exhausted.
+ * - `eventBufferError`: the event buffer threw an unexpected error.
+ * - `eventBufferOverflow`: the event buffer ran out of space.
+ */
+export type ReplayStopReason =
+ | 'manual'
+ | 'sessionExpired'
+ | 'sendError'
+ | 'mutationLimit'
+ | 'eventBufferError'
+ | 'eventBufferOverflow';
+
+/**
+ * Payload emitted on the `replayStart` client hook when a replay begins recording.
+ */
+export interface ReplayStartEvent {
+ sessionId: string;
+ recordingMode: ReplayRecordingMode;
+}
+
+/**
+ * Payload emitted on the `replayEnd` client hook when a replay stops recording.
+ */
+export interface ReplayEndEvent {
+ sessionId?: string;
+ reason: ReplayStopReason;
+}
diff --git a/packages/core/test/lib/instrument/console.test.ts b/packages/core/test/lib/instrument/console.test.ts
new file mode 100644
index 000000000000..2499a231712d
--- /dev/null
+++ b/packages/core/test/lib/instrument/console.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it, vi } from 'vitest';
+import { addConsoleInstrumentationHandler } from '../../../src/instrument/console';
+import { GLOBAL_OBJ } from '../../../src/utils/worldwide';
+
+describe('addConsoleInstrumentationHandler', () => {
+ it.each(['log', 'warn', 'error', 'debug', 'info'] as const)(
+ 'calls registered handler when console.%s is called',
+ level => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ GLOBAL_OBJ.console[level]('test message');
+
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['test message'], level }));
+ },
+ );
+
+ it('calls through to the underlying console method without throwing', () => {
+ addConsoleInstrumentationHandler(vi.fn());
+ expect(() => GLOBAL_OBJ.console.log('hello')).not.toThrow();
+ });
+});
diff --git a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
index 71590fd17712..d8cc546393ec 100644
--- a/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
+++ b/packages/core/test/lib/integrations/mcp-server/transportInstrumentation.test.ts
@@ -880,6 +880,95 @@ describe('MCP Server Transport Instrumentation', () => {
expect(mockSpan.end).toHaveBeenCalled();
});
+ it('should correlate spans correctly for stateless wrapper transports', async () => {
+ const { wrapper, inner } = createMockWrapperTransport('stateless-wrapper-session');
+ inner.sessionId = undefined;
+
+ const mockMcpServer = createMockMcpServer();
+ const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
+
+ await wrappedMcpServer.connect(wrapper);
+
+ const mockSpan = { setAttributes: vi.fn(), end: vi.fn() };
+ startInactiveSpanSpy.mockReturnValue(mockSpan as any);
+
+ inner.onmessage?.call(
+ inner,
+ {
+ jsonrpc: '2.0',
+ method: 'tools/call',
+ id: 'stateless-wrapper-req-1',
+ params: { name: 'test-tool' },
+ },
+ {},
+ );
+
+ await wrapper.send({
+ jsonrpc: '2.0',
+ id: 'stateless-wrapper-req-1',
+ result: { content: [{ type: 'text', text: 'success' }] },
+ });
+
+ expect(mockSpan.end).toHaveBeenCalled();
+ });
+
+ it('should preserve session metadata for later stateless wrapper spans', async () => {
+ const { wrapper, inner } = createMockWrapperTransport('stateless-wrapper-session');
+ inner.sessionId = undefined;
+
+ const mockMcpServer = createMockMcpServer();
+ const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
+
+ await wrappedMcpServer.connect(wrapper);
+
+ inner.onmessage?.call(
+ inner,
+ {
+ jsonrpc: '2.0',
+ method: 'initialize',
+ id: 'init-stateless',
+ params: {
+ protocolVersion: '2025-06-18',
+ clientInfo: { name: 'test-client', version: '1.0.0' },
+ },
+ },
+ {},
+ );
+
+ await wrapper.send({
+ jsonrpc: '2.0',
+ id: 'init-stateless',
+ result: {
+ protocolVersion: '2025-06-18',
+ serverInfo: { name: 'test-server', version: '2.0.0' },
+ capabilities: {},
+ },
+ });
+
+ inner.onmessage?.call(
+ inner,
+ {
+ jsonrpc: '2.0',
+ method: 'tools/call',
+ id: 'stateless-wrapper-req-2',
+ params: { name: 'test-tool' },
+ },
+ {},
+ );
+
+ expect(startInactiveSpanSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'mcp.client.name': 'test-client',
+ 'mcp.client.version': '1.0.0',
+ 'mcp.protocol.version': '2025-06-18',
+ 'mcp.server.name': 'test-server',
+ 'mcp.server.version': '2.0.0',
+ }),
+ }),
+ );
+ });
+
it('should handle initialize request/response with wrapper transport', async () => {
const { wrapper } = createMockWrapperTransport('init-wrapper-session');
const mockMcpServer = createMockMcpServer();
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
index 0481f7f42687..db449f9a8ede 100644
--- a/packages/core/test/lib/tracing/trace.test.ts
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -572,7 +572,7 @@ describe('startSpan', () => {
});
describe('onlyIfParent', () => {
- it('starts a non recording span if there is no parent', () => {
+ it('starts a non recording span and records no_parent_span client report if there is no parent', () => {
const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
@@ -581,10 +581,13 @@ describe('startSpan', () => {
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentryNonRecordingSpan);
- expect(spyOnDroppedEvent).not.toHaveBeenCalled();
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
return span;
@@ -595,6 +598,17 @@ describe('startSpan', () => {
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentrySpan);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ startSpan({ name: 'root span without onlyIfParent' }, span => {
+ return span;
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
@@ -1189,15 +1203,21 @@ describe('startSpanManual', () => {
});
describe('onlyIfParent', () => {
- it('does not create a span if there is no parent', () => {
+ it('does not create a span and records no_parent_span client report if there is no parent', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
return span;
});
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentryNonRecordingSpan);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
return span;
@@ -1208,6 +1228,18 @@ describe('startSpanManual', () => {
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentrySpan);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ startSpanManual({ name: 'root span without onlyIfParent' }, span => {
+ span.end();
+ return span;
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
@@ -1592,14 +1624,20 @@ describe('startInactiveSpan', () => {
});
describe('onlyIfParent', () => {
- it('does not create a span if there is no parent', () => {
+ it('does not create a span and records no_parent_span client report if there is no parent', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentryNonRecordingSpan);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
return span;
@@ -1607,6 +1645,16 @@ describe('startInactiveSpan', () => {
expect(span).toBeDefined();
expect(span).toBeInstanceOf(SentrySpan);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ const span = startInactiveSpan({ name: 'root span without onlyIfParent' });
+ span.end();
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
diff --git a/packages/core/test/lib/utils/weakRef.test.ts b/packages/core/test/lib/utils/weakRef.test.ts
index cf050ccf3d6e..36e4fcb6b8f3 100644
--- a/packages/core/test/lib/utils/weakRef.test.ts
+++ b/packages/core/test/lib/utils/weakRef.test.ts
@@ -1,4 +1,4 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import { derefWeakRef, makeWeakRef, type MaybeWeakRef } from '../../../src/utils/weakRef';
describe('Unit | util | weakRef', () => {
diff --git a/packages/effect/README.md b/packages/effect/README.md
index 78b2f6471dc0..bfe3c51ce8dc 100644
--- a/packages/effect/README.md
+++ b/packages/effect/README.md
@@ -6,11 +6,16 @@
> NOTICE: This package is in alpha state and may be subject to breaking changes.
+`@sentry/effect` supports both Effect v3 and Effect v4 (beta). The integration
+auto-detects the installed Effect version at runtime, but the layer composition
+APIs differ between the two major versions, so the setup code is slightly
+different.
+
## Getting Started
This SDK does not have docs yet. Stay tuned.
-## Usage
+## Usage with Effect v3
```typescript
import * as Sentry from '@sentry/effect/server';
@@ -33,16 +38,45 @@ const MainLive = HttpLive.pipe(Layer.provide(SentryLive));
MainLive.pipe(Layer.launch, NodeRuntime.runMain);
```
-The `effectLayer` function initializes Sentry. To enable Effect instrumentation, compose with:
+## Usage with Effect v4
-- `Layer.setTracer(Sentry.SentryEffectTracer)` - Effect spans traced as Sentry spans
-- `Logger.replace(Logger.defaultLogger, Sentry.SentryEffectLogger)` - Effect logs forwarded to Sentry
-- `Sentry.SentryEffectMetricsLayer` - Effect metrics sent to Sentry
+Effect v4 reorganized the `Tracer` and `Logger` layer APIs, so the wiring looks
+slightly different. The `effectLayer`, `SentryEffectTracer`,
+`SentryEffectLogger`, and `SentryEffectMetricsLayer` exports themselves are the
+same.
-## Links
+```typescript
+import * as Sentry from '@sentry/effect/server';
+import { NodeHttpServer, NodeRuntime } from '@effect/platform-node';
+import * as Layer from 'effect/Layer';
+import * as Logger from 'effect/Logger';
+import * as Tracer from 'effect/Tracer';
+import { HttpRouter } from 'effect/unstable/http';
+import { createServer } from 'http';
+import { Routes } from './Routes.js';
+
+const SentryLive = Layer.mergeAll(
+ Sentry.effectLayer({
+ dsn: '__DSN__',
+ tracesSampleRate: 1.0,
+ enableLogs: true,
+ }),
+ Layer.succeed(Tracer.Tracer, Sentry.SentryEffectTracer),
+ Logger.layer([Sentry.SentryEffectLogger]),
+ Sentry.SentryEffectMetricsLayer,
+);
-
+const HttpLive = HttpRouter.serve(Routes).pipe(
+ Layer.provide(NodeHttpServer.layer(() => createServer(), { port: 3030 })),
+ Layer.provide(SentryLive),
+);
+
+NodeRuntime.runMain(Layer.launch(HttpLive));
+```
+
+## Links
+- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/effect/)
- [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_effect)
- [Sentry Discord Server](https://discord.gg/Ww9hbqr)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry)
diff --git a/packages/effect/package.json b/packages/effect/package.json
index 412f884eca1a..669630577640 100644
--- a/packages/effect/package.json
+++ b/packages/effect/package.json
@@ -62,7 +62,7 @@
"@sentry/node-core": "10.49.0"
},
"peerDependencies": {
- "effect": "^3.0.0"
+ "effect": "^3.0.0 || ^4.0.0-beta.50"
},
"peerDependenciesMeta": {
"effect": {
@@ -70,8 +70,8 @@
}
},
"devDependencies": {
- "@effect/vitest": "^0.23.9",
- "effect": "^3.21.0"
+ "@effect/vitest": "^4.0.0-beta.50",
+ "effect": "^4.0.0-beta.50"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/effect/src/logger.ts b/packages/effect/src/logger.ts
index 833f5b6b7e95..7421bebc0dbe 100644
--- a/packages/effect/src/logger.ts
+++ b/packages/effect/src/logger.ts
@@ -1,5 +1,20 @@
import { logger as sentryLogger } from '@sentry/core';
import * as Logger from 'effect/Logger';
+import type * as LogLevel from 'effect/LogLevel';
+
+function getLogLevelTag(logLevel: LogLevel.LogLevel): LogLevel.LogLevel | 'Warning' {
+ // Effect v4: logLevel is a string literal directly
+ if (typeof logLevel === 'string') {
+ return logLevel;
+ }
+
+ // Effect v3: logLevel has _tag property
+ if (logLevel && typeof logLevel === 'object' && '_tag' in logLevel) {
+ return (logLevel as { _tag: LogLevel.LogLevel })._tag;
+ }
+
+ return 'Info';
+}
/**
* Effect Logger that sends logs to Sentry.
@@ -15,14 +30,17 @@ export const SentryEffectLogger = Logger.make(({ logLevel, message }) => {
msg = JSON.stringify(message);
}
- switch (logLevel._tag) {
+ const tag = getLogLevelTag(logLevel);
+
+ switch (tag) {
case 'Fatal':
sentryLogger.fatal(msg);
break;
case 'Error':
sentryLogger.error(msg);
break;
- case 'Warning':
+ case 'Warning': // Effect v3
+ case 'Warn': // Effect v4
sentryLogger.warn(msg);
break;
case 'Info':
@@ -38,6 +56,6 @@ export const SentryEffectLogger = Logger.make(({ logLevel, message }) => {
case 'None':
break;
default:
- logLevel satisfies never;
+ tag satisfies never;
}
});
diff --git a/packages/effect/src/metrics.ts b/packages/effect/src/metrics.ts
index 82daf5e67a5d..764149009be5 100644
--- a/packages/effect/src/metrics.ts
+++ b/packages/effect/src/metrics.ts
@@ -1,66 +1,75 @@
import { metrics as sentryMetrics } from '@sentry/core';
+import * as Context from 'effect/Context';
import * as Effect from 'effect/Effect';
-import type * as Layer from 'effect/Layer';
-import { scopedDiscard } from 'effect/Layer';
+import * as Layer from 'effect/Layer';
import * as Metric from 'effect/Metric';
-import * as MetricKeyType from 'effect/MetricKeyType';
-import type * as MetricPair from 'effect/MetricPair';
-import * as MetricState from 'effect/MetricState';
import * as Schedule from 'effect/Schedule';
type MetricAttributes = Record;
-function labelsToAttributes(labels: ReadonlyArray<{ key: string; value: string }>): MetricAttributes {
- return labels.reduce((acc, label) => ({ ...acc, [label.key]: label.value }), {});
+// =============================================================================
+// Effect v3 Types (vendored - not exported from effect@3.x)
+// =============================================================================
+
+interface V3MetricLabel {
+ key: string;
+ value: string;
}
-function sendMetricToSentry(pair: MetricPair.MetricPair.Untyped): void {
- const { metricKey, metricState } = pair;
- const name = metricKey.name;
- const attributes = labelsToAttributes(metricKey.tags);
+interface V3MetricPair {
+ metricKey: {
+ name: string;
+ tags: ReadonlyArray;
+ keyType: { _tag: string };
+ };
+ metricState: {
+ count?: number | bigint;
+ value?: number;
+ sum?: number;
+ min?: number;
+ max?: number;
+ occurrences?: Map;
+ };
+}
- if (MetricState.isCounterState(metricState)) {
- const value = Number(metricState.count);
- sentryMetrics.count(name, value, { attributes });
- } else if (MetricState.isGaugeState(metricState)) {
- const value = Number(metricState.value);
- sentryMetrics.gauge(name, value, { attributes });
- } else if (MetricState.isHistogramState(metricState)) {
- sentryMetrics.gauge(`${name}.sum`, metricState.sum, { attributes });
- sentryMetrics.gauge(`${name}.count`, metricState.count, { attributes });
- sentryMetrics.gauge(`${name}.min`, metricState.min, { attributes });
- sentryMetrics.gauge(`${name}.max`, metricState.max, { attributes });
- } else if (MetricState.isSummaryState(metricState)) {
- sentryMetrics.gauge(`${name}.sum`, metricState.sum, { attributes });
- sentryMetrics.gauge(`${name}.count`, metricState.count, { attributes });
- sentryMetrics.gauge(`${name}.min`, metricState.min, { attributes });
- sentryMetrics.gauge(`${name}.max`, metricState.max, { attributes });
- } else if (MetricState.isFrequencyState(metricState)) {
- for (const [word, count] of metricState.occurrences) {
- sentryMetrics.count(name, count, {
- attributes: { ...attributes, word },
- });
- }
- }
+// Effect v3 `MetricState` implementations brand themselves with a `Symbol.for(...)` TypeId
+// rather than a string `_tag`. We use these globally-registered symbols to classify state
+// instances returned by `Metric.unsafeSnapshot()` without importing `effect/MetricState`
+// (the module does not exist in Effect v4).
+const V3_COUNTER_STATE_TYPE_ID = Symbol.for('effect/MetricState/Counter');
+const V3_GAUGE_STATE_TYPE_ID = Symbol.for('effect/MetricState/Gauge');
+const V3_HISTOGRAM_STATE_TYPE_ID = Symbol.for('effect/MetricState/Histogram');
+const V3_SUMMARY_STATE_TYPE_ID = Symbol.for('effect/MetricState/Summary');
+const V3_FREQUENCY_STATE_TYPE_ID = Symbol.for('effect/MetricState/Frequency');
+
+function labelsToAttributes(labels: ReadonlyArray): MetricAttributes {
+ return labels.reduce((acc, label) => ({ ...acc, [label.key]: label.value }), {});
}
-function getMetricId(pair: MetricPair.MetricPair.Untyped): string {
+function getMetricIdV3(pair: V3MetricPair): string {
const tags = pair.metricKey.tags.map(t => `${t.key}=${t.value}`).join(',');
return `${pair.metricKey.name}:${tags}`;
}
-function sendDeltaMetricToSentry(
- pair: MetricPair.MetricPair.Untyped,
- previousCounterValues: Map,
-): void {
+function getMetricIdV4(snapshot: Metric.Metric.Snapshot): string {
+ const attrs = snapshot.attributes
+ ? Object.entries(snapshot.attributes)
+ .map(([k, v]) => `${k}=${v}`)
+ .join(',')
+ : '';
+ return `${snapshot.id}:${attrs}`;
+}
+
+function sendV3MetricToSentry(pair: V3MetricPair, previousCounterValues: Map): void {
const { metricKey, metricState } = pair;
const name = metricKey.name;
const attributes = labelsToAttributes(metricKey.tags);
- const metricId = getMetricId(pair);
+ const metricId = getMetricIdV3(pair);
- if (MetricState.isCounterState(metricState)) {
- const currentValue = Number(metricState.count);
+ const state = metricState as unknown as Record;
+ if (state[V3_COUNTER_STATE_TYPE_ID] !== undefined) {
+ const currentValue = Number(metricState.count);
const previousValue = previousCounterValues.get(metricId) ?? 0;
const delta = currentValue - previousValue;
@@ -69,41 +78,92 @@ function sendDeltaMetricToSentry(
}
previousCounterValues.set(metricId, currentValue);
- } else {
- sendMetricToSentry(pair);
+ } else if (state[V3_GAUGE_STATE_TYPE_ID] !== undefined) {
+ const value = Number(metricState.value);
+ sentryMetrics.gauge(name, value, { attributes });
+ } else if (state[V3_HISTOGRAM_STATE_TYPE_ID] !== undefined || state[V3_SUMMARY_STATE_TYPE_ID] !== undefined) {
+ sentryMetrics.gauge(`${name}.sum`, metricState.sum ?? 0, { attributes });
+ sentryMetrics.gauge(`${name}.count`, Number(metricState.count ?? 0), { attributes });
+ sentryMetrics.gauge(`${name}.min`, metricState.min ?? 0, { attributes });
+ sentryMetrics.gauge(`${name}.max`, metricState.max ?? 0, { attributes });
+ } else if (state[V3_FREQUENCY_STATE_TYPE_ID] !== undefined && metricState.occurrences) {
+ for (const [word, count] of metricState.occurrences) {
+ sentryMetrics.count(name, count, {
+ attributes: { ...attributes, word },
+ });
+ }
}
}
-/**
- * Flushes all Effect metrics to Sentry.
- * @param previousCounterValues - Map tracking previous counter values for delta calculation
- */
-function flushMetricsToSentry(previousCounterValues: Map): void {
- const snapshot = Metric.unsafeSnapshot();
+function sendV4MetricToSentry(snapshot: Metric.Metric.Snapshot, previousCounterValues: Map): void {
+ const name = snapshot.id;
+ const attributes: MetricAttributes = snapshot.attributes ? { ...snapshot.attributes } : {};
+ const metricId = getMetricIdV4(snapshot);
+
+ switch (snapshot.type) {
+ case 'Counter': {
+ const currentValue = Number(snapshot.state.count);
+ const previousValue = previousCounterValues.get(metricId) ?? 0;
+ const delta = currentValue - previousValue;
+
+ if (delta > 0) {
+ sentryMetrics.count(name, delta, { attributes });
+ }
- snapshot.forEach((pair: MetricPair.MetricPair.Untyped) => {
- if (MetricKeyType.isCounterKey(pair.metricKey.keyType)) {
- sendDeltaMetricToSentry(pair, previousCounterValues);
- } else {
- sendMetricToSentry(pair);
+ previousCounterValues.set(metricId, currentValue);
+ break;
+ }
+ case 'Gauge': {
+ const value = Number(snapshot.state.value);
+ sentryMetrics.gauge(name, value, { attributes });
+ break;
+ }
+ case 'Histogram':
+ case 'Summary': {
+ sentryMetrics.gauge(`${name}.sum`, snapshot.state.sum ?? 0, { attributes });
+ sentryMetrics.gauge(`${name}.count`, snapshot.state.count ?? 0, { attributes });
+ sentryMetrics.gauge(`${name}.min`, snapshot.state.min ?? 0, { attributes });
+ sentryMetrics.gauge(`${name}.max`, snapshot.state.max ?? 0, { attributes });
+ break;
+ }
+ case 'Frequency': {
+ for (const [word, count] of snapshot.state.occurrences) {
+ sentryMetrics.count(name, count, {
+ attributes: { ...attributes, word },
+ });
+ }
+ break;
}
- });
+ }
}
-/**
- * Creates a metrics flusher with its own isolated state for delta tracking.
- * Useful for testing scenarios where you need to control the lifecycle.
- * @internal
- */
-export function createMetricsFlusher(): {
- flush: () => void;
- clear: () => void;
-} {
- const previousCounterValues = new Map();
- return {
- flush: () => flushMetricsToSentry(previousCounterValues),
- clear: () => previousCounterValues.clear(),
- };
+// =============================================================================
+// Effect v3 snapshot function type (vendored - not exported from effect@3.x)
+// =============================================================================
+
+type V3UnsafeSnapshotFn = () => ReadonlyArray;
+
+// Use bracket notation to avoid Webpack static analysis flagging missing exports
+// This is important for Effect v3 compatibility.
+const MetricModule = Metric;
+const snapshotUnsafe = MetricModule['snapshotUnsafe'] as typeof Metric.snapshotUnsafe | undefined;
+// @ts-expect-error - unsafeSnapshot is not exported from effect@3.x
+const unsafeSnapshot = MetricModule['unsafeSnapshot'] as V3UnsafeSnapshotFn | undefined;
+
+function flushMetricsToSentry(previousCounterValues: Map): void {
+ if (snapshotUnsafe) {
+ // Effect v4
+ const snapshots = snapshotUnsafe(Context.empty());
+ for (const snapshot of snapshots) {
+ sendV4MetricToSentry(snapshot, previousCounterValues);
+ }
+ } else if (unsafeSnapshot) {
+ // Effect v3
+ const snapshots = unsafeSnapshot();
+ for (const pair of snapshots) {
+ sendV3MetricToSentry(pair, previousCounterValues);
+ }
+ }
}
function createMetricsReporterEffect(previousCounterValues: Map): Effect.Effect {
@@ -120,7 +180,7 @@ function createMetricsReporterEffect(previousCounterValues: Map)
* The layer manages its own state for delta counter calculations,
* which is automatically cleaned up when the layer is finalized.
*/
-export const SentryEffectMetricsLayer: Layer.Layer = scopedDiscard(
+export const SentryEffectMetricsLayer: Layer.Layer = Layer.effectDiscard(
Effect.gen(function* () {
const previousCounterValues = new Map();
diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts
index f755101e4417..a3149b8e7096 100644
--- a/packages/effect/src/tracer.ts
+++ b/packages/effect/src/tracer.ts
@@ -32,6 +32,46 @@ function isSentrySpan(span: EffectTracer.AnySpan): span is SentrySpanLike {
return SENTRY_SPAN_SYMBOL in span;
}
+function getErrorMessage(exit: Exit.Exit): string | undefined {
+ if (!Exit.isFailure(exit)) {
+ return undefined;
+ }
+
+ const cause = exit.cause as unknown;
+
+ // Effect v4: cause.reasons is an array of Reason objects
+ if (
+ cause &&
+ typeof cause === 'object' &&
+ 'reasons' in cause &&
+ Array.isArray((cause as { reasons: unknown }).reasons)
+ ) {
+ const reasons = (cause as { reasons: Array<{ _tag?: string; error?: unknown; defect?: unknown }> }).reasons;
+ for (const reason of reasons) {
+ if (reason._tag === 'Fail' && reason.error !== undefined) {
+ return String(reason.error);
+ }
+ if (reason._tag === 'Die' && reason.defect !== undefined) {
+ return String(reason.defect);
+ }
+ }
+ return 'internal_error';
+ }
+
+ // Effect v3: cause has _tag directly
+ if (cause && typeof cause === 'object' && '_tag' in cause) {
+ const v3Cause = cause as { _tag: string; error?: unknown; defect?: unknown };
+ if (v3Cause._tag === 'Fail') {
+ return String(v3Cause.error);
+ }
+ if (v3Cause._tag === 'Die') {
+ return String(v3Cause.defect);
+ }
+ }
+
+ return 'internal_error';
+}
+
class SentrySpanWrapper implements SentrySpanLike {
public readonly [SENTRY_SPAN_SYMBOL]: true;
public readonly _tag: 'Span';
@@ -43,6 +83,7 @@ class SentrySpanWrapper implements SentrySpanLike {
public readonly links: Array;
public status: EffectTracer.SpanStatus;
public readonly sentrySpan: Span;
+ public readonly annotations: Context.Context;
public constructor(
public readonly name: string,
@@ -59,6 +100,7 @@ class SentrySpanWrapper implements SentrySpanLike {
this.parent = parent;
this.links = [...links];
this.sentrySpan = existingSpan;
+ this.annotations = context;
const spanContext = this.sentrySpan.spanContext();
this.spanId = spanContext.spanId;
@@ -96,9 +138,7 @@ class SentrySpanWrapper implements SentrySpanLike {
}
if (Exit.isFailure(exit)) {
- const cause = exit.cause;
- const message =
- cause._tag === 'Fail' ? String(cause.error) : cause._tag === 'Die' ? String(cause.defect) : 'internal_error';
+ const message = getErrorMessage(exit) ?? 'internal_error';
this.sentrySpan.setStatus({ code: 2, message });
} else {
this.sentrySpan.setStatus({ code: 1 });
@@ -139,21 +179,71 @@ function createSentrySpan(
return new SentrySpanWrapper(name, parent, context, links, startTime, kind, newSpan);
}
-const makeSentryTracer = (): EffectTracer.Tracer =>
- EffectTracer.make({
- span(name, parent, context, links, startTime, kind) {
+// Check if we're running Effect v4 by checking the Exit/Cause structure
+// In v4, causes have a 'reasons' array
+// In v3, causes have '_tag' directly on the cause object
+const isEffectV4 = (() => {
+ try {
+ const testExit = Exit.fail('test') as unknown as { cause?: unknown };
+ const cause = testExit.cause;
+ // v4 causes have 'reasons' array, v3 causes have '_tag' directly
+ if (cause && typeof cause === 'object' && 'reasons' in cause) {
+ return true;
+ }
+ return false;
+ } catch {
+ return false;
+ }
+})();
+
+const makeSentryTracerV3 = (): EffectTracer.Tracer => {
+ // Effect v3 API: span(name, parent, context, links, startTime, kind)
+ return EffectTracer.make({
+ span(
+ name: string,
+ parent: Option.Option,
+ context: Context.Context,
+ links: ReadonlyArray,
+ startTime: bigint,
+ kind: EffectTracer.SpanKind,
+ ) {
return createSentrySpan(name, parent, context, links, startTime, kind);
},
- context(execution, fiber) {
+ context(execution: () => unknown, fiber: { currentSpan?: EffectTracer.AnySpan }) {
const currentSpan = fiber.currentSpan;
if (currentSpan === undefined || !isSentrySpan(currentSpan)) {
return execution();
}
return withActiveSpan(currentSpan.sentrySpan, execution);
},
+ } as unknown as EffectTracer.Tracer);
+};
+
+const makeSentryTracerV4 = (): EffectTracer.Tracer => {
+ const EFFECT_EVALUATE = '~effect/Effect/evaluate' as const;
+
+ return EffectTracer.make({
+ span(options) {
+ return createSentrySpan(
+ options.name,
+ options.parent,
+ options.annotations,
+ options.links,
+ options.startTime,
+ options.kind,
+ );
+ },
+ context(primitive, fiber) {
+ const currentSpan = fiber.currentSpan;
+ if (currentSpan === undefined || !isSentrySpan(currentSpan)) {
+ return primitive[EFFECT_EVALUATE](fiber);
+ }
+ return withActiveSpan(currentSpan.sentrySpan, () => primitive[EFFECT_EVALUATE](fiber));
+ },
});
+};
/**
* Effect Layer that sets up the Sentry tracer for Effect spans.
*/
-export const SentryEffectTracer = makeSentryTracer();
+export const SentryEffectTracer = isEffectV4 ? makeSentryTracerV4() : makeSentryTracerV3();
diff --git a/packages/effect/test/layer.test.ts b/packages/effect/test/layer.test.ts
index 1874fe9b0f53..255d751799d5 100644
--- a/packages/effect/test/layer.test.ts
+++ b/packages/effect/test/layer.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
import { getClient, getCurrentScope, getIsolationScope, SDK_VERSION } from '@sentry/core';
-import { Effect, Layer, Logger, LogLevel } from 'effect';
+import { Effect, Layer, Logger } from 'effect';
+import * as References from 'effect/References';
import { afterEach, beforeEach, vi } from 'vitest';
import * as sentryClient from '../src/index.client';
import * as sentryServer from '../src/index.server';
@@ -109,7 +110,7 @@ describe.each([
),
);
- it.effect('layer can be composed with tracer layer', () =>
+ it.effect('layer can be composed with tracer', () =>
Effect.gen(function* () {
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');
@@ -120,32 +121,30 @@ describe.each([
expect(result).toBe(84);
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
}).pipe(
+ Effect.withTracer(SentryEffectTracer),
Effect.provide(
- Layer.mergeAll(
- effectLayer({
- dsn: TEST_DSN,
- transport: getMockTransport(),
- }),
- Layer.setTracer(SentryEffectTracer),
- ),
+ effectLayer({
+ dsn: TEST_DSN,
+ transport: getMockTransport(),
+ }),
),
),
);
- it.effect('layer can be composed with logger layer', () =>
+ it.effect('layer can be composed with logger', () =>
Effect.gen(function* () {
yield* Effect.logInfo('test log');
const result = yield* Effect.succeed('logged');
expect(result).toBe('logged');
}).pipe(
+ Effect.provideService(References.MinimumLogLevel, 'All'),
Effect.provide(
Layer.mergeAll(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
- Logger.replace(Logger.defaultLogger, SentryEffectLogger),
- Logger.minimumLogLevel(LogLevel.All),
+ Logger.layer([SentryEffectLogger]),
),
),
),
@@ -164,15 +163,15 @@ describe.each([
expect(result).toBe(84);
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
}).pipe(
+ Effect.withTracer(SentryEffectTracer),
+ Effect.provideService(References.MinimumLogLevel, 'All'),
Effect.provide(
Layer.mergeAll(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
- Layer.setTracer(SentryEffectTracer),
- Logger.replace(Logger.defaultLogger, SentryEffectLogger),
- Logger.minimumLogLevel(LogLevel.All),
+ Logger.layer([SentryEffectLogger]),
),
),
),
diff --git a/packages/effect/test/logger.test.ts b/packages/effect/test/logger.test.ts
index c372784b483f..5069514fc2c7 100644
--- a/packages/effect/test/logger.test.ts
+++ b/packages/effect/test/logger.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
-import { Effect, Layer, Logger, LogLevel } from 'effect';
+import { Effect, Logger } from 'effect';
+import * as References from 'effect/References';
import { afterEach, vi } from 'vitest';
import { SentryEffectLogger } from '../src/logger';
@@ -25,10 +26,10 @@ describe('SentryEffectLogger', () => {
vi.clearAllMocks();
});
- const loggerLayer = Layer.mergeAll(
- Logger.replace(Logger.defaultLogger, SentryEffectLogger),
- Logger.minimumLogLevel(LogLevel.All),
- );
+ const loggerLayer = Logger.layer([SentryEffectLogger]);
+
+ const withAllLogLevels = (effect: Effect.Effect) =>
+ Effect.provideService(effect, References.MinimumLogLevel, 'All');
it.effect('forwards fatal logs to Sentry', () =>
Effect.gen(function* () {
@@ -62,14 +63,14 @@ describe('SentryEffectLogger', () => {
Effect.gen(function* () {
yield* Effect.logDebug('This is a debug message');
expect(sentryCore.logger.debug).toHaveBeenCalledWith('This is a debug message');
- }).pipe(Effect.provide(loggerLayer)),
+ }).pipe(withAllLogLevels, Effect.provide(loggerLayer)),
);
it.effect('forwards trace logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logTrace('This is a trace message');
expect(sentryCore.logger.trace).toHaveBeenCalledWith('This is a trace message');
- }).pipe(Effect.provide(loggerLayer)),
+ }).pipe(withAllLogLevels, Effect.provide(loggerLayer)),
);
it.effect('handles object messages by stringifying', () =>
diff --git a/packages/effect/test/metrics.test.ts b/packages/effect/test/metrics.test.ts
index 8c2b092b967f..a8d5a9813fa9 100644
--- a/packages/effect/test/metrics.test.ts
+++ b/packages/effect/test/metrics.test.ts
@@ -1,8 +1,10 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
-import { Duration, Effect, Metric, MetricBoundaries, MetricLabel } from 'effect';
+import * as Context from 'effect/Context';
+import { Duration, Effect, Layer, Metric } from 'effect';
+import { TestClock } from 'effect/testing';
import { afterEach, beforeEach, vi } from 'vitest';
-import { createMetricsFlusher } from '../src/metrics';
+import { SentryEffectMetricsLayer } from '../src/metrics';
describe('SentryEffectMetricsLayer', () => {
const mockCount = vi.fn();
@@ -24,12 +26,12 @@ describe('SentryEffectMetricsLayer', () => {
Effect.gen(function* () {
const counter = Metric.counter('test_counter');
- yield* Metric.increment(counter);
- yield* Metric.increment(counter);
- yield* Metric.incrementBy(counter, 5);
+ yield* Metric.update(counter, 1);
+ yield* Metric.update(counter, 1);
+ yield* Metric.update(counter, 5);
- const snapshot = Metric.unsafeSnapshot();
- const counterMetric = snapshot.find(p => p.metricKey.name === 'test_counter');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const counterMetric = snapshot.find(p => p.id === 'test_counter');
expect(counterMetric).toBeDefined();
}),
@@ -39,10 +41,10 @@ describe('SentryEffectMetricsLayer', () => {
Effect.gen(function* () {
const gauge = Metric.gauge('test_gauge');
- yield* Metric.set(gauge, 42);
+ yield* Metric.update(gauge, 42);
- const snapshot = Metric.unsafeSnapshot();
- const gaugeMetric = snapshot.find(p => p.metricKey.name === 'test_gauge');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const gaugeMetric = snapshot.find(p => p.id === 'test_gauge');
expect(gaugeMetric).toBeDefined();
}),
@@ -50,14 +52,16 @@ describe('SentryEffectMetricsLayer', () => {
it.effect('creates histogram metrics', () =>
Effect.gen(function* () {
- const histogram = Metric.histogram('test_histogram', MetricBoundaries.linear({ start: 0, width: 10, count: 10 }));
+ const histogram = Metric.histogram('test_histogram', {
+ boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 10 }),
+ });
yield* Metric.update(histogram, 5);
yield* Metric.update(histogram, 15);
yield* Metric.update(histogram, 25);
- const snapshot = Metric.unsafeSnapshot();
- const histogramMetric = snapshot.find(p => p.metricKey.name === 'test_histogram');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const histogramMetric = snapshot.find(p => p.id === 'test_histogram');
expect(histogramMetric).toBeDefined();
}),
@@ -65,8 +69,7 @@ describe('SentryEffectMetricsLayer', () => {
it.effect('creates summary metrics', () =>
Effect.gen(function* () {
- const summary = Metric.summary({
- name: 'test_summary',
+ const summary = Metric.summary('test_summary', {
maxAge: '1 minute',
maxSize: 100,
error: 0.01,
@@ -77,8 +80,8 @@ describe('SentryEffectMetricsLayer', () => {
yield* Metric.update(summary, 20);
yield* Metric.update(summary, 30);
- const snapshot = Metric.unsafeSnapshot();
- const summaryMetric = snapshot.find(p => p.metricKey.name === 'test_summary');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const summaryMetric = snapshot.find(p => p.id === 'test_summary');
expect(summaryMetric).toBeDefined();
}),
@@ -92,39 +95,41 @@ describe('SentryEffectMetricsLayer', () => {
yield* Metric.update(frequency, 'bar');
yield* Metric.update(frequency, 'foo');
- const snapshot = Metric.unsafeSnapshot();
- const frequencyMetric = snapshot.find(p => p.metricKey.name === 'test_frequency');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const frequencyMetric = snapshot.find(p => p.id === 'test_frequency');
expect(frequencyMetric).toBeDefined();
}),
);
- it.effect('supports metrics with labels', () =>
+ it.effect('supports metrics with attributes', () =>
Effect.gen(function* () {
const counter = Metric.counter('labeled_counter').pipe(
- Metric.taggedWithLabels([MetricLabel.make('env', 'test'), MetricLabel.make('service', 'my-service')]),
+ Metric.withAttributes({ env: 'test', service: 'my-service' }),
);
- yield* Metric.increment(counter);
+ yield* Metric.update(counter, 1);
- const snapshot = Metric.unsafeSnapshot();
- const labeledMetric = snapshot.find(p => p.metricKey.name === 'labeled_counter');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const labeledMetric = snapshot.find(p => p.id === 'labeled_counter');
expect(labeledMetric).toBeDefined();
- const tags = labeledMetric?.metricKey.tags ?? [];
- expect(tags.some(t => t.key === 'env' && t.value === 'test')).toBe(true);
- expect(tags.some(t => t.key === 'service' && t.value === 'my-service')).toBe(true);
+ const attrs = labeledMetric?.attributes ?? {};
+ expect(attrs['env']).toBe('test');
+ expect(attrs['service']).toBe('my-service');
}),
);
- it.effect('tracks Effect durations with timer metric', () =>
+ it.effect('tracks Effect durations with histogram metric', () =>
Effect.gen(function* () {
- const timer = Metric.timerWithBoundaries('operation_duration', [10, 50, 100, 500, 1000]);
+ const histogram = Metric.histogram('operation_duration', {
+ boundaries: Metric.linearBoundaries({ start: 10, width: 100, count: 10 }),
+ });
- yield* Effect.succeed('done').pipe(Metric.trackDuration(timer));
+ yield* Metric.update(histogram, Duration.millis(50));
- const snapshot = Metric.unsafeSnapshot();
- const timerMetric = snapshot.find(p => p.metricKey.name === 'operation_duration');
+ const snapshot = Metric.snapshotUnsafe(Context.empty());
+ const timerMetric = snapshot.find(p => p.id === 'operation_duration');
expect(timerMetric).toBeDefined();
}),
@@ -140,7 +145,7 @@ describe('SentryEffectMetricsLayer', () => {
);
});
-describe('createMetricsFlusher', () => {
+describe('SentryEffectMetricsLayer flushing', () => {
const mockCount = vi.fn();
const mockGauge = vi.fn();
const mockDistribution = vi.fn();
@@ -156,58 +161,54 @@ describe('createMetricsFlusher', () => {
vi.restoreAllMocks();
});
+ const TestLayer = SentryEffectMetricsLayer.pipe(Layer.provideMerge(TestClock.layer()));
+
it.effect('sends counter metrics to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const counter = Metric.counter('flush_test_counter');
- yield* Metric.increment(counter);
- yield* Metric.incrementBy(counter, 4);
+ yield* Metric.update(counter, 1);
+ yield* Metric.update(counter, 4);
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockCount).toHaveBeenCalledWith('flush_test_counter', 5, { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('sends gauge metrics to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const gauge = Metric.gauge('flush_test_gauge');
- yield* Metric.set(gauge, 42);
+ yield* Metric.update(gauge, 42);
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockGauge).toHaveBeenCalledWith('flush_test_gauge', 42, { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('sends histogram metrics to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
- const histogram = Metric.histogram(
- 'flush_test_histogram',
- MetricBoundaries.linear({ start: 0, width: 10, count: 5 }),
- );
+ const histogram = Metric.histogram('flush_test_histogram', {
+ boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 5 }),
+ });
yield* Metric.update(histogram, 5);
yield* Metric.update(histogram, 15);
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.sum', expect.any(Number), { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.count', expect.any(Number), { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.min', expect.any(Number), { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_histogram.max', expect.any(Number), { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('sends summary metrics to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
- const summary = Metric.summary({
- name: 'flush_test_summary',
+ const summary = Metric.summary('flush_test_summary', {
maxAge: '1 minute',
maxSize: 100,
error: 0.01,
@@ -218,104 +219,74 @@ describe('createMetricsFlusher', () => {
yield* Metric.update(summary, 20);
yield* Metric.update(summary, 30);
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.sum', 60, { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.count', 3, { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.min', 10, { attributes: {} });
expect(mockGauge).toHaveBeenCalledWith('flush_test_summary.max', 30, { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('sends frequency metrics to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const frequency = Metric.frequency('flush_test_frequency');
yield* Metric.update(frequency, 'apple');
yield* Metric.update(frequency, 'banana');
yield* Metric.update(frequency, 'apple');
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockCount).toHaveBeenCalledWith('flush_test_frequency', 2, { attributes: { word: 'apple' } });
expect(mockCount).toHaveBeenCalledWith('flush_test_frequency', 1, { attributes: { word: 'banana' } });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
- it.effect('sends metrics with labels as attributes to Sentry', () =>
+ it.effect('sends metrics with attributes to Sentry', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const gauge = Metric.gauge('flush_test_labeled_gauge').pipe(
- Metric.taggedWithLabels([MetricLabel.make('env', 'production'), MetricLabel.make('region', 'us-east')]),
+ Metric.withAttributes({ env: 'production', region: 'us-east' }),
);
- yield* Metric.set(gauge, 100);
+ yield* Metric.update(gauge, 100);
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockGauge).toHaveBeenCalledWith('flush_test_labeled_gauge', 100, {
attributes: { env: 'production', region: 'us-east' },
});
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('sends counter delta values on subsequent flushes', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const counter = Metric.counter('flush_test_delta_counter');
- yield* Metric.incrementBy(counter, 10);
- flusher.flush();
+ yield* Metric.update(counter, 10);
+ yield* TestClock.adjust('10 seconds');
mockCount.mockClear();
- yield* Metric.incrementBy(counter, 5);
- flusher.flush();
+ yield* Metric.update(counter, 5);
+ yield* TestClock.adjust('10 seconds');
expect(mockCount).toHaveBeenCalledWith('flush_test_delta_counter', 5, { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
it.effect('does not send counter when delta is zero', () =>
Effect.gen(function* () {
- const flusher = createMetricsFlusher();
const counter = Metric.counter('flush_test_zero_delta');
- yield* Metric.incrementBy(counter, 10);
- flusher.flush();
+ yield* Metric.update(counter, 10);
+ yield* TestClock.adjust('10 seconds');
mockCount.mockClear();
- flusher.flush();
+ yield* TestClock.adjust('10 seconds');
expect(mockCount).not.toHaveBeenCalledWith('flush_test_zero_delta', 0, { attributes: {} });
- }),
+ }).pipe(Effect.provide(TestLayer)),
);
-
- it.effect('clear() resets delta tracking state', () =>
- Effect.gen(function* () {
- const flusher = createMetricsFlusher();
- const counter = Metric.counter('flush_test_clear_counter');
-
- yield* Metric.incrementBy(counter, 10);
- flusher.flush();
-
- mockCount.mockClear();
- flusher.clear();
-
- flusher.flush();
-
- expect(mockCount).toHaveBeenCalledWith('flush_test_clear_counter', 10, { attributes: {} });
- }),
- );
-
- it('each flusher has isolated state', () => {
- const flusher1 = createMetricsFlusher();
- const flusher2 = createMetricsFlusher();
-
- expect(flusher1).not.toBe(flusher2);
- expect(flusher1.flush).not.toBe(flusher2.flush);
- expect(flusher1.clear).not.toBe(flusher2.clear);
- });
});
diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts
index 9583e7d12c5b..81d8cae64f42 100644
--- a/packages/effect/test/tracer.test.ts
+++ b/packages/effect/test/tracer.test.ts
@@ -1,11 +1,11 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { Effect, Layer } from 'effect';
+import { Effect } from 'effect';
import { afterEach, vi } from 'vitest';
import { SentryEffectTracer } from '../src/tracer';
-const TracerLayer = Layer.setTracer(SentryEffectTracer);
+const withSentryTracer = (effect: Effect.Effect) => Effect.withTracer(effect, SentryEffectTracer);
describe('SentryEffectTracer', () => {
afterEach(() => {
@@ -24,7 +24,7 @@ describe('SentryEffectTracer', () => {
);
expect(capturedSpanName).toBe('effect-span-executed');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('creates spans with correct attributes', () =>
@@ -32,7 +32,7 @@ describe('SentryEffectTracer', () => {
const result = yield* Effect.withSpan('my-operation')(Effect.succeed('success'));
expect(result).toBe('success');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('handles nested spans', () =>
@@ -45,7 +45,7 @@ describe('SentryEffectTracer', () => {
);
expect(result).toBe('outer-inner-result');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('propagates span context through Effect fibers', () =>
@@ -62,27 +62,30 @@ describe('SentryEffectTracer', () => {
);
expect(results).toEqual(['parent-start', 'child-1', 'child-2', 'parent-end']);
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('handles span failures correctly', () =>
Effect.gen(function* () {
const result = yield* Effect.withSpan('failing-span')(Effect.fail('expected-error')).pipe(
- Effect.catchAll(e => Effect.succeed(`caught: ${e}`)),
+ Effect.catchCause(cause => {
+ const error = cause.reasons[0]?._tag === 'Fail' ? cause.reasons[0].error : 'unknown';
+ return Effect.succeed(`caught: ${error}`);
+ }),
);
expect(result).toBe('caught: expected-error');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('handles span with defects (die)', () =>
Effect.gen(function* () {
const result = yield* Effect.withSpan('defect-span')(Effect.die('defect-value')).pipe(
- Effect.catchAllDefect(d => Effect.succeed(`caught-defect: ${d}`)),
+ Effect.catchDefect(d => Effect.succeed(`caught-defect: ${d}`)),
);
expect(result).toBe('caught-defect: defect-value');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('works with Effect.all for parallel operations', () =>
@@ -96,7 +99,7 @@ describe('SentryEffectTracer', () => {
);
expect(results).toEqual([1, 2, 3]);
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('supports span annotations', () =>
@@ -107,7 +110,7 @@ describe('SentryEffectTracer', () => {
);
expect(result).toBe('annotated');
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets span status to ok on success', () =>
@@ -130,7 +133,7 @@ describe('SentryEffectTracer', () => {
expect(setStatusCalls).toContainEqual({ code: 1 });
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets span status to error on failure', () =>
@@ -148,12 +151,12 @@ describe('SentryEffectTracer', () => {
} as unknown as sentryCore.Span;
});
- yield* Effect.withSpan('error-span')(Effect.fail('test-error')).pipe(Effect.catchAll(() => Effect.void));
+ yield* Effect.withSpan('error-span')(Effect.fail('test-error')).pipe(Effect.catchCause(() => Effect.void));
expect(setStatusCalls).toContainEqual({ code: 2, message: 'test-error' });
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets span status to error on defect', () =>
@@ -171,12 +174,12 @@ describe('SentryEffectTracer', () => {
} as unknown as sentryCore.Span;
});
- yield* Effect.withSpan('defect-span')(Effect.die('fatal-defect')).pipe(Effect.catchAllDefect(() => Effect.void));
+ yield* Effect.withSpan('defect-span')(Effect.die('fatal-defect')).pipe(Effect.catchDefect(() => Effect.void));
expect(setStatusCalls).toContainEqual({ code: 2, message: 'fatal-defect' });
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('propagates Sentry span context via withActiveSpan', () =>
@@ -197,7 +200,7 @@ describe('SentryEffectTracer', () => {
expect(withActiveSpanCalls.length).toBeGreaterThan(0);
mockWithActiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets origin to auto.function.effect for regular spans', () =>
@@ -222,7 +225,7 @@ describe('SentryEffectTracer', () => {
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.function.effect');
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets origin to auto.http.effect for http.server spans', () =>
@@ -247,7 +250,7 @@ describe('SentryEffectTracer', () => {
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect');
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('sets origin to auto.http.effect for http.client spans', () =>
@@ -272,7 +275,7 @@ describe('SentryEffectTracer', () => {
expect(capturedAttributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect');
mockStartInactiveSpan.mockRestore();
- }).pipe(Effect.provide(TracerLayer)),
+ }).pipe(withSentryTracer),
);
it.effect('can be used with Effect.withTracer', () =>
diff --git a/packages/hono/README.md b/packages/hono/README.md
index c359536c656e..236a4133bf67 100644
--- a/packages/hono/README.md
+++ b/packages/hono/README.md
@@ -29,7 +29,18 @@ npm install @sentry/hono
## Setup (Cloudflare Workers)
-### 1. Enable Node.js compatibility
+### 1. Install Peer Dependency
+
+Additionally to `@sentry/hono`, install the `@sentry/cloudflare` package:
+
+```bashbash
+npm install --save @sentry/cloudflare
+```
+
+Make sure the installed version always stays in sync. The `@sentry/cloudflare` package is a required peer dependency when using `@sentry/hono/cloudflare`.
+You won't import `@sentry/cloudflare` directly in your code, but it needs to be installed in your project.
+
+### 2. Enable Node.js compatibility
Set the `nodejs_compat` compatibility flag in your `wrangler.jsonc`/`wrangler.toml` config. This is because the SDK needs access to the `AsyncLocalStorage` API to work correctly.
@@ -43,7 +54,7 @@ Set the `nodejs_compat` compatibility flag in your `wrangler.jsonc`/`wrangler.to
compatibility_flags = ["nodejs_compat"]
```
-### 2. Initialize Sentry in your Hono app
+### 3. Initialize Sentry in your Hono app
Initialize the Sentry Hono middleware as early as possible in your app:
@@ -85,7 +96,18 @@ export default app;
## Setup (Node)
-### 1. Initialize Sentry in your Hono app
+### 1. Install Peer Dependency
+
+Additionally to `@sentry/hono`, install the `@sentry/node` package:
+
+```bashbash
+npm install --save @sentry/node
+```
+
+Make sure the installed version always stays in sync. The `@sentry/node` package is a required peer dependency when using `@sentry/hono/node`.
+You won't import `@sentry/node` directly in your code, but it needs to be installed in your project.
+
+### 2. Initialize Sentry in your Hono app
Initialize the Sentry Hono middleware as early as possible in your app:
@@ -109,7 +131,7 @@ app.use(
serve(app);
```
-### 2. Add `preload` script to start command
+### 3. Add `preload` script to start command
To ensure that Sentry can capture spans from third-party libraries (e.g. database clients) used in your Hono app, Sentry needs to wrap these libraries as early as possible.
@@ -126,3 +148,40 @@ NODE_OPTIONS="--import @sentry/node/preload"
```
Read more about this preload script in the docs: https://docs.sentry.io/platforms/javascript/guides/hono/install/late-initialization/#late-initialization-with-esm
+
+## Setup (Bun)
+
+### 1. Install Peer Dependency
+
+Additionally to `@sentry/hono`, install the `@sentry/bun` package:
+
+```bashbash
+npm install --save @sentry/bun
+```
+
+Make sure the installed version always stays in sync. The `@sentry/bun` package is a required peer dependency when using `@sentry/hono/bun`.
+You won't import `@sentry/bun` directly in your code, but it needs to be installed in your project.
+
+### 2. Initialize Sentry in your Hono app
+
+Initialize the Sentry Hono middleware as early as possible in your app:
+
+```ts
+import { Hono } from 'hono';
+import { serve } from '@hono/node-server';
+import { sentry } from '@sentry/hono/bun';
+
+const app = new Hono();
+
+// Initialize Sentry middleware right after creating the app
+app.use(
+ sentry(app, {
+ dsn: '__DSN__', // or process.env.SENTRY_DSN or Bun.env.SENTRY_DSN
+ tracesSampleRate: 1.0,
+ }),
+);
+
+// ... your routes and other middleware
+
+serve(app);
+```
diff --git a/packages/hono/package.json b/packages/hono/package.json
index c93820ebc20c..6f26fcaea8ab 100644
--- a/packages/hono/package.json
+++ b/packages/hono/package.json
@@ -46,6 +46,16 @@
"types": "./build/types/index.node.d.ts",
"default": "./build/cjs/index.node.js"
}
+ },
+ "./bun": {
+ "import": {
+ "types": "./build/types/index.bun.d.ts",
+ "default": "./build/esm/index.bun.js"
+ },
+ "require": {
+ "types": "./build/types/index.bun.d.ts",
+ "default": "./build/cjs/index.bun.js"
+ }
}
},
"typesVersions": {
@@ -58,6 +68,9 @@
],
"build/types/index.node.d.ts": [
"build/types-ts3.8/index.node.d.ts"
+ ],
+ "build/types/index.bun.d.ts": [
+ "build/types-ts3.8/index.bun.d.ts"
]
}
},
@@ -66,17 +79,27 @@
},
"dependencies": {
"@opentelemetry/api": "^1.9.1",
- "@sentry/cloudflare": "10.49.0",
- "@sentry/core": "10.49.0",
- "@sentry/node": "10.49.0"
+ "@sentry/core": "10.49.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x",
+ "@sentry/bun": "10.49.0",
+ "@sentry/cloudflare": "10.49.0",
+ "@sentry/node": "10.49.0",
"hono": "^4.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
+ },
+ "@sentry/bun": {
+ "optional": true
+ },
+ "@sentry/cloudflare": {
+ "optional": true
+ },
+ "@sentry/node": {
+ "optional": true
}
},
"devDependencies": {
diff --git a/packages/hono/rollup.npm.config.mjs b/packages/hono/rollup.npm.config.mjs
index a60ba1312cc9..2a03d7540bdc 100644
--- a/packages/hono/rollup.npm.config.mjs
+++ b/packages/hono/rollup.npm.config.mjs
@@ -1,7 +1,7 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
const baseConfig = makeBaseNPMConfig({
- entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts'],
+ entrypoints: ['src/index.ts', 'src/index.cloudflare.ts', 'src/index.node.ts', 'src/index.bun.ts'],
packageSpecificConfig: {
output: {
preserveModulesRoot: 'src',
diff --git a/packages/hono/src/bun/middleware.ts b/packages/hono/src/bun/middleware.ts
new file mode 100644
index 000000000000..fbcbffb15019
--- /dev/null
+++ b/packages/hono/src/bun/middleware.ts
@@ -0,0 +1,28 @@
+import { type BaseTransportOptions, debug, type Options } from '@sentry/core';
+import { init } from './sdk';
+import type { Hono, MiddlewareHandler } from 'hono';
+import { patchAppUse } from '../shared/patchAppUse';
+import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
+
+export interface HonoBunOptions extends Options {}
+
+/**
+ * Sentry middleware for Hono running in a Bun runtime environment.
+ */
+export const sentry = (app: Hono, options: HonoBunOptions): MiddlewareHandler => {
+ const isDebug = options.debug;
+
+ isDebug && debug.log('Initialized Sentry Hono middleware (Bun)');
+
+ init(options);
+
+ patchAppUse(app);
+
+ return async (context, next) => {
+ requestHandler(context);
+
+ await next(); // Handler runs in between Request above ⤴ and Response below ⤵
+
+ responseHandler(context);
+ };
+};
diff --git a/packages/hono/src/bun/sdk.ts b/packages/hono/src/bun/sdk.ts
new file mode 100644
index 000000000000..d30269058f4d
--- /dev/null
+++ b/packages/hono/src/bun/sdk.ts
@@ -0,0 +1,24 @@
+import type { Client } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
+import { init as initBun } from '@sentry/bun';
+import type { HonoBunOptions } from './middleware';
+import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
+
+/**
+ * Initializes Sentry for Hono running in a Bun runtime environment.
+ *
+ * In general, it is recommended to initialize Sentry via the `sentry()` middleware, as it sets up everything by default and calls `init` internally.
+ *
+ * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration.
+ */
+export function init(options: HonoBunOptions): Client | undefined {
+ applySdkMetadata(options, 'hono', ['hono', 'bun']);
+
+ // Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/bun
+ const filteredOptions: HonoBunOptions = {
+ ...options,
+ integrations: buildFilteredIntegrations(options.integrations, false),
+ };
+
+ return initBun(filteredOptions);
+}
diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts
index 1769bbd141a6..66151af2f87f 100644
--- a/packages/hono/src/cloudflare/middleware.ts
+++ b/packages/hono/src/cloudflare/middleware.ts
@@ -1,9 +1,9 @@
import { withSentry } from '@sentry/cloudflare';
-import { applySdkMetadata, type BaseTransportOptions, debug, getIntegrationsToSetup, type Options } from '@sentry/core';
+import { applySdkMetadata, type BaseTransportOptions, debug, type Options } from '@sentry/core';
import type { Env, Hono, MiddlewareHandler } from 'hono';
+import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
import { patchAppUse } from '../shared/patchAppUse';
-import { filterHonoIntegration } from '../shared/filterHonoIntegration';
export interface HonoCloudflareOptions extends Options {}
@@ -22,20 +22,11 @@ export function sentry(
honoOptions.debug && debug.log('Initialized Sentry Hono middleware (Cloudflare)');
- const { integrations: userIntegrations } = honoOptions;
return {
...honoOptions,
// Always filter out the Hono integration from defaults and user integrations.
// The Hono integration is already set up by withSentry, so adding it again would cause capturing too early (in Cloudflare SDK) and non-parametrized URLs.
- integrations: Array.isArray(userIntegrations)
- ? defaults =>
- getIntegrationsToSetup({
- defaultIntegrations: defaults.filter(filterHonoIntegration),
- integrations: userIntegrations.filter(filterHonoIntegration),
- })
- : typeof userIntegrations === 'function'
- ? defaults => userIntegrations(defaults).filter(filterHonoIntegration)
- : defaults => defaults.filter(filterHonoIntegration),
+ integrations: buildFilteredIntegrations(honoOptions.integrations, true),
};
},
// Cast needed because Hono exposes a narrower fetch signature than ExportedHandler
diff --git a/packages/hono/src/index.bun.ts b/packages/hono/src/index.bun.ts
new file mode 100644
index 000000000000..51fbac5fe01f
--- /dev/null
+++ b/packages/hono/src/index.bun.ts
@@ -0,0 +1,5 @@
+export { sentry } from './bun/middleware';
+
+export * from '@sentry/bun';
+
+export { init } from './bun/sdk';
diff --git a/packages/hono/src/node/middleware.ts b/packages/hono/src/node/middleware.ts
index 1dbca92d02e5..2a85575db0d8 100644
--- a/packages/hono/src/node/middleware.ts
+++ b/packages/hono/src/node/middleware.ts
@@ -9,7 +9,7 @@ export interface HonoNodeOptions extends Options {}
/**
* Sentry middleware for Hono running in a Node runtime environment.
*/
-export const sentry = (app: Hono, options: HonoNodeOptions | undefined = {}): MiddlewareHandler => {
+export const sentry = (app: Hono, options: HonoNodeOptions): MiddlewareHandler => {
const isDebug = options.debug;
isDebug && debug.log('Initialized Sentry Hono middleware (Node)');
diff --git a/packages/hono/src/node/sdk.ts b/packages/hono/src/node/sdk.ts
index ff71ffe55909..936cf612bb44 100644
--- a/packages/hono/src/node/sdk.ts
+++ b/packages/hono/src/node/sdk.ts
@@ -1,8 +1,8 @@
-import type { Client, Integration } from '@sentry/core';
-import { applySdkMetadata, getIntegrationsToSetup } from '@sentry/core';
+import type { Client } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import { init as initNode } from '@sentry/node';
import type { HonoNodeOptions } from './middleware';
-import { filterHonoIntegration } from '../shared/filterHonoIntegration';
+import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
/**
* Initializes Sentry for Hono running in a Node runtime environment.
@@ -14,20 +14,10 @@ import { filterHonoIntegration } from '../shared/filterHonoIntegration';
export function init(options: HonoNodeOptions): Client | undefined {
applySdkMetadata(options, 'hono', ['hono', 'node']);
- const { integrations: userIntegrations } = options;
-
// Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/node
const filteredOptions: HonoNodeOptions = {
...options,
- integrations: Array.isArray(userIntegrations)
- ? (defaults: Integration[]) =>
- getIntegrationsToSetup({
- defaultIntegrations: defaults.filter(filterHonoIntegration),
- integrations: userIntegrations, // user's explicit Hono integration is preserved
- })
- : typeof userIntegrations === 'function'
- ? (defaults: Integration[]) => userIntegrations(defaults.filter(filterHonoIntegration))
- : (defaults: Integration[]) => defaults.filter(filterHonoIntegration),
+ integrations: buildFilteredIntegrations(options.integrations, false),
};
return initNode(filteredOptions);
diff --git a/packages/hono/src/shared/buildFilteredIntegrations.ts b/packages/hono/src/shared/buildFilteredIntegrations.ts
new file mode 100644
index 000000000000..ccb0fd28029f
--- /dev/null
+++ b/packages/hono/src/shared/buildFilteredIntegrations.ts
@@ -0,0 +1,29 @@
+import type { Integration } from '@sentry/core';
+import { getIntegrationsToSetup } from '@sentry/core';
+import { filterHonoIntegration } from './filterHonoIntegration';
+
+/**
+ * Builds an `integrations` callback that removes the default Hono integration
+ * to prevent double instrumentation.
+ */
+export function buildFilteredIntegrations(
+ userIntegrations: Integration[] | ((defaults: Integration[]) => Integration[]) | undefined,
+ filterUserIntegrations: boolean,
+): (defaults: Integration[]) => Integration[] {
+ if (Array.isArray(userIntegrations)) {
+ const integrations = filterUserIntegrations ? userIntegrations.filter(filterHonoIntegration) : userIntegrations;
+ return (defaults: Integration[]) =>
+ getIntegrationsToSetup({
+ defaultIntegrations: defaults.filter(filterHonoIntegration),
+ integrations,
+ });
+ }
+
+ if (typeof userIntegrations === 'function') {
+ return filterUserIntegrations
+ ? (defaults: Integration[]) => userIntegrations(defaults).filter(filterHonoIntegration)
+ : (defaults: Integration[]) => userIntegrations(defaults.filter(filterHonoIntegration));
+ }
+
+ return (defaults: Integration[]) => defaults.filter(filterHonoIntegration);
+}
diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts
index 28c3c49e7193..f4bb9205c0f6 100644
--- a/packages/hono/src/shared/patchAppUse.ts
+++ b/packages/hono/src/shared/patchAppUse.ts
@@ -1,5 +1,7 @@
import {
captureException,
+ getActiveSpan,
+ getRootSpan,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
@@ -32,15 +34,20 @@ export function patchAppUse(app: Hono): void {
/**
* Wraps a Hono middleware handler so that its execution is traced as a Sentry span.
- * Uses startInactiveSpan so that all middleware spans are siblings under the request/transaction
- * (onion order: A → B → handler → B → A does not nest B under A in the trace).
+ * Explicitly parents each span under the root (transaction) span so that all middleware
+ * spans are siblings — even when OTel instrumentation introduces nested active contexts
+ * (onion order: A → B → handler → B → A would otherwise nest B under A).
*/
function wrapMiddlewareWithSpan(handler: MiddlewareHandler): MiddlewareHandler {
return async function sentryTracedMiddleware(context, next) {
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
+
const span = startInactiveSpan({
name: handler.name || '',
op: 'middleware.hono',
onlyIfParent: true,
+ parentSpan: rootSpan,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.hono',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN,
diff --git a/packages/hono/test/bun/middleware.test.ts b/packages/hono/test/bun/middleware.test.ts
new file mode 100644
index 000000000000..f3fc82d3696f
--- /dev/null
+++ b/packages/hono/test/bun/middleware.test.ts
@@ -0,0 +1,149 @@
+import * as SentryCore from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
+import { Hono } from 'hono';
+import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
+import { sentry } from '../../src/bun/middleware';
+
+vi.mock('@sentry/bun', () => ({
+ init: vi.fn(),
+}));
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+const { init: initBunMock } = await vi.importMock('@sentry/bun');
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+ return {
+ ...actual,
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ applySdkMetadata: vi.fn(actual.applySdkMetadata),
+ };
+});
+
+const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock;
+
+describe('Hono Bun Middleware', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('sentry middleware', () => {
+ it('calls applySdkMetadata with "hono" and "bun"', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ };
+
+ sentry(app, options);
+
+ expect(applySdkMetadataMock).toHaveBeenCalledTimes(1);
+ expect(applySdkMetadataMock).toHaveBeenCalledWith(options, 'hono', ['hono', 'bun']);
+ });
+
+ it('calls init from @sentry/bun', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ };
+
+ sentry(app, options);
+
+ expect(initBunMock).toHaveBeenCalledTimes(1);
+ expect(initBunMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ }),
+ );
+ });
+
+ it('sets SDK metadata before calling Bun init', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ };
+
+ sentry(app, options);
+
+ const applySdkMetadataCallOrder = applySdkMetadataMock.mock.invocationCallOrder[0];
+ const initBunCallOrder = (initBunMock as Mock).mock.invocationCallOrder[0];
+
+ expect(applySdkMetadataCallOrder).toBeLessThan(initBunCallOrder as number);
+ });
+
+ it('preserves all user options', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ environment: 'production',
+ sampleRate: 0.5,
+ tracesSampleRate: 1.0,
+ debug: true,
+ };
+
+ sentry(app, options);
+
+ expect(initBunMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ environment: 'production',
+ sampleRate: 0.5,
+ tracesSampleRate: 1.0,
+ debug: true,
+ }),
+ );
+ });
+
+ it('returns a middleware handler function', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ };
+
+ const middleware = sentry(app, options);
+
+ expect(middleware).toBeDefined();
+ expect(typeof middleware).toBe('function');
+ expect(middleware).toHaveLength(2); // Hono middleware takes (context, next)
+ });
+
+ it('returns an async middleware handler', () => {
+ const app = new Hono();
+ const middleware = sentry(app, {});
+
+ expect(middleware.constructor.name).toBe('AsyncFunction');
+ });
+
+ it('passes an integrations function to initBun (never a raw array)', () => {
+ const app = new Hono();
+ sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' });
+
+ const callArgs = (initBunMock as Mock).mock.calls[0]?.[0];
+ expect(typeof callArgs.integrations).toBe('function');
+ });
+
+ it('includes hono SDK metadata', () => {
+ const app = new Hono();
+ const options = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ };
+
+ sentry(app, options);
+
+ expect(initBunMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ _metadata: expect.objectContaining({
+ sdk: expect.objectContaining({
+ name: 'sentry.javascript.hono',
+ version: SDK_VERSION,
+ packages: [
+ { name: 'npm:@sentry/hono', version: SDK_VERSION },
+ { name: 'npm:@sentry/bun', version: SDK_VERSION },
+ ],
+ }),
+ }),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/hono/test/cloudflare/middleware.test.ts b/packages/hono/test/cloudflare/middleware.test.ts
index ac512d41afee..46e13956ec4e 100644
--- a/packages/hono/test/cloudflare/middleware.test.ts
+++ b/packages/hono/test/cloudflare/middleware.test.ts
@@ -164,102 +164,4 @@ describe('Hono Cloudflare Middleware', () => {
});
});
});
-
- describe('filters Hono integration from user-provided integrations', () => {
- const honoIntegration = { name: 'Hono' } as SentryCore.Integration;
- const otherIntegration = { name: 'Other' } as SentryCore.Integration;
-
- const getIntegrationsResult = () => {
- const optionsCallback = withSentryMock.mock.calls[0]?.[0];
- return optionsCallback().integrations;
- };
-
- it.each([
- ['filters Hono integration out', [honoIntegration, otherIntegration], [otherIntegration]],
- ['keeps non-Hono integrations', [otherIntegration], [otherIntegration]],
- ['returns empty array when only Hono integration provided', [honoIntegration], []],
- ])('%s (array)', (_name, input, expected) => {
- const app = new Hono();
- sentry(app, { integrations: input });
-
- const integrationsFn = getIntegrationsResult() as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- expect(integrationsFn([])).toEqual(expected);
- });
-
- it('filters Hono from defaults when user provides an array', () => {
- const app = new Hono();
- sentry(app, { integrations: [otherIntegration] });
-
- const integrationsFn = getIntegrationsResult() as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- // Defaults (from Cloudflare) include Hono; result must exclude it and deduplicate (user + defaults overlap)
- const defaultsWithHono = [honoIntegration, otherIntegration];
- expect(integrationsFn(defaultsWithHono)).toEqual([otherIntegration]);
- });
-
- it('deduplicates when user integrations overlap with defaults (by name)', () => {
- const app = new Hono();
- const duplicateIntegration = { name: 'Other' } as SentryCore.Integration;
- sentry(app, { integrations: [duplicateIntegration] });
-
- const integrationsFn = getIntegrationsResult() as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- const defaultsWithOverlap = [
- honoIntegration,
- otherIntegration, // same name as duplicateIntegration
- ];
- const result = integrationsFn(defaultsWithOverlap);
- expect(result).toHaveLength(1);
- expect(result[0]?.name).toBe('Other');
- });
-
- it('filters Hono integration out of a function result', () => {
- const app = new Hono();
- sentry(app, { integrations: () => [honoIntegration, otherIntegration] });
-
- const integrationsFn = getIntegrationsResult() as unknown as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- expect(integrationsFn([])).toEqual([otherIntegration]);
- });
-
- it('passes defaults through to the user-provided integrations function', () => {
- const app = new Hono();
- const userFn = vi.fn((_defaults: SentryCore.Integration[]) => [otherIntegration]);
- const defaults = [{ name: 'Default' } as SentryCore.Integration];
-
- sentry(app, { integrations: userFn });
-
- const integrationsFn = getIntegrationsResult() as unknown as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- integrationsFn(defaults);
-
- expect(userFn).toHaveBeenCalledWith(defaults);
- });
-
- it('filters Hono integration returned by the user-provided integrations function', () => {
- const app = new Hono();
- sentry(app, { integrations: (_defaults: SentryCore.Integration[]) => [honoIntegration] });
-
- const integrationsFn = getIntegrationsResult() as unknown as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- expect(integrationsFn([])).toEqual([]);
- });
-
- it('filters Hono integration from defaults when integrations is undefined', () => {
- const app = new Hono();
- sentry(app, {});
-
- const integrationsFn = getIntegrationsResult() as unknown as (
- defaults: SentryCore.Integration[],
- ) => SentryCore.Integration[];
- expect(integrationsFn([honoIntegration, otherIntegration])).toEqual([otherIntegration]);
- });
- });
});
diff --git a/packages/hono/test/node/middleware.test.ts b/packages/hono/test/node/middleware.test.ts
index 1473daf98acc..b6561098ed8a 100644
--- a/packages/hono/test/node/middleware.test.ts
+++ b/packages/hono/test/node/middleware.test.ts
@@ -3,7 +3,6 @@ import { SDK_VERSION } from '@sentry/core';
import { Hono } from 'hono';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { sentry } from '../../src/node/middleware';
-import type { Integration } from '@sentry/core';
vi.mock('@sentry/node', () => ({
init: vi.fn(),
@@ -147,113 +146,4 @@ describe('Hono Node Middleware', () => {
);
});
});
-
- describe('Hono integration filtering', () => {
- const honoIntegration = { name: 'Hono' } as Integration;
- const otherIntegration = { name: 'Other' } as Integration;
-
- const getIntegrationsFn = (): ((defaults: Integration[]) => Integration[]) => {
- const callArgs = (initNodeMock as Mock).mock.calls[0]?.[0];
- return callArgs.integrations as (defaults: Integration[]) => Integration[];
- };
-
- describe('when integrations is an array', () => {
- it('keeps a user-explicitly-provided Hono integration', () => {
- const app = new Hono();
- sentry(app, { integrations: [honoIntegration, otherIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- const result = integrationsFn([]);
- expect(result.map(i => i.name)).toContain('Hono');
- expect(result.map(i => i.name)).toContain('Other');
- });
-
- it('keeps non-Hono user integrations', () => {
- const app = new Hono();
- sentry(app, { integrations: [otherIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- expect(integrationsFn([])).toEqual([otherIntegration]);
- });
-
- it('preserves user-provided Hono even when defaults would also provide it', () => {
- const app = new Hono();
- sentry(app, { integrations: [honoIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- // Defaults include Hono, but it should be filtered from defaults; user's copy is kept
- const result = integrationsFn([honoIntegration, otherIntegration]);
- expect(result.filter(i => i.name === 'Hono')).toHaveLength(1);
- });
-
- it('removes Hono from defaults when user does not explicitly provide it', () => {
- const app = new Hono();
- sentry(app, { integrations: [otherIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- const defaultsWithHono = [honoIntegration, otherIntegration];
- const result = integrationsFn(defaultsWithHono);
- expect(result.map(i => i.name)).not.toContain('Hono');
- });
-
- it('deduplicates non-Hono integrations when user integrations overlap with defaults', () => {
- const app = new Hono();
- const duplicateIntegration = { name: 'Other' } as Integration;
- sentry(app, { integrations: [duplicateIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- const defaultsWithOverlap = [honoIntegration, otherIntegration];
- const result = integrationsFn(defaultsWithOverlap);
- expect(result).toHaveLength(1);
- expect(result[0]?.name).toBe('Other');
- });
- });
-
- describe('when integrations is a function', () => {
- it('passes defaults without Hono to the user function', () => {
- const app = new Hono();
- const userFn = vi.fn((_defaults: Integration[]) => [otherIntegration]);
- const defaultIntegration = { name: 'Default' } as Integration;
-
- sentry(app, { integrations: userFn });
-
- const integrationsFn = getIntegrationsFn();
- integrationsFn([honoIntegration, defaultIntegration]);
-
- const receivedDefaults = userFn.mock.calls[0]?.[0] as Integration[];
- expect(receivedDefaults.map(i => i.name)).not.toContain('Hono');
- expect(receivedDefaults.map(i => i.name)).toContain('Default');
- });
-
- it('preserves a Hono integration explicitly returned by the user function', () => {
- const app = new Hono();
- sentry(app, { integrations: () => [honoIntegration, otherIntegration] });
-
- const integrationsFn = getIntegrationsFn();
- const result = integrationsFn([]);
- expect(result.map(i => i.name)).toContain('Hono');
- expect(result.map(i => i.name)).toContain('Other');
- });
-
- it('does not include Hono when user function just returns defaults', () => {
- const app = new Hono();
- sentry(app, { integrations: (defaults: Integration[]) => defaults });
-
- const integrationsFn = getIntegrationsFn();
- const result = integrationsFn([honoIntegration, otherIntegration]);
- expect(result.map(i => i.name)).not.toContain('Hono');
- expect(result.map(i => i.name)).toContain('Other');
- });
- });
-
- describe('when integrations is undefined', () => {
- it('removes Hono from defaults', () => {
- const app = new Hono();
- sentry(app, {});
-
- const integrationsFn = getIntegrationsFn();
- expect(integrationsFn([honoIntegration, otherIntegration])).toEqual([otherIntegration]);
- });
- });
- });
});
diff --git a/packages/hono/test/shared/buildFilteredIntegrations.test.ts b/packages/hono/test/shared/buildFilteredIntegrations.test.ts
new file mode 100644
index 000000000000..e2aec16d1119
--- /dev/null
+++ b/packages/hono/test/shared/buildFilteredIntegrations.test.ts
@@ -0,0 +1,149 @@
+import type { Integration } from '@sentry/core';
+import { describe, expect, it, vi } from 'vitest';
+import { buildFilteredIntegrations } from '../../src/shared/buildFilteredIntegrations';
+
+const hono = { name: 'Hono' } as Integration;
+const other = { name: 'Other' } as Integration;
+const dflt = { name: 'Default' } as Integration;
+
+function names(integrations: Integration[]): string[] {
+ return integrations.map(i => i.name);
+}
+
+describe('buildFilteredIntegrations', () => {
+ it.each([
+ { label: 'array', input: [] as Integration[], filterUser: false },
+ { label: 'function', input: () => [] as Integration[], filterUser: false },
+ { label: 'undefined', input: undefined, filterUser: false },
+ { label: 'array', input: [] as Integration[], filterUser: true },
+ { label: 'function', input: () => [] as Integration[], filterUser: true },
+ { label: 'undefined', input: undefined, filterUser: true },
+ ])('returns a function when userIntegrations=$label, filterUserIntegrations=$filterUser', ({ input, filterUser }) => {
+ expect(typeof buildFilteredIntegrations(input, filterUser)).toBe('function');
+ });
+
+ it.each([false, true])(
+ 'removes Hono from defaults when userIntegrations is undefined (filterUserIntegrations=%j)',
+ filterUser => {
+ const fn = buildFilteredIntegrations(undefined, filterUser);
+ expect(fn([hono, other])).toEqual([other]);
+ },
+ );
+
+ it.each([false, true])(
+ 'deduplicates when user integrations overlap with defaults (filterUserIntegrations=%j)',
+ filterUser => {
+ const duplicate = { name: 'Other' } as Integration;
+ const fn = buildFilteredIntegrations([duplicate], filterUser);
+ const result = fn([hono, other]);
+ expect(result).toHaveLength(1);
+ expect(result[0]?.name).toBe('Other');
+ },
+ );
+
+ describe('filterUserIntegrations: false (Node / Bun)', () => {
+ describe('when userIntegrations is an array', () => {
+ it.each([
+ {
+ scenario: 'removes Hono from defaults',
+ user: [other],
+ defaults: [hono, dflt],
+ includes: ['Other', 'Default'],
+ excludes: ['Hono'],
+ },
+ {
+ scenario: 'preserves user-provided Hono',
+ user: [hono, other],
+ defaults: [],
+ includes: ['Hono', 'Other'],
+ excludes: [],
+ },
+ ])('$scenario', ({ user, defaults, includes, excludes }) => {
+ const fn = buildFilteredIntegrations(user, false);
+ const result = names(fn(defaults));
+ for (const name of includes) {
+ expect(result).toContain(name);
+ }
+ for (const name of excludes) {
+ expect(result).not.toContain(name);
+ }
+ });
+
+ it('preserves user-provided Hono even when defaults also include it', () => {
+ const fn = buildFilteredIntegrations([hono], false);
+ const result = fn([hono, other]);
+ expect(result.filter(i => i.name === 'Hono')).toHaveLength(1);
+ });
+ });
+
+ describe('when userIntegrations is a function', () => {
+ it('filters Hono from defaults before passing to the user function', () => {
+ const userFn = vi.fn((_defaults: Integration[]) => [other]);
+ const fn = buildFilteredIntegrations(userFn, false);
+ fn([hono, dflt]);
+
+ expect(userFn).toHaveBeenCalledWith([dflt]);
+ });
+
+ it('preserves Hono when explicitly returned by the user function', () => {
+ const fn = buildFilteredIntegrations(() => [hono, other], false);
+ expect(names(fn([]))).toEqual(['Hono', 'Other']);
+ });
+
+ it('excludes Hono when user function passes defaults through', () => {
+ const fn = buildFilteredIntegrations(defaults => defaults, false);
+ expect(names(fn([hono, other]))).toEqual(['Other']);
+ });
+ });
+ });
+
+ describe('filterUserIntegrations: true (Cloudflare)', () => {
+ describe('when userIntegrations is an array', () => {
+ it.each([
+ {
+ scenario: 'removes Hono from both user array and defaults',
+ user: [hono, other],
+ defaults: [hono, dflt],
+ includes: ['Other', 'Default'],
+ excludes: ['Hono'],
+ },
+ {
+ scenario: 'returns empty when only Hono is provided',
+ user: [hono],
+ defaults: [],
+ includes: [],
+ excludes: ['Hono'],
+ },
+ { scenario: 'keeps non-Hono integrations', user: [other], defaults: [], includes: ['Other'], excludes: [] },
+ ])('$scenario', ({ user, defaults, includes, excludes }) => {
+ const fn = buildFilteredIntegrations(user, true);
+ const result = names(fn(defaults));
+ for (const name of includes) {
+ expect(result).toContain(name);
+ }
+ for (const name of excludes) {
+ expect(result).not.toContain(name);
+ }
+ });
+ });
+
+ describe('when userIntegrations is a function', () => {
+ it('passes defaults through to the user function unfiltered', () => {
+ const userFn = vi.fn((_defaults: Integration[]) => [other]);
+ const defaults = [dflt];
+ const fn = buildFilteredIntegrations(userFn, true);
+ fn(defaults);
+
+ expect(userFn).toHaveBeenCalledWith(defaults);
+ });
+
+ it.each([
+ { scenario: 'filters Hono from result', userFn: () => [hono, other], expected: [other] },
+ { scenario: 'returns empty when user function only returns Hono', userFn: () => [hono], expected: [] },
+ ])('$scenario', ({ userFn, expected }) => {
+ const fn = buildFilteredIntegrations(userFn, true);
+ expect(fn([])).toEqual(expected);
+ });
+ });
+ });
+});
diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts
index 8f4e3bc0cc6c..0482d3569c84 100644
--- a/packages/hono/test/shared/patchAppUse.test.ts
+++ b/packages/hono/test/shared/patchAppUse.test.ts
@@ -155,4 +155,23 @@ describe('patchAppUse (middleware spans)', () => {
expect(fakeApp._capturedThis).toBe(fakeApp);
});
+
+ // todo: support sub-app (Hono route groups) patching in the future
+ it('does not wrap middleware on sub-apps (instance-level patching limitation)', async () => {
+ const app = new Hono();
+ patchAppUse(app);
+
+ // Route Grouping: https://hono.dev/docs/api/routing#grouping
+ const subApp = new Hono();
+ subApp.use(async function subMiddleware(_c: unknown, next: () => Promise) {
+ await next();
+ });
+ subApp.get('/', () => new Response('sub'));
+
+ app.route('/sub', subApp);
+
+ await app.fetch(new Request('http://localhost/sub'));
+
+ expect(startInactiveSpanMock).not.toHaveBeenCalledWith(expect.objectContaining({ name: 'subMiddleware' }));
+ });
});
diff --git a/packages/integration-shims/src/SpanStreaming.ts b/packages/integration-shims/src/SpanStreaming.ts
new file mode 100644
index 000000000000..7b445f086145
--- /dev/null
+++ b/packages/integration-shims/src/SpanStreaming.ts
@@ -0,0 +1,17 @@
+import { consoleSandbox, defineIntegration } from '@sentry/core';
+
+/**
+ * This is a shim for the SpanStreaming integration.
+ * It is needed in order for the CDN bundles to continue working when users add/remove span streaming
+ * from it, without changing their config. This is necessary for the loader mechanism.
+ */
+export const spanStreamingIntegrationShim = defineIntegration(() => {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('You are using spanStreamingIntegration() even though this bundle does not include tracing.');
+ });
+
+ return {
+ name: 'SpanStreaming',
+ };
+});
diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts
index 4cabb8a5e36f..fa820ad55145 100644
--- a/packages/integration-shims/src/index.ts
+++ b/packages/integration-shims/src/index.ts
@@ -4,3 +4,4 @@ export { browserTracingIntegrationShim } from './BrowserTracing';
export { launchDarklyIntegrationShim, buildLaunchDarklyFlagUsedHandlerShim } from './launchDarkly';
export { elementTimingIntegrationShim } from './ElementTiming';
export { loggerShim, consoleLoggingIntegrationShim } from './logs';
+export { spanStreamingIntegrationShim } from './SpanStreaming';
diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts
index 2ae03ae3f204..341e583e90d1 100644
--- a/packages/nextjs/src/index.types.ts
+++ b/packages/nextjs/src/index.types.ts
@@ -23,6 +23,7 @@ export declare function init(
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
+export declare const consoleIntegration: typeof serverSdk.consoleIntegration;
export declare const spanStreamingIntegration: typeof clientSdk.spanStreamingIntegration;
export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan;
diff --git a/packages/node-core/src/common-exports.ts b/packages/node-core/src/common-exports.ts
index 1c724d2c29f6..b2f1deee7f4a 100644
--- a/packages/node-core/src/common-exports.ts
+++ b/packages/node-core/src/common-exports.ts
@@ -27,6 +27,7 @@ export { systemErrorIntegration } from './integrations/systemError';
export { childProcessIntegration } from './integrations/childProcess';
export { createSentryWinstonTransport } from './integrations/winston';
export { pinoIntegration } from './integrations/pino';
+export { consoleIntegration } from './integrations/console';
// SDK utilities
export { getSentryRelease, defaultStackParser } from './sdk/api';
@@ -117,7 +118,6 @@ export {
profiler,
consoleLoggingIntegration,
createConsolaReporter,
- consoleIntegration,
wrapMcpServerWithSentry,
featureFlagsIntegration,
spanStreamingIntegration,
diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts
new file mode 100644
index 000000000000..d958e00bdf12
--- /dev/null
+++ b/packages/node-core/src/integrations/console.ts
@@ -0,0 +1,127 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { ConsoleLevel, HandlerDataConsole, WrappedFunction } from '@sentry/core';
+import {
+ CONSOLE_LEVELS,
+ GLOBAL_OBJ,
+ consoleIntegration as coreConsoleIntegration,
+ defineIntegration,
+ fill,
+ markFunctionWrapped,
+ maybeInstrument,
+ originalConsoleMethods,
+ triggerHandlers,
+} from '@sentry/core';
+
+interface ConsoleIntegrationOptions {
+ levels: ConsoleLevel[];
+}
+
+/**
+ * Node-specific console integration that captures breadcrumbs and handles
+ * the AWS Lambda runtime replacing console methods after our patch.
+ *
+ * In Lambda, console methods are patched via `Object.defineProperty` so that
+ * external replacements (by the Lambda runtime) are absorbed as the delegate
+ * while our wrapper stays in place. Outside Lambda, this delegates entirely
+ * to the core `consoleIntegration` which uses the simpler `fill`-based patch.
+ */
+export const consoleIntegration = defineIntegration((options: Partial = {}) => {
+ return {
+ name: 'Console',
+ setup(client) {
+ if (process.env.LAMBDA_TASK_ROOT) {
+ maybeInstrument('console', instrumentConsoleLambda);
+ }
+
+ // Delegate breadcrumb handling to the core console integration.
+ const core = coreConsoleIntegration(options);
+ core.setup?.(client);
+ },
+ };
+});
+
+function instrumentConsoleLambda(): void {
+ const consoleObj = GLOBAL_OBJ?.console;
+ if (!consoleObj) {
+ return;
+ }
+
+ CONSOLE_LEVELS.forEach((level: ConsoleLevel) => {
+ if (level in consoleObj) {
+ patchWithDefineProperty(consoleObj, level);
+ }
+ });
+}
+
+function patchWithDefineProperty(consoleObj: Console, level: ConsoleLevel): void {
+ const nativeMethod = consoleObj[level] as (...args: unknown[]) => void;
+ originalConsoleMethods[level] = nativeMethod;
+
+ let delegate: Function = nativeMethod;
+ let savedDelegate: Function | undefined;
+ let isExecuting = false;
+
+ const wrapper = function (...args: any[]): void {
+ if (isExecuting) {
+ // Re-entrant call: a third party captured `wrapper` via the getter and calls it from inside their replacement. We must
+ // use `nativeMethod` (not `delegate`) to break the cycle, and we intentionally skip `triggerHandlers` to avoid duplicate
+ // breadcrumbs. The outer invocation already triggered the handlers for this console call.
+ nativeMethod.apply(consoleObj, args);
+ return;
+ }
+ isExecuting = true;
+ try {
+ triggerHandlers('console', { args, level } as HandlerDataConsole);
+ delegate.apply(consoleObj, args);
+ } finally {
+ isExecuting = false;
+ }
+ };
+ markFunctionWrapped(wrapper as unknown as WrappedFunction, nativeMethod as unknown as WrappedFunction);
+
+ // consoleSandbox reads originalConsoleMethods[level] to temporarily bypass instrumentation. We replace it with a distinct reference (.bind creates a
+ // new function identity) so the setter can tell apart "consoleSandbox bypass" from "external code restoring a native method captured before Sentry init."
+ const sandboxBypass = nativeMethod.bind(consoleObj);
+ originalConsoleMethods[level] = sandboxBypass;
+
+ try {
+ let current: any = wrapper;
+
+ Object.defineProperty(consoleObj, level, {
+ configurable: true,
+ enumerable: true,
+ get() {
+ return current;
+ },
+ set(newValue) {
+ if (newValue === wrapper) {
+ // consoleSandbox restoring the wrapper: recover the saved delegate.
+ if (savedDelegate !== undefined) {
+ delegate = savedDelegate;
+ savedDelegate = undefined;
+ }
+ current = wrapper;
+ } else if (newValue === sandboxBypass) {
+ // consoleSandbox entering bypass: save delegate, let getter return sandboxBypass directly so calls skip the wrapper entirely.
+ savedDelegate = delegate;
+ current = sandboxBypass;
+ } else if (typeof newValue === 'function' && !(newValue as WrappedFunction).__sentry_original__) {
+ delegate = newValue;
+ current = wrapper;
+ } else {
+ current = newValue;
+ }
+ },
+ });
+ } catch {
+ // Fall back to fill-based patching if defineProperty fails
+ fill(consoleObj, level, function (originalConsoleMethod: () => any): Function {
+ originalConsoleMethods[level] = originalConsoleMethod;
+
+ return function (this: Console, ...args: any[]): void {
+ triggerHandlers('console', { args, level } as HandlerDataConsole);
+ originalConsoleMethods[level]?.apply(this, args);
+ };
+ });
+ }
+}
diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
index b25d32138aa9..60cf7bbae9aa 100644
--- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
+++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts
@@ -456,10 +456,16 @@ function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, Span
];
}
-function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes {
+/**
+ * Exported for testing purposes.
+ */
+export function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes {
const { statusCode, statusMessage, httpVersion, socket } = response;
- const transport = httpVersion.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp';
+ // httpVersion can be undefined in some cases and we seem to have encountered this before:
+ // https://github.com/getsentry/sentry-javascript/blob/ec8c8c64cde6001123db0199a8ca017b8863eac8/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts#L158
+ // see: #20415
+ const transport = httpVersion?.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp';
const additionalAttributes: SpanAttributes = {
[ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode,
diff --git a/packages/node-core/src/integrations/node-fetch/types.ts b/packages/node-core/src/integrations/node-fetch/types.ts
index 0139dadde413..26ed710f2474 100644
--- a/packages/node-core/src/integrations/node-fetch/types.ts
+++ b/packages/node-core/src/integrations/node-fetch/types.ts
@@ -15,7 +15,8 @@
*/
/**
- * Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/types.ts
+ * Aligned with upstream Undici request shape; see `packages/node/.../node-fetch/vendored/types.ts`
+ * (vendored from `@opentelemetry/instrumentation-undici`).
*/
export interface UndiciRequest {
@@ -24,9 +25,9 @@ export interface UndiciRequest {
path: string;
/**
* Serialized string of headers in the form `name: value\r\n` for v5
- * Array of strings v6
+ * Array of strings `[key1, value1, ...]` for v6 (values may be `string | string[]`)
*/
- headers: string | string[];
+ headers: string | (string | string[])[];
/**
* Helper method to add headers (from v6)
*/
diff --git a/packages/node-core/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts
index af40bacfda57..8e2483d6a8cb 100644
--- a/packages/node-core/src/integrations/onunhandledrejection.ts
+++ b/packages/node-core/src/integrations/onunhandledrejection.ts
@@ -85,7 +85,7 @@ export function makeUnhandledPromiseHandler(
client: Client,
options: OnUnhandledRejectionOptions,
): (reason: unknown, promise: unknown) => void {
- return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
+ return function sendUnhandledPromise(reason: unknown, _promise: unknown): void {
// Only handle for the active client
if (getClient() !== client) {
return;
@@ -109,7 +109,7 @@ export function makeUnhandledPromiseHandler(
activeSpanWrapper(() => {
captureException(reason, {
- originalException: promise,
+ originalException: reason,
captureContext: {
extra: { unhandledPromiseRejection: true },
level,
diff --git a/packages/node-core/src/light/sdk.ts b/packages/node-core/src/light/sdk.ts
index 77b62e9ab2f9..1d57da67a0ab 100644
--- a/packages/node-core/src/light/sdk.ts
+++ b/packages/node-core/src/light/sdk.ts
@@ -1,7 +1,6 @@
import type { Integration, Options } from '@sentry/core';
import {
applySdkMetadata,
- consoleIntegration,
consoleSandbox,
debug,
envToBool,
@@ -25,6 +24,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
import { processSessionIntegration } from '../integrations/processSession';
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
+import { consoleIntegration } from '../integrations/console';
import { systemErrorIntegration } from '../integrations/systemError';
import { defaultStackParser, getSentryRelease } from '../sdk/api';
import { makeNodeTransport } from '../transports';
diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts
index 5ae840e6e976..52271ee62363 100644
--- a/packages/node-core/src/sdk/index.ts
+++ b/packages/node-core/src/sdk/index.ts
@@ -1,7 +1,6 @@
import type { Integration, Options } from '@sentry/core';
import {
applySdkMetadata,
- consoleIntegration,
consoleSandbox,
conversationIdIntegration,
debug,
@@ -35,6 +34,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
import { processSessionIntegration } from '../integrations/processSession';
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
+import { consoleIntegration } from '../integrations/console';
import { systemErrorIntegration } from '../integrations/systemError';
import { makeNodeTransport } from '../transports';
import type { NodeClientOptions, NodeOptions } from '../types';
diff --git a/packages/node-core/src/utils/outgoingFetchRequest.ts b/packages/node-core/src/utils/outgoingFetchRequest.ts
index cad20496e478..85edd6a73b58 100644
--- a/packages/node-core/src/utils/outgoingFetchRequest.ts
+++ b/packages/node-core/src/utils/outgoingFetchRequest.ts
@@ -42,7 +42,13 @@ export function addTracePropagationHeadersToFetchRequest(
const { 'sentry-trace': sentryTrace, baggage, traceparent } = addedHeaders;
- const requestHeaders = Array.isArray(request.headers) ? request.headers : stringToArrayHeaders(request.headers);
+ // Undici can expose headers either as a raw string (v5-style) or as a flat array of pairs (v6-style).
+ // In the array form, even indices are header names and odd indices are values; in undici v6 a value
+ // may be `string | string[]` when a header has multiple values. The helpers below (_deduplicateArrayHeader,
+ // push, etc.) expect each value slot to be a single string, so we normalize array headers first.
+ const requestHeaders: string[] = Array.isArray(request.headers)
+ ? normalizeUndiciHeaderPairs(request.headers)
+ : stringToArrayHeaders(request.headers);
// OTel's UndiciInstrumentation calls propagation.inject() which unconditionally
// appends headers to the request. When the user also sets headers via getTraceData(),
@@ -84,12 +90,37 @@ export function addTracePropagationHeadersToFetchRequest(
}
}
- if (!Array.isArray(request.headers)) {
- // For original string request headers, we need to write them back to the request
+ if (Array.isArray(request.headers)) {
+ // Replace contents in place so we keep the same array reference undici/fetch still holds.
+ // `requestHeaders` is already normalized (string pairs only); splice writes them back.
+ request.headers.splice(0, request.headers.length, ...requestHeaders);
+ } else {
request.headers = arrayToStringHeaders(requestHeaders);
}
}
+/**
+ * Convert undici’s header array into `[name, value, name, value, ...]` where every value is a string.
+ *
+ * Undici v6 uses this shape: `[k1, v1, k2, v2, ...]`. Types allow each `v` to be `string | string[]` when
+ * that header has multiple values. Sentry’s dedupe/merge helpers expect one string per value slot, so
+ * multi-value arrays are joined with `', '`. Missing value slots become `''`.
+ */
+function normalizeUndiciHeaderPairs(headers: (string | string[])[]): string[] {
+ const out: string[] = [];
+ for (let i = 0; i < headers.length; i++) {
+ const entry = headers[i];
+ if (i % 2 === 0) {
+ // Header name (should always be a string; coerce defensively).
+ out.push(typeof entry === 'string' ? entry : String(entry));
+ } else {
+ // Header value: flatten `string[]` to a single string for downstream string-only helpers.
+ out.push(Array.isArray(entry) ? entry.join(', ') : (entry ?? ''));
+ }
+ }
+ return out;
+}
+
function stringToArrayHeaders(requestHeaders: string): string[] {
const headersArray = requestHeaders.split('\r\n');
const headers: string[] = [];
diff --git a/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts
new file mode 100644
index 000000000000..182abaa3663f
--- /dev/null
+++ b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts
@@ -0,0 +1,48 @@
+import type * as http from 'node:http';
+import { describe, expect, it } from 'vitest';
+import { _getOutgoingRequestEndedSpanData } from '../../src/integrations/http/SentryHttpInstrumentation';
+
+function createResponse(overrides: Partial): http.IncomingMessage {
+ return {
+ statusCode: 200,
+ statusMessage: 'OK',
+ httpVersion: '1.1',
+ headers: {},
+ socket: undefined,
+ ...overrides,
+ } as unknown as http.IncomingMessage;
+}
+
+describe('_getOutgoingRequestEndedSpanData', () => {
+ it('sets ip_tcp transport for HTTP/1.1', () => {
+ const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: '1.1' }));
+
+ expect(attributes['network.transport']).toBe('ip_tcp');
+ expect(attributes['net.transport']).toBe('ip_tcp');
+ expect(attributes['network.protocol.version']).toBe('1.1');
+ expect(attributes['http.flavor']).toBe('1.1');
+ });
+
+ it('sets ip_udp transport for QUIC', () => {
+ const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: 'QUIC' }));
+
+ expect(attributes['network.transport']).toBe('ip_udp');
+ expect(attributes['net.transport']).toBe('ip_udp');
+ });
+
+ it('does not throw when httpVersion is null', () => {
+ expect(() =>
+ _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string })),
+ ).not.toThrow();
+
+ const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string }));
+ expect(attributes['network.transport']).toBe('ip_tcp');
+ expect(attributes['net.transport']).toBe('ip_tcp');
+ });
+
+ it('does not throw when httpVersion is undefined', () => {
+ expect(() =>
+ _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: undefined as unknown as string })),
+ ).not.toThrow();
+ });
+});
diff --git a/packages/node-core/test/integrations/console.test.ts b/packages/node-core/test/integrations/console.test.ts
new file mode 100644
index 000000000000..0355fe2d076b
--- /dev/null
+++ b/packages/node-core/test/integrations/console.test.ts
@@ -0,0 +1,259 @@
+// Set LAMBDA_TASK_ROOT before any imports so consoleIntegration uses patchWithDefineProperty
+process.env.LAMBDA_TASK_ROOT = '/var/task';
+
+import { afterAll, describe, expect, it, vi } from 'vitest';
+import type { WrappedFunction } from '@sentry/core';
+import {
+ addConsoleInstrumentationHandler,
+ consoleSandbox,
+ markFunctionWrapped,
+ originalConsoleMethods,
+ GLOBAL_OBJ,
+} from '@sentry/core';
+import { consoleIntegration } from '../../src/integrations/console';
+
+// Capture the real native method before any patches are installed.
+// This simulates external code doing `const log = console.log` before Sentry init.
+// oxlint-disable-next-line no-console
+const nativeConsoleLog = console.log;
+
+afterAll(() => {
+ delete process.env.LAMBDA_TASK_ROOT;
+});
+
+describe('consoleIntegration in Lambda (patchWithDefineProperty)', () => {
+ it('calls registered handler when console.log is called', () => {
+ const handler = vi.fn();
+ // Setup the integration so it calls maybeInstrument with the Lambda strategy
+ consoleIntegration().setup?.({ on: vi.fn() } as any);
+
+ addConsoleInstrumentationHandler(handler);
+
+ GLOBAL_OBJ.console.log('test');
+
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['test'], level: 'log' }));
+ });
+
+ describe('external replacement (e.g. Lambda runtime overwriting console)', () => {
+ it('keeps firing the handler after console.log is replaced externally', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ GLOBAL_OBJ.console.log = vi.fn();
+ handler.mockClear();
+
+ GLOBAL_OBJ.console.log('after replacement');
+
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['after replacement'], level: 'log' }));
+ });
+
+ it('calls the external replacement as the underlying method', () => {
+ addConsoleInstrumentationHandler(vi.fn());
+
+ const lambdaLogger = vi.fn();
+ GLOBAL_OBJ.console.log = lambdaLogger;
+
+ GLOBAL_OBJ.console.log('hello');
+
+ expect(lambdaLogger).toHaveBeenCalledWith('hello');
+ });
+
+ it('always delegates to the latest replacement', () => {
+ addConsoleInstrumentationHandler(vi.fn());
+
+ const first = vi.fn();
+ const second = vi.fn();
+
+ GLOBAL_OBJ.console.log = first;
+ GLOBAL_OBJ.console.log = second;
+
+ GLOBAL_OBJ.console.log('latest');
+
+ expect(first).not.toHaveBeenCalled();
+ expect(second).toHaveBeenCalledWith('latest');
+ });
+
+ it('does not mutate originalConsoleMethods (kept safe for consoleSandbox)', () => {
+ addConsoleInstrumentationHandler(vi.fn());
+
+ const nativeLog = originalConsoleMethods.log;
+ GLOBAL_OBJ.console.log = vi.fn();
+
+ expect(originalConsoleMethods.log).toBe(nativeLog);
+ });
+ });
+
+ describe('__sentry_original__ detection', () => {
+ it('accepts a function with __sentry_original__ without re-wrapping', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ const otherWrapper = vi.fn();
+ markFunctionWrapped(otherWrapper as unknown as WrappedFunction, vi.fn() as unknown as WrappedFunction);
+
+ GLOBAL_OBJ.console.log = otherWrapper;
+
+ expect(GLOBAL_OBJ.console.log).toBe(otherWrapper);
+ });
+
+ it('does not fire our handler when a __sentry_original__ wrapper is installed', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ const otherWrapper = vi.fn();
+ markFunctionWrapped(otherWrapper as unknown as WrappedFunction, vi.fn() as unknown as WrappedFunction);
+
+ GLOBAL_OBJ.console.log = otherWrapper;
+ handler.mockClear();
+
+ GLOBAL_OBJ.console.log('via other wrapper');
+
+ expect(handler).not.toHaveBeenCalled();
+ expect(otherWrapper).toHaveBeenCalledWith('via other wrapper');
+ });
+
+ it('re-wraps a plain function without __sentry_original__', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ GLOBAL_OBJ.console.log = vi.fn();
+ handler.mockClear();
+
+ GLOBAL_OBJ.console.log('plain');
+
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['plain'], level: 'log' }));
+ });
+ });
+
+ describe('consoleSandbox interaction', () => {
+ it('does not fire the handler inside consoleSandbox', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+ handler.mockClear();
+
+ consoleSandbox(() => {
+ GLOBAL_OBJ.console.log('sandbox message');
+ });
+
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it('resumes firing the handler after consoleSandbox returns', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ consoleSandbox(() => {
+ GLOBAL_OBJ.console.log('inside sandbox');
+ });
+ handler.mockClear();
+
+ GLOBAL_OBJ.console.log('after sandbox');
+
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['after sandbox'], level: 'log' }));
+ expect(handler).not.toHaveBeenCalledWith(expect.objectContaining({ args: ['inside sandbox'], level: 'log' }));
+ });
+
+ it('does not fire the handler inside consoleSandbox after a Lambda-style replacement', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ GLOBAL_OBJ.console.log = vi.fn();
+ handler.mockClear();
+
+ consoleSandbox(() => {
+ GLOBAL_OBJ.console.log('sandbox after lambda');
+ });
+
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('third-party capture-and-call wrapping', () => {
+ it('does not cause infinite recursion when a third party wraps console with the capture pattern', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+ handler.mockClear();
+
+ const prevLog = GLOBAL_OBJ.console.log;
+ const thirdPartyExtra = vi.fn();
+ GLOBAL_OBJ.console.log = (...args: any[]) => {
+ prevLog(...args);
+ thirdPartyExtra(...args);
+ };
+
+ expect(() => GLOBAL_OBJ.console.log('should not overflow')).not.toThrow();
+
+ expect(thirdPartyExtra).toHaveBeenCalledWith('should not overflow');
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['should not overflow'], level: 'log' }));
+ });
+
+ it('fires the handler exactly once on re-entrant calls', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+ handler.mockClear();
+
+ const callOrder: string[] = [];
+
+ const prevLog = GLOBAL_OBJ.console.log;
+ GLOBAL_OBJ.console.log = (...args: any[]) => {
+ callOrder.push('delegate-before-prev');
+ prevLog(...args);
+ callOrder.push('delegate-after-prev');
+ };
+
+ handler.mockImplementation(() => {
+ callOrder.push('handler');
+ });
+
+ GLOBAL_OBJ.console.log('re-entrant test');
+
+ // The handler fires exactly once — on the first (outer) entry.
+ // The re-entrant call through prev() must NOT trigger it a second time.
+ expect(handler).toHaveBeenCalledTimes(1);
+
+ // Verify the full call order:
+ // 1. wrapper enters → triggerHandlers → handler fires
+ // 2. wrapper calls consoleDelegate (third-party fn)
+ // 3. third-party fn calls prev() → re-enters wrapper → nativeMethod (no handler)
+ // 4. third-party fn continues after prev()
+ expect(callOrder).toEqual(['handler', 'delegate-before-prev', 'delegate-after-prev']);
+ });
+
+ it('consoleSandbox still bypasses the handler after third-party wrapping', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ const prevLog = GLOBAL_OBJ.console.log;
+ GLOBAL_OBJ.console.log = (...args: any[]) => {
+ prevLog(...args);
+ };
+ handler.mockClear();
+
+ consoleSandbox(() => {
+ GLOBAL_OBJ.console.log('should bypass');
+ });
+
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it('keeps firing the handler when console.log is set back to the original native method', () => {
+ const handler = vi.fn();
+ addConsoleInstrumentationHandler(handler);
+
+ // Simulate Lambda-style replacement
+ GLOBAL_OBJ.console.log = vi.fn();
+ handler.mockClear();
+
+ // Simulate external code restoring a native method reference it captured
+ // before Sentry init — this should NOT clobber the wrapper.
+ GLOBAL_OBJ.console.log = nativeConsoleLog;
+
+ GLOBAL_OBJ.console.log('after restore to original');
+
+ expect(handler).toHaveBeenCalledWith(
+ expect.objectContaining({ args: ['after restore to original'], level: 'log' }),
+ );
+ });
+ });
+});
diff --git a/packages/node-core/test/integrations/onunhandledrejection.test.ts b/packages/node-core/test/integrations/onunhandledrejection.test.ts
new file mode 100644
index 000000000000..1f2c2c3c2581
--- /dev/null
+++ b/packages/node-core/test/integrations/onunhandledrejection.test.ts
@@ -0,0 +1,54 @@
+import * as SentryCore from '@sentry/core';
+import type { Client } from '@sentry/core';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import {
+ makeUnhandledPromiseHandler,
+ onUnhandledRejectionIntegration,
+} from '../../src/integrations/onunhandledrejection';
+
+// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed
+global.console.warn = () => null;
+global.console.error = () => null;
+
+describe('unhandled promises', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('installs a global listener', () => {
+ const client = { getOptions: () => ({}) } as unknown as Client;
+ SentryCore.setCurrentClient(client);
+
+ const beforeListeners = process.listeners('unhandledRejection').length;
+
+ const integration = onUnhandledRejectionIntegration();
+ integration.setup!(client);
+
+ expect(process.listeners('unhandledRejection').length).toBe(beforeListeners + 1);
+ });
+
+ it('passes the rejection reason (not the promise) as originalException', () => {
+ const client = { getOptions: () => ({}) } as unknown as Client;
+ SentryCore.setCurrentClient(client);
+
+ const reason = new Error('boom');
+ const promise = Promise.reject(reason);
+ // swallow the rejection so it does not leak into the test runner
+ promise.catch(() => {});
+
+ const captureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'test');
+
+ const handler = makeUnhandledPromiseHandler(client, { mode: 'warn', ignore: [] });
+ handler(reason, promise);
+
+ expect(captureException).toHaveBeenCalledTimes(1);
+ const [capturedReason, hint] = captureException.mock.calls[0]!;
+ expect(capturedReason).toBe(reason);
+ expect(hint?.originalException).toBe(reason);
+ expect(hint?.originalException).not.toBe(promise);
+ expect(hint?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.node.onunhandledrejection',
+ });
+ });
+});
diff --git a/packages/node/package.json b/packages/node/package.json
index d1c5cb5bbdc7..051f8366862f 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -88,7 +88,6 @@
"@opentelemetry/instrumentation-pg": "0.66.0",
"@opentelemetry/instrumentation-redis": "0.62.0",
"@opentelemetry/instrumentation-tedious": "0.33.0",
- "@opentelemetry/instrumentation-undici": "0.24.0",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@prisma/instrumentation": "7.6.0",
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index ce9458079980..3bd5e1edba1c 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -137,7 +137,6 @@ export {
profiler,
consoleLoggingIntegration,
createConsolaReporter,
- consoleIntegration,
wrapMcpServerWithSentry,
featureFlagsIntegration,
spanStreamingIntegration,
@@ -192,6 +191,7 @@ export {
processSessionIntegration,
nodeRuntimeMetricsIntegration,
type NodeRuntimeMetricsOptions,
+ consoleIntegration,
pinoIntegration,
createSentryWinstonTransport,
SentryContextManager,
diff --git a/packages/node/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch/index.ts
similarity index 96%
rename from packages/node/src/integrations/node-fetch.ts
rename to packages/node/src/integrations/node-fetch/index.ts
index 74bfff2dab47..2aa277b211c4 100644
--- a/packages/node/src/integrations/node-fetch.ts
+++ b/packages/node/src/integrations/node-fetch/index.ts
@@ -1,5 +1,5 @@
-import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation-undici';
-import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
+import type { UndiciInstrumentationConfig } from './vendored/types';
+import { UndiciInstrumentation } from './vendored/undici';
import type { IntegrationFn } from '@sentry/core';
import {
defineIntegration,
@@ -12,7 +12,7 @@ import {
} from '@sentry/core';
import type { NodeClient } from '@sentry/node-core';
import { generateInstrumentOnce, SentryNodeFetchInstrumentation } from '@sentry/node-core';
-import type { NodeClientOptions } from '../types';
+import type { NodeClientOptions } from '../../types';
const INTEGRATION_NAME = 'NodeFetch';
diff --git a/packages/node/src/integrations/node-fetch/vendored/internal-types.ts b/packages/node/src/integrations/node-fetch/vendored/internal-types.ts
new file mode 100644
index 000000000000..cde91d1e139c
--- /dev/null
+++ b/packages/node/src/integrations/node-fetch/vendored/internal-types.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * NOTICE from the Sentry authors:
+ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
+ * - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
+ * - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
+ */
+/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */
+
+import type { UndiciRequest, UndiciResponse } from './types';
+
+export interface ListenerRecord {
+ name: string;
+ unsubscribe: () => void;
+}
+
+export interface RequestMessage {
+ request: UndiciRequest;
+}
+
+export interface RequestHeadersMessage {
+ request: UndiciRequest;
+ socket: any;
+}
+
+export interface ResponseHeadersMessage {
+ request: UndiciRequest;
+ response: UndiciResponse;
+}
+
+export interface RequestTrailersMessage {
+ request: UndiciRequest;
+ response: UndiciResponse;
+}
+
+export interface RequestErrorMessage {
+ request: UndiciRequest;
+ error: Error;
+}
diff --git a/packages/node/src/integrations/node-fetch/vendored/types.ts b/packages/node/src/integrations/node-fetch/vendored/types.ts
new file mode 100644
index 000000000000..f7c7d46c014a
--- /dev/null
+++ b/packages/node/src/integrations/node-fetch/vendored/types.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * NOTICE from the Sentry authors:
+ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
+ * - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
+ * - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
+ */
+/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */
+
+import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
+import type { Attributes, Span } from '@opentelemetry/api';
+
+export interface UndiciRequest {
+ origin: string;
+ method: string;
+ path: string;
+ /**
+ * Serialized string of headers in the form `name: value\r\n` for v5
+ * Array of strings `[key1, value1, key2, value2]`, where values are
+ * `string | string[]` for v6
+ */
+ headers: string | (string | string[])[];
+ /**
+ * Helper method to add headers (from v6)
+ */
+ addHeader: (name: string, value: string) => void;
+ throwOnError: boolean;
+ completed: boolean;
+ aborted: boolean;
+ idempotent: boolean;
+ contentLength: number | null;
+ contentType: string | null;
+ body: any;
+}
+
+export interface UndiciResponse {
+ headers: Buffer[];
+ statusCode: number;
+ statusText: string;
+}
+
+export interface IgnoreRequestFunction {
+ (request: T): boolean;
+}
+
+export interface RequestHookFunction {
+ (span: Span, request: T): void;
+}
+
+export interface ResponseHookFunction {
+ (span: Span, info: { request: RequestType; response: ResponseType }): void;
+}
+
+export interface StartSpanHookFunction {
+ (request: T): Attributes;
+}
+
+// This package will instrument HTTP requests made through `undici` or `fetch` global API
+// so it seems logical to have similar options than the HTTP instrumentation
+export interface UndiciInstrumentationConfig<
+ RequestType = UndiciRequest,
+ ResponseType = UndiciResponse,
+> extends InstrumentationConfig {
+ /** Not trace all outgoing requests that matched with custom function */
+ ignoreRequestHook?: IgnoreRequestFunction;
+ /** Function for adding custom attributes before request is handled */
+ requestHook?: RequestHookFunction;
+ /** Function called once response headers have been received */
+ responseHook?: ResponseHookFunction;
+ /** Function for adding custom attributes before a span is started */
+ startSpanHook?: StartSpanHookFunction;
+ /** Require parent to create span for outgoing requests */
+ requireParentforSpans?: boolean;
+ /** Map the following HTTP headers to span attributes. */
+ headersToSpanAttributes?: {
+ requestHeaders?: string[];
+ responseHeaders?: string[];
+ };
+}
diff --git a/packages/node/src/integrations/node-fetch/vendored/undici.ts b/packages/node/src/integrations/node-fetch/vendored/undici.ts
new file mode 100644
index 000000000000..55e09d7c4d53
--- /dev/null
+++ b/packages/node/src/integrations/node-fetch/vendored/undici.ts
@@ -0,0 +1,522 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * NOTICE from the Sentry authors:
+ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/ed97091c9890dd18e52759f2ea98e9d7593b3ae4/packages/instrumentation-undici
+ * - Upstream version: @opentelemetry/instrumentation-undici@0.24.0
+ * - Tracking issue: https://github.com/getsentry/sentry-javascript/issues/20165
+ * - Minor TypeScript strictness adjustments for this repository's compiler settings
+ */
+/* eslint-disable -- vendored @opentelemetry/instrumentation-undici (#20165) */
+
+import * as diagch from 'diagnostics_channel';
+import { URL } from 'url';
+
+import { InstrumentationBase, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
+import type { Attributes, Histogram, HrTime, Span } from '@opentelemetry/api';
+import {
+ context,
+ INVALID_SPAN_CONTEXT,
+ propagation,
+ SpanKind,
+ SpanStatusCode,
+ trace,
+ ValueType,
+} from '@opentelemetry/api';
+import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core';
+import {
+ ATTR_ERROR_TYPE,
+ ATTR_HTTP_REQUEST_METHOD,
+ ATTR_HTTP_REQUEST_METHOD_ORIGINAL,
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
+ ATTR_NETWORK_PEER_ADDRESS,
+ ATTR_NETWORK_PEER_PORT,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+ ATTR_URL_FULL,
+ ATTR_URL_PATH,
+ ATTR_URL_QUERY,
+ ATTR_URL_SCHEME,
+ ATTR_USER_AGENT_ORIGINAL,
+ METRIC_HTTP_CLIENT_REQUEST_DURATION,
+} from '@opentelemetry/semantic-conventions';
+
+import type {
+ ListenerRecord,
+ RequestHeadersMessage,
+ RequestMessage,
+ RequestTrailersMessage,
+ ResponseHeadersMessage,
+} from './internal-types';
+import type { UndiciInstrumentationConfig, UndiciRequest } from './types';
+
+import { SDK_VERSION } from '@sentry/core';
+
+interface InstrumentationRecord {
+ span: Span;
+ attributes: Attributes;
+ startTime: HrTime;
+}
+
+const PACKAGE_NAME = '@sentry/instrumentation-undici';
+
+// A combination of https://github.com/elastic/apm-agent-nodejs and
+// https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts
+export class UndiciInstrumentation extends InstrumentationBase {
+ // Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for
+ // unsubscribing.
+ declare private _channelSubs: Array;
+ private _recordFromReq = new WeakMap();
+
+ declare private _httpClientDurationHistogram: Histogram;
+
+ constructor(config: UndiciInstrumentationConfig = {}) {
+ super(PACKAGE_NAME, SDK_VERSION, config);
+ }
+
+ // No need to instrument files/modules
+ protected override init() {
+ return undefined;
+ }
+
+ override disable(): void {
+ super.disable();
+ this._channelSubs.forEach(sub => sub.unsubscribe());
+ this._channelSubs.length = 0;
+ }
+
+ override enable(): void {
+ // "enabled" handling is currently a bit messy with InstrumentationBase.
+ // If constructed with `{enabled: false}`, this `.enable()` is still called,
+ // and `this.getConfig().enabled !== this.isEnabled()`, creating confusion.
+ //
+ // For now, this class will setup for instrumenting if `.enable()` is
+ // called, but use `this.getConfig().enabled` to determine if
+ // instrumentation should be generated. This covers the more likely common
+ // case of config being given a construction time, rather than later via
+ // `instance.enable()`, `.disable()`, or `.setConfig()` calls.
+ super.enable();
+
+ // This method is called by the super-class constructor before ours is
+ // called. So we need to ensure the property is initalized.
+ this._channelSubs = this._channelSubs || [];
+
+ // Avoid to duplicate subscriptions
+ if (this._channelSubs.length > 0) {
+ return;
+ }
+
+ this.subscribeToChannel('undici:request:create', this.onRequestCreated.bind(this));
+ this.subscribeToChannel('undici:client:sendHeaders', this.onRequestHeaders.bind(this));
+ this.subscribeToChannel('undici:request:headers', this.onResponseHeaders.bind(this));
+ this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this));
+ this.subscribeToChannel('undici:request:error', this.onError.bind(this));
+ }
+
+ protected override _updateMetricInstruments() {
+ this._httpClientDurationHistogram = this.meter.createHistogram(METRIC_HTTP_CLIENT_REQUEST_DURATION, {
+ description: 'Measures the duration of outbound HTTP requests.',
+ unit: 's',
+ valueType: ValueType.DOUBLE,
+ advice: {
+ explicitBucketBoundaries: [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10],
+ },
+ });
+ }
+
+ private subscribeToChannel(diagnosticChannel: string, onMessage: (message: any, name: string | symbol) => void) {
+ // `diagnostics_channel` had a ref counting bug until v18.19.0.
+ // https://github.com/nodejs/node/pull/47520
+ const [major = 0, minor = 0] = process.version
+ .replace('v', '')
+ .split('.')
+ .map(n => Number(n));
+ const useNewSubscribe = major > 18 || (major === 18 && minor >= 19);
+
+ let unsubscribe: () => void;
+ if (useNewSubscribe) {
+ diagch.subscribe?.(diagnosticChannel, onMessage);
+ unsubscribe = () => diagch.unsubscribe?.(diagnosticChannel, onMessage);
+ } else {
+ const channel = diagch.channel(diagnosticChannel);
+ channel.subscribe(onMessage);
+ unsubscribe = () => channel.unsubscribe(onMessage);
+ }
+
+ this._channelSubs.push({
+ name: diagnosticChannel,
+ unsubscribe,
+ });
+ }
+
+ private parseRequestHeaders(request: UndiciRequest) {
+ const result = new Map();
+
+ if (Array.isArray(request.headers)) {
+ // headers are an array [k1, v2, k2, v2] (undici v6+)
+ // values could be string or a string[] for multiple values
+ for (let i = 0; i < request.headers.length; i += 2) {
+ const key = request.headers[i];
+ const value = request.headers[i + 1];
+
+ // Key should always be a string, but the types don't know that, and let's be safe
+ if (typeof key === 'string' && value !== undefined) {
+ result.set(key.toLowerCase(), value);
+ }
+ }
+ } else if (typeof request.headers === 'string') {
+ // headers are a raw string (undici v5)
+ // headers could be repeated in several lines for multiple values
+ const headers = request.headers.split('\r\n');
+ for (const line of headers) {
+ if (!line) {
+ continue;
+ }
+ const colonIndex = line.indexOf(':');
+ if (colonIndex === -1) {
+ // Invalid header? Probably this can't happen, but again let's be safe.
+ continue;
+ }
+ const key = line.substring(0, colonIndex).toLowerCase();
+ const value = line.substring(colonIndex + 1).trim();
+ const allValues = result.get(key);
+
+ if (allValues && Array.isArray(allValues)) {
+ allValues.push(value);
+ } else if (allValues) {
+ result.set(key, [allValues, value]);
+ } else {
+ result.set(key, value);
+ }
+ }
+ }
+ return result;
+ }
+
+ // This is the 1st message we receive for each request (fired after request creation). Here we will
+ // create the span and populate some atttributes, then link the span to the request for further
+ // span processing
+ private onRequestCreated({ request }: RequestMessage): void {
+ // Ignore if:
+ // - instrumentation is disabled
+ // - ignored by config
+ // - method is 'CONNECT'
+ const config = this.getConfig();
+ const enabled = config.enabled !== false;
+ const shouldIgnoreReq = safeExecuteInTheMiddle(
+ () => !enabled || request.method === 'CONNECT' || config.ignoreRequestHook?.(request),
+ e => e && this._diag.error('caught ignoreRequestHook error: ', e),
+ true,
+ );
+
+ if (shouldIgnoreReq) {
+ return;
+ }
+
+ const startTime = hrTime();
+ let requestUrl;
+ try {
+ requestUrl = new URL(request.path, request.origin);
+ } catch (err) {
+ this._diag.warn('could not determine url.full:', err);
+ // Skip instrumenting this request.
+ return;
+ }
+ const urlScheme = requestUrl.protocol.replace(':', '');
+ const requestMethod = this.getRequestMethod(request.method);
+ const attributes: Attributes = {
+ [ATTR_HTTP_REQUEST_METHOD]: requestMethod,
+ [ATTR_HTTP_REQUEST_METHOD_ORIGINAL]: request.method,
+ [ATTR_URL_FULL]: requestUrl.toString(),
+ [ATTR_URL_PATH]: requestUrl.pathname,
+ [ATTR_URL_QUERY]: requestUrl.search,
+ [ATTR_URL_SCHEME]: urlScheme,
+ };
+
+ const schemePorts: Record = { https: '443', http: '80' };
+ const serverAddress = requestUrl.hostname;
+ const serverPort = requestUrl.port || schemePorts[urlScheme];
+
+ attributes[ATTR_SERVER_ADDRESS] = serverAddress;
+ if (serverPort && !isNaN(Number(serverPort))) {
+ attributes[ATTR_SERVER_PORT] = Number(serverPort);
+ }
+
+ // Get user agent from headers
+ const headersMap = this.parseRequestHeaders(request);
+ const userAgentValues = headersMap.get('user-agent');
+
+ if (userAgentValues) {
+ // NOTE: having multiple user agents is not expected so
+ // we're going to take last one like `curl` does
+ // ref: https://curl.se/docs/manpage.html#-A
+ const userAgent = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
+ attributes[ATTR_USER_AGENT_ORIGINAL] = userAgent;
+ }
+
+ // Get attributes from the hook if present
+ const hookAttributes = safeExecuteInTheMiddle(
+ () => config.startSpanHook?.(request),
+ e => e && this._diag.error('caught startSpanHook error: ', e),
+ true,
+ );
+ if (hookAttributes) {
+ Object.entries(hookAttributes).forEach(([key, val]) => {
+ attributes[key] = val;
+ });
+ }
+
+ // Check if parent span is required via config and:
+ // - if a parent is required but not present, we use a `NoopSpan` to still
+ // propagate context without recording it.
+ // - create a span otherwise
+ const activeCtx = context.active();
+ const currentSpan = trace.getSpan(activeCtx);
+ let span: Span;
+
+ if (config.requireParentforSpans && (!currentSpan || !trace.isSpanContextValid(currentSpan.spanContext()))) {
+ span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
+ } else {
+ span = this.tracer.startSpan(
+ requestMethod === '_OTHER' ? 'HTTP' : requestMethod,
+ {
+ kind: SpanKind.CLIENT,
+ attributes: attributes,
+ },
+ activeCtx,
+ );
+ }
+
+ // Execute the request hook if defined
+ safeExecuteInTheMiddle(
+ () => config.requestHook?.(span, request),
+ e => e && this._diag.error('caught requestHook error: ', e),
+ true,
+ );
+
+ // Context propagation goes last so no hook can tamper
+ // the propagation headers
+ const requestContext = trace.setSpan(context.active(), span);
+ const addedHeaders: Record = {};
+ propagation.inject(requestContext, addedHeaders);
+
+ const headerEntries = Object.entries(addedHeaders);
+
+ for (let i = 0; i < headerEntries.length; i++) {
+ const pair = headerEntries[i];
+ if (!pair) {
+ continue;
+ }
+ const [k, v] = pair;
+
+ if (typeof request.addHeader === 'function') {
+ request.addHeader(k, v);
+ } else if (typeof request.headers === 'string') {
+ request.headers += `${k}: ${v}\r\n`;
+ } else if (Array.isArray(request.headers)) {
+ // undici@6.11.0 accidentally, briefly removed `request.addHeader()`.
+ request.headers.push(k, v);
+ }
+ }
+ this._recordFromReq.set(request, { span, attributes, startTime });
+ }
+
+ // This is the 2nd message we receive for each request. It is fired when connection with
+ // the remote is established and about to send the first byte. Here we do have info about the
+ // remote address and port so we can populate some `network.*` attributes into the span
+ private onRequestHeaders({ request, socket }: RequestHeadersMessage): void {
+ const record = this._recordFromReq.get(request);
+
+ if (!record) {
+ return;
+ }
+
+ const config = this.getConfig();
+ const { span } = record;
+ const { remoteAddress, remotePort } = socket;
+ const spanAttributes: Attributes = {
+ [ATTR_NETWORK_PEER_ADDRESS]: remoteAddress,
+ [ATTR_NETWORK_PEER_PORT]: remotePort,
+ };
+
+ // After hooks have been processed (which may modify request headers)
+ // we can collect the headers based on the configuration
+ if (config.headersToSpanAttributes?.requestHeaders) {
+ const headersToAttribs = new Set(config.headersToSpanAttributes.requestHeaders.map(n => n.toLowerCase()));
+ const headersMap = this.parseRequestHeaders(request);
+
+ for (const [name, value] of headersMap.entries()) {
+ if (headersToAttribs.has(name)) {
+ const attrValue = Array.isArray(value) ? value : [value];
+ spanAttributes[`http.request.header.${name}`] = attrValue;
+ }
+ }
+ }
+
+ span.setAttributes(spanAttributes);
+ }
+
+ // This is the 3rd message we get for each request and it's fired when the server
+ // headers are received, body may not be accessible yet.
+ // From the response headers we can set the status and content length
+ private onResponseHeaders({ request, response }: ResponseHeadersMessage): void {
+ const record = this._recordFromReq.get(request);
+
+ if (!record) {
+ return;
+ }
+
+ const { span, attributes } = record;
+ const spanAttributes: Attributes = {
+ [ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode,
+ };
+
+ const config = this.getConfig();
+
+ // Execute the response hook if defined
+ safeExecuteInTheMiddle(
+ () => config.responseHook?.(span, { request, response }),
+ e => e && this._diag.error('caught responseHook error: ', e),
+ true,
+ );
+
+ if (config.headersToSpanAttributes?.responseHeaders) {
+ const headersToAttribs = new Set();
+ config.headersToSpanAttributes?.responseHeaders.forEach(name => headersToAttribs.add(name.toLowerCase()));
+
+ for (let idx = 0; idx < response.headers.length; idx = idx + 2) {
+ const nameBuf = response.headers[idx];
+ const valueBuf = response.headers[idx + 1];
+ if (nameBuf === undefined || valueBuf === undefined) {
+ continue;
+ }
+ const name = nameBuf.toString().toLowerCase();
+ const value = valueBuf;
+
+ if (headersToAttribs.has(name)) {
+ const attrName = `http.response.header.${name}`;
+ if (!Object.prototype.hasOwnProperty.call(spanAttributes, attrName)) {
+ spanAttributes[attrName] = [value.toString()];
+ } else {
+ (spanAttributes[attrName] as string[]).push(value.toString());
+ }
+ }
+ }
+ }
+
+ span.setAttributes(spanAttributes);
+ span.setStatus({
+ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET,
+ });
+ record.attributes = Object.assign(attributes, spanAttributes);
+ }
+
+ // This is the last event we receive if the request went without any errors
+ private onDone({ request }: RequestTrailersMessage): void {
+ const record = this._recordFromReq.get(request);
+
+ if (!record) {
+ return;
+ }
+
+ const { span, attributes, startTime } = record;
+
+ // End the span
+ span.end();
+ this._recordFromReq.delete(request);
+
+ // Record metrics
+ this.recordRequestDuration(attributes, startTime);
+ }
+
+ // This is the event we get when something is wrong in the request like
+ // - invalid options when calling `fetch` global API or any undici method for request
+ // - connectivity errors such as unreachable host
+ // - requests aborted through an `AbortController.signal`
+ // NOTE: server errors are considered valid responses and it's the lib consumer
+ // who should deal with that.
+ private onError({ request, error }: any): void {
+ const record = this._recordFromReq.get(request);
+
+ if (!record) {
+ return;
+ }
+
+ const { span, attributes, startTime } = record;
+
+ // NOTE: in `undici@6.3.0` when request aborted the error type changes from
+ // a custom error (`RequestAbortedError`) to a built-in `DOMException` carrying
+ // some differences:
+ // - `code` is from DOMEXception (ABORT_ERR: 20)
+ // - `message` changes
+ // - stacktrace is smaller and contains node internal frames
+ span.recordException(error);
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: error.message,
+ });
+ span.end();
+ this._recordFromReq.delete(request);
+
+ // Record metrics (with the error)
+ attributes[ATTR_ERROR_TYPE] = error.message;
+ this.recordRequestDuration(attributes, startTime);
+ }
+
+ private recordRequestDuration(attributes: Attributes, startTime: HrTime) {
+ // Time to record metrics
+ const metricsAttributes: Attributes = {};
+ // Get the attribs already in span attributes
+ const keysToCopy = [
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
+ ATTR_HTTP_REQUEST_METHOD,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+ ATTR_URL_SCHEME,
+ ATTR_ERROR_TYPE,
+ ];
+ keysToCopy.forEach(key => {
+ if (key in attributes) {
+ metricsAttributes[key] = attributes[key];
+ }
+ });
+
+ // Take the duration and record it
+ const durationSeconds = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000;
+ this._httpClientDurationHistogram.record(durationSeconds, metricsAttributes);
+ }
+
+ private getRequestMethod(original: string): string {
+ const knownMethods = {
+ CONNECT: true,
+ OPTIONS: true,
+ HEAD: true,
+ GET: true,
+ POST: true,
+ PUT: true,
+ PATCH: true,
+ DELETE: true,
+ TRACE: true,
+ // QUERY from https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/
+ QUERY: true,
+ };
+
+ if (original.toUpperCase() in knownMethods) {
+ return original.toUpperCase();
+ }
+
+ return '_OTHER';
+ }
+}
diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json
index 122dc7b92f83..fa0484e51c6d 100644
--- a/packages/opentelemetry/package.json
+++ b/packages/opentelemetry/package.json
@@ -26,6 +26,16 @@
"types": "./build/types/index.d.ts",
"default": "./build/cjs/index.js"
}
+ },
+ "./tracing-channel": {
+ "import": {
+ "types": "./build/types/tracingChannel.d.ts",
+ "default": "./build/esm/tracingChannel.js"
+ },
+ "require": {
+ "types": "./build/types/tracingChannel.d.ts",
+ "default": "./build/cjs/tracingChannel.js"
+ }
}
},
"typesVersions": {
diff --git a/packages/opentelemetry/rollup.npm.config.mjs b/packages/opentelemetry/rollup.npm.config.mjs
index e015fea4935e..e6f5ecdd4871 100644
--- a/packages/opentelemetry/rollup.npm.config.mjs
+++ b/packages/opentelemetry/rollup.npm.config.mjs
@@ -2,6 +2,9 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu
export default makeNPMConfigVariants(
makeBaseNPMConfig({
+ // `tracingChannel` is a Node.js-only subpath so `node:diagnostics_channel`
+ // isn't pulled into the main bundle (breaks edge/browser builds).
+ entrypoints: ['src/index.ts', 'src/tracingChannel.ts'],
packageSpecificConfig: {
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts
index 5c0c47423284..1e65e9d15d14 100644
--- a/packages/opentelemetry/src/sampler.ts
+++ b/packages/opentelemetry/src/sampler.ts
@@ -77,6 +77,7 @@ export class SentrySampler implements Sampler {
// If we have a http.client span that has no local parent, we never want to sample it
// but we want to leave downstream sampling decisions up to the server
if (spanKind === SpanKind.CLIENT && maybeSpanHttpMethod && (!parentSpan || parentContext?.isRemote)) {
+ this._client.recordDroppedEvent('no_parent_span', 'span');
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
}
diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts
index 7c9d09a169b9..b60bda367704 100644
--- a/packages/opentelemetry/src/trace.ts
+++ b/packages/opentelemetry/src/trace.ts
@@ -48,8 +48,12 @@ function _startSpan(options: OpenTelemetrySpanContext, callback: (span: Span)
return wrapper(() => {
const activeCtx = getContext(options.scope, options.forceTransaction);
- const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
- const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
+ const missingRequiredParent = options.onlyIfParent && !trace.getSpan(activeCtx);
+ const ctx = missingRequiredParent ? suppressTracing(activeCtx) : activeCtx;
+
+ if (missingRequiredParent) {
+ getClient()?.recordDroppedEvent('no_parent_span', 'span');
+ }
const spanOptions = getSpanOptions(options);
@@ -151,8 +155,12 @@ export function startInactiveSpan(options: OpenTelemetrySpanContext): Span {
return wrapper(() => {
const activeCtx = getContext(options.scope, options.forceTransaction);
- const shouldSkipSpan = options.onlyIfParent && !trace.getSpan(activeCtx);
- let ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
+ const missingRequiredParent = options.onlyIfParent && !trace.getSpan(activeCtx);
+ let ctx = missingRequiredParent ? suppressTracing(activeCtx) : activeCtx;
+
+ if (missingRequiredParent) {
+ getClient()?.recordDroppedEvent('no_parent_span', 'span');
+ }
const spanOptions = getSpanOptions(options);
diff --git a/packages/opentelemetry/src/tracingChannel.ts b/packages/opentelemetry/src/tracingChannel.ts
new file mode 100644
index 000000000000..984986b7cdcb
--- /dev/null
+++ b/packages/opentelemetry/src/tracingChannel.ts
@@ -0,0 +1,92 @@
+/**
+ * Vendored and adapted from https://github.com/logaretm/otel-tracing-channel
+ *
+ * Creates a TracingChannel with proper OpenTelemetry context propagation
+ * using Node.js diagnostic_channel's `bindStore` mechanism.
+ */
+import type { TracingChannel, TracingChannelSubscribers } from 'node:diagnostics_channel';
+import { tracingChannel as nativeTracingChannel } from 'node:diagnostics_channel';
+import type { Span } from '@opentelemetry/api';
+import { context, trace } from '@opentelemetry/api';
+import { logger } from '@sentry/core';
+import type { SentryAsyncLocalStorageContextManager } from './asyncLocalStorageContextManager';
+import type { AsyncLocalStorageLookup } from './contextManager';
+import { DEBUG_BUILD } from './debug-build';
+
+/**
+ * Transform function that creates a span from the channel data.
+ */
+export type OtelTracingChannelTransform = (data: TData) => Span;
+
+type WithSpan = TData & { _sentrySpan?: Span };
+
+/**
+ * A TracingChannel whose `subscribe` / `unsubscribe` accept partial subscriber
+ * objects — you only need to provide handlers for the events you care about.
+ */
+export interface OtelTracingChannel<
+ TData extends object = object,
+ TDataWithSpan extends object = WithSpan,
+> extends Omit, 'subscribe' | 'unsubscribe'> {
+ subscribe(subscribers: Partial>): void;
+ unsubscribe(subscribers: Partial>): void;
+}
+
+interface ContextApi {
+ _getContextManager(): SentryAsyncLocalStorageContextManager;
+}
+
+/**
+ * Creates a new tracing channel with proper OTel context propagation.
+ *
+ * When the channel's `tracePromise` / `traceSync` / `traceCallback` is called,
+ * the `transformStart` function runs inside `bindStore` so that:
+ * 1. A new span is created from the channel data.
+ * 2. The span is set on the OTel context stored in AsyncLocalStorage.
+ * 3. Downstream code (including Sentry's span processor) sees the correct parent.
+ *
+ * @param channelNameOrInstance - Either a channel name string or an existing TracingChannel instance.
+ * @param transformStart - Function that creates an OpenTelemetry span from the channel data.
+ * @returns The tracing channel with OTel context bound.
+ */
+export function tracingChannel(
+ channelNameOrInstance: string,
+ transformStart: OtelTracingChannelTransform,
+): OtelTracingChannel> {
+ const channel = nativeTracingChannel, WithSpan>(
+ channelNameOrInstance,
+ ) as unknown as OtelTracingChannel>;
+
+ let lookup: AsyncLocalStorageLookup | undefined;
+ try {
+ const contextManager = (context as unknown as ContextApi)._getContextManager();
+ lookup = contextManager.getAsyncLocalStorageLookup();
+ } catch {
+ // getAsyncLocalStorageLookup may not exist if using a non-Sentry context manager
+ }
+
+ if (!lookup) {
+ DEBUG_BUILD &&
+ logger.warn(
+ '[TracingChannel] Could not access OpenTelemetry AsyncLocalStorage, context propagation will not work.',
+ );
+ return channel;
+ }
+
+ const otelStorage = lookup.asyncLocalStorage;
+
+ // Bind the start channel so that each trace invocation runs the transform
+ // and stores the resulting context (with span) in AsyncLocalStorage.
+ // @ts-expect-error bindStore types don't account for AsyncLocalStorage of a different generic type
+ channel.start.bindStore(otelStorage, (data: WithSpan) => {
+ const span = transformStart(data);
+
+ // Store the span on data so downstream event handlers (asyncEnd, error, etc.) can access it.
+ data._sentrySpan = span;
+
+ // Return the context with the span set — this is what gets stored in AsyncLocalStorage.
+ return trace.setSpan(context.active(), span);
+ });
+
+ return channel;
+}
diff --git a/packages/opentelemetry/test/sampler.test.ts b/packages/opentelemetry/test/sampler.test.ts
index 654d96be91c4..22fa724fa161 100644
--- a/packages/opentelemetry/test/sampler.test.ts
+++ b/packages/opentelemetry/test/sampler.test.ts
@@ -120,7 +120,7 @@ describe('SentrySampler', () => {
spyOnDroppedEvent.mockReset();
});
- it('ignores local http client root spans', () => {
+ it('ignores local http client root spans and records no_parent_span client report', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 0 }));
const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
const sampler = new SentrySampler(client);
@@ -139,7 +139,8 @@ describe('SentrySampler', () => {
decision: SamplingDecision.NOT_RECORD,
traceState: new TraceState(),
});
- expect(spyOnDroppedEvent).toHaveBeenCalledTimes(0);
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
spyOnDroppedEvent.mockReset();
});
diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts
index a6a7f35ab76a..0aeacc5284ad 100644
--- a/packages/opentelemetry/test/trace.test.ts
+++ b/packages/opentelemetry/test/trace.test.ts
@@ -530,15 +530,23 @@ describe('trace', () => {
// TODO: propagation scope is not picked up by spans...
describe('onlyIfParent', () => {
- it('does not create a span if there is no parent', () => {
+ it('does not create a span and records no_parent_span client report if there is no parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
return span;
});
expect(isSpan(span)).toBe(false);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
return span;
@@ -548,6 +556,33 @@ describe('trace', () => {
});
expect(isSpan(span)).toBe(true);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ startSpan({ name: 'root span without onlyIfParent' }, span => {
+ return span;
+ });
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is false even without a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ startSpan({ name: 'root span', onlyIfParent: false }, span => {
+ return span;
+ });
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
});
@@ -824,13 +859,21 @@ describe('trace', () => {
});
describe('onlyIfParent', () => {
- it('does not create a span if there is no parent', () => {
+ it('does not create a span and records no_parent_span client report if there is no parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
expect(isSpan(span)).toBe(false);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
@@ -838,6 +881,31 @@ describe('trace', () => {
});
expect(isSpan(span)).toBe(true);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ const span = startInactiveSpan({ name: 'root span without onlyIfParent' });
+ span.end();
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is false even without a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ const span = startInactiveSpan({ name: 'root span', onlyIfParent: false });
+ span.end();
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
@@ -1192,15 +1260,23 @@ describe('trace', () => {
});
describe('onlyIfParent', () => {
- it('does not create a span if there is no parent', () => {
+ it('does not create a span and records no_parent_span client report if there is no parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
return span;
});
expect(isSpan(span)).toBe(false);
+ expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span');
+ expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1);
});
it('creates a span if there is a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
const span = startSpan({ name: 'parent span' }, () => {
const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
return span;
@@ -1210,6 +1286,35 @@ describe('trace', () => {
});
expect(isSpan(span)).toBe(true);
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is not set', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ startSpanManual({ name: 'root span without onlyIfParent' }, span => {
+ span.end();
+ return span;
+ });
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
+ });
+
+ it('does not record no_parent_span client report when onlyIfParent is false even without a parent', () => {
+ const client = getClient()!;
+ const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent');
+
+ context.with(ROOT_CONTEXT, () => {
+ startSpanManual({ name: 'root span', onlyIfParent: false }, span => {
+ span.end();
+ return span;
+ });
+ });
+
+ expect(spyOnDroppedEvent).not.toHaveBeenCalledWith('no_parent_span', 'span');
});
});
});
diff --git a/packages/opentelemetry/test/tracingChannel.test.ts b/packages/opentelemetry/test/tracingChannel.test.ts
new file mode 100644
index 000000000000..2b5f72327352
--- /dev/null
+++ b/packages/opentelemetry/test/tracingChannel.test.ts
@@ -0,0 +1,251 @@
+import { context, trace } from '@opentelemetry/api';
+import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
+import { type Span, spanToJSON } from '@sentry/core';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { startSpanManual } from '../src/trace';
+import { tracingChannel } from '../src/tracingChannel';
+import { getActiveSpan } from '../src/utils/getActiveSpan';
+import { getParentSpanId } from '../src/utils/getParentSpanId';
+import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit';
+
+describe('tracingChannel', () => {
+ beforeEach(() => {
+ mockSdkInit({ tracesSampleRate: 1 });
+ });
+
+ afterEach(async () => {
+ await cleanupOtel();
+ });
+
+ it('sets the created span as the active span inside traceSync', () => {
+ const channel = tracingChannel<{ op: string }>('test:sync:active', data => {
+ return startSpanManual({ name: 'channel-span', op: data.op }, span => span);
+ });
+
+ channel.subscribe({
+ end: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ channel.traceSync(
+ () => {
+ const active = getActiveSpan();
+ expect(active).toBeDefined();
+ expect(spanToJSON(active!).op).toBe('test.op');
+ },
+ { op: 'test.op' },
+ );
+ });
+
+ it('sets the created span as the active span inside tracePromise', async () => {
+ const channel = tracingChannel<{ op: string }>('test:promise:active', data => {
+ return startSpanManual({ name: 'channel-span', op: data.op }, span => span);
+ });
+
+ channel.subscribe({
+ asyncEnd: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ await channel.tracePromise(
+ async () => {
+ const active = getActiveSpan();
+ expect(active).toBeDefined();
+ expect(spanToJSON(active!).op).toBe('test.op');
+ },
+ { op: 'test.op' },
+ );
+ });
+
+ it('creates correct parent-child relationship with nested tracing channels', () => {
+ const outerChannel = tracingChannel<{ name: string }>('test:nested:outer', data => {
+ return startSpanManual({ name: data.name, op: 'outer' }, span => span);
+ });
+
+ const innerChannel = tracingChannel<{ name: string }>('test:nested:inner', data => {
+ return startSpanManual({ name: data.name, op: 'inner' }, span => span);
+ });
+
+ outerChannel.subscribe({
+ end: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ innerChannel.subscribe({
+ end: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ let outerSpanId: string | undefined;
+ let innerParentSpanId: string | undefined;
+
+ outerChannel.traceSync(
+ () => {
+ const outerSpan = getActiveSpan();
+ outerSpanId = outerSpan?.spanContext().spanId;
+
+ innerChannel.traceSync(
+ () => {
+ const innerSpan = getActiveSpan();
+ innerParentSpanId = getParentSpanId(innerSpan as unknown as ReadableSpan);
+ },
+ { name: 'inner-span' },
+ );
+ },
+ { name: 'outer-span' },
+ );
+
+ expect(outerSpanId).toBeDefined();
+ expect(innerParentSpanId).toBe(outerSpanId);
+ });
+
+ it('creates correct parent-child relationship with nested async tracing channels', async () => {
+ const outerChannel = tracingChannel<{ name: string }>('test:nested-async:outer', data => {
+ return startSpanManual({ name: data.name, op: 'outer' }, span => span);
+ });
+
+ const innerChannel = tracingChannel<{ name: string }>('test:nested-async:inner', data => {
+ return startSpanManual({ name: data.name, op: 'inner' }, span => span);
+ });
+
+ outerChannel.subscribe({
+ asyncEnd: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ innerChannel.subscribe({
+ asyncEnd: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ let outerSpanId: string | undefined;
+ let innerParentSpanId: string | undefined;
+
+ await outerChannel.tracePromise(
+ async () => {
+ const outerSpan = getActiveSpan();
+ outerSpanId = outerSpan?.spanContext().spanId;
+
+ await innerChannel.tracePromise(
+ async () => {
+ const innerSpan = getActiveSpan();
+ innerParentSpanId = getParentSpanId(innerSpan as unknown as ReadableSpan);
+ },
+ { name: 'inner-span' },
+ );
+ },
+ { name: 'outer-span' },
+ );
+
+ expect(outerSpanId).toBeDefined();
+ expect(innerParentSpanId).toBe(outerSpanId);
+ });
+
+ it('creates correct parent when a tracing channel is nested inside startSpanManual', () => {
+ const channel = tracingChannel<{ name: string }>('test:inside-startspan', data => {
+ return startSpanManual({ name: data.name, op: 'channel' }, span => span);
+ });
+
+ channel.subscribe({
+ end: data => {
+ data._sentrySpan?.end();
+ },
+ });
+
+ let manualSpanId: string | undefined;
+ let channelParentSpanId: string | undefined;
+
+ startSpanManual({ name: 'manual-parent' }, parentSpan => {
+ manualSpanId = parentSpan.spanContext().spanId;
+
+ channel.traceSync(
+ () => {
+ const channelSpan = getActiveSpan();
+ channelParentSpanId = getParentSpanId(channelSpan as unknown as ReadableSpan);
+ },
+ { name: 'channel-child' },
+ );
+
+ parentSpan.end();
+ });
+
+ expect(manualSpanId).toBeDefined();
+ expect(channelParentSpanId).toBe(manualSpanId);
+ });
+
+ it('makes the channel span available on data.span', () => {
+ let spanFromData: unknown;
+
+ const channel = tracingChannel<{ name: string }>('test:data-span', data => {
+ return startSpanManual({ name: data.name }, span => span);
+ });
+
+ channel.subscribe({
+ end: data => {
+ spanFromData = data._sentrySpan;
+ data._sentrySpan?.end();
+ },
+ });
+
+ channel.traceSync(() => {}, { name: 'test-span' });
+
+ expect(spanFromData).toBeDefined();
+ expect(spanToJSON(spanFromData as unknown as Span).description).toBe('test-span');
+ });
+
+ it('shares the same trace ID across nested channels', () => {
+ const outerChannel = tracingChannel<{ name: string }>('test:trace-id:outer', data => {
+ return startSpanManual({ name: data.name }, span => span);
+ });
+
+ const innerChannel = tracingChannel<{ name: string }>('test:trace-id:inner', data => {
+ return startSpanManual({ name: data.name }, span => span);
+ });
+
+ outerChannel.subscribe({ end: data => data._sentrySpan?.end() });
+ innerChannel.subscribe({ end: data => data._sentrySpan?.end() });
+
+ let outerTraceId: string | undefined;
+ let innerTraceId: string | undefined;
+
+ outerChannel.traceSync(
+ () => {
+ outerTraceId = getActiveSpan()?.spanContext().traceId;
+
+ innerChannel.traceSync(
+ () => {
+ innerTraceId = getActiveSpan()?.spanContext().traceId;
+ },
+ { name: 'inner' },
+ );
+ },
+ { name: 'outer' },
+ );
+
+ expect(outerTraceId).toBeDefined();
+ expect(innerTraceId).toBe(outerTraceId);
+ });
+
+ it('does not leak context outside of traceSync', () => {
+ const channel = tracingChannel<{ name: string }>('test:no-leak', data => {
+ return startSpanManual({ name: data.name }, span => span);
+ });
+
+ channel.subscribe({ end: data => data._sentrySpan?.end() });
+
+ const activeBefore = trace.getSpan(context.active());
+
+ channel.traceSync(() => {}, { name: 'scoped-span' });
+
+ const activeAfter = trace.getSpan(context.active());
+
+ expect(activeBefore).toBeUndefined();
+ expect(activeAfter).toBeUndefined();
+ });
+});
diff --git a/packages/opentelemetry/test/utils/contextData.test.ts b/packages/opentelemetry/test/utils/contextData.test.ts
index 597b9fa2b637..0d04dc6556a5 100644
--- a/packages/opentelemetry/test/utils/contextData.test.ts
+++ b/packages/opentelemetry/test/utils/contextData.test.ts
@@ -1,6 +1,6 @@
import { ROOT_CONTEXT } from '@opentelemetry/api';
import { Scope } from '@sentry/core';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { describe, expect, it } from 'vitest';
import {
getContextFromScope,
getScopesFromContext,
diff --git a/packages/profiling-node/README.md b/packages/profiling-node/README.md
index 962bc8e6834f..51e447640c14 100644
--- a/packages/profiling-node/README.md
+++ b/packages/profiling-node/README.md
@@ -239,13 +239,19 @@ the binaries will be copied. This is wasteful as you will likely only need one o
runtime.
To prune the other libraries, profiling-node ships with a small utility script that helps you prune unused binaries. The
-script can be invoked via `sentry-prune-profiler-binaries`, use `--help` to see a list of available options or
-`--dry-run` if you want it to log the binaries that would have been deleted.
+script can be invoked via `sentry-prune-profiler-binaries`:
+
+```bash
+npx --package=@sentry/profiling-node sentry-prune-profiler-binaries
+```
+
+Use `--help` to see a list of available options or `--dry-run` if you want it to log the binaries that would have been
+deleted.
Example of only preserving a binary to run node16 on linux x64 musl.
```bash
-sentry-prune-profiler-binaries --target_dir_path=./dist --target_platform=linux --target_node=16 --target_stdlib=musl --target_arch=x64
+npx --package=@sentry/profiling-node sentry-prune-profiler-binaries --target_dir_path=./dist --target_platform=linux --target_node=16 --target_stdlib=musl --target_arch=x64
```
Which will output something like
diff --git a/packages/remix/README.md b/packages/remix/README.md
index 9260def2b700..2589ed9f7e6b 100644
--- a/packages/remix/README.md
+++ b/packages/remix/README.md
@@ -122,8 +122,13 @@ Sentry.captureEvent({
The Remix SDK provides a script that automatically creates a release and uploads sourcemaps. To generate sourcemaps with
Remix, you need to call `remix build` with the `--sourcemap` option.
-On release, call `sentry-upload-sourcemaps` to upload source maps and create a release. To see more details on how to
-use the command, call `sentry-upload-sourcemaps --help`.
+On release, call `sentry-upload-sourcemaps` to upload source maps and create a release:
+
+```bash
+npx --package=@sentry/remix sentry-upload-sourcemaps
+```
+
+To see more details on how to use the command, call `npx --package=@sentry/remix sentry-upload-sourcemaps --help`.
For more advanced configuration,
[directly use `sentry-cli` to upload source maps.](https://github.com/getsentry/sentry-cli).
diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts
index a940ef746979..ec762eacd8dd 100644
--- a/packages/replay-internal/src/integration.ts
+++ b/packages/replay-internal/src/integration.ts
@@ -297,7 +297,7 @@ export class Replay implements Integration {
return Promise.resolve();
}
- return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
+ return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session', reason: 'manual' });
}
/**
diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts
index cab408ca9d5d..d80f47a6704b 100644
--- a/packages/replay-internal/src/replay.ts
+++ b/packages/replay-internal/src/replay.ts
@@ -1,5 +1,5 @@
/* eslint-disable max-lines */ // TODO: We might want to split this file up
-import type { ReplayRecordingMode, Span } from '@sentry/core';
+import type { ReplayRecordingMode, ReplayStopReason, Span } from '@sentry/core';
import { getActiveSpan, getClient, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
import { EventType, record } from '@sentry-internal/rrweb';
import {
@@ -495,7 +495,10 @@ export class ReplayContainer implements ReplayContainerInterface {
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
* does not support a teardown
*/
- public async stop({ forceFlush = false, reason }: { forceFlush?: boolean; reason?: string } = {}): Promise {
+ public async stop({
+ forceFlush = false,
+ reason,
+ }: { forceFlush?: boolean; reason?: ReplayStopReason } = {}): Promise {
if (!this._isEnabled) {
return;
}
@@ -508,8 +511,11 @@ export class ReplayContainer implements ReplayContainerInterface {
// breadcrumbs to trigger a flush (e.g. in `addUpdate()`)
this.recordingMode = 'buffer';
+ const stopReason: ReplayStopReason = reason ?? 'manual';
+ getClient()?.emit('replayEnd', { sessionId: this.session?.id, reason: stopReason });
+
try {
- DEBUG_BUILD && debug.log(`Stopping Replay${reason ? ` triggered by ${reason}` : ''}`);
+ DEBUG_BUILD && debug.log(`Stopping Replay triggered by ${stopReason}`);
resetReplayIdOnDynamicSamplingContext();
@@ -862,6 +868,13 @@ export class ReplayContainer implements ReplayContainerInterface {
this._isEnabled = true;
this._isPaused = false;
+ if (this.session) {
+ getClient()?.emit('replayStart', {
+ sessionId: this.session.id,
+ recordingMode: this.recordingMode,
+ });
+ }
+
this.startRecording();
}
@@ -926,7 +939,7 @@ export class ReplayContainer implements ReplayContainerInterface {
if (!this._isEnabled) {
return;
}
- await this.stop({ reason: 'refresh session' });
+ await this.stop({ reason: 'sessionExpired' });
this.initializeSampling(session.id);
}
@@ -1212,7 +1225,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stop({ reason: 'sendReplay' });
+ this.stop({ reason: 'sendError' });
const client = getClient();
diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts
index 6f8d836611bb..95cfbdd849bf 100644
--- a/packages/replay-internal/src/types/replay.ts
+++ b/packages/replay-internal/src/types/replay.ts
@@ -1,4 +1,11 @@
-import type { Breadcrumb, ErrorEvent, ReplayRecordingData, ReplayRecordingMode, Span } from '@sentry/core';
+import type {
+ Breadcrumb,
+ ErrorEvent,
+ ReplayRecordingData,
+ ReplayRecordingMode,
+ ReplayStopReason,
+ Span,
+} from '@sentry/core';
import type { SKIPPED, THROTTLED } from '../util/throttle';
import type { AllPerformanceEntry, AllPerformanceEntryData, ReplayPerformanceEntry } from './performance';
import type { ReplayFrameEvent } from './replayFrame';
@@ -507,7 +514,7 @@ export interface ReplayContainer {
getContext(): InternalEventContext;
initializeSampling(): void;
start(): void;
- stop(options?: { reason?: string; forceflush?: boolean }): Promise;
+ stop(options?: { reason?: ReplayStopReason; forceFlush?: boolean }): Promise;
pause(): void;
resume(): void;
startRecording(): void;
diff --git a/packages/replay-internal/src/util/addEvent.ts b/packages/replay-internal/src/util/addEvent.ts
index 0cd76227379c..d7c11f5f0ab6 100644
--- a/packages/replay-internal/src/util/addEvent.ts
+++ b/packages/replay-internal/src/util/addEvent.ts
@@ -82,7 +82,7 @@ async function _addEvent(
return await eventBuffer.addEvent(eventAfterPossibleCallback);
} catch (error) {
const isExceeded = error && error instanceof EventBufferSizeExceededError;
- const reason = isExceeded ? 'addEventSizeExceeded' : 'addEvent';
+ const reason = isExceeded ? 'eventBufferOverflow' : 'eventBufferError';
const client = getClient();
if (client) {
diff --git a/packages/replay-internal/test/integration/lifecycleHooks.test.ts b/packages/replay-internal/test/integration/lifecycleHooks.test.ts
new file mode 100644
index 000000000000..814e50491bfb
--- /dev/null
+++ b/packages/replay-internal/test/integration/lifecycleHooks.test.ts
@@ -0,0 +1,109 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import '../utils/mock-internal-setTimeout';
+import type { ReplayEndEvent, ReplayStartEvent } from '@sentry/core';
+import { getClient } from '@sentry/core';
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Replay } from '../../src/integration';
+import type { ReplayContainer } from '../../src/replay';
+import { BASE_TIMESTAMP } from '../index';
+import { resetSdkMock } from '../mocks/resetSdkMock';
+
+describe('Integration | lifecycle hooks', () => {
+ let replay: ReplayContainer;
+ let integration: Replay;
+ let startEvents: ReplayStartEvent[];
+ let endEvents: ReplayEndEvent[];
+ let unsubscribes: Array<() => void>;
+
+ beforeAll(() => {
+ vi.useFakeTimers();
+ });
+
+ beforeEach(async () => {
+ ({ replay, integration } = await resetSdkMock({
+ replayOptions: { stickySession: false },
+ sentryOptions: { replaysSessionSampleRate: 0.0 },
+ autoStart: false,
+ }));
+
+ startEvents = [];
+ endEvents = [];
+ const client = getClient()!;
+ unsubscribes = [
+ client.on('replayStart', event => startEvents.push(event)),
+ client.on('replayEnd', event => endEvents.push(event)),
+ ];
+
+ await vi.runAllTimersAsync();
+ });
+
+ afterEach(async () => {
+ unsubscribes.forEach(off => off());
+ await integration.stop();
+ await vi.runAllTimersAsync();
+ vi.setSystemTime(new Date(BASE_TIMESTAMP));
+ });
+
+ it('fires replayStart with session mode when start() is called', () => {
+ integration.start();
+
+ expect(startEvents).toHaveLength(1);
+ expect(startEvents[0]).toEqual({
+ sessionId: expect.any(String),
+ recordingMode: 'session',
+ });
+ expect(startEvents[0]!.sessionId).toBe(replay.session!.id);
+ });
+
+ it('fires replayStart with buffer mode when startBuffering() is called', () => {
+ integration.startBuffering();
+
+ expect(startEvents).toHaveLength(1);
+ expect(startEvents[0]).toEqual({
+ sessionId: expect.any(String),
+ recordingMode: 'buffer',
+ });
+ });
+
+ it('fires replayEnd with reason "manual" when integration.stop() is called', async () => {
+ integration.start();
+ const sessionId = replay.session!.id;
+
+ await integration.stop();
+
+ expect(endEvents).toHaveLength(1);
+ expect(endEvents[0]).toEqual({ sessionId, reason: 'manual' });
+ });
+
+ it('forwards the internal stop reason to replayEnd subscribers', async () => {
+ integration.start();
+ const sessionId = replay.session!.id;
+
+ await replay.stop({ reason: 'mutationLimit' });
+
+ expect(endEvents).toHaveLength(1);
+ expect(endEvents[0]).toEqual({ sessionId, reason: 'mutationLimit' });
+ });
+
+ it('does not fire replayEnd twice when stop() is called while already stopped', async () => {
+ integration.start();
+
+ await replay.stop({ reason: 'sendError' });
+ await replay.stop({ reason: 'sendError' });
+
+ expect(endEvents).toHaveLength(1);
+ expect(endEvents[0]!.reason).toBe('sendError');
+ });
+
+ it('stops invoking callbacks after the returned unsubscribe is called', () => {
+ const [offStart] = unsubscribes;
+ offStart!();
+
+ integration.start();
+
+ expect(startEvents).toHaveLength(0);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index a718a01b0bf2..d95a0b67c008 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3205,10 +3205,10 @@
dependencies:
"@edge-runtime/primitives" "6.0.0"
-"@effect/vitest@^0.23.9":
- version "0.23.13"
- resolved "https://registry.yarnpkg.com/@effect/vitest/-/vitest-0.23.13.tgz#17edf9d8e3443f080ff8fe93bd37b023612a07a4"
- integrity sha512-F3x2phMXuVzqWexdcYp8v0z1qQHkKxp2UaHNbqZaEjPEp8FBz/iMwbi6iS/oIWzLfGF8XqdP8BGJptvGIJONNw==
+"@effect/vitest@^4.0.0-beta.50":
+ version "4.0.0-beta.50"
+ resolved "https://registry.yarnpkg.com/@effect/vitest/-/vitest-4.0.0-beta.50.tgz#c3945b4a0206fa07160896b641445e16eb5d3214"
+ integrity sha512-bju/iCLZB8oHsVia1i6olo9ZntkZ5TrqmsINudFsRkZfHhu5UuTR3vjic29wykZpPXXONX1wKO0KZZCk+stcKg==
"@ember-data/rfc395-data@^0.0.4":
version "0.0.4"
@@ -5290,6 +5290,36 @@
dependencies:
sparse-bitfield "^3.0.3"
+"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11"
+ integrity sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==
+
+"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz#33677a275204898ad8acbf62734fc4dc0b6a4855"
+ integrity sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz#19edf7cdc2e7063ee328403c1d895a86dd28f4bb"
+ integrity sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz#94fb0543ba2e28766c3fc439cabbe0440ae70159"
+ integrity sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==
+
+"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz#4a0609ab5fe44d07c9c60a11e4484d3c38bbd6e3"
+ integrity sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==
+
+"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242"
+ integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==
+
"@napi-rs/wasm-runtime@0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz#d27788176f250d86e498081e3c5ff48a17606918"
@@ -5793,55 +5823,55 @@
consola "^2.15.0"
node-fetch "^2.6.1"
-"@nx/nx-darwin-arm64@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.5.0.tgz#290f2ed933b08284492307a6d079b887513dcbff"
- integrity sha512-MHnzv6tzucvLsh4oS9FTepj+ct/o8/DPXrQow+9Jid7GSgY59xrDX/8CleJOrwL5lqKEyGW7vv8TR+4wGtEWTA==
-
-"@nx/nx-darwin-x64@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.5.0.tgz#6f1d554054c9255112dea61db3b25aa0276346a9"
- integrity sha512-/0w43hbR5Kia0XeCDZHDt/18FHhpwQs+Y+8TO8/ZsF1RgCI0knJDCyJieYk1yEZAq6E8dStAJnuzxK9uvETs4A==
-
-"@nx/nx-freebsd-x64@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.5.0.tgz#0bd678ac05790fd19bbdf4cd98e6e08ce7b431c6"
- integrity sha512-d4Pd1VFpD272R7kJTWm/Pj49BIz44GZ+QIVSfxlx3GWxyaPd25X9GBanUngL6qpactS+aLTwcoBmnSbZ4PEcEQ==
-
-"@nx/nx-linux-arm-gnueabihf@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.5.0.tgz#f0d8f30c77ec67a2e9f488088889aa5f7beb024d"
- integrity sha512-cCyG23PikIlqE7I6s9j0aHJSqIxnpdOjFOXyRd224XmFyAB8tOyKl7vDD/WugcpAceos28i+Rgz4na189zm48A==
-
-"@nx/nx-linux-arm64-gnu@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.5.0.tgz#0103bc0bb74ca42a4108ab84ad6f21e97929050d"
- integrity sha512-vkQw8737fpta6oVEEqskzwq+d0GeZkGhtyl+U3pAcuUcYTdqbsZaofSQACFnGfngsqpYmlJCWJGU5Te00qcPQw==
-
-"@nx/nx-linux-arm64-musl@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.5.0.tgz#8866ba44bcf2bd2420ab6a6532316029482f0f98"
- integrity sha512-BkEsFBsnKrDK11N914rr5YKyIJwYoSVItJ7VzsQZIqAX0C7PdJeQ7KzqOGwoezbabdLmzFOBNg6s/o1ujoEYxw==
-
-"@nx/nx-linux-x64-gnu@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.5.0.tgz#0a27449cdaeba2d11d6673a42b6494a171d04210"
- integrity sha512-Dsqoz4hWmqehMMm8oJY6Q0ckEUeeHz4+T/C8nHyDaaj/REKCSmqYf/+QV6f2Z5Up/CsQ/hoAsWYEhCHZ0tcSFg==
-
-"@nx/nx-linux-x64-musl@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.5.0.tgz#6263f05c1f984502f40d6f301fed89645f9fa0cc"
- integrity sha512-Lcj/61BpsT85Qhm3hNTwQFrqGtsjLC+y4Kk21dh22d1/E5pOdVAwPXBuWrSPNo4lX+ESNoKmwxWjfgW3uoB05g==
-
-"@nx/nx-win32-arm64-msvc@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.5.0.tgz#0b75a32d592eda78740fff6fc5f2b259518e7f82"
- integrity sha512-0DlnBDLvqNtseCyBBoBst0gwux+N91RBc4E41JDDcLcWpfntcwCQM39D6lA5qdma/0L7U0PUM7MYV9Q6igJMkQ==
-
-"@nx/nx-win32-x64-msvc@22.5.0":
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.5.0.tgz#90b5b8230c37e2c6e09f1ed77169cabb79c4123f"
- integrity sha512-kMMsU4PxKQ76NvmPFKT0/RlzRTiuUfuNWVJUmsWF1onVcBkXgQNKkmLcSJk3wGwML5/tHChjtlI7Hpo705Uv/g==
+"@nx/nx-darwin-arm64@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.6.5.tgz#d1c88bc4b94c24b4d3e217dd52a0888ef0c63def"
+ integrity sha512-qT77Omkg5xQuL2+pDbneX2tI+XW5ZeayMylu7UUgK8OhTrAkJLKjpuYRH4xT5XBipxbDtlxmO3aLS3Ib1pKzJQ==
+
+"@nx/nx-darwin-x64@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.6.5.tgz#55b5e7c9137dbfea2acfd7043a58bdaca4f4e9df"
+ integrity sha512-9jICxb7vfJ56y/7Yuh3b/n1QJqWxO9xnXKYEs6SO8xPoW/KomVckILGc1C6RQSs6/3ixVJC7k1Dh1wm5tKPFrg==
+
+"@nx/nx-freebsd-x64@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.6.5.tgz#499e1fb013cf6fab217257d89405b9e19bf624a9"
+ integrity sha512-6B1wEKpqz5dI3AGMqttAVnA6M3DB/besAtuGyQiymK9ROlta1iuWgCcIYwcCQyhLn2Rx7vqj447KKcgCa8HlVw==
+
+"@nx/nx-linux-arm-gnueabihf@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.6.5.tgz#e492bd050aee479976c290fb5b23f290c284d665"
+ integrity sha512-xV50B8mnDPboct7JkAHftajI02s+8FszA8WTzhore+YGR+lEKHTLpucwGEaQuMlSdLplH7pQix4B4uK5pcMhZw==
+
+"@nx/nx-linux-arm64-gnu@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.6.5.tgz#4bdab184c1405e87680bbc49bb1ee79a3d8b0200"
+ integrity sha512-2JkWuMGj+HpW6oPAvU5VdAx1afTnEbiM10Y3YOrl3fipWV4BiP5VDx762QTrfCraP4hl6yqTgvTe7F9xaby+jQ==
+
+"@nx/nx-linux-arm64-musl@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.6.5.tgz#c1eb8ea0bb92cfbe50f3ccebdf931ce79d2052f6"
+ integrity sha512-Z/zMqFClnEyqDXouJKEPoWVhMQIif5F0YuECWBYjd3ZLwQsXGTItoh+6Wm3XF/nGMA2uLOHyTq/X7iFXQY3RzA==
+
+"@nx/nx-linux-x64-gnu@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.6.5.tgz#4f905688d30649c51035d71dca26a55666819cc2"
+ integrity sha512-FlotSyqNnaXSn0K+yWw+hRdYBwusABrPgKLyixfJIYRzsy+xPKN6pON6vZfqGwzuWF/9mEGReRz+iM8PiW0XSg==
+
+"@nx/nx-linux-x64-musl@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.6.5.tgz#94fed5da71aad67290ec10e35e30b9d065ed1c2e"
+ integrity sha512-RVOe2qcwhoIx6mxQURPjUfAW5SEOmT2gdhewvdcvX9ICq1hj5B2VarmkhTg0qroO7xiyqOqwq26mCzoV2I3NgQ==
+
+"@nx/nx-win32-arm64-msvc@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.6.5.tgz#60e3ad6379fbd6e9ae1d189d9803f156bcc29774"
+ integrity sha512-ZqurqI8VuYnsr2Kn4K4t+Gx6j/BZdf6qz/6Tv4A7XQQ6oNYVQgTqoNEFj+CCkVaIe6aIdCWpousFLqs+ZgBqYQ==
+
+"@nx/nx-win32-x64-msvc@22.6.5":
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.6.5.tgz#d07d4d9d48c99f7b4c8c8a0dc2b14bf9239a3076"
+ integrity sha512-i2QFBJIuaYg9BHxrrnBV4O7W9rVL2k0pSIdk/rRp3EYJEU93iUng+qbZiY9wh1xvmXuUCE2G7TRd+8/SG/RFKg==
"@octokit/auth-token@^2.4.4":
version "2.5.0"
@@ -6261,15 +6291,6 @@
"@opentelemetry/semantic-conventions" "^1.33.0"
"@types/tedious" "^4.0.14"
-"@opentelemetry/instrumentation-undici@0.24.0":
- version "0.24.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.24.0.tgz#6ad41245012742899294edf65aa79fd190369094"
- integrity sha512-oKzZ3uvqP17sV0EsoQcJgjEfIp0kiZRbYu/eD8p13Cbahumf8lb/xpYeNr/hfAJ4owzEtIDcGIjprfLcYbIKBQ==
- dependencies:
- "@opentelemetry/core" "^2.0.0"
- "@opentelemetry/instrumentation" "^0.214.0"
- "@opentelemetry/semantic-conventions" "^1.24.0"
-
"@opentelemetry/instrumentation@0.214.0", "@opentelemetry/instrumentation@^0.214.0":
version "0.214.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz#2649e8a29a8c4748bc583d35281c80632f046e25"
@@ -6358,7 +6379,7 @@
"@opentelemetry/resources" "2.6.1"
"@opentelemetry/semantic-conventions" "^1.29.0"
-"@opentelemetry/semantic-conventions@^1.24.0", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0":
+"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0":
version "1.40.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3"
integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==
@@ -8529,10 +8550,10 @@
resolved "https://registry.yarnpkg.com/@speed-highlight/core/-/core-1.2.14.tgz#5d7fe87410d2d779bd0b7680f7a706466f363314"
integrity sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==
-"@standard-schema/spec@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
- integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
+"@standard-schema/spec@^1.0.0", "@standard-schema/spec@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
+ integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@supabase/auth-js@2.69.1":
version "2.69.1"
@@ -10826,7 +10847,7 @@ accepts@^2.0.0:
mime-types "^3.0.0"
negotiator "^1.0.0"
-accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
+accepts@~1.3.4, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
@@ -10980,9 +11001,9 @@ ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.5:
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.10.0, ajv@^8.9.0:
- version "8.17.1"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
- integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+ version "8.18.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
+ integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==
dependencies:
fast-deep-equal "^3.1.3"
fast-uri "^3.0.1"
@@ -11586,9 +11607,9 @@ async@^2.4.1, async@^2.6.4:
lodash "^4.17.14"
async@^3.2.3, async@^3.2.4:
- version "3.2.5"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
- integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+ integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
async@~0.2.9:
version "0.2.10"
@@ -11638,7 +11659,7 @@ 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.15.0, axios@^1.12.0:
+axios@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f"
integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==
@@ -12247,10 +12268,10 @@ brace-expansion@^2.0.1, brace-expansion@^2.0.2:
dependencies:
balanced-match "^1.0.0"
-brace-expansion@^5.0.2:
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef"
- integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==
+brace-expansion@^5.0.2, brace-expansion@^5.0.5:
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb"
+ integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==
dependencies:
balanced-match "^4.0.2"
@@ -12754,7 +12775,7 @@ buffer-crc32@~0.2.3:
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
-buffer-equal-constant-time@1.0.1:
+buffer-equal-constant-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
@@ -12834,12 +12855,7 @@ bytes@1:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
integrity sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=
-bytes@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
- integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
-
-bytes@^3.1.2, bytes@~3.1.2:
+bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
@@ -13107,7 +13123,7 @@ chalk@^3.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
+chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@@ -13598,7 +13614,7 @@ compress-commons@^6.0.2:
normalize-path "^3.0.0"
readable-stream "^4.0.0"
-compressible@~2.0.16:
+compressible@~2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
@@ -13606,16 +13622,16 @@ compressible@~2.0.16:
mime-db ">= 1.43.0 < 2"
compression@^1.7.4:
- version "1.7.4"
- resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
- integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
+ integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
dependencies:
- accepts "~1.3.5"
- bytes "3.0.0"
- compressible "~2.0.16"
+ bytes "3.1.2"
+ compressible "~2.0.18"
debug "2.6.9"
- on-headers "~1.0.2"
- safe-buffer "5.1.2"
+ negotiator "~0.6.4"
+ on-headers "~1.1.0"
+ safe-buffer "5.2.1"
vary "~1.1.2"
concat-map@0.0.1:
@@ -14542,7 +14558,7 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
-detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2:
+detect-libc@^2.0.0, detect-libc@^2.0.1, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4, detect-libc@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
@@ -14927,20 +14943,26 @@ effect@3.16.12:
"@standard-schema/spec" "^1.0.0"
fast-check "^3.23.1"
-effect@^3.21.0:
- version "3.21.0"
- resolved "https://registry.yarnpkg.com/effect/-/effect-3.21.0.tgz#ce222ce8f785b9e63f104b9a4ead985e7965f2c0"
- integrity sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==
- dependencies:
- "@standard-schema/spec" "^1.0.0"
- fast-check "^3.23.1"
-
-ejs@^3.1.7:
- version "3.1.8"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b"
- integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==
- dependencies:
- jake "^10.8.5"
+effect@^4.0.0-beta.50:
+ version "4.0.0-beta.50"
+ resolved "https://registry.yarnpkg.com/effect/-/effect-4.0.0-beta.50.tgz#c4fbc42adad53428242b8002390bde69b48feb0d"
+ integrity sha512-UsENighZms6LWDSnF/05F9JinDAewV3sGXHAt9M7+dL3VnoFZIwduFxXvmFc7QJm7iV1s7rB98hv1SD3ALA9qg==
+ dependencies:
+ "@standard-schema/spec" "^1.1.0"
+ fast-check "^4.6.0"
+ find-my-way-ts "^0.1.6"
+ ini "^6.0.0"
+ kubernetes-types "^1.30.0"
+ msgpackr "^1.11.9"
+ multipasta "^0.2.7"
+ toml "^4.1.1"
+ uuid "^13.0.0"
+ yaml "^2.8.3"
+
+ejs@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-5.0.1.tgz#179523a437ed448543ad1b76ca4fb4c2e8950304"
+ integrity sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==
electron-to-chromium@^1.5.263:
version "1.5.286"
@@ -15552,9 +15574,9 @@ encoding@^0.1.13:
iconv-lite "^0.6.2"
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
- version "1.4.4"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
- integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
+ integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
dependencies:
once "^1.4.0"
@@ -17023,6 +17045,13 @@ fast-check@^3.23.1:
dependencies:
pure-rand "^6.1.0"
+fast-check@^4.6.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-4.7.0.tgz#36c0051b9c968965e8970e88e63eee946fe45f8f"
+ integrity sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==
+ dependencies:
+ pure-rand "^8.0.0"
+
fast-content-type-parse@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb"
@@ -17234,13 +17263,6 @@ file-uri-to-path@1.0.0:
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
-filelist@^1.0.1:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
- integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
- dependencies:
- minimatch "^5.0.1"
-
filesize@^10.0.5:
version "10.1.6"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361"
@@ -17348,6 +17370,11 @@ find-index@^1.1.0:
resolved "https://registry.yarnpkg.com/find-index/-/find-index-1.1.1.tgz#4b221f8d46b7f8bea33d8faed953f3ca7a081cbc"
integrity sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==
+find-my-way-ts@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz#37f7b8433d0f61e7fe7290772240b0c133b0ebf2"
+ integrity sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==
+
find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -18807,9 +18834,9 @@ htmlparser2@^6.1.0:
entities "^2.0.0"
http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
- integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
+ integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
http-deceiver@^1.2.7:
version "1.2.7"
@@ -19138,6 +19165,11 @@ ini@^2.0.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
+ini@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-6.0.0.tgz#efc7642b276f6a37d22fdf56ef50889d7146bf30"
+ integrity sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==
+
injection-js@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.4.0.tgz#ebe8871b1a349f23294eaa751bbd8209a636e754"
@@ -19961,16 +19993,6 @@ jackspeak@^3.1.2:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
-jake@^10.8.5:
- version "10.8.5"
- resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
- integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
- dependencies:
- async "^3.2.3"
- chalk "^4.0.2"
- filelist "^1.0.1"
- minimatch "^3.0.4"
-
jest-diff@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a"
@@ -20273,11 +20295,11 @@ jsonparse@^1.3.1:
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jsonwebtoken@^9.0.0:
- version "9.0.2"
- resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
- integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
+ integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==
dependencies:
- jws "^3.2.2"
+ jws "^4.0.1"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
@@ -20296,38 +20318,21 @@ jsonwebtoken@^9.0.0:
array-includes "^3.1.2"
object.assign "^4.1.2"
-jwa@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
- integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
- dependencies:
- buffer-equal-constant-time "1.0.1"
- ecdsa-sig-formatter "1.0.11"
- safe-buffer "^5.0.1"
-
-jwa@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
- integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
+jwa@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
+ integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==
dependencies:
- buffer-equal-constant-time "1.0.1"
+ buffer-equal-constant-time "^1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
-jws@^3.2.2:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
- integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
- dependencies:
- jwa "^1.4.1"
- safe-buffer "^5.0.1"
-
-jws@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
- integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
+jws@^4.0.0, jws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
+ integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==
dependencies:
- jwa "^2.0.0"
+ jwa "^2.0.1"
safe-buffer "^5.0.1"
jwt-decode@^4.0.0:
@@ -20416,6 +20421,11 @@ knitwork@^1.2.0, knitwork@^1.3.0:
resolved "https://registry.yarnpkg.com/knitwork/-/knitwork-1.3.0.tgz#4a0d0b0d45378cac909ee1117481392522bd08a4"
integrity sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==
+kubernetes-types@^1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz#f686cacb08ffc5f7e89254899c2153c723420116"
+ integrity sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==
+
kuler@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
@@ -20895,11 +20905,16 @@ lodash.uniq@^4.2.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.21:
+lodash@4.17.23, lodash@~4.17.21:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
+lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
+ version "4.18.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
+ integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
+
log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -21952,7 +21967,7 @@ minimalistic-assert@^1.0.0:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-minimatch@10.1.1, minimatch@10.2.4, minimatch@^10.2.2, minimatch@^10.2.4:
+minimatch@10.2.4:
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
@@ -21973,7 +21988,14 @@ minimatch@5.1.0, minimatch@5.1.9, minimatch@^5.0.1, minimatch@^5.1.0:
dependencies:
brace-expansion "^2.0.1"
-minimatch@^7.4.1:
+minimatch@^10.2.2, minimatch@^10.2.4:
+ version "10.2.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1"
+ integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==
+ dependencies:
+ brace-expansion "^5.0.5"
+
+minimatch@^7.4.1, minimatch@~7.4.9:
version "7.4.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.9.tgz#ef35412b1b36261b78ef1b2f0db29b759bbcaf5d"
integrity sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==
@@ -21988,11 +22010,11 @@ minimatch@^8.0.2:
brace-expansion "^2.0.1"
minimatch@^9.0.0, minimatch@^9.0.4:
- version "9.0.8"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.8.tgz#bb3aa36d7b42ea77a93c44d5c1082b188112497c"
- integrity sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==
+ version "9.0.9"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
+ integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
- brace-expansion "^5.0.2"
+ brace-expansion "^2.0.2"
minimist@^0.2.1:
version "0.2.4"
@@ -22350,6 +22372,27 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+msgpackr-extract@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz#e9d87023de39ce714872f9e9504e3c1996d61012"
+ integrity sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==
+ dependencies:
+ node-gyp-build-optional-packages "5.2.2"
+ optionalDependencies:
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.3"
+ "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
+
+msgpackr@^1.11.9:
+ version "1.11.9"
+ resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.9.tgz#1aa99ed379a066374ac82b62f8ad70723bbd3a59"
+ integrity sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==
+ optionalDependencies:
+ msgpackr-extract "^3.0.2"
+
multer@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.2.tgz#08a8aa8255865388c387aaf041426b0c87bf58dd"
@@ -22371,6 +22414,11 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
+multipasta@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/multipasta/-/multipasta-0.2.7.tgz#fa8fb38be65eb951fa57cad9e8e758107946eee9"
+ integrity sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==
+
mustache@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
@@ -22508,11 +22556,16 @@ needle@^3.1.0:
iconv-lite "^0.6.3"
sax "^1.2.4"
-negotiator@0.6.3, negotiator@^0.6.3:
+negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+negotiator@^0.6.3, negotiator@~0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
+ integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
+
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
@@ -22769,6 +22822,13 @@ node-forge@^1, node-forge@^1.3.1:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2"
integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==
+node-gyp-build-optional-packages@5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4"
+ integrity sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==
+ dependencies:
+ detect-libc "^2.0.1"
+
node-gyp-build@^4.2.2:
version "4.6.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055"
@@ -22795,11 +22855,6 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
-node-machine-id@1.1.12:
- version "1.1.12"
- resolved "https://registry.yarnpkg.com/node-machine-id/-/node-machine-id-1.1.12.tgz#37904eee1e59b320bb9c5d6c0a59f3b469cb6267"
- integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==
-
node-mock-http@^1.0.0, node-mock-http@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.4.tgz#21f2ab4ce2fe4fbe8a660d7c5195a1db85e042a4"
@@ -23176,22 +23231,22 @@ nwsapi@^2.2.4:
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.9.tgz#7f3303218372db2e9f27c27766bcfc59ae7e61c6"
integrity sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==
-nx@22.5.0:
- version "22.5.0"
- resolved "https://registry.yarnpkg.com/nx/-/nx-22.5.0.tgz#200898de49eec4df41eef5a44152721b6ee453df"
- integrity sha512-GOHhDHXvuscD28Hpj1bP38oVrCgZ/+5UWjA8R/VkpbtkfMHgRZ0uHlfKLYXQAZIsjmTq7Tr+e4QchJt0e76n0w==
+nx@22.6.5:
+ version "22.6.5"
+ resolved "https://registry.yarnpkg.com/nx/-/nx-22.6.5.tgz#71bfb4ccc35f2103397eb6ef251204256d0af541"
+ integrity sha512-VRKhDAt684dXNSz9MNjE7MekkCfQF41P2PSx5jEWQjDEP1Z4jFZbyeygWs5ZyOroG7/n0MoWAJTe6ftvIcBOAg==
dependencies:
"@napi-rs/wasm-runtime" "0.2.4"
"@yarnpkg/lockfile" "^1.1.0"
"@yarnpkg/parsers" "3.0.2"
"@zkochan/js-yaml" "0.0.7"
- axios "^1.12.0"
+ axios "1.15.0"
cli-cursor "3.1.0"
cli-spinners "2.6.1"
cliui "^8.0.1"
dotenv "~16.4.5"
dotenv-expand "~11.0.6"
- ejs "^3.1.7"
+ ejs "5.0.1"
enquirer "~2.3.6"
figures "3.2.0"
flat "^5.0.2"
@@ -23200,14 +23255,14 @@ nx@22.5.0:
jest-diff "^30.0.2"
jsonc-parser "3.2.0"
lines-and-columns "2.0.3"
- minimatch "10.1.1"
- node-machine-id "1.1.12"
+ minimatch "10.2.4"
npm-run-path "^4.0.1"
open "^8.4.0"
ora "5.3.0"
picocolors "^1.1.0"
resolve.exports "2.0.3"
semver "^7.6.3"
+ smol-toml "1.6.1"
string-width "^4.2.3"
tar-stream "~2.2.0"
tmp "~0.2.1"
@@ -23218,16 +23273,16 @@ nx@22.5.0:
yargs "^17.6.2"
yargs-parser "21.1.1"
optionalDependencies:
- "@nx/nx-darwin-arm64" "22.5.0"
- "@nx/nx-darwin-x64" "22.5.0"
- "@nx/nx-freebsd-x64" "22.5.0"
- "@nx/nx-linux-arm-gnueabihf" "22.5.0"
- "@nx/nx-linux-arm64-gnu" "22.5.0"
- "@nx/nx-linux-arm64-musl" "22.5.0"
- "@nx/nx-linux-x64-gnu" "22.5.0"
- "@nx/nx-linux-x64-musl" "22.5.0"
- "@nx/nx-win32-arm64-msvc" "22.5.0"
- "@nx/nx-win32-x64-msvc" "22.5.0"
+ "@nx/nx-darwin-arm64" "22.6.5"
+ "@nx/nx-darwin-x64" "22.6.5"
+ "@nx/nx-freebsd-x64" "22.6.5"
+ "@nx/nx-linux-arm-gnueabihf" "22.6.5"
+ "@nx/nx-linux-arm64-gnu" "22.6.5"
+ "@nx/nx-linux-arm64-musl" "22.6.5"
+ "@nx/nx-linux-x64-gnu" "22.6.5"
+ "@nx/nx-linux-x64-musl" "22.6.5"
+ "@nx/nx-win32-arm64-msvc" "22.6.5"
+ "@nx/nx-win32-x64-msvc" "22.6.5"
nypm@^0.6.0, nypm@^0.6.2, nypm@^0.6.5:
version "0.6.5"
@@ -23416,6 +23471,11 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+on-headers@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
+ integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -25352,9 +25412,9 @@ property-information@^7.0.0:
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
protobufjs@^7.0.0:
- version "7.5.4"
- resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a"
- integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.5.tgz#b7089ca4410374c75150baf277353ef76db69f96"
+ integrity sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
@@ -25429,6 +25489,11 @@ pure-rand@^6.1.0:
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2"
integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==
+pure-rand@^8.0.0:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-8.4.0.tgz#1d9e26e9c0555486e08ae300d02796af8dec1cd0"
+ integrity sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==
+
qs@^6.14.0, qs@^6.14.1, qs@^6.4.0, qs@~6.14.0:
version "6.14.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.2.tgz#b5634cf9d9ad9898e31fba3504e866e8efb6798c"
@@ -27422,6 +27487,11 @@ smob@^1.0.0:
resolved "https://registry.yarnpkg.com/smob/-/smob-1.4.1.tgz#66270e7df6a7527664816c5b577a23f17ba6f5b5"
integrity sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==
+smol-toml@1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.6.1.tgz#4fceb5f7c4b86c2544024ef686e12ff0983465be"
+ integrity sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==
+
snake-case@^3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
@@ -28887,6 +28957,11 @@ token-types@^6.1.1:
"@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1"
+toml@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/toml/-/toml-4.1.1.tgz#ab8248d0403ba2c02ffcf8515b42f0dcf0d6d1b5"
+ integrity sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==
+
totalist@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd"
@@ -29918,6 +29993,11 @@ uuid@^11.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
+uuid@^13.0.0:
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
+ integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==
+
uuid@^9.0.0, uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
@@ -31051,16 +31131,16 @@ yam@^1.0.0:
fs-extra "^4.0.2"
lodash.merge "^4.6.0"
-yaml@2.8.3, yaml@^2.6.0, yaml@^2.8.0:
- version "2.8.3"
- resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
- integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
-
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yaml@^2.6.0, yaml@^2.8.0, yaml@^2.8.3:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d"
+ integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==
+
yargs-parser@21.1.1, yargs-parser@^21.0.0, yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"