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

+ +
+
+

Error Tests

+ +
+ +
+

Effect Span Tests

+ + +
+ +
+

Effect Failure Tests

+ + +
+ + +
+ +
+

Log Tests

+ + +
+ + +
+ + +
+ + 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"