diff --git a/.craft.yml b/.craft.yml index d9be4d4b3fa4..b5646547f113 100644 --- a/.craft.yml +++ b/.craft.yml @@ -157,6 +157,24 @@ targets: - nodejs20.x license: MIT + # NOTE: We publish the v8 layer under its own name so people on v8 can still get patches + # whenever we release a new v8 versionβ€”otherwise we would overwrite the current major lambda layer. + + # AWS Lambda Layer target + - name: aws-lambda-layer + includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ + layerName: SentryNodeServerlessSDKv8 + compatibleRuntimes: + - name: node + versions: + - nodejs10.x + - nodejs12.x + - nodejs14.x + - nodejs16.x + - nodejs18.x + - nodejs20.x + license: MIT + # CDN Bundle Target - name: gcs id: 'browser-cdn-bundles' diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index 8fc0aeba7330..5eac05a32a2d 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -4,6 +4,9 @@ inputs: browsers: description: 'What browsers to install.' default: 'chromium webkit firefox' + cwd: + description: 'The working directory to run Playwright in.' + default: '.' runs: using: "composite" @@ -12,6 +15,8 @@ runs: id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT shell: bash + working-directory: ${{ inputs.cwd }} + - name: Restore cached playwright binaries uses: actions/cache/restore@v4 @@ -26,11 +31,13 @@ runs: run: npx playwright install chromium webkit firefox --with-deps if: steps.playwright-cache.outputs.cache-hit != 'true' shell: bash + working-directory: ${{ inputs.cwd }} - name: Install Playwright system dependencies only (cached) run: npx playwright install-deps ${{ inputs.browsers || 'chromium webkit firefox' }} if: steps.playwright-cache.outputs.cache-hit == 'true' shell: bash + working-directory: ${{ inputs.cwd }} # Only store cache on develop branch - name: Store cached playwright binaries diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a0fb3888d22..bf9ba21376bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -920,11 +920,6 @@ jobs: if: steps.restore-tarball-cache.outputs.cache-hit != 'true' run: yarn build:tarball - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - - name: Get node version id: versions run: | @@ -945,6 +940,12 @@ jobs: timeout-minutes: 7 run: pnpm ${{ matrix.build-command || 'test:build' }} + - name: Install Playwright + uses: ./.github/actions/install-playwright + with: + browsers: chromium + cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 @@ -984,7 +985,7 @@ jobs: # - We skip optional tests on release branches job_optional_e2e_tests: - name: E2E ${{ matrix.label || matrix.test-application }} Test + name: E2E ${{ matrix.label || matrix.test-application }} Test (optional) # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks # We need to add the `always()` check here because the previous step has this as well :( # See: https://github.com/actions/runner/issues/2205 @@ -1039,11 +1040,6 @@ jobs: if: steps.restore-tarball-cache.outputs.cache-hit != 'true' run: yarn build:tarball - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - - name: Get node version id: versions run: | @@ -1064,6 +1060,12 @@ jobs: timeout-minutes: 7 run: pnpm ${{ matrix.build-command || 'test:build' }} + - name: Install Playwright + uses: ./.github/actions/install-playwright + with: + browsers: chromium + cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 @@ -1408,6 +1410,10 @@ jobs: key: ${{ needs.job_build.outputs.dependency_cache_key }} enableCrossOsArchive: true + - name: Increase yarn network timeout on Windows + if: contains(matrix.os, 'windows') + run: yarn config set network-timeout 600000 -g + - name: Install dependencies env: SKIP_PLAYWRIGHT_BROWSER_INSTALL: "1" @@ -1418,10 +1424,6 @@ jobs: run: | git config --global --add safe.directory "*" - - name: Increase yarn network timeout on Windows - if: contains(matrix.os, 'windows') - run: yarn config set network-timeout 600000 -g - - name: Setup python uses: actions/setup-python@v5 if: ${{ !contains(matrix.container, 'alpine') }} diff --git a/.github/workflows/issue-package-label.yml b/.github/workflows/issue-package-label.yml index ad5edbd3b398..39aaeaafdcc7 100644 --- a/.github/workflows/issue-package-label.yml +++ b/.github/workflows/issue-package-label.yml @@ -83,12 +83,12 @@ jobs: "@sentry.solid": { "label": "Package: solidstart" }, - "@sentry.svelte": { - "label": "Package: svelte" - }, "@sentry.sveltekit": { "label": "Package: sveltekit" }, + "@sentry.svelte": { + "label": "Package: svelte" + }, "@sentry.vue": { "label": "Package: vue" }, diff --git a/.size-limit.js b/.size-limit.js index dc2b7af8128f..844d57447495 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -79,7 +79,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '79 KB', + limit: '80 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -166,7 +166,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay)', path: createCDNPath('bundle.tracing.replay.min.js'), gzip: true, - limit: '74 KB', + limit: '80 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback)', @@ -194,7 +194,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '230 KB', + limit: '240 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed', @@ -228,7 +228,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '140 KB', + limit: '170 KB', }, { name: '@sentry/node - without tracing', @@ -260,7 +260,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '130 KB', + limit: '135 KB', }, ]; diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ae2d6bca2c..435c5b3d2d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,115 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott +## 8.43.0 + +### Important Changes + +- **feat(nuxt): Add option autoInjectServerSentry (no default import()) ([#14553](https://github.com/getsentry/sentry-javascript/pull/14553))** + + Using the dynamic `import()` as the default behavior for initializing the SDK on the server-side did not work for every project. + The default behavior of the SDK has been changed, and you now need to **use the `--import` flag to initialize Sentry on the server-side** to leverage full functionality. + + Example with `--import`: + + ```bash + node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs + ``` + + In case you are not able to use the `--import` flag, you can enable auto-injecting Sentry in the `nuxt.config.ts` (comes with limitations): + + ```ts + sentry: { + autoInjectServerSentry: 'top-level-import', // or 'experimental_dynamic-import' + }, + ``` + +- **feat(browser): Adds LaunchDarkly and OpenFeature integrations ([#14207](https://github.com/getsentry/sentry-javascript/pull/14207))** + + Adds browser SDK integrations for tracking feature flag evaluations through the LaunchDarkly JS SDK and OpenFeature Web SDK: + + ```ts + import * as Sentry from '@sentry/browser'; + + Sentry.init({ + integrations: [ + // Track LaunchDarkly feature flags + Sentry.launchDarklyIntegration(), + // Track OpenFeature feature flags + Sentry.openFeatureIntegration(), + ], + }); + ``` + + - Read more about the [Feature Flags](https://develop.sentry.dev/sdk/expected-features/#feature-flags) feature in Sentry. + - Read more about the [LaunchDarkly SDK Integration](https://docs.sentry.io/platforms/javascript/configuration/integrations/launchdarkly/). + - Read more about the [OpenFeature SDK Integration](https://docs.sentry.io/platforms/javascript/configuration/integrations/openfeature/). + +- **feat(browser): Add `featureFlagsIntegration` for custom tracking of flag evaluations ([#14582](https://github.com/getsentry/sentry-javascript/pull/14582))** + + Adds a browser integration to manually track feature flags with an API. Feature flags are attached to subsequent error events: + + ```ts + import * as Sentry from '@sentry/browser'; + + const featureFlagsIntegrationInstance = Sentry.featureFlagsIntegration(); + + Sentry.init({ + // Initialize the SDK with the feature flag integration + integrations: [featureFlagsIntegrationInstance], + }); + + // Manually track a feature flag + featureFlagsIntegrationInstance.addFeatureFlag('my-feature', true); + ``` + +- **feat(astro): Add Astro 5 support ([#14613](https://github.com/getsentry/sentry-javascript/pull/14613))** + + With this release, the Sentry Astro SDK officially supports Astro 5. + +### Deprecations + +- feat(nextjs): Deprecate typedef for `hideSourceMaps` ([#14594](https://github.com/getsentry/sentry-javascript/pull/14594)) + + The functionality of `hideSourceMaps` was removed in version 8 but was forgotten to be deprecated and removed. + It will be completely removed in the next major version. + +- feat(core): Deprecate APIs around `RequestSession`s ([#14566](https://github.com/getsentry/sentry-javascript/pull/14566)) + + The APIs around `RequestSession`s are mostly used internally. + Going forward the SDK will not expose concepts around `RequestSession`s. + Instead, functionality around server-side [Release Health](https://docs.sentry.io/product/releases/health/) will be managed in integrations. + +### Other Changes + +- feat(browser): Add `browserSessionIntegration` ([#14551](https://github.com/getsentry/sentry-javascript/pull/14551)) +- feat(core): Add `raw_security` envelope types ([#14562](https://github.com/getsentry/sentry-javascript/pull/14562)) +- feat(deps): Bump @opentelemetry/instrumentation from 0.55.0 to 0.56.0 ([#14625](https://github.com/getsentry/sentry-javascript/pull/14625)) +- feat(deps): Bump @sentry/cli from 2.38.2 to 2.39.1 ([#14626](https://github.com/getsentry/sentry-javascript/pull/14626)) +- feat(deps): Bump @sentry/rollup-plugin from 2.22.6 to 2.22.7 ([#14622](https://github.com/getsentry/sentry-javascript/pull/14622)) +- feat(deps): Bump @sentry/webpack-plugin from 2.22.6 to 2.22.7 ([#14623](https://github.com/getsentry/sentry-javascript/pull/14623)) +- feat(nestjs): Add fastify support ([#14549](https://github.com/getsentry/sentry-javascript/pull/14549)) +- feat(node): Add @vercel/ai instrumentation ([#13892](https://github.com/getsentry/sentry-javascript/pull/13892)) +- feat(node): Add `disableAnrDetectionForCallback` function ([#14359](https://github.com/getsentry/sentry-javascript/pull/14359)) +- feat(node): Add `trackIncomingRequestsAsSessions` option to http integration ([#14567](https://github.com/getsentry/sentry-javascript/pull/14567)) +- feat(nuxt): Add option `autoInjectServerSentry` (no default `import()`) ([#14553](https://github.com/getsentry/sentry-javascript/pull/14553)) +- feat(nuxt): Add warning when Netlify or Vercel build is discovered ([#13868](https://github.com/getsentry/sentry-javascript/pull/13868)) +- feat(nuxt): Improve serverless event flushing and scope isolation ([#14605](https://github.com/getsentry/sentry-javascript/pull/14605)) +- feat(opentelemetry): Stop looking at propagation context for span creation ([#14481](https://github.com/getsentry/sentry-javascript/pull/14481)) +- feat(opentelemetry): Update OpenTelemetry dependencies to `^1.29.0` ([#14590](https://github.com/getsentry/sentry-javascript/pull/14590)) +- feat(opentelemetry): Update OpenTelemetry dependencies to `1.28.0` ([#14547](https://github.com/getsentry/sentry-javascript/pull/14547)) +- feat(replay): Upgrade rrweb packages to 2.30.0 ([#14597](https://github.com/getsentry/sentry-javascript/pull/14597)) +- fix(core): Decode `filename` and `module` stack frame properties in Node stack parser ([#14544](https://github.com/getsentry/sentry-javascript/pull/14544)) +- fix(core): Filter out unactionable CEFSharp promise rejection error by default ([#14595](https://github.com/getsentry/sentry-javascript/pull/14595)) +- fix(nextjs): Don't show warning about devtool option ([#14552](https://github.com/getsentry/sentry-javascript/pull/14552)) +- fix(nextjs): Only apply tracing metadata to data fetcher data when data is an object ([#14575](https://github.com/getsentry/sentry-javascript/pull/14575)) +- fix(node): Guard against invalid `maxSpanWaitDuration` values ([#14632](https://github.com/getsentry/sentry-javascript/pull/14632)) +- fix(react): Match routes with `parseSearch` option in TanStack Router instrumentation ([#14328](https://github.com/getsentry/sentry-javascript/pull/14328)) +- fix(sveltekit): Fix git SHA not being picked up for release ([#14540](https://github.com/getsentry/sentry-javascript/pull/14540)) +- fix(types): Fix generic exports with default ([#14576](https://github.com/getsentry/sentry-javascript/pull/14576)) + +Work in this release was contributed by @lsmurray. Thank you for your contribution! + ## 8.42.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index ee0d598fa960..28c12a099559 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -42,7 +42,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.44.1", - "@sentry-internal/rrweb": "2.29.0", + "@sentry-internal/rrweb": "2.30.0", "@sentry/browser": "8.42.0", "axios": "1.7.7", "babel-loader": "^8.2.2", diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts new file mode 100644 index 000000000000..ed909b19d1fa --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); + for (let i = 1; i <= bufferSize; i++) { + flagsIntegration.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration.addFeatureFlag(`feat${bufferSize + 1}`, true); // eviction + flagsIntegration.addFeatureFlag('feat3', true); // update + return true; + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); // trigger error + const req = await reqPromise; + const event = envelopeRequestParser(req); + + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js new file mode 100644 index 000000000000..894f46aaa102 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +// Not using this as we want to test the getIntegrationByName() approach +// window.sentryFeatureFlagsIntegration = Sentry.featureFlagsIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [Sentry.featureFlagsIntegration()], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js new file mode 100644 index 000000000000..e6697408128c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js @@ -0,0 +1,3 @@ +document.getElementById('error').addEventListener('click', () => { + throw new Error('Button triggered error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html new file mode 100644 index 000000000000..9330c6c679f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts new file mode 100644 index 000000000000..97ecf2d961a7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +import type { Scope } from '@sentry/browser'; + +sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + + await page.evaluate(() => { + const Sentry = (window as any).Sentry; + const errorButton = document.querySelector('#error') as HTMLButtonElement; + const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); + + flagsIntegration.addFeatureFlag('shared', true); + + Sentry.withScope((scope: Scope) => { + flagsIntegration.addFeatureFlag('forked', true); + flagsIntegration.addFeatureFlag('shared', false); + scope.setTag('isForked', true); + if (errorButton) { + errorButton.click(); + } + }); + + flagsIntegration.addFeatureFlag('main', true); + Sentry.getCurrentScope().setTag('isForked', false); + errorButton.click(); + return true; + }); + + const forkedReq = await forkedReqPromise; + const forkedEvent = envelopeRequestParser(forkedReq); + + const mainReq = await mainReqPromise; + const mainEvent = envelopeRequestParser(mainReq); + + expect(forkedEvent.contexts?.flags?.values).toEqual([ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ]); + + expect(mainEvent.contexts?.flags?.values).toEqual([ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts new file mode 100644 index 000000000000..e97cb70761ba --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const ldClient = (window as any).initializeLD(); + for (let i = 1; i <= bufferSize; i++) { + ldClient.variation(`feat${i}`, false); + } + ldClient.variation(`feat${bufferSize + 1}`, true); // eviction + ldClient.variation('feat3', true); // update + return true; + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); + const req = await reqPromise; + const event = envelopeRequestParser(req); + + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js new file mode 100644 index 000000000000..aeea903b4eab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryLDIntegration = Sentry.launchDarklyIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryLDIntegration], +}); + +// Manually mocking this because LD only has mock test utils for the React SDK. +// Also, no SDK has mock utils for FlagUsedHandler's. +const MockLaunchDarkly = { + initialize(_clientId, context, options) { + const flagUsedHandler = options && options.inspectors ? options.inspectors[0].method : undefined; + + return { + variation(key, defaultValue) { + if (flagUsedHandler) { + flagUsedHandler(key, { value: defaultValue }, context); + } + return defaultValue; + }, + }; + }, +}; + +window.initializeLD = () => { + return MockLaunchDarkly.initialize( + 'example-client-id', + { kind: 'user', key: 'example-context-key' }, + { inspectors: [Sentry.buildLaunchDarklyFlagUsedHandler()] }, + ); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js new file mode 100644 index 000000000000..e6697408128c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js @@ -0,0 +1,3 @@ +document.getElementById('error').addEventListener('click', () => { + throw new Error('Button triggered error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html new file mode 100644 index 000000000000..9330c6c679f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts new file mode 100644 index 000000000000..6046da6241be --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +import type { Scope } from '@sentry/browser'; + +sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + + await page.evaluate(() => { + const Sentry = (window as any).Sentry; + const errorButton = document.querySelector('#error') as HTMLButtonElement; + const ldClient = (window as any).initializeLD(); + + ldClient.variation('shared', true); + + Sentry.withScope((scope: Scope) => { + ldClient.variation('forked', true); + ldClient.variation('shared', false); + scope.setTag('isForked', true); + if (errorButton) { + errorButton.click(); + } + }); + + ldClient.variation('main', true); + Sentry.getCurrentScope().setTag('isForked', false); + errorButton.click(); + return true; + }); + + const forkedReq = await forkedReqPromise; + const forkedEvent = envelopeRequestParser(forkedReq); + + const mainReq = await mainReqPromise; + const mainEvent = envelopeRequestParser(mainReq); + + expect(forkedEvent.contexts?.flags?.values).toEqual([ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ]); + + expect(mainEvent.contexts?.flags?.values).toEqual([ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts new file mode 100644 index 000000000000..a3de589677ea --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts @@ -0,0 +1,47 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const client = (window as any).initialize(); + for (let i = 1; i <= bufferSize; i++) { + client.getBooleanValue(`feat${i}`, false); + } + client.getBooleanValue(`feat${bufferSize + 1}`, true); // eviction + client.getBooleanValue('feat3', true); // update + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); + const req = await reqPromise; + const event = envelopeRequestParser(req); + + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js new file mode 100644 index 000000000000..971e08755fe6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryOpenFeatureIntegration], +}); + +window.initialize = () => { + return { + getBooleanValue(flag, value) { + let hook = new Sentry.OpenFeatureIntegrationHook(); + hook.error({ flagKey: flag, defaultValue: false }, new Error('flag eval error')); + return value; + }, + }; +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts new file mode 100644 index 000000000000..719782d0b0ab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts @@ -0,0 +1,49 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const client = (window as any).initialize(); + for (let i = 1; i <= bufferSize; i++) { + client.getBooleanValue(`feat${i}`, false); + } + client.getBooleanValue(`feat${bufferSize + 1}`, true); // eviction + client.getBooleanValue('feat3', true); // update + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); + const req = await reqPromise; + const event = envelopeRequestParser(req); + + // Default value is mocked as false -- these will all error and use default + // value + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false }); + expectedFlags.push({ flag: 'feat3', result: false }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js new file mode 100644 index 000000000000..b2b48519b8a9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryOpenFeatureIntegration], +}); + +window.initialize = () => { + return { + getBooleanValue(flag, value) { + let hook = new Sentry.OpenFeatureIntegrationHook(); + hook.after(null, { flagKey: flag, value: value }); + return value; + }, + }; +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js new file mode 100644 index 000000000000..e6697408128c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js @@ -0,0 +1,3 @@ +document.getElementById('error').addEventListener('click', () => { + throw new Error('Button triggered error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html new file mode 100644 index 000000000000..9330c6c679f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts new file mode 100644 index 000000000000..7edb9b2e533b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +import type { Scope } from '@sentry/browser'; + +sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + + await page.evaluate(() => { + const Sentry = (window as any).Sentry; + const errorButton = document.querySelector('#error') as HTMLButtonElement; + const client = (window as any).initialize(); + + client.getBooleanValue('shared', true); + + Sentry.withScope((scope: Scope) => { + client.getBooleanValue('forked', true); + client.getBooleanValue('shared', false); + scope.setTag('isForked', true); + if (errorButton) { + errorButton.click(); + } + }); + + client.getBooleanValue('main', true); + Sentry.getCurrentScope().setTag('isForked', false); + errorButton.click(); + return true; + }); + + const forkedReq = await forkedReqPromise; + const forkedEvent = envelopeRequestParser(forkedReq); + + const mainReq = await mainReqPromise; + const mainEvent = envelopeRequestParser(mainReq); + + expect(forkedEvent.contexts?.flags?.values).toEqual([ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ]); + + expect(mainEvent.contexts?.flags?.values).toEqual([ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/debug/test.ts b/dev-packages/browser-integration-tests/suites/public-api/debug/test.ts index 8e4e953d69d9..6738215c0d5a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/debug/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/debug/test.ts @@ -34,6 +34,7 @@ sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) => 'Sentry Logger [log]: Integration installed: Dedupe', 'Sentry Logger [log]: Integration installed: HttpContext', 'Sentry Logger [warn]: Discarded session because of missing or non-string release', + 'Sentry Logger [log]: Integration installed: BrowserSession', 'test log', ] : ['[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.', 'test log'], diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts index 85353801980d..e581a8eacd57 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -34,7 +34,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT event_id: expect.stringMatching(/\w{32}/), environment: 'production', sdk: { - integrations: [ + integrations: expect.arrayContaining([ 'InboundFilters', 'FunctionToString', 'BrowserApiErrors', @@ -43,8 +43,9 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'Replay', - ], + ]), version: SDK_VERSION, name: 'sentry.javascript.browser', }, @@ -71,7 +72,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT event_id: expect.stringMatching(/\w{32}/), environment: 'production', sdk: { - integrations: [ + integrations: expect.arrayContaining([ 'InboundFilters', 'FunctionToString', 'BrowserApiErrors', @@ -80,8 +81,9 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'Replay', - ], + ]), version: SDK_VERSION, name: 'sentry.javascript.browser', }, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 82bbe104ab98..3a10ea72e18c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -34,7 +34,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g event_id: expect.stringMatching(/\w{32}/), environment: 'production', sdk: { - integrations: [ + integrations: expect.arrayContaining([ 'InboundFilters', 'FunctionToString', 'BrowserApiErrors', @@ -43,8 +43,9 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'Replay', - ], + ]), version: SDK_VERSION, name: 'sentry.javascript.browser', }, @@ -71,7 +72,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g event_id: expect.stringMatching(/\w{32}/), environment: 'production', sdk: { - integrations: [ + integrations: expect.arrayContaining([ 'InboundFilters', 'FunctionToString', 'BrowserApiErrors', @@ -80,8 +81,9 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'Replay', - ], + ]), version: SDK_VERSION, name: 'sentry.javascript.browser', }, diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index df60adfcaabb..91c96298fbce 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -285,6 +285,18 @@ export function shouldSkipMetricsTest(): boolean { return bundle != null && !bundle.includes('tracing') && !bundle.includes('esm') && !bundle.includes('cjs'); } +/** + * We only test feature flags integrations in certain bundles/packages: + * - NPM (ESM, CJS) + * - Not CDNs. + * + * @returns `true` if we should skip the feature flags test + */ +export function shouldSkipFeatureFlagsTest(): boolean { + const bundle = process.env.PW_BUNDLE as string | undefined; + return bundle != null && !bundle.includes('esm') && !bundle.includes('cjs'); +} + /** * Waits until a number of requests matching urlRgx at the given URL arrive. * If the timeout option is configured, this function will abort waiting, even if it hasn't received the configured diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts index 63152d54870b..52dbbca1c086 100644 --- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -16,7 +16,7 @@ const DEFAULT_REPLAY_EVENT = { event_id: expect.stringMatching(/\w{32}/), environment: 'production', sdk: { - integrations: [ + integrations: expect.arrayContaining([ 'InboundFilters', 'FunctionToString', 'BrowserApiErrors', @@ -25,8 +25,9 @@ const DEFAULT_REPLAY_EVENT = { 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'Replay', - ], + ]), version: SDK_VERSION, name: 'sentry.javascript.browser', }, diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index e8635447a9a1..919d74e78542 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -57,8 +57,14 @@ EOF Make sure to add a `test:build` and `test:assert` command to the new app's `package.json` file. -Add the new test app to `test-application` matrix in `.github/workflows/build.yml` for the `job_e2e_tests` job. If you -want to run a canary test, add it to the `canary.yml` workflow. +Test apps in the folder `test-applications` will be automatically picked up by CI in the job `job_e2e_tests` (in `.github/workflows/build.yml`). +The test matrix for CI is generated in `dev-packages/e2e-tests/lib/getTestMatrix.ts`. + +For each test app, CI checks its dependencies (and devDependencies) to see if any of them have changed in the current PR (based on nx affected projects). +For example, if something is changed in the browser package, only E2E test apps that depend on browser will run, while others will be skipped. + +You can add additional information about the test (e.g. canary versions, optional in CI) by adding `sentryTest` in the `package.json` +of a test application. **An important thing to note:** In the context of the build/test commands the fake test registry is available at `http://127.0.0.1:4873`. It hosts all of our packages as if they were to be published with the state of the current 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 b7e9b40c2a01..682c47d30329 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -9,7 +9,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test", "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" }, 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 9b632d69f834..aec1b1d9dac0 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json @@ -9,7 +9,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test", "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" }, 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 88b4334edbff..c8ae32b52378 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -9,7 +9,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test", "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" }, 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 f20c10f25448..1aa316170a64 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/package.json +++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json @@ -8,7 +8,7 @@ "build": "astro check && astro build", "preview": "astro preview", "astro": "astro", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "TEST_ENV=production playwright test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.client.test.ts index 4cbf4bf36604..e1e13d231fef 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/errors.client.test.ts @@ -59,6 +59,7 @@ test.describe('client-side errors', () => { 'LinkedErrors', 'Dedupe', 'HttpContext', + 'BrowserSession', 'BrowserTracing', ]), name: 'sentry.javascript.astro', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/.gitignore b/dev-packages/e2e-tests/test-applications/astro-5/.gitignore new file mode 100644 index 000000000000..560782d47d98 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/.gitignore @@ -0,0 +1,26 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ + +test-results diff --git a/dev-packages/e2e-tests/test-applications/astro-5/.npmrc b/dev-packages/e2e-tests/test-applications/astro-5/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/astro-5/README.md b/dev-packages/e2e-tests/test-applications/astro-5/README.md new file mode 100644 index 000000000000..ff19a3e7ece8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/README.md @@ -0,0 +1,48 @@ +# Astro Starter Kit: Basics + +```sh +npm create astro@latest -- --template basics +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) + +> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! + +![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) + +## πŸš€ Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +/ +β”œβ”€β”€ public/ +β”‚ └── favicon.svg +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ layouts/ +β”‚ β”‚ └── Layout.astro +β”‚ └── pages/ +β”‚ └── index.astro +└── package.json +``` + +To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## πŸ‘€ Want to learn more? + +Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs new file mode 100644 index 000000000000..f38dac6171bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/astro.config.mjs @@ -0,0 +1,21 @@ +import sentry from '@sentry/astro'; +// @ts-check +import { defineConfig } from 'astro/config'; + +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + sentry({ + debug: true, + sourceMapsUploadOptions: { + enabled: false, + }, + }), + ], + output: 'server', + adapter: node({ + mode: 'standalone', + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/package.json b/dev-packages/e2e-tests/test-applications/astro-5/package.json new file mode 100644 index 000000000000..4e02fc855830 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/package.json @@ -0,0 +1,21 @@ +{ + "name": "astro-5", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "test:build": "pnpm install && pnpm build", + "test:assert": "TEST_ENV=production playwright test" + }, + "dependencies": { + "@astrojs/internal-helpers": "^0.4.2", + "@astrojs/node": "^9.0.0", + "@playwright/test": "^1.46.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/astro": "^8.42.0", + "astro": "^5.0.3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/astro-5/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/astro-5/playwright.config.mjs new file mode 100644 index 000000000000..cd6ed611fb4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/playwright.config.mjs @@ -0,0 +1,13 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig({ + startCommand: 'node ./dist/server/entry.mjs', +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg b/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js new file mode 100644 index 000000000000..7bb40f0c60d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + integrations: [Sentry.browserTracingIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js new file mode 100644 index 000000000000..2b79ec0ed337 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg new file mode 100644 index 000000000000..8cf8fb0c7da6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/astro.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg new file mode 100644 index 000000000000..4b2be0ac0e47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/assets/background.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro new file mode 100644 index 000000000000..09a539f14e64 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Avatar.astro @@ -0,0 +1,3 @@ +--- +--- +User avatar diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro new file mode 100644 index 000000000000..6b7b9c70e869 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/components/Welcome.astro @@ -0,0 +1,209 @@ +--- +import astroLogo from '../assets/astro.svg'; +import background from '../assets/background.svg'; +--- + +
+ +
+
+ Astro Homepage +

+ To get started, open the
src/pages
directory in your project. +

+ +
+
+ + + +

What's New in Astro 5.0?

+

+ From content layers to server islands, click to learn more about the new features and + improvements in Astro 5.0 +

+
+
+ + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/layouts/Layout.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/layouts/Layout.astro new file mode 100644 index 000000000000..e455c6106729 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/layouts/Layout.astro @@ -0,0 +1,22 @@ + + + + + + + + Astro Basics + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro new file mode 100644 index 000000000000..facd6f077a6e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/client-error/index.astro @@ -0,0 +1,11 @@ +--- +import Layout from "../../layouts/Layout.astro"; +--- + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/api.ts b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/api.ts new file mode 100644 index 000000000000..a76accdba010 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/api.ts @@ -0,0 +1,15 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +export const GET: APIRoute = ({ request, url }) => { + if (url.searchParams.has('error')) { + throw new Error('Endpoint Error'); + } + return new Response( + JSON.stringify({ + search: url.search, + sp: url.searchParams, + }), + ); +}; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/index.astro new file mode 100644 index 000000000000..f025c76f8365 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/endpoint-error/index.astro @@ -0,0 +1,9 @@ +--- +import Layout from "../../layouts/Layout.astro"; + +export const prerender = false; +--- + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/index.astro new file mode 100644 index 000000000000..457d94f43457 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/index.astro @@ -0,0 +1,21 @@ +--- +import Welcome from '../components/Welcome.astro'; +import Layout from '../layouts/Layout.astro'; + +// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build +// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. +--- + + +
+

Astro E2E Test App

+ +
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/server-island/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/server-island/index.astro new file mode 100644 index 000000000000..d0544ac4f32f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/server-island/index.astro @@ -0,0 +1,16 @@ +--- +import Avatar from '../../components/Avatar.astro'; +import Layout from '../../layouts/Layout.astro'; + +export const prerender = true; +--- + + +

This page is static, except for the avatar which is loaded dynamically from the server

+ + +

Fallback

+
+ +
+ diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/ssr-error/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/ssr-error/index.astro new file mode 100644 index 000000000000..4ecb7466de70 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/ssr-error/index.astro @@ -0,0 +1,13 @@ +--- +import Layout from "../../layouts/Layout.astro"; + +const a = {} as any; +console.log(a.foo.x); +export const prerender = false; +--- + + + +

Page with SSR error

+ +
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-ssr/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-ssr/index.astro new file mode 100644 index 000000000000..58f5d80198d7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-ssr/index.astro @@ -0,0 +1,15 @@ +--- +import Layout from "../../layouts/Layout.astro" + +export const prerender = false +--- + + + +

+ This is a server page +

+ + + +
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-static/index.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-static/index.astro new file mode 100644 index 000000000000..f71bf00c9adf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/test-static/index.astro @@ -0,0 +1,15 @@ +--- +import Layout from "../../layouts/Layout.astro"; + +export const prerender = true; +--- + + + +

+ This is a static page +

+ + + +
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/astro-5/start-event-proxy.mjs new file mode 100644 index 000000000000..875a9a2afac1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'astro-5', +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts new file mode 100644 index 000000000000..22572d009202 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.client.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorEventPromise = waitForError('astro-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'client error'; + }); + + await page.goto('/client-error'); + + await page.getByText('Throw Error').click(); + + const errorEvent = await errorEventPromise; + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.stringContaining('/client-error'), + function: 'HTMLButtonElement.onclick', + in_app: true, + }), + ); + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + mechanism: { + handled: false, + type: 'onerror', + }, + type: 'Error', + value: 'client error', + stacktrace: expect.any(Object), // detailed check above + }, + ], + }, + level: 'error', + platform: 'javascript', + request: { + url: expect.stringContaining('/client-error'), + headers: { + 'User-Agent': expect.any(String), + }, + }, + event_id: expect.stringMatching(/[a-f0-9]{32}/), + timestamp: expect.any(Number), + sdk: { + integrations: expect.arrayContaining([ + 'InboundFilters', + 'FunctionToString', + 'BrowserApiErrors', + 'Breadcrumbs', + 'GlobalHandlers', + 'LinkedErrors', + 'Dedupe', + 'HttpContext', + 'BrowserSession', + 'BrowserTracing', + ]), + name: 'sentry.javascript.astro', + version: expect.any(String), + packages: expect.any(Array), + }, + transaction: '/client-error', + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + environment: 'qa', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts new file mode 100644 index 000000000000..d6a9514da1d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts @@ -0,0 +1,164 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures SSR error', async ({ page }) => { + const errorEventPromise = waitForError('astro-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; + }); + + const transactionEventPromise = waitForTransaction('astro-5', transactionEvent => { + return transactionEvent.transaction === 'GET /ssr-error'; + }); + + await page.goto('/ssr-error'); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toMatchObject({ + transaction: 'GET /ssr-error', + spans: [], + }); + + const traceId = transactionEvent.contexts?.trace?.trace_id; + const spanId = transactionEvent.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanId).toMatch(/[a-f0-9]{16}/); + expect(transactionEvent.contexts?.trace?.parent_span_id).toBeUndefined(); + + expect(errorEvent).toMatchObject({ + contexts: { + app: expect.any(Object), + cloud_resource: expect.any(Object), + culture: expect.any(Object), + device: expect.any(Object), + os: expect.any(Object), + runtime: expect.any(Object), + trace: { + span_id: spanId, + trace_id: traceId, + }, + }, + environment: 'qa', + event_id: expect.stringMatching(/[a-f0-9]{32}/), + exception: { + values: [ + { + mechanism: { + data: { + function: 'astroMiddleware', + }, + handled: false, + type: 'astro', + }, + stacktrace: expect.any(Object), + type: 'TypeError', + value: "Cannot read properties of undefined (reading 'x')", + }, + ], + }, + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + // demonstrates that requestData integration is getting data + host: 'localhost:3030', + 'user-agent': expect.any(String), + }), + method: 'GET', + url: expect.stringContaining('/ssr-error'), + }, + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + server_name: expect.any(String), + timestamp: expect.any(Number), + transaction: 'GET /ssr-error', + }); + }); + + test('captures endpoint error', async ({ page }) => { + const errorEventPromise = waitForError('astro-5', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; + }); + const transactionEventApiPromise = waitForTransaction('astro-5', transactionEvent => { + return transactionEvent.transaction === 'GET /endpoint-error/api'; + }); + const transactionEventEndpointPromise = waitForTransaction('astro-5', transactionEvent => { + return transactionEvent.transaction === 'GET /endpoint-error'; + }); + + await page.goto('/endpoint-error'); + await page.getByText('Get Data').click(); + + const errorEvent = await errorEventPromise; + const transactionEventApi = await transactionEventApiPromise; + const transactionEventEndpoint = await transactionEventEndpointPromise; + + expect(transactionEventEndpoint).toMatchObject({ + transaction: 'GET /endpoint-error', + spans: [], + }); + + const traceId = transactionEventEndpoint.contexts?.trace?.trace_id; + const endpointSpanId = transactionEventApi.contexts?.trace?.span_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(endpointSpanId).toMatch(/[a-f0-9]{16}/); + + expect(transactionEventApi).toMatchObject({ + transaction: 'GET /endpoint-error/api', + spans: [], + }); + + const spanId = transactionEventApi.contexts?.trace?.span_id; + const parentSpanId = transactionEventApi.contexts?.trace?.parent_span_id; + + expect(spanId).toMatch(/[a-f0-9]{16}/); + // TODO: This is incorrect, for whatever reason, it should be the endpointSpanId ideally + expect(parentSpanId).toMatch(/[a-f0-9]{16}/); + expect(parentSpanId).not.toEqual(endpointSpanId); + + expect(errorEvent).toMatchObject({ + contexts: { + trace: { + parent_span_id: parentSpanId, + span_id: spanId, + trace_id: traceId, + }, + }, + exception: { + values: [ + { + mechanism: { + data: { + function: 'astroMiddleware', + }, + handled: false, + type: 'astro', + }, + stacktrace: expect.any(Object), + type: 'Error', + value: 'Endpoint Error', + }, + ], + }, + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + }), + method: 'GET', + query_string: 'error=1', + url: expect.stringContaining('endpoint-error/api?error=1'), + }, + transaction: 'GET /endpoint-error/api', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts new file mode 100644 index 000000000000..8c0e2c0c8850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in dynamically rendered (ssr) routes', () => { + test('sends server and client pageload spans with the same trace id', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction === '/test-ssr'; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction === 'GET /test-ssr'; + }); + + await page.goto('/test-ssr'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const serverPageRequestTraceId = serverPageRequestTxn.contexts?.trace?.trace_id; + const serverPageloadSpanId = serverPageRequestTxn.contexts?.trace?.span_id; + + expect(clientPageloadTraceId).toEqual(serverPageRequestTraceId); + expect(clientPageloadParentSpanId).toEqual(serverPageloadSpanId); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + '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}/), + parent_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', + request: expect.any(Object), + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/test-ssr', + transaction_info: { + source: 'url', + }, + type: 'transaction', + }); + + expect(serverPageRequestTxn).toMatchObject({ + breadcrumbs: expect.any(Array), + contexts: { + app: expect.any(Object), + cloud_resource: expect.any(Object), + culture: expect.any(Object), + device: expect.any(Object), + os: expect.any(Object), + otel: expect.any(Object), + runtime: expect.any(Object), + trace: { + data: { + 'http.response.status_code': 200, + method: 'GET', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + url: expect.stringContaining('/test-ssr'), + }, + op: 'http.server', + origin: 'auto.http.astro', + status: 'ok', + 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}/), + platform: 'node', + request: { + cookies: {}, + headers: expect.objectContaining({ + // demonstrates that request data integration can extract headers + accept: expect.any(String), + 'accept-encoding': expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: expect.stringContaining('/test-ssr'), + }, + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.astro', + packages: expect.any(Array), + version: expect.any(String), + }, + server_name: expect.any(String), + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /test-ssr', + transaction_info: { + source: 'route', + }, + type: 'transaction', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts new file mode 100644 index 000000000000..a6b288f4de71 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in static routes with server islands', () => { + test('only sends client pageload transaction and server island endpoint transaction', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction === '/server-island'; + }); + + const serverIslandEndpointTxnPromise = waitForTransaction('astro-5', evt => { + return !!evt.transaction?.startsWith('GET /_server-islands'); + }); + + await page.goto('/server-island'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const sentryTraceMetaTagContent = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); + const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); + + const [metaTraceId, metaParentSpanId, metaSampled] = sentryTraceMetaTagContent?.split('-') || []; + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toMatch(/[a-f0-9]{16}/); + expect(metaSampled).toBe('1'); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + parent_span_id: metaParentSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: metaTraceId, + }, + }, + platform: 'javascript', + transaction: '/server-island', + transaction_info: { + source: 'url', + }, + type: 'transaction', + }); + + const pageloadSpans = clientPageloadTxn.spans; + + // pageload transaction contains a resource link span for the preloaded server island request + expect(pageloadSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + op: 'resource.link', + origin: 'auto.resource.browser.metrics', + description: expect.stringMatching(/\/_server-islands\/Avatar.*$/), + }), + ]), + ); + + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-sampled=true'); + + const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; + + expect(serverIslandEndpointTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'http.server', + origin: 'auto.http.astro', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + transaction: 'GET /_server-islands/[name]', + }); + + const serverIslandEndpointTraceId = serverIslandEndpointTxn.contexts?.trace?.trace_id; + + // unfortunately, the server island trace id is not the same as the client pageload trace id + // this is because the server island endpoint request is made as a resource link request, + // meaning our fetch instrumentation can't attach headers to the request :( + expect(serverIslandEndpointTraceId).not.toBe(clientPageloadTraceId); + + await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts new file mode 100644 index 000000000000..9c202da53542 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('tracing in static/pre-rendered routes', () => { + test('only sends client pageload span with traceId from pre-rendered tags', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction === '/test-static'; + }); + + waitForTransaction('astro-5', evt => { + if (evt.platform !== 'javascript') { + throw new Error('Server transaction should not be sent'); + } + return false; + }); + + await page.goto('/test-static'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + + const sentryTraceMetaTagContent = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); + const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); + + const [metaTraceId, metaParentSpanId, metaSampled] = sentryTraceMetaTagContent?.split('-') || []; + + expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadParentSpanId).toMatch(/[a-f0-9]{16}/); + expect(metaSampled).toBe('1'); + + expect(clientPageloadTxn).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + op: 'pageload', + origin: 'auto.pageload.browser', + parent_span_id: metaParentSpanId, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: metaTraceId, + }, + }, + platform: 'javascript', + transaction: '/test-static', + transaction_info: { + source: 'url', + }, + type: 'transaction', + }); + + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-sampled=true'); + + await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tsconfig.json b/dev-packages/e2e-tests/test-applications/astro-5/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts index babd00bd4d7c..3393b2a559dd 100644 --- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts @@ -33,6 +33,7 @@ test('Lambda layer SDK bundle sends events', async ({ request }) => { 'sentry.op': 'function.aws.lambda', 'cloud.account.id': '123453789012', 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda', + 'faas.coldstart': true, 'otel.kind': 'SERVER', }, op: 'function.aws.lambda', diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts index 62190c78f4a3..b27e16bdaa85 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts @@ -28,18 +28,22 @@ test('AWS Serverless SDK sends events in ESM mode', async ({ request }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: { 'sentry.sample_rate': 1, - 'sentry.source': 'component', - 'sentry.origin': 'auto.function.serverless', + 'sentry.source': 'custom', + 'sentry.origin': 'auto.otel.aws-lambda', 'sentry.op': 'function.aws.lambda', + 'cloud.account.id': '123453789012', + 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda', + 'faas.coldstart': true, + 'otel.kind': 'SERVER', }, op: 'function.aws.lambda', - origin: 'auto.function.serverless', + origin: 'auto.otel.aws-lambda', span_id: expect.stringMatching(/[a-f0-9]{16}/), status: 'ok', trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); - expect(transactionEvent.spans).toHaveLength(2); + expect(transactionEvent.spans).toHaveLength(3); // shows that the Otel Http instrumentation is working expect(transactionEvent.spans).toContainEqual( @@ -54,6 +58,19 @@ test('AWS Serverless SDK sends events in ESM mode', async ({ request }) => { }), ); + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.op': 'function.aws.lambda', + 'sentry.origin': 'auto.function.serverless', + 'sentry.source': 'component', + }, + description: 'my-lambda', + op: 'function.aws.lambda', + origin: 'auto.function.serverless', + }), + ); + // shows that the manual span creation is working expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index 316fb561cdf3..e91c0ee135e5 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -7,8 +7,8 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml .next", "test:prod": "TEST_ENV=prod playwright test", "test:dev": "TEST_ENV=dev playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-13": "pnpm install && pnpm add next@13.4.19 && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-13": "pnpm install && pnpm add next@13.4.19 && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json index 047218b29e3f..5c362ffb97a1 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json @@ -9,7 +9,7 @@ "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test" }, "dependencies": { 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 6e9f884bedde..aeee72f96477 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 @@ -8,7 +8,7 @@ "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test" }, "dependencies": { 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 047218b29e3f..5c362ffb97a1 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 @@ -9,7 +9,7 @@ "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json index 1d99cf43d3b6..40842474282a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json @@ -8,7 +8,7 @@ "start": "remix-serve build", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test", "test:assert-sourcemaps": "pnpm upload-sourcemaps" }, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json index 3772ea5c76f6..77058ff26783 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/package.json @@ -7,7 +7,7 @@ "start": "remix-serve build/index.js", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test" }, "dependencies": { 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 d1600004d97f..977408d0945a 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 @@ -7,7 +7,7 @@ "start": "NODE_OPTIONS='--require=./instrument.server.cjs' remix-serve build/index.js", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 34ec42143c9e..7e0e587be0d0 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -8,7 +8,7 @@ "start": "NODE_OPTIONS='--require=./instrument.server.cjs' remix-serve build", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm playwright test", "test:assert-sourcemaps": "pnpm upload-sourcemaps" }, 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 d6286c2423b6..dc31366f2ea8 100644 --- a/dev-packages/e2e-tests/test-applications/default-browser/package.json +++ b/dev-packages/e2e-tests/test-applications/default-browser/package.json @@ -12,7 +12,7 @@ "build": "node build.mjs", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "browserslist": { 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 681f9fabb919..4c887cda10ea 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json @@ -15,65 +15,68 @@ "build": "ember build --environment=production", "start": "ember serve --prod", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add ember-source@latest && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-latest": "pnpm install && pnpm add ember-source@latest && pnpm build", "test:assert": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml dist" }, "devDependencies": { - "@ember/optional-features": "^2.0.0", - "@glimmer/component": "^1.1.2", - "@glimmer/tracking": "^1.1.2", - "@playwright/test": "^1.44.1", - "@ember/string": "^3.1.1", + "@ember/optional-features": "~2.0.0", + "@glimmer/component": "~1.1.2", + "@glimmer/tracking": "~1.1.2", + "@playwright/test": "~1.44.1", + "@ember/string": "~3.1.1", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/ember": "latest || *", - "@tsconfig/ember": "^3.0.6", + "@tsconfig/ember": "~3.0.6", "@tsconfig/node18": "18.2.4", - "@types/ember": "^4.0.11", - "@types/ember-resolver": "^9.0.0", - "@types/ember__application": "^4.0.11", - "@types/ember__array": "^4.0.10", - "@types/ember__component": "^4.0.22", - "@types/ember__controller": "^4.0.12", - "@types/ember__debug": "^4.0.8", - "@types/ember__destroyable": "^4.0.5", - "@types/ember__engine": "^4.0.11", - "@types/ember__error": "^4.0.6", - "@types/ember__object": "^4.0.12", - "@types/ember__polyfills": "^4.0.6", - "@types/ember__routing": "^4.0.22", - "@types/ember__runloop": "^4.0.10", - "@types/ember__service": "^4.0.9", - "@types/ember__string": "^3.0.15", - "@types/ember__template": "^4.0.7", - "@types/ember__utils": "^4.0.7", + "@types/ember": "~4.0.11", + "@types/ember-resolver": "~9.0.0", + "@types/ember__application": "~4.0.11", + "@types/ember__array": "~4.0.10", + "@types/ember__component": "~4.0.22", + "@types/ember__controller": "~4.0.12", + "@types/ember__debug": "~4.0.8", + "@types/ember__destroyable": "~4.0.5", + "@types/ember__engine": "~4.0.11", + "@types/ember__error": "~4.0.6", + "@types/ember__object": "~4.0.12", + "@types/ember__polyfills": "~4.0.6", + "@types/ember__routing": "~4.0.22", + "@types/ember__runloop": "~4.0.10", + "@types/ember__service": "~4.0.9", + "@types/ember__string": "~3.0.15", + "@types/ember__template": "~4.0.7", + "@types/ember__utils": "~4.0.7", "@types/node": "18.18.0", - "@types/rsvp": "^4.0.9", - "broccoli-asset-rev": "^3.0.0", - "ember-auto-import": "^2.4.3", + "@types/rsvp": "~4.0.9", + "broccoli-asset-rev": "~3.0.0", + "ember-auto-import": "~2.4.3", "ember-cli": "~4.8.0", - "ember-cli-app-version": "^5.0.0", - "ember-cli-babel": "^7.26.11", - "ember-cli-dependency-checker": "^3.3.1", - "ember-cli-htmlbars": "^6.1.1", - "ember-cli-inject-live-reload": "^2.1.0", - "ember-cli-sri": "^2.1.1", - "ember-cli-terser": "^4.0.2", - "ember-cli-typescript": "^5.3.0", - "ember-fetch": "^8.1.2", - "ember-load-initializers": "^2.1.2", - "ember-page-title": "^7.0.0", - "ember-qunit": "^6.0.0", - "ember-resolver": "^8.0.3", + "ember-cli-app-version": "~5.0.0", + "ember-cli-babel": "~7.26.11", + "ember-cli-dependency-checker": "~3.3.1", + "ember-cli-htmlbars": "~6.1.1", + "ember-cli-inject-live-reload": "~2.1.0", + "ember-cli-sri": "~2.1.1", + "ember-cli-terser": "~4.0.2", + "ember-cli-typescript": "~5.3.0", + "ember-fetch": "~8.1.2", + "ember-load-initializers": "~2.1.2", + "ember-page-title": "~7.0.0", + "ember-resolver": "~8.0.3", "ember-source": "~4.8.0", - "loader.js": "^4.7.0", + "loader.js": "~4.7.0", "ts-node": "10.9.1", - "typescript": "^5.4.5" + "typescript": "~5.4.5", + "webpack": "~5.97.0" }, "engines": { "node": "14.* || 16.* || >= 18" }, + "resolutions": { + "@babel/traverse": "~7.25.9" + }, "ember": { "edition": "octane" }, 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 bc312e034a22..a8a4db191d81 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json @@ -15,8 +15,8 @@ "build": "ember build --environment=production", "start": "ember serve --prod", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add ember-source@latest && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-latest": "pnpm install && pnpm add ember-source@latest && pnpm build", "test:assert": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml dist" }, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts index 2e50c90658ca..e60e94691210 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts @@ -26,6 +26,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/tests/errors.test.ts index babc9af99090..62103cfafbd9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/tests/errors.test.ts @@ -26,6 +26,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); @@ -114,5 +115,6 @@ test('Sends graphql exception to Sentry', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts index 7054f26562cb..748730985cf6 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts @@ -26,6 +26,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore @@ -0,0 +1,56 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md b/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md new file mode 100644 index 000000000000..63c3e90d9b1a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md @@ -0,0 +1,85 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ yarn install +``` + +## Compile and run the project + +```bash +# development +$ yarn run start + +# watch mode +$ yarn run start:dev + +# production mode +$ yarn run start:prod +``` + +## Run tests + +```bash +# unit tests +$ yarn run test + +# e2e tests +$ yarn run test:e2e + +# test coverage +$ yarn run test:cov +``` + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil MyΕ›liwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json new file mode 100644 index 000000000000..6da132e74a4c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -0,0 +1,48 @@ +{ + "name": "nestjs-fastify", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test": "playwright test", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/schedule": "^4.1.0", + "@nestjs/platform-fastify": "^10.0.0", + "@sentry/nestjs": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "fastify": "^4.28.1" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/node": "18.15.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-loader": "^9.4.3", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-fastify/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/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/nestjs-fastify/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts new file mode 100644 index 000000000000..33a6b1957d99 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts @@ -0,0 +1,124 @@ +import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; +import { AppService } from './app.service'; +import { AsyncInterceptor } from './async-example.interceptor'; +import { ExampleInterceptor1 } from './example-1.interceptor'; +import { ExampleInterceptor2 } from './example-2.interceptor'; +import { ExampleExceptionGlobalFilter } from './example-global-filter.exception'; +import { ExampleExceptionLocalFilter } from './example-local-filter.exception'; +import { ExampleLocalFilter } from './example-local.filter'; +import { ExampleGuard } from './example.guard'; + +@Controller() +@UseFilters(ExampleLocalFilter) +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get('test-transaction') + testTransaction() { + return this.appService.testTransaction(); + } + + @Get('test-middleware-instrumentation') + testMiddlewareInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-guard-instrumentation') + @UseGuards(ExampleGuard) + testGuardInstrumentation() { + return {}; + } + + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor1, ExampleInterceptor2) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-async-interceptor-instrumentation') + @UseInterceptors(AsyncInterceptor) + testAsyncInterceptorInstrumentation() { + return this.appService.testSpan(); + } + + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + + @Get('test-exception/:id') + async testException(@Param('id') id: string) { + return this.appService.testException(id); + } + + @Get('test-expected-400-exception/:id') + async testExpected400Exception(@Param('id') id: string) { + return this.appService.testExpected400Exception(id); + } + + @Get('test-expected-500-exception/:id') + async testExpected500Exception(@Param('id') id: string) { + return this.appService.testExpected500Exception(id); + } + + @Get('test-expected-rpc-exception/:id') + async testExpectedRpcException(@Param('id') id: string) { + return this.appService.testExpectedRpcException(id); + } + + @Get('test-span-decorator-async') + async testSpanDecoratorAsync() { + return { result: await this.appService.testSpanDecoratorAsync() }; + } + + @Get('test-span-decorator-sync') + async testSpanDecoratorSync() { + return { result: await this.appService.testSpanDecoratorSync() }; + } + + @Get('kill-test-cron/:job') + async killTestCron(@Param('job') job: string) { + this.appService.killTestCron(job); + } + + @Get('flush') + async flush() { + await flush(); + } + + @Get('example-exception-global-filter') + async exampleExceptionGlobalFilter() { + throw new ExampleExceptionGlobalFilter(); + } + + @Get('example-exception-local-filter') + async exampleExceptionLocalFilter() { + throw new ExampleExceptionLocalFilter(); + } + + @Get('test-service-use') + testServiceWithUseMethod() { + return this.appService.use(); + } + + @Get('test-service-transform') + testServiceWithTransform() { + return this.appService.transform(); + } + + @Get('test-service-intercept') + testServiceWithIntercept() { + return this.appService.intercept(); + } + + @Get('test-service-canActivate') + testServiceWithCanActivate() { + return this.appService.canActivate(); + } + + @Get('test-function-name') + testFunctionName() { + return this.appService.getFunctionName(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts new file mode 100644 index 000000000000..3de3c82dc925 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts @@ -0,0 +1,29 @@ +import { MiddlewareConsumer, Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { ScheduleModule } from '@nestjs/schedule'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { ExampleGlobalFilter } from './example-global.filter'; +import { ExampleMiddleware } from './example.middleware'; + +@Module({ + imports: [SentryModule.forRoot(), ScheduleModule.forRoot()], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + { + provide: APP_FILTER, + useClass: ExampleGlobalFilter, + }, + ], +}) +export class AppModule { + configure(consumer: MiddlewareConsumer): void { + consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts new file mode 100644 index 000000000000..242b4c778a0e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts @@ -0,0 +1,113 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { Cron, SchedulerRegistry } from '@nestjs/schedule'; +import type { MonitorConfig } from '@sentry/core'; +import * as Sentry from '@sentry/nestjs'; +import { SentryCron, SentryTraced } from '@sentry/nestjs'; + +const monitorConfig: MonitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, +}; + +@Injectable() +export class AppService { + constructor(private schedulerRegistry: SchedulerRegistry) {} + + testTransaction() { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + } + + testSpan() { + // span that should not be a child span of the middleware span + Sentry.startSpan({ name: 'test-controller-span' }, () => {}); + } + + testException(id: string) { + throw new Error(`This is an exception with id ${id}`); + } + + testExpected400Exception(id: string) { + throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST); + } + + testExpected500Exception(id: string) { + throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR); + } + + testExpectedRpcException(id: string) { + throw new RpcException(`This is an expected RPC exception with id ${id}`); + } + + @SentryTraced('wait and return a string') + async wait() { + await new Promise(resolve => setTimeout(resolve, 500)); + return 'test'; + } + + async testSpanDecoratorAsync() { + return await this.wait(); + } + + @SentryTraced('return a string') + getString(): { result: string } { + return { result: 'test' }; + } + + @SentryTraced('return the function name') + getFunctionName(): { result: string } { + return { result: this.getFunctionName.name }; + } + + async testSpanDecoratorSync() { + const returned = this.getString(); + // Will fail if getString() is async, because returned will be a Promise<> + return returned.result; + } + + /* + Actual cron schedule differs from schedule defined in config because Sentry + only supports minute granularity, but we don't want to wait (worst case) a + full minute for the tests to finish. + */ + @Cron('*/5 * * * * *', { name: 'test-cron-job' }) + @SentryCron('test-cron-slug', monitorConfig) + async testCron() { + console.log('Test cron!'); + } + + /* + Actual cron schedule differs from schedule defined in config because Sentry + only supports minute granularity, but we don't want to wait (worst case) a + full minute for the tests to finish. + */ + @Cron('*/5 * * * * *', { name: 'test-cron-error' }) + @SentryCron('test-cron-error-slug', monitorConfig) + async testCronError() { + throw new Error('Test error from cron job'); + } + + async killTestCron(job: string) { + this.schedulerRegistry.deleteCronJob(job); + } + + use() { + console.log('Test use!'); + } + + transform() { + console.log('Test transform!'); + } + + intercept() { + console.log('Test intercept!'); + } + + canActivate() { + console.log('Test canActivate!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts @@ -0,0 +1,17 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { tap } from 'rxjs'; + +@Injectable() +export class AsyncInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-async-interceptor-span' }, () => {}); + return Promise.resolve( + next.handle().pipe( + tap(() => { + Sentry.startSpan({ name: 'test-async-interceptor-span-after-route' }, () => {}); + }), + ), + ); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts @@ -0,0 +1,15 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { tap } from 'rxjs'; + +@Injectable() +export class ExampleInterceptor1 implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span-1' }, () => {}); + return next.handle().pipe( + tap(() => { + Sentry.startSpan({ name: 'test-interceptor-span-after-route' }, () => {}); + }), + ); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts new file mode 100644 index 000000000000..2cf9dfb9e043 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor2 implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span-2' }, () => {}); + return next.handle().pipe(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts new file mode 100644 index 000000000000..41981ba748fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts @@ -0,0 +1,5 @@ +export class ExampleExceptionGlobalFilter extends Error { + constructor() { + super('Original global example exception!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts new file mode 100644 index 000000000000..fba749f2232c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { ExampleExceptionGlobalFilter } from './example-global-filter.exception'; + +@Catch(ExampleExceptionGlobalFilter) +export class ExampleGlobalFilter implements ExceptionFilter { + catch(exception: BadRequestException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(400).send({ + statusCode: 400, + timestamp: new Date().toISOString(), + path: request.url, + message: 'Example exception was handled by global filter!', + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts new file mode 100644 index 000000000000..8f76520a3b94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts @@ -0,0 +1,5 @@ +export class ExampleExceptionLocalFilter extends Error { + constructor() { + super('Original local example exception!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts new file mode 100644 index 000000000000..aadf09983947 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { ExampleExceptionLocalFilter } from './example-local-filter.exception'; + +@Catch(ExampleExceptionLocalFilter) +export class ExampleLocalFilter implements ExceptionFilter { + catch(exception: BadRequestException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(400).send({ + statusCode: 400, + timestamp: new Date().toISOString(), + path: request.url, + message: 'Example exception was handled by local filter!', + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts new file mode 100644 index 000000000000..e12bbdc4e994 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts @@ -0,0 +1,10 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + Sentry.startSpan({ name: 'test-guard-span' }, () => {}); + return true; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts new file mode 100644 index 000000000000..8eb319cef309 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts @@ -0,0 +1,12 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { FastifyReply, FastifyRequest } from 'fastify'; + +@Injectable() +export class ExampleMiddleware implements NestMiddleware { + use(req: FastifyRequest, res: FastifyReply, next: () => void) { + // span that should be a child span of the middleware span + Sentry.startSpan({ name: 'test-middleware-span' }, () => {}); + next(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts new file mode 100644 index 000000000000..4f16ebb36d11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/nestjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts new file mode 100644 index 000000000000..7c7c6e4142d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts @@ -0,0 +1,16 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; +import { AppModule } from './app.module'; + +const PORT = 3030; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, new FastifyAdapter()); + await app.listen(PORT); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs new file mode 100644 index 000000000000..c6a92da76970 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-fastify', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts new file mode 100644 index 000000000000..e352e8fdba8f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts @@ -0,0 +1,81 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem, waitForError } from '@sentry-internal/test-utils'; + +test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-fastify', envelope => { + return ( + envelope[0].type === 'check_in' && + envelope[1]['monitor_slug'] === 'test-cron-slug' && + envelope[1]['status'] === 'in_progress' + ); + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs-fastify', envelope => { + return ( + envelope[0].type === 'check_in' && + envelope[1]['monitor_slug'] === 'test-cron-slug' && + envelope[1]['status'] === 'ok' + ); + }); + + const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; + + expect(inProgressEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'in_progress', + environment: 'qa', + monitor_config: { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + }, + contexts: { + trace: { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }), + ); + + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }), + ); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron/test-cron-job`); +}); + +test('Sends exceptions to Sentry on error in cron job', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-fastify', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron job'; + }); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from cron job'); + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron/test-cron-error`); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts new file mode 100644 index 000000000000..4eea05edd36f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts @@ -0,0 +1,167 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends exception to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-fastify', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + const response = await fetch(`${baseURL}/test-exception/123`); + expect(response.status).toBe(500); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/:id'); + + expect(errorEvent.contexts?.trace).toEqual({ + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); + +test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-fastify', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + waitForError('nestjs-fastify', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-500-exception/:id'; + }); + + const transactionEventPromise400 = waitForTransaction('nestjs-fastify', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + const transactionEventPromise500 = waitForTransaction('nestjs-fastify', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; + }); + + const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`); + expect(response400.status).toBe(400); + + const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`); + expect(response500.status).toBe(500); + + await transactionEventPromise400; + await transactionEventPromise500; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-fastify', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); + expect(response.status).toBe(500); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Global exception filter registered in main module is applied and exception is not sent to Sentry', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('nestjs-fastify', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /example-exception-global-filter'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return transactionEvent?.transaction === 'GET /example-exception-global-filter'; + }); + + const response = await fetch(`${baseURL}/example-exception-global-filter`); + const responseBody = await response.json(); + + expect(response.status).toBe(400); + expect(responseBody).toEqual({ + statusCode: 400, + timestamp: expect.any(String), + path: '/example-exception-global-filter', + message: 'Example exception was handled by global filter!', + }); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Local exception filter registered in main module is applied and exception is not sent to Sentry', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('nestjs-fastify', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /example-exception-local-filter'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return transactionEvent?.transaction === 'GET /example-exception-local-filter'; + }); + + const response = await fetch(`${baseURL}/example-exception-local-filter`); + const responseBody = await response.json(); + + expect(response.status).toBe(400); + expect(responseBody).toEqual({ + statusCode: 400, + timestamp: expect.any(String), + path: '/example-exception-local-filter', + message: 'Example exception was handled by local filter!', + }); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts new file mode 100644 index 000000000000..6efb193751b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-async' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-async`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'wait and return a string', + }, + description: 'wait', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'wait and return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-sync' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-sync`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'return a string', + }, + description: 'getString', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('preserves original function name on decorated functions', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-function-name`); + const body = await response.json(); + + expect(body.result).toEqual('getFunctionName'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts new file mode 100644 index 000000000000..609e01709650 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -0,0 +1,810 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'manual', + 'fastify.type': 'middleware', + 'plugin.name': 'fastify -> @fastify/middie', + 'hook.name': 'onRequest', + }, + description: 'middleware - runMiddie', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.http.otel.fastify', + 'sentry.op': 'request_handler.fastify', + 'plugin.name': 'fastify -> @fastify/middie', + 'fastify.type': 'request_handler', + 'http.route': '/test-transaction', + }, + description: '@fastify/middie', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'request_handler.fastify', + origin: 'auto.http.otel.fastify', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.http.otel.nestjs', + 'sentry.op': 'request_context.nestjs', + component: '@nestjs/core', + 'nestjs.version': expect.any(String), + 'nestjs.type': 'request_context', + 'http.method': 'GET', + 'http.url': '/test-transaction', + 'http.route': '/test-transaction', + 'nestjs.controller': 'AppController', + 'nestjs.callback': 'testTransaction', + url: '/test-transaction', + }, + description: 'GET /test-transaction', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'request_context.nestjs', + origin: 'auto.http.otel.nestjs', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.middleware.nestjs', + 'sentry.op': 'middleware.nestjs', + }, + description: 'SentryTracingInterceptor', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.middleware.nestjs', + 'sentry.op': 'middleware.nestjs', + }, + description: 'SentryTracingInterceptor', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.http.otel.nestjs', + 'sentry.op': 'handler.nestjs', + component: '@nestjs/core', + 'nestjs.version': expect.any(String), + 'nestjs.type': 'handler', + 'nestjs.callback': 'testTransaction', + }, + description: 'testTransaction', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'handler.nestjs', + origin: 'auto.http.otel.nestjs', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { 'sentry.origin': 'manual' }, + description: 'test-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { 'sentry.origin': 'manual' }, + description: 'child-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.origin': 'auto.middleware.nestjs', + 'sentry.op': 'middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); +}); + +test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-middleware-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-middleware-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleMiddleware', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware'); + const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-middleware-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleMiddleware' is the parent of 'test-middleware-span' + expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId); + + // 'ExampleMiddleware' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId); +}); + +test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ + baseURL, +}) => { + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-guard-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-guard-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleGuard', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard'); + const exampleGuardSpanId = exampleGuardSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-guard-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span'); + + // 'ExampleGuard' is the parent of 'test-guard-span' + expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId); +}); + +test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); + expect(response.status).toBe(400); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest interceptor spans before route execution. Spans created in and after interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans before route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor1', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor2', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // get interceptor spans + const exampleInterceptor1Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor1'); + const exampleInterceptor1SpanId = exampleInterceptor1Span?.span_id; + const exampleInterceptor2Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor2'); + const exampleInterceptor2SpanId = exampleInterceptor2Span?.span_id; + + // check if manually started spans exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-interceptor-span-1', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-interceptor-span-2', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptor1Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-1'); + const testInterceptor2Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-2'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor1' is the parent of 'test-interceptor-span-1' + expect(testInterceptor1Span.parent_span_id).toBe(exampleInterceptor1SpanId); + + // 'ExampleInterceptor1' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor1SpanId); + + // 'ExampleInterceptor2' is the parent of 'test-interceptor-span-2' + expect(testInterceptor2Span.parent_span_id).toBe(exampleInterceptor2SpanId); + + // 'ExampleInterceptor2' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor2SpanId); +}); + +test('API route transaction includes exactly one nest interceptor span after route execution. Spans created in controller and in interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans after route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // check that exactly one after route span is sent + const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( + span => span.description === 'Interceptors - After Route', + ); + expect(allInterceptorSpansAfterRoute.length).toBe(1); + + // get interceptor span + const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'Interceptors - After Route', + ); + const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; + + // check if manually started span in interceptor after route exists + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-interceptor-span-after-route', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-interceptor-span-after-route', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' + expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); + + // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); +}); + +test('API route transaction includes nest async interceptor spans before route execution. Spans created in and after async interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans before route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'AsyncInterceptor', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // get interceptor spans + const exampleAsyncInterceptor = transactionEvent.spans.find(span => span.description === 'AsyncInterceptor'); + const exampleAsyncInterceptorSpanId = exampleAsyncInterceptor?.span_id; + + // check if manually started spans exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-async-interceptor-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testAsyncInterceptorSpan = transactionEvent.spans.find( + span => span.description === 'test-async-interceptor-span', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'AsyncInterceptor' is the parent of 'test-async-interceptor-span' + expect(testAsyncInterceptorSpan.parent_span_id).toBe(exampleAsyncInterceptorSpanId); + + // 'AsyncInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleAsyncInterceptorSpanId); +}); + +test('API route transaction includes exactly one nest async interceptor span after route execution. Spans created in controller and in async interceptor are nested correctly', async ({ + baseURL, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + // check if interceptor spans after route execution exist + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + // check that exactly one after route span is sent + const allInterceptorSpansAfterRoute = transactionEvent.spans.filter( + span => span.description === 'Interceptors - After Route', + ); + expect(allInterceptorSpansAfterRoute.length).toBe(1); + + // get interceptor span + const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'Interceptors - After Route', + ); + const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id; + + // check if manually started span in interceptor after route exists + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: expect.any(Object), + description: 'test-async-interceptor-span-after-route', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-async-interceptor-span-after-route', + ); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route' + expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId); + + // 'Interceptor - After Route' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId); +}); + +test('Calling use method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-use`); + expect(response.status).toBe(200); +}); + +test('Calling transform method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-transform`); + expect(response.status).toBe(200); +}); + +test('Calling intercept method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-intercept`); + expect(response.status).toBe(200); +}); + +test('Calling canActivate method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-service-canActivate`); + expect(response.status).toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json new file mode 100644 index 000000000000..64f86c6bd2bb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json new file mode 100644 index 000000000000..797d8abe0ead --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts index 0352557e7753..9f9e7cdc0d17 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts @@ -45,5 +45,6 @@ test('Sends exception to Sentry', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/errors.test.ts index 89fd7a990138..fc7520fbcb7b 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/errors.test.ts @@ -34,6 +34,7 @@ test('Sends unexpected exception to Sentry if thrown in module with global filte expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); @@ -70,6 +71,7 @@ test('Sends unexpected exception to Sentry if thrown in module with local filter expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); @@ -108,6 +110,7 @@ test('Sends unexpected exception to Sentry if thrown in module that was register expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts index 5501f058abbd..e29d4677397f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts @@ -26,6 +26,7 @@ test('Sends unexpected exception to Sentry if thrown in module with global filte expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); @@ -54,6 +55,7 @@ test('Sends unexpected exception to Sentry if thrown in module with local filter expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); @@ -84,6 +86,7 @@ test('Sends unexpected exception to Sentry if thrown in module that was register expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); 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 81e4b844d072..de03f89fce27 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -7,9 +7,9 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml .next", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { 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 b03722a5dccc..d1ef013e6ccc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -7,9 +7,9 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { 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 19638112649e..ca92feb9c254 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -7,10 +7,10 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", "//": "15.0.0-canary.194 is the canary release attached to Next.js RC 1. We need to use the canary version instead of the RC because PPR will not work without. The specific react version is also attached to RC 1.", - "test:build-latest": "pnpm install && pnpm add next@15.0.0-canary.194 && pnpm add react@19.0.0-rc-cd22717c-20241013 && pnpm add react-dom@19.0.0-rc-cd22717c-20241013 && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@15.0.0-canary.194 && pnpm add react@19.0.0-rc-cd22717c-20241013 && pnpm add react-dom@19.0.0-rc-cd22717c-20241013 && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { 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 dd145692aa23..4b09aff7f937 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 @@ -7,11 +7,11 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:test-build": "pnpm ts-node --script-mode assert-build.ts", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", - "test:build-13": "pnpm install && pnpm add next@13.4.19 && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", + "test:build-13": "pnpm install && pnpm add next@13.4.19 && pnpm build", "test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json index d5c3a9d20f0d..2fd54b440e2e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json @@ -8,9 +8,9 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { @@ -33,7 +33,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@types/eslint": "^8.56.10", "@types/node": "^20.14.10", - "@types/react": "^18.3.3", + "@types/react": "18.3.1", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 4ca726c474b8..10630c257349 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -7,9 +7,9 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && npx playwright install && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts index 55a59fc90080..f79eb30e9b4c 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts @@ -25,5 +25,6 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts index 4e147b969199..1b63fe0e0c55 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts @@ -25,5 +25,6 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/assert.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/assert.test.ts index 1180303eb708..e274f451b959 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/assert.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/assert.test.ts @@ -26,6 +26,7 @@ test('Returns 400 from failed assert', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts index f28bb17372bd..cadf29fbda90 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts @@ -25,5 +25,6 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts index bb069b7e3e11..678841bdb249 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts @@ -34,17 +34,55 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => { const scopeSpans = json.resourceSpans?.[0]?.scopeSpans; expect(scopeSpans).toBeDefined(); - // Http server span & undici client spans are emitted + // Http server span & undici client spans are emitted, + // as well as the spans emitted via `Sentry.startSpan()` // But our default node-fetch spans are not emitted - expect(scopeSpans.length).toEqual(2); + expect(scopeSpans.length).toEqual(3); const httpScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http'); const undiciScopes = scopeSpans?.filter( scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-undici', ); + const startSpanScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@sentry/node'); expect(httpScopes.length).toBe(1); + expect(startSpanScopes.length).toBe(1); + expect(startSpanScopes[0].spans.length).toBe(2); + expect(startSpanScopes[0].spans).toEqual([ + { + traceId: expect.any(String), + spanId: expect.any(String), + parentSpanId: expect.any(String), + name: 'test-span', + kind: 1, + startTimeUnixNano: expect.any(String), + endTimeUnixNano: expect.any(String), + attributes: [], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + status: { code: 0 }, + links: [], + droppedLinksCount: 0, + }, + { + traceId: expect.any(String), + spanId: expect.any(String), + name: 'test-transaction', + kind: 1, + startTimeUnixNano: expect.any(String), + endTimeUnixNano: expect.any(String), + attributes: [{ key: 'sentry.op', value: { stringValue: 'e2e-test' } }], + droppedAttributesCount: 0, + events: [], + droppedEventsCount: 0, + status: { code: 0 }, + links: [], + droppedLinksCount: 0, + }, + ]); + // Undici spans are emitted correctly expect(undiciScopes.length).toBe(1); expect(undiciScopes[0].spans.length).toBe(1); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.gitignore b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.gitignore new file mode 100644 index 000000000000..4a7f73a2ed0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example 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 new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/app.vue new file mode 100644 index 000000000000..23283a522546 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/app.vue @@ -0,0 +1,17 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/components/ErrorButton.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/components/ErrorButton.vue new file mode 100644 index 000000000000..92ea714ae489 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/components/ErrorButton.vue @@ -0,0 +1,22 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash new file mode 100644 index 000000000000..0e04d001c968 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/copyIITM.bash @@ -0,0 +1,7 @@ +# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules` +# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects. +# +# Things we tried (that did not fix the problem): +# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project) +# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules` +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts new file mode 100644 index 000000000000..9379acaf978a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/nuxt.config.ts @@ -0,0 +1,23 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + modules: ['@sentry/nuxt/module'], + imports: { + autoImport: false, + }, + runtimeConfig: { + public: { + sentry: { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }, + }, + }, + nitro: { + rollupConfig: { + // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output + external: [/@sentry\/.*/], + }, + }, + sentry: { + autoInjectServerSentry: 'experimental_dynamic-import', + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json new file mode 100644 index 000000000000..ac18cebec975 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/package.json @@ -0,0 +1,29 @@ +{ + "name": "nuxt-3-dynamic-import", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build && bash ./copyIITM.bash", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "start": "node .output/server/index.mjs", + "clean": "npx nuxi cleanup", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/nuxt": "latest || *", + "nuxt": "^3.14.0" + }, + "devDependencies": { + "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "overrides": { + "nitropack": "~2.9.7", + "ofetch": "^1.4.0" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/client-error.vue new file mode 100644 index 000000000000..5e1a14931f84 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/client-error.vue @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue new file mode 100644 index 000000000000..8cb2a9997e58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/fetch-server-error.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/index.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/index.vue new file mode 100644 index 000000000000..74513c5697f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue new file mode 100644 index 000000000000..e83392b37b5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/pages/test-param/[param].vue @@ -0,0 +1,23 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/playwright.config.ts new file mode 100644 index 000000000000..aa1ff8e9743c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/playwright.config.ts @@ -0,0 +1,19 @@ +import { fileURLToPath } from 'node:url'; +import type { ConfigOptions } from '@nuxt/test-utils/playwright'; +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const nuxtConfigOptions: ConfigOptions = { + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + }, +}; + +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + use: { ...nuxtConfigOptions }, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/public/favicon.ico new file mode 100644 index 000000000000..18993ad91cfd Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts new file mode 100644 index 000000000000..9a9566051452 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/nuxt'; +import { useRuntimeConfig } from '#imports'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: useRuntimeConfig().public.sentry.dsn, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + }, + }), + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts new file mode 100644 index 000000000000..729b2296c683 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nuxt'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/param-error/[param].ts new file mode 100644 index 000000000000..389d8ac4d633 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/param-error/[param].ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(_e => { + throw new Error('Nuxt 3 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/server-error.ts new file mode 100644 index 000000000000..ec961a010510 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/server-error.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(event => { + throw new Error('Nuxt 3 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/test-param/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/test-param/[param].ts new file mode 100644 index 000000000000..1867874cd494 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/api/test-param/[param].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const param = getRouterParam(event, 'param'); + + return `Param: ${param}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/tsconfig.json new file mode 100644 index 000000000000..b9ed69c19eaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/start-event-proxy.mjs new file mode 100644 index 000000000000..a54cf2320488 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nuxt-3-dynamic-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..2fdd4f79cc46 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.client.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', async () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3 E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('/client-error'); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('shows parametrized route on button error', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; + }); + + await page.goto(`/test-param/1234`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error.transaction).toEqual('/test-param/:param()'); + expect(error.request.url).toMatch(/\/test-param\/1234/); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Param Route Button', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('page is still interactive after client error', async ({ page }) => { + const error1Promise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3 E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error1 = await error1Promise; + + const error2Promise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Another Error thrown from Nuxt-3 E2E test app'; + }); + + await page.locator('#errorBtn2').click(); + + const error2 = await error2Promise; + + expect(error1).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + + expect(error2).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Another Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..b781642c2b4f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', async () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error'; + }); + + await page.goto(`/fetch-server-error`); + await page.getByText('Fetch Server Data', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/server-error'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Server error'); + expect(exception.mechanism.handled).toBe(false); + }); + + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Param Server error'; + }); + + await page.goto(`/test-param/1234`); + await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/param-error/1234'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Param Server error'); + expect(exception.mechanism.handled).toBe(false); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts new file mode 100644 index 000000000000..e4cc6d7b35b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { Span } from '@sentry/nuxt'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', async transactionEvent => { + return transactionEvent.transaction === '/client-error'; + }); + + await page.goto(`/client-error`); + + const rootSpan = await transactionPromise; + const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue '); + + const expected = { + data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.vue.mount' }, + description: 'Vue ', + op: 'ui.vue.mount', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.vue', + }; + + expect(errorButtonSpan).toMatchObject(expected); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts new file mode 100644 index 000000000000..fd7a12e5e15d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transaction = await transactionPromise; + + expect(transaction.contexts.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', + }), + }), + ); +}); + +test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { + let buildAssetFolderOccurred = false; + + waitForTransaction('nuxt-3-dynamic-import', transactionEvent => { + if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + buildAssetFolderOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transactionEvent = await transactionEventPromise; + + expect(buildAssetFolderOccurred).toBe(false); + + // todo: url not yet parametrized + expect(transactionEvent.transaction).toBe('GET /test-param/1234'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts new file mode 100644 index 000000000000..fc14335e0bd9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('distributed tracing', () => { + const PARAM = 's0me-param'; + + test('capture a distributed pageload trace', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const serverTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { + return txnEvent.transaction.includes('GET /test-param/'); + }); + + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto(`/test-param/${PARAM}`), + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/test-param/:param()', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /test-param/s0me-param', // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + }); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tsconfig.json new file mode 100644 index 000000000000..a746f2a70c28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} 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 34180346b252..54bacf4ee358 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 @@ -9,9 +9,10 @@ "generate": "nuxt generate", "preview": "nuxt preview", "start": "node .output/server/index.mjs", + "start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs", "clean": "npx nuxi cleanup", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts index aa1ff8e9743c..6cea405151bd 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/playwright.config.ts @@ -12,7 +12,7 @@ const nuxtConfigOptions: ConfigOptions = { * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: `pnpm start:import`, use: { ...nuxtConfigOptions }, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.gitignore b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.gitignore new file mode 100644 index 000000000000..4a7f73a2ed0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example 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 new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/app.vue new file mode 100644 index 000000000000..23283a522546 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/app.vue @@ -0,0 +1,17 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/components/ErrorButton.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/components/ErrorButton.vue new file mode 100644 index 000000000000..92ea714ae489 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/components/ErrorButton.vue @@ -0,0 +1,22 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash new file mode 100644 index 000000000000..0e04d001c968 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/copyIITM.bash @@ -0,0 +1,7 @@ +# This script copies the `import-in-the-middle` content of the E2E test project root `node_modules` to the build output `node_modules` +# For some reason, some files are missing in the output (like `hook.mjs`) and this is not reproducible in external, standalone projects. +# +# Things we tried (that did not fix the problem): +# - Adding a resolution for `@vercel/nft` v0.27.0 (this worked in the standalone project) +# - Also adding `@vercel/nft` v0.27.0 to pnpm `peerDependencyRules` +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules/import-in-the-middle diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts new file mode 100644 index 000000000000..d5828016d034 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/nuxt.config.ts @@ -0,0 +1,23 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + modules: ['@sentry/nuxt/module'], + imports: { + autoImport: false, + }, + runtimeConfig: { + public: { + sentry: { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }, + }, + }, + nitro: { + rollupConfig: { + // @sentry/... is set external to prevent bundling all of Sentry into the `runtime.mjs` file in the build output + external: [/@sentry\/.*/], + }, + }, + sentry: { + autoInjectServerSentry: 'top-level-import', + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json new file mode 100644 index 000000000000..9d3dc0066912 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/package.json @@ -0,0 +1,25 @@ +{ + "name": "nuxt-3-top-level-import", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build && bash ./copyIITM.bash", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "start": "node .output/server/index.mjs", + "clean": "npx nuxi cleanup", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/nuxt": "latest || *", + "nuxt": "^3.14.0" + }, + "devDependencies": { + "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/client-error.vue new file mode 100644 index 000000000000..5e1a14931f84 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/client-error.vue @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue new file mode 100644 index 000000000000..8cb2a9997e58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/fetch-server-error.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/index.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/index.vue new file mode 100644 index 000000000000..74513c5697f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue new file mode 100644 index 000000000000..e83392b37b5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/pages/test-param/[param].vue @@ -0,0 +1,23 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/playwright.config.ts new file mode 100644 index 000000000000..aa1ff8e9743c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/playwright.config.ts @@ -0,0 +1,19 @@ +import { fileURLToPath } from 'node:url'; +import type { ConfigOptions } from '@nuxt/test-utils/playwright'; +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const nuxtConfigOptions: ConfigOptions = { + nuxt: { + rootDir: fileURLToPath(new URL('.', import.meta.url)), + }, +}; + +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + use: { ...nuxtConfigOptions }, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/public/favicon.ico new file mode 100644 index 000000000000..18993ad91cfd Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.client.config.ts new file mode 100644 index 000000000000..9a9566051452 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.client.config.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/nuxt'; +import { useRuntimeConfig } from '#imports'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: useRuntimeConfig().public.sentry.dsn, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + }, + }), + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts new file mode 100644 index 000000000000..f08dea23ae03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nuxt'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/param-error/[param].ts new file mode 100644 index 000000000000..389d8ac4d633 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/param-error/[param].ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(_e => { + throw new Error('Nuxt 3 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/server-error.ts new file mode 100644 index 000000000000..779286fa8262 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/server-error.ts @@ -0,0 +1,10 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nuxt'; +import { defineEventHandler } from '#imports'; + +export default defineEventHandler(event => { + Sentry.setTag('my-isolated-tag', true); + Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope + + throw new Error('Nuxt 3 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/test-param/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/test-param/[param].ts new file mode 100644 index 000000000000..1867874cd494 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/api/test-param/[param].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from '#imports'; + +export default defineEventHandler(event => { + const param = getRouterParam(event, 'param'); + + return `Param: ${param}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/tsconfig.json new file mode 100644 index 000000000000..b9ed69c19eaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/start-event-proxy.mjs new file mode 100644 index 000000000000..96222429339e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nuxt-3-top-level-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..667075693223 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.client.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', async () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3 E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('/client-error'); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('shows parametrized route on button error', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button'; + }); + + await page.goto(`/test-param/1234`); + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error.sdk.name).toEqual('sentry.javascript.nuxt'); + expect(error.transaction).toEqual('/test-param/:param()'); + expect(error.request.url).toMatch(/\/test-param\/1234/); + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Param Route Button', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('page is still interactive after client error', async ({ page }) => { + const error1Promise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3 E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error1 = await error1Promise; + + const error2Promise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Another Error thrown from Nuxt-3 E2E test app'; + }); + + await page.locator('#errorBtn2').click(); + + const error2 = await error2Promise; + + expect(error1).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + + expect(error2).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Another Error thrown from Nuxt-3 E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..053ec5b6ab67 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', async () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === 'GET /api/server-error'; + }); + + const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error'; + }); + + await page.goto(`/fetch-server-error`); + await page.getByText('Fetch Server Data', { exact: true }).click(); + + const transactionEvent = await transactionEventPromise; + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/server-error'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Server error'); + expect(exception.mechanism.handled).toBe(false); + + expect(error.tags?.['my-isolated-tag']).toBe(true); + expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); + expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + }); + + test('isolates requests', async ({ page }) => { + const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === 'GET /api/server-error'; + }); + + const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error'; + }); + + await page.goto(`/fetch-server-error`); + await page.getByText('Fetch Server Data', { exact: true }).click(); + + const transactionEvent = await transactionEventPromise; + const error = await errorPromise; + + expect(error.tags?.['my-isolated-tag']).toBe(true); + expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); + expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + }); + + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Param Server error'; + }); + + await page.goto(`/test-param/1234`); + await page.getByRole('button', { name: 'Fetch Server Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual('GET /api/param-error/1234'); + + const exception = error.exception.values[0]; + expect(exception.type).toEqual('Error'); + expect(exception.value).toEqual('Nuxt 3 Param Server error'); + expect(exception.mechanism.handled).toBe(false); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.client.test.ts new file mode 100644 index 000000000000..44b37a90f7a0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.client.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { Span } from '@sentry/nuxt'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => { + return transactionEvent.transaction === '/client-error'; + }); + + await page.goto(`/client-error`); + + const rootSpan = await transactionPromise; + const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue '); + + const expected = { + data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.vue.mount' }, + description: 'Vue ', + op: 'ui.vue.mount', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.vue', + }; + + expect(errorButtonSpan).toMatchObject(expected); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts new file mode 100644 index 000000000000..748c7f25354b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3-top-level-import', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transaction = await transactionPromise; + + expect(transaction.contexts.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', + }), + }), + ); +}); + +test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { + let buildAssetFolderOccurred = false; + + waitForTransaction('nuxt-3-top-level-import', transactionEvent => { + if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + buildAssetFolderOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transactionEvent = await transactionEventPromise; + + expect(buildAssetFolderOccurred).toBe(false); + + // todo: url not yet parametrized + expect(transactionEvent.transaction).toBe('GET /test-param/1234'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts new file mode 100644 index 000000000000..e8df55587799 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test.describe('distributed tracing', () => { + const PARAM = 's0me-param'; + + test('capture a distributed pageload trace', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const serverTxnEventPromise = waitForTransaction('nuxt-3-top-level-import', txnEvent => { + return txnEvent.transaction.includes('GET /test-param/'); + }); + + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto(`/test-param/${PARAM}`), + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/test-param/:param()', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /test-param/s0me-param', // todo: parametrize (nitro) + transaction_info: { source: 'url' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + }); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tsconfig.json new file mode 100644 index 000000000000..a746f2a70c28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} 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 8cc66d2d408e..80b76aed58ac 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -8,9 +8,10 @@ "generate": "nuxt generate", "preview": "nuxt preview", "start": "node .output/server/index.mjs", + "start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs", "clean": "npx nuxi cleanup", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts index aa1ff8e9743c..6cea405151bd 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts @@ -12,7 +12,7 @@ const nuxtConfigOptions: ConfigOptions = { * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: `pnpm start:import`, use: { ...nuxtConfigOptions }, }); 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 178804768e87..0a278f07eedd 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -8,9 +8,10 @@ "generate": "nuxt generate", "preview": "nuxt preview", "start": "node .output/server/index.mjs", + "start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs", "clean": "npx nuxi cleanup", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts index aa1ff8e9743c..6cea405151bd 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts @@ -12,7 +12,7 @@ const nuxtConfigOptions: ConfigOptions = { * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: `pnpm start:import`, use: { ...nuxtConfigOptions }, }); 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 db60c16938dc..9f6762325609 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/package.json +++ b/dev-packages/e2e-tests/test-applications/react-17/package.json @@ -18,9 +18,9 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 058fd0bb847a..5de946437a44 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/package.json +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -20,7 +20,7 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 d8d6a58fe16a..e475fb505fc8 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 @@ -18,8 +18,8 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 55ccf5492d9f..0b208b3f5a65 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 @@ -22,7 +22,7 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 540f16e84a21..ca78e6af7310 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 @@ -17,9 +17,9 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 b3ef37f6bc4a..d086c765091c 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 @@ -20,9 +20,9 @@ "start:server": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 e9009b1c2aa3..1313fe2eed0e 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 @@ -23,9 +23,9 @@ "preview": "vite preview", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { 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 a6ba509bc09a..836707b3017f 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 @@ -18,9 +18,9 @@ "start": "serve -s build", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", - "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", - "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", "test:assert": "pnpm test" }, "eslintConfig": { diff --git a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json index 62050fc0d10c..cbb7afd9d09c 100644 --- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "start": "vite", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "license": "MIT", diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json index 5196494cca78..bb37aa10f263 100644 --- a/dev-packages/e2e-tests/test-applications/solid/package.json +++ b/dev-packages/e2e-tests/test-applications/solid/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "start": "vite", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "license": "MIT", 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 c23b50b766f3..f4ff0802e159 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -7,7 +7,7 @@ "build": "vinxi build && sh ./post_build.sh", "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "type": "module", diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index dee336da6d07..032a4af9058a 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -7,7 +7,7 @@ "build": "vinxi build && sh ./post_build.sh", "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "type": "module", 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 d1230c56ef40..1022247cc6ea 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "devDependencies": { 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 7aee187f4469..1ce9273bba52 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 @@ -11,7 +11,7 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json index 4738b14b39aa..0c531cd72357 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json @@ -11,7 +11,7 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "dependencies": { 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 75b5aa195356..39f47c873a5f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -11,7 +11,7 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index 546fec8b9cf5..369e1715adcb 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json @@ -10,7 +10,7 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" }, "dependencies": { 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 ccb6891b2bc7..54387ae46cde 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -8,7 +8,7 @@ "start": "vite preview", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" }, "dependencies": { 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 364c67996c76..f34bdf6d6c0e 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -11,7 +11,7 @@ "build-only": "vite build", "type-check": "vue-tsc --build --force", "test": "playwright test", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test" }, "dependencies": { 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 311c2dcc468c..2195742a148a 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-4/package.json +++ b/dev-packages/e2e-tests/test-applications/webpack-4/package.json @@ -4,7 +4,7 @@ "scripts": { "start": "serve -s build", "build": "node build.mjs", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test" }, "devDependencies": { 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 996b4f240b74..389f817292cd 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-5/package.json +++ b/dev-packages/e2e-tests/test-applications/webpack-5/package.json @@ -4,7 +4,7 @@ "scripts": { "start": "serve -s build", "build": "node build.mjs", - "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build": "pnpm install && pnpm build", "test:assert": "playwright test" }, "devDependencies": { diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 3159dc8cb9b3..4b1b9ada20db 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -37,6 +37,7 @@ "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", + "ai": "^4.0.6", "amqplib": "^0.10.4", "apollo-server": "^3.11.1", "axios": "^1.7.7", diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/s3/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/s3/test.ts index 6ffc6f6303fa..90e2f94a9bed 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/s3/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/s3/test.ts @@ -14,6 +14,7 @@ const EXPECTED_TRANSCATION = { 'rpc.method': 'PutObject', 'rpc.service': 'S3', 'aws.region': 'us-east-1', + 'aws.s3.bucket': 'ot-demo-test', 'otel.kind': 'CLIENT', }, }), @@ -26,6 +27,6 @@ describe('awsIntegration', () => { }); test('should auto-instrument aws-sdk v2 package.', done => { - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSCATION }).start(done); + createRunner(__dirname, 'scenario.js').ignore('event').expect({ transaction: EXPECTED_TRANSCATION }).start(done); }); }); diff --git a/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs new file mode 100644 index 000000000000..b3b8dda3720c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs @@ -0,0 +1,9 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs b/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs new file mode 100644 index 000000000000..9e9c52cd0928 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs @@ -0,0 +1,13 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, +}); + +Sentry.captureException(new Error('Test Error')); + +// some more post context diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs b/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs new file mode 100644 index 000000000000..ce9b5ee327df --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node'; + +Sentry.captureException(new Error('Test Error')); + +// some more post context diff --git a/dev-packages/node-integration-tests/suites/contextLines/test.ts b/dev-packages/node-integration-tests/suites/contextLines/test.ts new file mode 100644 index 000000000000..1912f0b57f04 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/test.ts @@ -0,0 +1,83 @@ +import { join } from 'path'; +import { conditionalTest } from '../../utils'; +import { createRunner } from '../../utils/runner'; + +conditionalTest({ min: 18 })('ContextLines integration in ESM', () => { + test('reads encoded context lines from filenames with spaces', done => { + expect.assertions(1); + const instrumentPath = join(__dirname, 'instrument.mjs'); + + createRunner(__dirname, 'scenario with space.mjs') + .withFlags('--import', instrumentPath) + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + stacktrace: { + frames: expect.arrayContaining([ + { + filename: expect.stringMatching(/\/scenario with space.mjs$/), + context_line: "Sentry.captureException(new Error('Test Error'));", + pre_context: ["import * as Sentry from '@sentry/node';", ''], + post_context: ['', '// some more post context'], + colno: 25, + lineno: 3, + function: '?', + in_app: true, + module: 'scenario with space', + }, + ]), + }, + }, + ], + }, + }, + }) + .start(done); + }); +}); + +describe('ContextLines integration in CJS', () => { + test('reads context lines from filenames with spaces', done => { + expect.assertions(1); + + createRunner(__dirname, 'scenario with space.cjs') + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + stacktrace: { + frames: expect.arrayContaining([ + { + filename: expect.stringMatching(/\/scenario with space.cjs$/), + context_line: "Sentry.captureException(new Error('Test Error'));", + pre_context: [ + 'Sentry.init({', + " dsn: 'https://public@dsn.ingest.sentry.io/1337',", + " release: '1.0',", + ' autoSessionTracking: false,', + ' transport: loggingTransport,', + '});', + '', + ], + post_context: ['', '// some more post context'], + colno: 25, + lineno: 11, + function: 'Object.?', + in_app: true, + module: 'scenario with space', + }, + ]), + }, + }, + ], + }, + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts index eff1564d3f0a..24073af67fa4 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts @@ -2,6 +2,7 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as Sentry from '@sentry/node'; Sentry.init({ + debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts index ec761a7d591d..7c4f702f5df8 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts @@ -15,6 +15,18 @@ Sentry.withScope(scope => { traceId: '12345678901234567890123456789012', }); - Sentry.startSpan({ name: 'test_span_1' }, () => undefined); - Sentry.startSpan({ name: 'test_span_2' }, () => undefined); + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts index b0f79680eea7..ecc45e46b4a0 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts @@ -6,28 +6,19 @@ afterAll(() => { test('should send manually started parallel root spans outside of root context with parentSpanId', done => { createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) .expect({ - transaction: { - transaction: 'test_span_1', - contexts: { - trace: { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - parent_span_id: '1234567890123456', - trace_id: '12345678901234567890123456789012', - }, - }, - }, - }) - .expect({ - transaction: { - transaction: 'test_span_2', - contexts: { - trace: { - span_id: expect.stringMatching(/[a-f0-9]{16}/), - parent_span_id: '1234567890123456', - trace_id: '12345678901234567890123456789012', - }, - }, + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); }, }) .start(done); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts index 97ceaa1e382c..58cf67c7c69a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts @@ -19,8 +19,8 @@ test('should send manually started parallel root spans outside of root context', const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; expect(trace1Id).toBeDefined(); - // Same trace ID as the first span - expect(trace1Id).toBe(traceId); + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); }, }) .start(done); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index 2415140b6140..62b154accd45 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -13,6 +13,7 @@ import express from 'express'; const app = express(); +// eslint-disable-next-line deprecation/deprecation const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; // Flush after 2 seconds (to avoid waiting for the default 60s) diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/scenario.js b/dev-packages/node-integration-tests/suites/tracing/ai/scenario.js new file mode 100644 index 000000000000..780e322c0639 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/ai/scenario.js @@ -0,0 +1,58 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +const { generateText } = require('ai'); +const { MockLanguageModelV1 } = require('ai/test'); + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'First span here!', + }), + }), + prompt: 'Where is the first span?', + }); + + // This span should have input and output prompts attached because telemetry is explicitly enabled. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'Second span here!', + }), + }), + prompt: 'Where is the second span?', + }); + + // This span should not be captured because we've disabled telemetry + await generateText({ + experimental_telemetry: { isEnabled: false }, + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'Third span here!', + }), + }), + prompt: 'Where is the third span?', + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/ai/test.ts new file mode 100644 index 000000000000..e269f9da9db3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/ai/test.ts @@ -0,0 +1,131 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +// `ai` SDK only support Node 18+ +conditionalTest({ min: 18 })('ai', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('creates ai related spans', done => { + const EXPECTED_TRANSACTION = { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'ai.completion_tokens.used': 20, + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.model_id': 'mock-model-id', + 'ai.operationId': 'ai.generateText', + 'ai.pipeline.name': 'generateText', + 'ai.prompt_tokens.used': 10, + 'ai.response.finishReason': 'stop', + 'ai.settings.maxRetries': 2, + 'ai.settings.maxSteps': 1, + 'ai.streaming': false, + 'ai.total_tokens.used': 30, + 'ai.usage.completionTokens': 20, + 'ai.usage.promptTokens': 10, + 'operation.name': 'ai.generateText', + 'sentry.op': 'ai.pipeline.generateText', + 'sentry.origin': 'auto.vercelai.otel', + }), + description: 'generateText', + op: 'ai.pipeline.generateText', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.vercelai.otel', + 'sentry.op': 'ai.run.doGenerate', + 'operation.name': 'ai.generateText.doGenerate', + 'ai.operationId': 'ai.generateText.doGenerate', + 'ai.model.provider': 'mock-provider', + 'ai.model.id': 'mock-model-id', + 'ai.settings.maxRetries': 2, + 'gen_ai.system': 'mock-provider', + 'gen_ai.request.model': 'mock-model-id', + 'ai.pipeline.name': 'generateText.doGenerate', + 'ai.model_id': 'mock-model-id', + 'ai.streaming': false, + 'ai.response.finishReason': 'stop', + 'ai.response.model': 'mock-model-id', + 'ai.usage.promptTokens': 10, + 'ai.usage.completionTokens': 20, + 'gen_ai.response.finish_reasons': ['stop'], + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 20, + 'ai.completion_tokens.used': 20, + 'ai.prompt_tokens.used': 10, + 'ai.total_tokens.used': 30, + }), + description: 'generateText.doGenerate', + op: 'ai.run.doGenerate', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'ai.completion_tokens.used': 20, + 'ai.model.id': 'mock-model-id', + 'ai.model.provider': 'mock-provider', + 'ai.model_id': 'mock-model-id', + 'ai.prompt': '{"prompt":"Where is the second span?"}', + 'ai.operationId': 'ai.generateText', + 'ai.pipeline.name': 'generateText', + 'ai.prompt_tokens.used': 10, + 'ai.response.finishReason': 'stop', + 'ai.input_messages': '{"prompt":"Where is the second span?"}', + 'ai.settings.maxRetries': 2, + 'ai.settings.maxSteps': 1, + 'ai.streaming': false, + 'ai.total_tokens.used': 30, + 'ai.usage.completionTokens': 20, + 'ai.usage.promptTokens': 10, + 'operation.name': 'ai.generateText', + 'sentry.op': 'ai.pipeline.generateText', + 'sentry.origin': 'auto.vercelai.otel', + }), + description: 'generateText', + op: 'ai.pipeline.generateText', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.vercelai.otel', + 'sentry.op': 'ai.run.doGenerate', + 'operation.name': 'ai.generateText.doGenerate', + 'ai.operationId': 'ai.generateText.doGenerate', + 'ai.model.provider': 'mock-provider', + 'ai.model.id': 'mock-model-id', + 'ai.settings.maxRetries': 2, + 'gen_ai.system': 'mock-provider', + 'gen_ai.request.model': 'mock-model-id', + 'ai.pipeline.name': 'generateText.doGenerate', + 'ai.model_id': 'mock-model-id', + 'ai.streaming': false, + 'ai.response.finishReason': 'stop', + 'ai.response.model': 'mock-model-id', + 'ai.usage.promptTokens': 10, + 'ai.usage.completionTokens': 20, + 'gen_ai.response.finish_reasons': ['stop'], + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 20, + 'ai.completion_tokens.used': 20, + 'ai.prompt_tokens.used': 10, + 'ai.total_tokens.used': 30, + }), + description: 'generateText.doGenerate', + op: 'ai.run.doGenerate', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + ]), + }; + + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts index b69354ae00c4..72f625aedeb7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-esm/test.ts @@ -3,7 +3,7 @@ import { conditionalTest } from '../../../../utils'; import { createRunner } from '../../../../utils/runner'; import { createTestServer } from '../../../../utils/server'; -conditionalTest({ min: 18 })('outgoing sampled http requests are correctly instrumented in ESM', () => { +conditionalTest({ min: 18 })('outgoing http in ESM', () => { test('outgoing sampled http requests are correctly instrumented in ESM', done => { expect.assertions(11); diff --git a/docs/assets/node-sdk-trace-propagation.png b/docs/assets/node-sdk-trace-propagation.png new file mode 100644 index 000000000000..f4d5c7bf1b5f Binary files /dev/null and b/docs/assets/node-sdk-trace-propagation.png differ diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 684fd40ab894..d3b31b4f2570 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -47,6 +47,10 @@ - Deprecated `addTracingHeadersToFetchRequest` method - this was only meant for internal use and is not needed anymore. - Deprecated `generatePropagationContext()` in favor of using `generateTraceId()` directly. - Deprecated `spanId` field on `propagationContext` - this field will be removed in v9, and should neither be read or set anymore. +- Deprecated `RequestSession` type. No replacements. +- Deprecated `RequestSessionStatus` type. No replacements. +- Deprecated `SessionFlusherLike` type. No replacements. +- Deprecated `SessionFlusher`. No replacements. ## `@sentry/nestjs` @@ -69,6 +73,9 @@ - **The `@sentry/types` package has been deprecated. Import everything from `@sentry/core` instead.** - Deprecated `Request` in favor of `RequestEventData`. +- Deprecated `RequestSession`. No replacements. +- Deprecated `RequestSessionStatus`. No replacements. +- Deprecated `SessionFlusherLike`. No replacements. ## `@sentry/nuxt` @@ -108,6 +115,10 @@ - Deprecated `wrapUseRoutes`. Use `wrapUseRoutesV6` or `wrapUseRoutesV7` instead. - Deprecated `wrapCreateBrowserRouter`. Use `wrapCreateBrowserRouterV6` or `wrapCreateBrowserRouterV7` instead. +## `@sentry/nextjs` + +- Deprecated `hideSourceMaps`. No replacements. The SDK emits hidden sourcemaps by default. + ## `@sentry/opentelemetry` - Deprecated `generateSpanContextForPropagationContext` in favor of doing this manually - we do not need this export anymore. diff --git a/docs/trace-propagation.md b/docs/trace-propagation.md new file mode 100644 index 000000000000..14de136135e1 --- /dev/null +++ b/docs/trace-propagation.md @@ -0,0 +1,14 @@ +# How Trace Propagation Works in the JavaScript SDKs + +Trace propagation describes how and when traceId & spanId are set and send for various types of events. +How this behaves varies a bit from Browser to Node SDKs. + +## Node SDKs (OpenTelemetry based) + +In the Node SDK and related OpenTelemetry-based SDKs, trace propagation works as follows: + +![node-sdk-trace-propagation-scenarios](./assets/node-sdk-trace-propagation.png) + +## Browser/Other SDKs + +TODO diff --git a/jest/jest.config.js b/jest/jest.config.js index ce564a640f2d..495035994b37 100644 --- a/jest/jest.config.js +++ b/jest/jest.config.js @@ -10,6 +10,7 @@ module.exports = { testMatch: ['/**/*.test.ts', '/**/*.test.tsx'], moduleNameMapper: { '^axios$': require.resolve('axios'), + '@opentelemetry/semantic-conventions/incubating': require.resolve('@opentelemetry/semantic-conventions/incubating'), }, globals: { 'ts-jest': { diff --git a/package.json b/package.json index 59d4fb4acdb6..780c68c65f33 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "packages/integration-shims", "packages/nestjs", "packages/nextjs", + "packages/nitro-utils", "packages/node", "packages/nuxt", "packages/opentelemetry", diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index f5bf79be2f59..5a9de0bf9fb4 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -2,6 +2,7 @@ import { VERSION } from '@angular/core'; import type { BrowserOptions } from '@sentry/browser'; import { breadcrumbsIntegration, + browserSessionIntegration, globalHandlersIntegration, httpContextIntegration, init as browserInit, @@ -22,7 +23,7 @@ import { IS_DEBUG_BUILD } from './flags'; /** * Get the default integrations for the Angular SDK. */ -export function getDefaultIntegrations(): Integration[] { +export function getDefaultIntegrations(options: BrowserOptions = {}): Integration[] { // Don't include the BrowserApiErrors integration as it interferes with the Angular SDK's `ErrorHandler`: // BrowserApiErrors would catch certain errors before they reach the `ErrorHandler` and // thus provide a lower fidelity error than what `SentryErrorHandler` @@ -31,7 +32,7 @@ export function getDefaultIntegrations(): Integration[] { // see: // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 // - https://github.com/getsentry/sentry-javascript/issues/2744 - return [ + const integrations = [ inboundFiltersIntegration(), functionToStringIntegration(), breadcrumbsIntegration(), @@ -40,6 +41,12 @@ export function getDefaultIntegrations(): Integration[] { dedupeIntegration(), httpContextIntegration(), ]; + + if (options.autoSessionTracking !== false) { + integrations.push(browserSessionIntegration()); + } + + return integrations; } /** diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 4fcfbeffb0af..c347a5e19b2e 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -271,8 +271,9 @@ export class TraceDirective implements OnInit, AfterViewInit { * @inheritdoc */ public ngAfterViewInit(): void { - if (this._tracingSpan) { - runOutsideAngular(() => this._tracingSpan!.end()); + const span = this._tracingSpan; + if (span) { + runOutsideAngular(() => span.end()); } } } @@ -302,8 +303,7 @@ export function TraceClass(options?: TraceClassOptions): ClassDecorator { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ return target => { const originalOnInit = target.prototype.ngOnInit; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - target.prototype.ngOnInit = function (...args: any[]): ReturnType { + target.prototype.ngOnInit = function (...args: unknown[]): ReturnType { tracingSpan = runOutsideAngular(() => startInactiveSpan({ onlyIfParent: true, @@ -321,8 +321,7 @@ export function TraceClass(options?: TraceClassOptions): ClassDecorator { }; const originalAfterViewInit = target.prototype.ngAfterViewInit; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - target.prototype.ngAfterViewInit = function (...args: any[]): ReturnType { + target.prototype.ngAfterViewInit = function (...args: unknown[]): ReturnType { if (tracingSpan) { runOutsideAngular(() => tracingSpan.end()); } @@ -345,11 +344,9 @@ interface TraceMethodOptions { * Decorator function that can be used to capture a single lifecycle methods of the component. */ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { - // eslint-disable-next-line @typescript-eslint/ban-types - return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { + return (_target: unknown, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]): ReturnType { + descriptor.value = function (...args: unknown[]): ReturnType { const now = timestampInSeconds(); runOutsideAngular(() => { diff --git a/packages/angular/test/sdk.test.ts b/packages/angular/test/sdk.test.ts index d3d41df6f5bf..25beb721a448 100644 --- a/packages/angular/test/sdk.test.ts +++ b/packages/angular/test/sdk.test.ts @@ -15,7 +15,7 @@ describe('init', () => { }); it('does not include the BrowserApiErrors integration', () => { - const browserDefaultIntegrationsWithoutBrowserApiErrors = SentryBrowser.getDefaultIntegrations() + const browserDefaultIntegrationsWithoutBrowserApiErrors = SentryBrowser.getDefaultIntegrations({}) .filter(i => i.name !== 'BrowserApiErrors') .map(i => i.name) .sort(); diff --git a/packages/astro/package.json b/packages/astro/package.json index 48d2424414ba..a678100bbd90 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -4,22 +4,14 @@ "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", - "keywords": [ - "withastro", - "astro-component", - "astro-integration", - "sentry", - "apm" - ], + "keywords": ["withastro", "astro-component", "astro-integration", "sentry", "apm"], "author": "Sentry", "license": "MIT", "engines": { "node": ">=18.14.1" }, "type": "module", - "files": [ - "/build" - ], + "files": ["/build"], "main": "build/cjs/index.client.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", @@ -53,7 +45,7 @@ "access": "public" }, "peerDependencies": { - "astro": ">=3.x || >=4.0.0-beta" + "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { "@sentry/browser": "8.42.0", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 85603431379b..7eca9de9a41a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -17,6 +17,7 @@ export { addRequestDataToEvent, amqplibIntegration, anrIntegration, + disableAnrDetectionForCallback, captureCheckIn, captureConsoleIntegration, captureEvent, diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 8aa8ef5cb6f9..59a937cfdfff 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -64,9 +64,10 @@ "access": "public" }, "dependencies": { - "@opentelemetry/instrumentation": "^0.54.0", - "@opentelemetry/instrumentation-aws-lambda": "0.44.0", - "@opentelemetry/instrumentation-aws-sdk": "0.45.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/instrumentation": "^0.56.0", + "@opentelemetry/instrumentation-aws-lambda": "0.49.0", + "@opentelemetry/instrumentation-aws-sdk": "0.48.0", "@sentry/core": "8.42.0", "@sentry/node": "8.42.0", "@types/aws-lambda": "^8.10.62" diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 6063c0c2f93d..3f167b62a7e3 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -49,6 +49,7 @@ export { extractRequestData, createGetModuleFromFilename, anrIntegration, + disableAnrDetectionForCallback, consoleIntegration, httpIntegration, nativeNodeFetchIntegration, diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index c9bd6ae6834d..fc67aaa432ef 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -336,6 +336,7 @@ export function wrapHandler( // Only start a trace and root span if the handler is not already wrapped by Otel instrumentation // Otherwise, we create two root spans (one from otel, one from our wrapper). // If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler. + // TODO(v9): Since bumping the OTEL Instrumentation, this is likely not needed anymore, we can possibly remove this if (options.startTrace && !isWrappedByOtel(handler)) { const traceData = getAwsTraceData(event as { headers?: Record }, context); diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 827d2a90c993..e6f57c13fe6b 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -15,10 +15,7 @@ export { captureFeedback, } from '@sentry/core'; -export { - replayIntegration, - getReplay, -} from '@sentry-internal/replay'; +export { replayIntegration, getReplay } from '@sentry-internal/replay'; export type { ReplayEventType, ReplayEventWithTime, @@ -36,17 +33,11 @@ export { replayCanvasIntegration } from '@sentry-internal/replay-canvas'; import { feedbackAsyncIntegration } from './feedbackAsync'; import { feedbackSyncIntegration } from './feedbackSync'; export { feedbackAsyncIntegration, feedbackSyncIntegration, feedbackSyncIntegration as feedbackIntegration }; -export { - getFeedback, - sendFeedback, -} from '@sentry-internal/feedback'; +export { getFeedback, sendFeedback } from '@sentry-internal/feedback'; export * from './metrics'; -export { - defaultRequestInstrumentationOptions, - instrumentOutgoingRequests, -} from './tracing/request'; +export { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './tracing/request'; export { browserTracingIntegration, startBrowserTracingNavigationSpan, @@ -77,3 +68,10 @@ export type { Span } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; export { spotlightBrowserIntegration } from './integrations/spotlight'; +export { browserSessionIntegration } from './integrations/browsersession'; +export { + featureFlagsIntegration, + type FeatureFlagsIntegration, +} from './integrations/featureFlags'; +export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; +export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts new file mode 100644 index 000000000000..7863351f182f --- /dev/null +++ b/packages/browser/src/integrations/browsersession.ts @@ -0,0 +1,39 @@ +import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; +import { captureSession, defineIntegration, logger, startSession } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { WINDOW } from '../helpers'; + +/** + * When added, automatically creates sessions which allow you to track adoption and crashes (crash free rate) in your Releases in Sentry. + * More information: https://docs.sentry.io/product/releases/health/ + * + * Note: In order for session tracking to work, you need to set up Releases: https://docs.sentry.io/product/releases/ + */ +export const browserSessionIntegration = defineIntegration(() => { + return { + name: 'BrowserSession', + setupOnce() { + if (typeof WINDOW.document === 'undefined') { + DEBUG_BUILD && + logger.warn('Using the `browserSessionIntegration` in non-browser environments is not supported.'); + return; + } + + // The session duration for browser sessions does not track a meaningful + // concept that can be used as a metric. + // Automatically captured sessions are akin to page views, and thus we + // discard their duration. + startSession({ ignoreDuration: true }); + captureSession(); + + // We want to create a session for every navigation as well + addHistoryInstrumentationHandler(({ from, to }) => { + // Don't create an additional session for the initial route or if the location did not change + if (from !== undefined && from !== to) { + startSession({ ignoreDuration: true }); + captureSession(); + } + }); + }, + }; +}); diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts new file mode 100644 index 000000000000..01bc73190202 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -0,0 +1,47 @@ +import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; + +import { defineIntegration } from '@sentry/core'; +import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; + +export interface FeatureFlagsIntegration extends Integration { + addFeatureFlag: (name: string, value: unknown) => void; +} + +/** + * Sentry integration for buffering feature flags manually with an API, and + * capturing them on error events. We recommend you do this on each flag + * evaluation. Flags are buffered per Sentry scope and limited to 100 per event. + * + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import * as Sentry from '@sentry/browser'; + * import { type FeatureFlagsIntegration } from '@sentry/browser'; + * + * // Setup + * Sentry.init(..., integrations: [Sentry.featureFlagsIntegration()]) + * + * // Verify + * const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + * if (flagsIntegration) { + * flagsIntegration.addFeatureFlag('my-flag', true); + * } else { + * // check your setup + * } + * Sentry.captureException(Exception('broke')); // 'my-flag' should be captured to this Sentry event. + * ``` + */ +export const featureFlagsIntegration = defineIntegration(() => { + return { + name: 'FeatureFlags', + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, + + addFeatureFlag(name: string, value: unknown): void { + insertFlagToScope(name, value); + }, + }; +}) as IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/index.ts b/packages/browser/src/integrations/featureFlags/index.ts new file mode 100644 index 000000000000..2106ee7accf0 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/index.ts @@ -0,0 +1 @@ +export { featureFlagsIntegration, type FeatureFlagsIntegration } from './featureFlagsIntegration'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/index.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/index.ts new file mode 100644 index 000000000000..7a81279ad319 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/index.ts @@ -0,0 +1 @@ +export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integration'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts new file mode 100644 index 000000000000..d08c34a59c5e --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -0,0 +1,52 @@ +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; + +import { defineIntegration } from '@sentry/core'; +import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; + +/** + * Sentry integration for capturing feature flags from LaunchDarkly. + * + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import * as Sentry from '@sentry/browser'; + * import {launchDarklyIntegration, buildLaunchDarklyFlagUsedInspector} from '@sentry/browser'; + * import * as LaunchDarkly from 'launchdarkly-js-client-sdk'; + * + * Sentry.init(..., integrations: [launchDarklyIntegration()]) + * const ldClient = LaunchDarkly.initialize(..., {inspectors: [buildLaunchDarklyFlagUsedHandler()]}); + * ``` + */ +export const launchDarklyIntegration = defineIntegration(() => { + return { + name: 'LaunchDarkly', + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, + }; +}) satisfies IntegrationFn; + +/** + * LaunchDarkly hook that listens for flag evaluations and updates the `flags` + * context in our Sentry scope. This needs to be registered as an + * 'inspector' in LaunchDarkly initialize() options, separately from + * `launchDarklyIntegration`. Both are needed to collect feature flags on error. + */ +export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler { + return { + name: 'sentry-flag-auditor', + type: 'flag-used', + + synchronous: true, + + /** + * Handle a flag evaluation by storing its name and value on the current scope. + */ + method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { + insertFlagToScope(flagKey, flagDetail.value); + }, + }; +} diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/types.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/types.ts new file mode 100644 index 000000000000..55a388109e60 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/types.ts @@ -0,0 +1,50 @@ +/** + * Inline definitions of LaunchDarkly types so we don't have to include their + * SDK in devDependencies. These are only for type-checking and can be extended + * as needed - for exact definitions, reference `launchdarkly-js-client-sdk`. + */ + +/** + * Currently, the Sentry integration does not read from values of this type. + */ +export type LDContext = object; + +/** + * An object that combines the result of a feature flag evaluation with information about + * how it was calculated. + */ +export interface LDEvaluationDetail { + value: unknown; + // unused optional props: variationIndex and reason +} + +/** + * Callback interface for collecting information about the SDK at runtime. + * + * This interface is used to collect information about flag usage. + * + * This interface should not be used by the application to access flags for the purpose of controlling application + * flow. It is intended for monitoring, analytics, or debugging purposes. + */ +export interface LDInspectionFlagUsedHandler { + type: 'flag-used'; + + /** + * Name of the inspector. Will be used for logging issues with the inspector. + */ + name: string; + + /** + * If `true`, then the inspector will be ran synchronously with evaluation. + * Synchronous inspectors execute inline with evaluation and care should be taken to ensure + * they have minimal performance overhead. + */ + synchronous?: boolean; + + /** + * This method is called when a flag is accessed via a variation method, or it can be called based on actions in + * wrapper SDKs which have different methods of tracking when a flag was accessed. It is not called when a call is made + * to allFlags. + */ + method: (flagKey: string, flagDetail: LDEvaluationDetail, context: LDContext) => void; +} diff --git a/packages/browser/src/integrations/featureFlags/openfeature/index.ts b/packages/browser/src/integrations/featureFlags/openfeature/index.ts new file mode 100644 index 000000000000..e3d425aeac29 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/openfeature/index.ts @@ -0,0 +1 @@ +export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integration'; diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts new file mode 100644 index 000000000000..5b5b56d65d18 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -0,0 +1,41 @@ +/** + * OpenFeature integration. + * + * Add the openFeatureIntegration() function call to your integration lists. + * Add the integration hook to your OpenFeature object. + * - OpenFeature.getClient().addHooks(new OpenFeatureIntegrationHook()); + */ +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; + +import { defineIntegration } from '@sentry/core'; +import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; + +export const openFeatureIntegration = defineIntegration(() => { + return { + name: 'OpenFeature', + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, + }; +}) satisfies IntegrationFn; + +/** + * OpenFeature Hook class implementation. + */ +export class OpenFeatureIntegrationHook implements OpenFeatureHook { + /** + * Successful evaluation result. + */ + public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { + insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + } + + /** + * On error evaluation result. + */ + public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { + insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + } +} diff --git a/packages/browser/src/integrations/featureFlags/openfeature/types.ts b/packages/browser/src/integrations/featureFlags/openfeature/types.ts new file mode 100644 index 000000000000..835e684d86eb --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/openfeature/types.ts @@ -0,0 +1,89 @@ +export type FlagValue = boolean | string | number | JsonValue; +export type FlagValueType = 'boolean' | 'string' | 'number' | 'object'; +export type JsonArray = JsonValue[]; +export type JsonObject = { [key: string]: JsonValue }; +export type JsonValue = PrimitiveValue | JsonObject | JsonArray; +export type Metadata = Record; +export type PrimitiveValue = null | boolean | string | number; +export type FlagMetadata = Record; +export const StandardResolutionReasons = { + STATIC: 'STATIC', + DEFAULT: 'DEFAULT', + TARGETING_MATCH: 'TARGETING_MATCH', + SPLIT: 'SPLIT', + CACHED: 'CACHED', + DISABLED: 'DISABLED', + UNKNOWN: 'UNKNOWN', + STALE: 'STALE', + ERROR: 'ERROR', +} as const; +export enum ErrorCode { + PROVIDER_NOT_READY = 'PROVIDER_NOT_READY', + PROVIDER_FATAL = 'PROVIDER_FATAL', + FLAG_NOT_FOUND = 'FLAG_NOT_FOUND', + PARSE_ERROR = 'PARSE_ERROR', + TYPE_MISMATCH = 'TYPE_MISMATCH', + TARGETING_KEY_MISSING = 'TARGETING_KEY_MISSING', + INVALID_CONTEXT = 'INVALID_CONTEXT', + GENERAL = 'GENERAL', +} +export interface Logger { + error(...args: unknown[]): void; + warn(...args: unknown[]): void; + info(...args: unknown[]): void; + debug(...args: unknown[]): void; +} +export type ResolutionReason = keyof typeof StandardResolutionReasons | (string & Record); +export type EvaluationContextValue = + | PrimitiveValue + | Date + | { [key: string]: EvaluationContextValue } + | EvaluationContextValue[]; +export type EvaluationContext = { + targetingKey?: string; +} & Record; +export interface ProviderMetadata extends Readonly { + readonly name: string; +} +export interface ClientMetadata { + readonly name?: string; + readonly domain?: string; + readonly version?: string; + readonly providerMetadata: ProviderMetadata; +} +export type HookHints = Readonly>; +export interface HookContext { + readonly flagKey: string; + readonly defaultValue: T; + readonly flagValueType: FlagValueType; + readonly context: Readonly; + readonly clientMetadata: ClientMetadata; + readonly providerMetadata: ProviderMetadata; + readonly logger: Logger; +} +export interface BeforeHookContext extends HookContext { + context: EvaluationContext; +} +export type ResolutionDetails = { + value: U; + variant?: string; + flagMetadata?: FlagMetadata; + reason?: ResolutionReason; + errorCode?: ErrorCode; + errorMessage?: string; +}; +export type EvaluationDetails = { + flagKey: string; + flagMetadata: Readonly; +} & ResolutionDetails; +export interface BaseHook { + before?(hookContext: BeforeHookContext, hookHints?: HookHints): BeforeHookReturn; + after?( + hookContext: Readonly>, + evaluationDetails: EvaluationDetails, + hookHints?: HookHints, + ): HooksReturn; + error?(hookContext: Readonly>, error: unknown, hookHints?: HookHints): HooksReturn; + finally?(hookContext: Readonly>, hookHints?: HookHints): HooksReturn; +} +export type OpenFeatureHook = BaseHook; diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 1f0d0a4b35c1..abb768082c3a 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -153,8 +153,12 @@ function _eventFromRejectionWithPrimitive(reason: Primitive): Event { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function _enhanceEventWithInitialFrame(event: Event, url: any, line: any, column: any): Event { +function _enhanceEventWithInitialFrame( + event: Event, + url: string | undefined, + line: number | undefined, + column: number | undefined, +): Event { // event.exception const e = (event.exception = event.exception || {}); // event.exception.values @@ -166,8 +170,8 @@ function _enhanceEventWithInitialFrame(event: Event, url: any, line: any, column // event.exception.values[0].stacktrace.frames const ev0sf = (ev0s.frames = ev0s.frames || []); - const colno = isNaN(parseInt(column, 10)) ? undefined : column; - const lineno = isNaN(parseInt(line, 10)) ? undefined : line; + const colno = column; + const lineno = line; const filename = isString(url) && url.length > 0 ? url : getLocationHref(); // event.exception.values[0].stacktrace.frames diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 70d21bfd3501..caddc31bc167 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,6 +1,4 @@ -import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; import { - captureSession, consoleSandbox, dedupeIntegration, functionToStringIntegration, @@ -13,7 +11,6 @@ import { lastEventId, logger, stackParserFromStackParserOptions, - startSession, supportsFetch, } from '@sentry/core'; import type { Client, DsnLike, Integration, Options, UserFeedback } from '@sentry/core'; @@ -23,6 +20,7 @@ import { DEBUG_BUILD } from './debug-build'; import { WINDOW } from './helpers'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { browserApiErrorsIntegration } from './integrations/browserapierrors'; +import { browserSessionIntegration } from './integrations/browsersession'; import { globalHandlersIntegration } from './integrations/globalhandlers'; import { httpContextIntegration } from './integrations/httpcontext'; import { linkedErrorsIntegration } from './integrations/linkederrors'; @@ -30,12 +28,12 @@ import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; /** Get the default integrations for the browser SDK. */ -export function getDefaultIntegrations(_options: Options): Integration[] { +export function getDefaultIntegrations(options: Options): Integration[] { /** * Note: Please make sure this stays in sync with Angular SDK, which re-exports * `getDefaultIntegrations` but with an adjusted set of integrations. */ - return [ + const integrations = [ inboundFiltersIntegration(), functionToStringIntegration(), browserApiErrorsIntegration(), @@ -45,6 +43,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] { dedupeIntegration(), httpContextIntegration(), ]; + + if (options.autoSessionTracking !== false) { + integrations.push(browserSessionIntegration()); + } + + return integrations; } function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { @@ -187,19 +191,14 @@ export function init(browserOptions: BrowserOptions = {}): Client | undefined { transport: options.transport || makeFetchTransport, }; - const client = initAndBind(BrowserClient, clientOptions); - - if (options.autoSessionTracking) { - startSessionTracking(); - } - - return client; + return initAndBind(BrowserClient, clientOptions); } /** * All properties the report dialog supports */ export interface ReportDialogOptions { + // TODO(v9): Change this to [key: string]: unknkown; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; eventId?: string; @@ -308,32 +307,6 @@ export function onLoad(callback: () => void): void { callback(); } -/** - * Enable automatic Session Tracking for the initial page load. - */ -function startSessionTracking(): void { - if (typeof WINDOW.document === 'undefined') { - DEBUG_BUILD && logger.warn('Session tracking in non-browser environment with @sentry/browser is not supported.'); - return; - } - - // The session duration for browser sessions does not track a meaningful - // concept that can be used as a metric. - // Automatically captured sessions are akin to page views, and thus we - // discard their duration. - startSession({ ignoreDuration: true }); - captureSession(); - - // We want to create a session for every navigation as well - addHistoryInstrumentationHandler(({ from, to }) => { - // Don't create an additional session for the initial route or if the location did not change - if (from !== undefined && from !== to) { - startSession({ ignoreDuration: true }); - captureSession(); - } - }); -} - /** * Captures user feedback and sends it to Sentry. * diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts new file mode 100644 index 000000000000..9793eda49191 --- /dev/null +++ b/packages/browser/src/utils/featureFlags.ts @@ -0,0 +1,90 @@ +import type { Event, FeatureFlag } from '@sentry/core'; + +import { getCurrentScope, logger } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; + +/** + * Ordered LRU cache for storing feature flags in the scope context. The name + * of each flag in the buffer is unique, and the output of getAll() is ordered + * from oldest to newest. + */ + +/** + * Max size of the LRU flag buffer stored in Sentry scope and event contexts. + */ +export const FLAG_BUFFER_SIZE = 100; + +/** + * Copies feature flags that are in current scope context to the event context + */ +export function copyFlagsFromScopeToEvent(event: Event): Event { + const scope = getCurrentScope(); + const flagContext = scope.getScopeData().contexts.flags; + const flagBuffer = flagContext ? flagContext.values : []; + + if (!flagBuffer.length) { + return event; + } + + if (event.contexts === undefined) { + event.contexts = {}; + } + event.contexts.flags = { values: [...flagBuffer] }; + return event; +} + +/** + * Creates a feature flags values array in current context if it does not exist + * and inserts the flag into a FeatureFlag array while maintaining ordered LRU + * properties. Not thread-safe. After inserting: + * - `flags` is sorted in order of recency, with the newest flag at the end. + * - No other flags with the same name exist in `flags`. + * - The length of `flags` does not exceed `maxSize`. The oldest flag is evicted + * as needed. + * + * @param name Name of the feature flag to insert. + * @param value Value of the feature flag. + * @param maxSize Max number of flags the buffer should store. It's recommended + * to keep this consistent across insertions. Default is FLAG_BUFFER_SIZE + */ +export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { + const scopeContexts = getCurrentScope().getScopeData().contexts; + if (!scopeContexts.flags) { + scopeContexts.flags = { values: [] }; + } + const flags = scopeContexts.flags.values as FeatureFlag[]; + insertToFlagBuffer(flags, name, value, maxSize); +} + +/** + * Exported for tests. Currently only accepts boolean values (otherwise no-op). + */ +export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { + if (typeof value !== 'boolean') { + return; + } + + if (flags.length > maxSize) { + DEBUG_BUILD && logger.error(`[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize=${maxSize}`); + return; + } + + // Check if the flag is already in the buffer - O(n) + const index = flags.findIndex(f => f.flag === name); + + if (index !== -1) { + // The flag was found, remove it from its current position - O(n) + flags.splice(index, 1); + } + + if (flags.length === maxSize) { + // If at capacity, pop the earliest flag - O(n) + flags.shift(); + } + + // Push the flag to the end - O(1) + flags.push({ + flag: name, + result: value, + }); +} diff --git a/packages/browser/test/utils/featureFlags.test.ts b/packages/browser/test/utils/featureFlags.test.ts new file mode 100644 index 000000000000..48d2f20e3c91 --- /dev/null +++ b/packages/browser/test/utils/featureFlags.test.ts @@ -0,0 +1,109 @@ +import type { FeatureFlag } from '@sentry/core'; + +import { getCurrentScope, logger } from '@sentry/core'; +import { vi } from 'vitest'; +import { insertFlagToScope, insertToFlagBuffer } from '../../src/utils/featureFlags'; + +describe('flags', () => { + describe('insertFlagToScope()', () => { + it('adds flags to the current scope context', () => { + const maxSize = 3; + insertFlagToScope('feat1', true, maxSize); + insertFlagToScope('feat2', true, maxSize); + insertFlagToScope('feat3', true, maxSize); + insertFlagToScope('feat4', true, maxSize); + + const scope = getCurrentScope(); + expect(scope.getScopeData().contexts.flags?.values).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: true }, + { flag: 'feat4', result: true }, + ]); + }); + }); + + describe('insertToFlagBuffer()', () => { + const loggerSpy = vi.spyOn(logger, 'error'); + + afterEach(() => { + loggerSpy.mockClear(); + }); + + it('maintains ordering and evicts the oldest entry', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 3; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', true, maxSize); + insertToFlagBuffer(buffer, 'feat4', true, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: true }, + { flag: 'feat4', result: true }, + ]); + }); + + it('does not duplicate same-name flags and updates order and values', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 3; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', false, maxSize); + insertToFlagBuffer(buffer, 'feat1', false, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: false }, + { flag: 'feat1', result: false }, + ]); + }); + + it('does not allocate unnecessary space', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 1000; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + }); + + it('does not accept non-boolean values', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 1000; + insertToFlagBuffer(buffer, 'feat1', 1, maxSize); + insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); + + expect(buffer).toEqual([]); + }); + + it('logs error and is a no-op when buffer is larger than maxSize', () => { + const buffer: FeatureFlag[] = [ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]; + + insertToFlagBuffer(buffer, 'feat1', true, 1); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), + ); + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + + insertToFlagBuffer(buffer, 'feat1', true, -2); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), + ); + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + }); + }); +}); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 7377d2ac3bbd..1ba5f2de4786 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -71,6 +71,7 @@ export { extractRequestData, createGetModuleFromFilename, anrIntegration, + disableAnrDetectionForCallback, consoleIntegration, httpIntegration, nativeNodeFetchIntegration, diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 548b40a3ce30..23b0306d2e27 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -47,6 +47,7 @@ export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: DsnComponents, tunnel export function getReportDialogEndpoint( dsnLike: DsnLike, dialogOptions: { + // TODO(v9): Change this to [key: string]: unknown; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; user?: { name?: string; email?: string }; diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index 0e1a84961f5a..68c72fb8e92d 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -2,7 +2,6 @@ import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScop import { Scope } from '../scope'; import type { Client, Scope as ScopeInterface } from '../types-hoist'; import { isThenable } from '../utils-hoist/is'; - import { getMainCarrier, getSentryCarrier } from './../carrier'; import type { AsyncContextStrategy } from './types'; diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 5d06d539b6ec..c394a0d77a95 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -158,8 +158,7 @@ export abstract class BaseClient implements Client { /** * @inheritDoc */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public captureException(exception: any, hint?: EventHint, scope?: Scope): string { + public captureException(exception: unknown, hint?: EventHint, scope?: Scope): string { const eventId = uuid4(); // ensure we haven't captured this very object before @@ -915,8 +914,7 @@ export abstract class BaseClient implements Client { /** * @inheritDoc */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public abstract eventFromException(_exception: any, _hint?: EventHint): PromiseLike; + public abstract eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike; /** * @inheritDoc diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 7281f2d2ea7f..999fb0681cf0 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -7,6 +7,9 @@ import type { Event, EventEnvelope, EventItem, + LegacyCSPReport, + RawSecurityEnvelope, + RawSecurityItem, SdkInfo, SdkMetadata, Session, @@ -24,6 +27,7 @@ import { createSpanEnvelopeItem, getSdkMetadataForEnvelopeHeader, } from './utils-hoist/envelope'; +import { uuid4 } from './utils-hoist/misc'; import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** @@ -141,3 +145,26 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? return createEnvelope(headers, items); } + +/** + * Create an Envelope from a CSP report. + */ +export function createRawSecurityEnvelope( + report: LegacyCSPReport, + dsn: DsnComponents, + tunnel?: string, + release?: string, + environment?: string, +): RawSecurityEnvelope { + const envelopeHeaders = { + event_id: uuid4(), + ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), + }; + + const eventItem: RawSecurityItem = [ + { type: 'raw_security', sentry_release: release, sentry_environment: environment }, + report, + ]; + + return createEnvelope(envelopeHeaders, [eventItem]); +} diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 0ae3fa3d2775..cf7e872fb001 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -34,11 +34,7 @@ import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; * @param hint Optional additional data to attach to the Sentry event. * @returns the id of the captured Sentry event. */ -export function captureException( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - exception: any, - hint?: ExclusiveEventHintOrCaptureContext, -): string { +export function captureException(exception: unknown, hint?: ExclusiveEventHintOrCaptureContext): string { return getCurrentScope().captureException(exception, parseEventHintOrCaptureContext(hint)); } @@ -73,8 +69,7 @@ export function captureEvent(event: Event, hint?: EventHint): string { * @param name of the context * @param context Any kind of data. This data will be normalized. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function setContext(name: string, context: { [key: string]: any } | null): void { +export function setContext(name: string, context: { [key: string]: unknown } | null): void { getIsolationScope().setContext(name, context); } diff --git a/packages/core/src/featureFlags.ts b/packages/core/src/featureFlags.ts new file mode 100644 index 000000000000..f80e17ef7f9d --- /dev/null +++ b/packages/core/src/featureFlags.ts @@ -0,0 +1 @@ +export type FeatureFlag = { readonly flag: string; readonly result: boolean }; diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 7385298f925c..55ea867a763a 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -14,8 +14,6 @@ type PolymorphicRequestHeaders = | Array<[string, string]> // the below is not precisely the Header type used in Request, but it'll pass duck-typing | { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; append: (key: string, value: string) => void; get: (key: string) => string | null | undefined; }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 751279cde693..77259d2434d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,6 +47,7 @@ export { export { setAsyncContextStrategy } from './asyncContext'; export { getMainCarrier } from './carrier'; export { makeSession, closeSession, updateSession } from './session'; +// eslint-disable-next-line deprecation/deprecation export { SessionFlusher } from './sessionflusher'; export { Scope } from './scope'; export { notifyEventProcessors } from './eventProcessors'; @@ -124,3 +125,5 @@ export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; export * from './utils-hoist/index'; // TODO(v9): Make this structure pretty again and don't do "export *" export * from './types-hoist/index'; + +export type { FeatureFlag } from './featureFlags'; diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index ddac187ac7c9..cba170bb5722 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -19,8 +19,7 @@ const _functionToStringIntegration = (() => { // intrinsics (like Function.prototype) might be immutable in some environments // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Function.prototype.toString = function (this: WrappedFunction, ...args: any[]): string { + Function.prototype.toString = function (this: WrappedFunction, ...args: unknown[]): string { const originalFunction = getOriginalFunction(this); const context = SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined ? originalFunction : this; diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index c78b602d3304..9d9f803a69f5 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -17,6 +17,7 @@ const DEFAULT_IGNORE_ERRORS = [ 'can\'t redefine non-configurable property "solana"', // Probably a browser extension or custom browser (Brave) throwing this error "vv().getRestrictions is not a function. (In 'vv().getRestrictions(1,a)', 'vv().getRestrictions' is undefined)", // Error thrown by GTM, seemingly not affecting end-users "Can't find variable: _AutofillCallbackHandler", // Unactionable error in instagram webview https://developers.facebook.com/community/threads/320013549791141/ + /^Non-Error promise rejection captured with value: Object Not Found Matching Id:\d+, MethodName:simulateEvent, ParamCount:\d+$/, // unactionable error from CEFSharp, a .NET library that embeds chromium in .NET apps ]; /** Options for the InboundFilters integration */ diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index b3213ef87f0c..972c6b3336ad 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -20,7 +20,7 @@ export class MetricsAggregator implements MetricsAggregatorBase { // that we store in memory. private _bucketsTotalWeight; - // We adjust the type here to add the `unref()` part, as setInterval can technically return a number of a NodeJS.Timer. + // We adjust the type here to add the `unref()` part, as setInterval can technically return a number or a NodeJS.Timer private readonly _interval: ReturnType & { unref?: () => void }; // SDKs are required to shift the flush interval by random() * rollup_in_seconds. diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 656ada3d6355..5bba8615e876 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -94,6 +94,7 @@ class ScopeClass implements ScopeInterface { protected _session?: Session; /** Request Mode Session Status */ + // eslint-disable-next-line deprecation/deprecation protected _requestSession?: RequestSession; /** The client on this scope */ @@ -130,6 +131,14 @@ class ScopeClass implements ScopeInterface { newScope._tags = { ...this._tags }; newScope._extra = { ...this._extra }; newScope._contexts = { ...this._contexts }; + if (this._contexts.flags) { + // We need to copy the `values` array so insertions on a cloned scope + // won't affect the original array. + newScope._contexts.flags = { + values: [...this._contexts.flags.values], + }; + } + newScope._user = this._user; newScope._level = this._level; newScope._session = this._session; @@ -222,6 +231,7 @@ class ScopeClass implements ScopeInterface { /** * @inheritDoc */ + // eslint-disable-next-line deprecation/deprecation public getRequestSession(): RequestSession | undefined { return this._requestSession; } @@ -229,6 +239,7 @@ class ScopeClass implements ScopeInterface { /** * @inheritDoc */ + // eslint-disable-next-line deprecation/deprecation public setRequestSession(requestSession?: RequestSession): this { this._requestSession = requestSession; return this; @@ -350,7 +361,8 @@ class ScopeClass implements ScopeInterface { const [scopeInstance, requestSession] = scopeToMerge instanceof Scope - ? [scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()] + ? // eslint-disable-next-line deprecation/deprecation + [scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()] : isPlainObject(scopeToMerge) ? [captureContext as ScopeContext, (captureContext as ScopeContext).requestSession] : []; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 5435d5c8fff0..bc9489406282 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -42,6 +42,7 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { + // eslint-disable-next-line deprecation/deprecation protected _sessionFlusher: SessionFlusher | undefined; /** @@ -78,12 +79,13 @@ export class ServerRuntimeClient< /** * @inheritDoc */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public captureException(exception: any, hint?: EventHint, scope?: Scope): string { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used + public captureException(exception: unknown, hint?: EventHint, scope?: Scope): string { + // Check if `_sessionFlusher` exists because it is initialized (defined) only when the `autoSessionTracking` is enabled. + // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. + // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. + // TODO(v9): This should move into the httpIntegration. if (this._options.autoSessionTracking && this._sessionFlusher) { + // eslint-disable-next-line deprecation/deprecation const requestSession = getIsolationScope().getRequestSession(); // Necessary checks to ensure this is code block is executed only within a request @@ -100,9 +102,10 @@ export class ServerRuntimeClient< * @inheritDoc */ public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used + // Check if `_sessionFlusher` exists because it is initialized only when the `autoSessionTracking` is enabled. + // The expectation is that session aggregates are only sent when `autoSessionTracking` is enabled. + // TODO(v9): Our goal in the future is to not have the `autoSessionTracking` option and instead rely on integrations doing the creation and sending of sessions. We will not have a central kill-switch for sessions. + // TODO(v9): This should move into the httpIntegration. if (this._options.autoSessionTracking && this._sessionFlusher) { const eventType = event.type || 'exception'; const isException = @@ -110,6 +113,7 @@ export class ServerRuntimeClient< // If the event is of type Exception, then a request session should be captured if (isException) { + // eslint-disable-next-line deprecation/deprecation const requestSession = getIsolationScope().getRequestSession(); // Ensure that this is happening within the bounds of a request, and make sure not to override @@ -134,12 +138,19 @@ export class ServerRuntimeClient< return super.close(timeout); } - /** Method that initialises an instance of SessionFlusher on Client */ + /** + * Initializes an instance of SessionFlusher on the client which will aggregate and periodically flush session data. + * + * NOTICE: This method will implicitly create an interval that is periodically called. + * To clean up this resources, call `.close()` when you no longer intend to use the client. + * Not doing so will result in a memory leak. + */ public initSessionFlusher(): void { const { release, environment } = this._options; if (!release) { - DEBUG_BUILD && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); + DEBUG_BUILD && logger.warn('Cannot initialize an instance of SessionFlusher if no release is provided!'); } else { + // eslint-disable-next-line deprecation/deprecation this._sessionFlusher = new SessionFlusher(this, { release, environment, @@ -214,6 +225,8 @@ export class ServerRuntimeClient< /** * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment * appropriate session aggregates bucket + * + * @deprecated This method should not be used or extended. It's functionality will move into the `httpIntegration` and not be part of any public API. */ protected _captureRequestSession(): void { if (!this._sessionFlusher) { diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts index e35b8f70785d..2434023bf797 100644 --- a/packages/core/src/sessionflusher.ts +++ b/packages/core/src/sessionflusher.ts @@ -14,15 +14,16 @@ type ReleaseHealthAttributes = { }; /** - * @inheritdoc + * @deprecated `SessionFlusher` is deprecated and will be removed in the next major version of the SDK. */ +// TODO(v9): The goal for the SessionFlusher is to become a stupidly simple mechanism to aggregate "Sessions" (actually "RequestSessions"). It should probably live directly inside the Http integration/instrumentation. +// eslint-disable-next-line deprecation/deprecation export class SessionFlusher implements SessionFlusherLike { public readonly flushTimeout: number; private _pendingAggregates: Map; private _sessionAttrs: ReleaseHealthAttributes; - // Cast to any so that it can use Node.js timeout - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _intervalId: any; + // We adjust the type here to add the `unref()` part, as setInterval can technically return a number or a NodeJS.Timer + private readonly _intervalId: ReturnType & { unref?: () => void }; private _isEnabled: boolean; private _client: Client; @@ -34,9 +35,7 @@ export class SessionFlusher implements SessionFlusherLike { // Call to setInterval, so that flush is called every 60 seconds. this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (this._intervalId.unref) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access this._intervalId.unref(); } this._sessionAttrs = attrs; @@ -80,12 +79,14 @@ export class SessionFlusher implements SessionFlusherLike { return; } const isolationScope = getIsolationScope(); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); if (requestSession && requestSession.status) { this._incrementSessionStatusCount(requestSession.status, new Date()); // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in // case captureRequestSession is called more than once to prevent double count + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession(undefined); /* eslint-enable @typescript-eslint/no-unsafe-member-access */ } @@ -95,6 +96,7 @@ export class SessionFlusher implements SessionFlusherLike { * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of * the session received */ + // eslint-disable-next-line deprecation/deprecation private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys const sessionStartedTrunc = new Date(date).setSeconds(0, 0); diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 89997cb0ad35..e7c9a8e9ac41 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -7,7 +7,7 @@ import type { SpanStatus, SpanTimeInput, } from '../types-hoist'; -import { uuid4 } from '../utils-hoist/misc'; +import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; import { TRACE_FLAG_NONE } from '../utils/spanUtils'; /** @@ -18,8 +18,8 @@ export class SentryNonRecordingSpan implements Span { private _spanId: string; public constructor(spanContext: SentrySpanArguments = {}) { - this._traceId = spanContext.traceId || uuid4(); - this._spanId = spanContext.spanId || uuid4().substring(16); + this._traceId = spanContext.traceId || generateTraceId(); + this._spanId = spanContext.spanId || generateSpanId(); } /** @inheritdoc */ diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index fcc071c39603..126702dfad2b 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -25,8 +25,8 @@ import type { TransactionSource, } from '../types-hoist'; import { logger } from '../utils-hoist/logger'; -import { uuid4 } from '../utils-hoist/misc'; import { dropUndefinedKeys } from '../utils-hoist/object'; +import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; import { timestampInSeconds } from '../utils-hoist/time'; import { TRACE_FLAG_NONE, @@ -75,8 +75,8 @@ export class SentrySpan implements Span { * @hidden */ public constructor(spanContext: SentrySpanArguments = {}) { - this._traceId = spanContext.traceId || uuid4(); - this._spanId = spanContext.spanId || uuid4().substring(16); + this._traceId = spanContext.traceId || generateTraceId(); + this._spanId = spanContext.spanId || generateSpanId(); this._startTime = spanContext.startTimestamp || timestampInSeconds(); this._attributes = {}; diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 10fc61420e25..60aa60b38868 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -1,3 +1,4 @@ +import type { FeatureFlag } from '../featureFlags'; import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; @@ -13,6 +14,7 @@ export interface Contexts extends Record { cloud_resource?: CloudResourceContext; state?: StateContext; profile?: ProfileContext; + flags?: FeatureFlagContext; } export interface StateContext extends Record { @@ -124,3 +126,13 @@ export interface MissingInstrumentationContext extends Record { package: string; ['javascript.is_cjs']?: boolean; } + +/** + * Used to buffer flag evaluation data on the current scope and attach it to + * error events. `values` should be initialized as empty ([]), and modifying + * directly is not recommended. Use the functions in @sentry/browser + * src/utils/featureFlags instead. + */ +export interface FeatureFlagContext extends Record { + values: FeatureFlag[]; +} diff --git a/packages/core/src/types-hoist/csp.ts b/packages/core/src/types-hoist/csp.ts new file mode 100644 index 000000000000..d1c49be6b858 --- /dev/null +++ b/packages/core/src/types-hoist/csp.ts @@ -0,0 +1,15 @@ +export interface LegacyCSPReport { + readonly 'csp-report': { + readonly 'document-uri'?: string; + readonly referrer?: string; + readonly 'blocked-uri'?: string; + readonly 'effective-directive'?: string; + readonly 'violated-directive'?: string; + readonly 'original-policy'?: string; + readonly disposition: 'enforce' | 'report' | 'reporting'; + readonly 'status-code'?: number; + readonly status?: string; + readonly 'script-sample'?: string; + readonly sample?: string; + }; +} diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts index 20c67b8857fe..b5e2599942d4 100644 --- a/packages/core/src/types-hoist/envelope.ts +++ b/packages/core/src/types-hoist/envelope.ts @@ -1,6 +1,7 @@ import type { AttachmentType } from './attachment'; import type { SerializedCheckIn } from './checkin'; import type { ClientReport } from './clientreport'; +import type { LegacyCSPReport } from './csp'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; import type { FeedbackEvent, UserFeedback } from './feedback'; @@ -41,7 +42,8 @@ export type EnvelopeItemType = | 'replay_recording' | 'check_in' | 'statsd' - | 'span'; + | 'span' + | 'raw_security'; export type BaseEnvelopeHeaders = { [key: string]: unknown; @@ -84,6 +86,7 @@ type ProfileItemHeaders = { type: 'profile' }; type ProfileChunkItemHeaders = { type: 'profile_chunk' }; type StatsdItemHeaders = { type: 'statsd'; length: number }; type SpanItemHeaders = { type: 'span' }; +type RawSecurityHeaders = { type: 'raw_security'; sentry_release?: string; sentry_environment?: string }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -100,6 +103,7 @@ export type FeedbackItem = BaseEnvelopeItem; export type ProfileItem = BaseEnvelopeItem; export type ProfileChunkItem = BaseEnvelopeItem; export type SpanItem = BaseEnvelopeItem>; +export type RawSecurityItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial }; type SessionEnvelopeHeaders = { sent_at: string }; @@ -120,6 +124,7 @@ export type CheckInEnvelope = BaseEnvelope; export type StatsdEnvelope = BaseEnvelope; export type SpanEnvelope = BaseEnvelope; export type ProfileChunkEnvelope = BaseEnvelope; +export type RawSecurityEnvelope = BaseEnvelope; export type Envelope = | EventEnvelope @@ -129,6 +134,7 @@ export type Envelope = | ReplayEnvelope | CheckInEnvelope | StatsdEnvelope - | SpanEnvelope; + | SpanEnvelope + | RawSecurityEnvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 5dd1839aeba7..3433c17092cf 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -43,6 +43,8 @@ export type { UserFeedbackItem, CheckInItem, CheckInEnvelope, + RawSecurityEnvelope, + RawSecurityItem, StatsdItem, StatsdEnvelope, ProfileItem, @@ -104,8 +106,11 @@ export type { Session, SessionContext, SessionStatus, + // eslint-disable-next-line deprecation/deprecation RequestSession, + // eslint-disable-next-line deprecation/deprecation RequestSessionStatus, + // eslint-disable-next-line deprecation/deprecation SessionFlusherLike, SerializedSession, } from './session'; @@ -179,3 +184,4 @@ export type { export type { ParameterizedString } from './parameterize'; export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; +export type { LegacyCSPReport } from './csp'; diff --git a/packages/core/src/types-hoist/scope.ts b/packages/core/src/types-hoist/scope.ts index d3fe29f5e4b1..57990d310820 100644 --- a/packages/core/src/types-hoist/scope.ts +++ b/packages/core/src/types-hoist/scope.ts @@ -23,6 +23,7 @@ export interface ScopeContext { contexts: Contexts; tags: { [key: string]: Primitive }; fingerprint: string[]; + // eslint-disable-next-line deprecation/deprecation requestSession: RequestSession; propagationContext: PropagationContext; } @@ -168,12 +169,18 @@ export interface Scope { /** * Returns the `RequestSession` if there is one + * + * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; */ + // eslint-disable-next-line deprecation/deprecation getRequestSession(): RequestSession | undefined; /** * Sets the `RequestSession` on the scope + * + * @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`; */ + // eslint-disable-next-line deprecation/deprecation setRequestSession(requestSession?: RequestSession): this; /** diff --git a/packages/core/src/types-hoist/session.ts b/packages/core/src/types-hoist/session.ts index 5bc49b9a7733..47cfa348acbb 100644 --- a/packages/core/src/types-hoist/session.ts +++ b/packages/core/src/types-hoist/session.ts @@ -1,6 +1,10 @@ import type { User } from './user'; +/** + * @deprecated This type is deprecated and will be removed in the next major version of the SDK. + */ export interface RequestSession { + // eslint-disable-next-line deprecation/deprecation status?: RequestSessionStatus; } @@ -35,6 +39,10 @@ export interface Session { export type SessionContext = Partial; export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal'; + +/** + * @deprecated This type is deprecated and will be removed in the next major version of the SDK. + */ export type RequestSessionStatus = 'ok' | 'errored' | 'crashed'; /** JSDoc */ @@ -46,6 +54,9 @@ export interface SessionAggregates { aggregates: Array; } +/** + * @deprecated This type is deprecated and will be removed in the next major version of the SDK. + */ export interface SessionFlusherLike { /** * Increments the Session Status bucket in SessionAggregates Object corresponding to the status of the session diff --git a/packages/core/src/utils-hoist/envelope.ts b/packages/core/src/utils-hoist/envelope.ts index 5014d6fe130d..be640b90ad4f 100644 --- a/packages/core/src/utils-hoist/envelope.ts +++ b/packages/core/src/utils-hoist/envelope.ts @@ -224,6 +224,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { feedback: 'feedback', span: 'span', statsd: 'metric_bucket', + raw_security: 'security', }; /** diff --git a/packages/core/src/utils-hoist/instrument/globalError.ts b/packages/core/src/utils-hoist/instrument/globalError.ts index 6565f42c2329..bebb301b18d6 100644 --- a/packages/core/src/utils-hoist/instrument/globalError.ts +++ b/packages/core/src/utils-hoist/instrument/globalError.ts @@ -20,6 +20,8 @@ export function addGlobalErrorInstrumentationHandler(handler: (data: HandlerData function instrumentError(): void { _oldOnErrorHandler = GLOBAL_OBJ.onerror; + // Note: The reason we are doing window.onerror instead of window.addEventListener('error') + // is that we are using this handler in the Loader Script, to handle buffered errors consistently GLOBAL_OBJ.onerror = function ( msg: string | object, url?: string, @@ -36,7 +38,7 @@ function instrumentError(): void { }; triggerHandlers('error', handlerData); - if (_oldOnErrorHandler && !_oldOnErrorHandler.__SENTRY_LOADER__) { + if (_oldOnErrorHandler) { // eslint-disable-next-line prefer-rest-params return _oldOnErrorHandler.apply(this, arguments); } diff --git a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts b/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts index 5e47cd125a35..4b4173233a43 100644 --- a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts +++ b/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts @@ -1,7 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import type { HandlerDataUnhandledRejection } from '../../types-hoist'; - import { GLOBAL_OBJ } from '../worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; @@ -24,11 +21,13 @@ export function addGlobalUnhandledRejectionInstrumentationHandler( function instrumentUnhandledRejection(): void { _oldOnUnhandledRejectionHandler = GLOBAL_OBJ.onunhandledrejection; - GLOBAL_OBJ.onunhandledrejection = function (e: any): boolean { + // Note: The reason we are doing window.onunhandledrejection instead of window.addEventListener('unhandledrejection') + // is that we are using this handler in the Loader Script, to handle buffered rejections consistently + GLOBAL_OBJ.onunhandledrejection = function (e: unknown): boolean { const handlerData: HandlerDataUnhandledRejection = e; triggerHandlers('unhandledrejection', handlerData); - if (_oldOnUnhandledRejectionHandler && !_oldOnUnhandledRejectionHandler.__SENTRY_LOADER__) { + if (_oldOnUnhandledRejectionHandler) { // eslint-disable-next-line prefer-rest-params return _oldOnUnhandledRejectionHandler.apply(this, arguments); } diff --git a/packages/core/src/utils-hoist/logger.ts b/packages/core/src/utils-hoist/logger.ts index a18b19be6db6..90d306f11434 100644 --- a/packages/core/src/utils-hoist/logger.ts +++ b/packages/core/src/utils-hoist/logger.ts @@ -21,8 +21,7 @@ type LoggerConsoleMethods = Record; /** This may be mutated by the console instrumentation. */ export const originalConsoleMethods: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key in ConsoleLevel]?: (...args: any[]) => void; + [key in ConsoleLevel]?: (...args: unknown[]) => void; } = {}; /** JSDoc */ @@ -79,8 +78,7 @@ function makeLogger(): Logger { if (DEBUG_BUILD) { CONSOLE_LEVELS.forEach(name => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - logger[name] = (...args: any[]) => { + logger[name] = (...args: Parameters<(typeof GLOBAL_OBJ.console)[typeof name]>) => { if (enabled) { consoleSandbox(() => { GLOBAL_OBJ.console[name](`${PREFIX}[${name}]:`, ...args); diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils-hoist/misc.ts index 5a3190e0f87b..58a416b0ae31 100644 --- a/packages/core/src/utils-hoist/misc.ts +++ b/packages/core/src/utils-hoist/misc.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Event, Exception, Mechanism, StackFrame } from '../types-hoist'; import { addNonEnumerableProperty } from './object'; diff --git a/packages/core/src/utils-hoist/node-stack-trace.ts b/packages/core/src/utils-hoist/node-stack-trace.ts index 946492ede855..46f036cb0270 100644 --- a/packages/core/src/utils-hoist/node-stack-trace.ts +++ b/packages/core/src/utils-hoist/node-stack-trace.ts @@ -113,7 +113,7 @@ export function node(getModule?: GetModuleFn): StackLineParserFn { } return { - filename, + filename: filename ? decodeURI(filename) : undefined, module: getModule ? getModule(filename) : undefined, function: functionName, lineno: _parseIntOrUndefined(lineMatch[3]), diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index f5703feaa1da..c18247a62f55 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -94,9 +94,10 @@ export function getOriginalFunction(func: WrappedFunction * * @deprecated This function is deprecated and will be removed in the next major version of the SDK. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function urlEncode(object: { [key: string]: any }): string { - return Object.keys(object) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`) + return Object.entries(object) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } @@ -237,7 +238,7 @@ function _dropUndefinedKeys(inputValue: T, memoizationMap: Map { expect(eventProcessor(GOOGLETAG_EVENT, {})).toBe(null); }); + it('uses default filters (CEFSharp)', () => { + const eventProcessor = createInboundFiltersEventProcessor(); + expect(eventProcessor(CEFSHARP_EVENT, {})).toBe(null); + }); + it('filters on last exception when multiple present', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['incorrect type given for parameter `chewToy`'], diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index df95e08dcb97..76130e779fed 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -261,8 +261,10 @@ describe('Scope', () => { test('_requestSession clone', () => { const parentScope = new Scope(); + // eslint-disable-next-line deprecation/deprecation parentScope.setRequestSession({ status: 'errored' }); const scope = parentScope.clone(); + // eslint-disable-next-line deprecation/deprecation expect(parentScope.getRequestSession()).toEqual(scope.getRequestSession()); }); @@ -288,15 +290,19 @@ describe('Scope', () => { // Test that ensures if the status value of `status` of `_requestSession` is changed in a child scope // that it should also change in parent scope because we are copying the reference to the object const parentScope = new Scope(); + // eslint-disable-next-line deprecation/deprecation parentScope.setRequestSession({ status: 'errored' }); const scope = parentScope.clone(); + // eslint-disable-next-line deprecation/deprecation const requestSession = scope.getRequestSession(); if (requestSession) { requestSession.status = 'ok'; } + // eslint-disable-next-line deprecation/deprecation expect(parentScope.getRequestSession()).toEqual({ status: 'ok' }); + // eslint-disable-next-line deprecation/deprecation expect(scope.getRequestSession()).toEqual({ status: 'ok' }); }); @@ -316,6 +322,7 @@ describe('Scope', () => { scope.setUser({ id: '1' }); scope.setFingerprint(['abcd']); scope.addBreadcrumb({ message: 'test' }); + // eslint-disable-next-line deprecation/deprecation scope.setRequestSession({ status: 'ok' }); expect(scope['_extra']).toEqual({ a: 2 }); scope.clear(); @@ -349,6 +356,7 @@ describe('Scope', () => { scope.setUser({ id: '1337' }); scope.setLevel('info'); scope.setFingerprint(['foo']); + // eslint-disable-next-line deprecation/deprecation scope.setRequestSession({ status: 'ok' }); }); @@ -453,6 +461,7 @@ describe('Scope', () => { level: 'warning' as const, tags: { bar: '3', baz: '4' }, user: { id: '42' }, + // eslint-disable-next-line deprecation/deprecation requestSession: { status: 'errored' as RequestSessionStatus }, propagationContext: { traceId: '8949daf83f4a4a70bee4c1eb9ab242ed', diff --git a/packages/core/test/lib/sessionflusher.test.ts b/packages/core/test/lib/sessionflusher.test.ts index 00f9becfdbf0..53fe930b58c2 100644 --- a/packages/core/test/lib/sessionflusher.test.ts +++ b/packages/core/test/lib/sessionflusher.test.ts @@ -19,6 +19,7 @@ describe('Session Flusher', () => { }); test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const date = new Date('2021-04-08T12:18:23.043Z'); @@ -42,6 +43,7 @@ describe('Session Flusher', () => { }); test('test undefined attributes are excluded, on incrementSessionStatusCount call', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.0' }); const date = new Date('2021-04-08T12:18:23.043Z'); @@ -55,6 +57,7 @@ describe('Session Flusher', () => { }); test('flush is called every 60 seconds after initialisation of an instance of SessionFlusher', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); jest.advanceTimersByTime(59000); @@ -68,6 +71,7 @@ describe('Session Flusher', () => { }); test('sendSessions is called on flush if sessions were captured', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); const date = new Date('2021-04-08T12:18:23.043Z'); @@ -88,6 +92,7 @@ describe('Session Flusher', () => { }); test('sendSessions is not called on flush if no sessions were captured', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); @@ -98,12 +103,14 @@ describe('Session Flusher', () => { }); test('calling close on SessionFlusher should disable SessionFlusher', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); flusher.close(); expect((flusher as any)._isEnabled).toEqual(false); }); test('calling close on SessionFlusher will force call flush', () => { + // eslint-disable-next-line deprecation/deprecation const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); const date = new Date('2021-04-08T12:18:23.043Z'); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index edd594bfac5b..f7187695a025 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -14,9 +14,48 @@ import { } from '../../../src'; import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist'; import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; +import { spanToTraceContext } from '../../../src/utils/spanUtils'; import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; +function createMockedOtelSpan({ + spanId, + traceId, + isRemote, + attributes = {}, + startTime = Date.now(), + name = 'test span', + status = { code: SPAN_STATUS_UNSET }, + endTime = Date.now(), + parentSpanId, +}: { + spanId: string; + traceId: string; + attributes?: SpanAttributes; + startTime?: SpanTimeInput; + isRemote?: boolean; + name?: string; + status?: SpanStatus; + endTime?: SpanTimeInput; + parentSpanId?: string; +}): Span { + return { + spanContext: () => { + return { + spanId, + traceId, + isRemote, + }; + }, + attributes, + startTime, + name, + status, + endTime, + parentSpanId, + } as OpenTelemetrySdkTraceBaseSpan; +} + describe('spanToTraceHeader', () => { test('simple', () => { const span = new SentrySpan(); @@ -28,6 +67,88 @@ describe('spanToTraceHeader', () => { }); }); +describe('spanToTraceContext', () => { + it('works with a minimal span', () => { + const span = new SentrySpan({ spanId: '1234', traceId: 'ABCD' }); + + expect(spanToTraceContext(span)).toEqual({ + span_id: '1234', + trace_id: 'ABCD', + }); + }); + + it('works with a span with parentSpanId', () => { + const span = new SentrySpan({ + spanId: '1234', + traceId: 'ABCD', + parentSpanId: '5678', + }); + + expect(spanToTraceContext(span)).toEqual({ + span_id: '1234', + trace_id: 'ABCD', + parent_span_id: '5678', + }); + }); + + it('works with a local OTEL span', () => { + const span = createMockedOtelSpan({ + spanId: '1234', + traceId: 'ABCD', + isRemote: false, + }); + + expect(spanToTraceContext(span)).toEqual({ + span_id: '1234', + trace_id: 'ABCD', + }); + }); + + it('works with a local OTEL span with parentSpanId', () => { + const span = createMockedOtelSpan({ + spanId: '1234', + traceId: 'ABCD', + isRemote: false, + parentSpanId: 'XYZ', + }); + + expect(spanToTraceContext(span)).toEqual({ + parent_span_id: 'XYZ', + span_id: '1234', + trace_id: 'ABCD', + }); + }); + + it('works with a remote OTEL span', () => { + const span = createMockedOtelSpan({ + spanId: '1234', + traceId: 'ABCD', + isRemote: true, + }); + + expect(spanToTraceContext(span)).toEqual({ + parent_span_id: '1234', + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: 'ABCD', + }); + }); + + it('works with a remote OTEL span with parentSpanId', () => { + const span = createMockedOtelSpan({ + spanId: '1234', + traceId: 'ABCD', + isRemote: true, + parentSpanId: 'XYZ', + }); + + expect(spanToTraceContext(span)).toEqual({ + parent_span_id: '1234', + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: 'ABCD', + }); + }); +}); + describe('spanTimeInputToSeconds', () => { it('works with undefined', () => { const now = timestampInSeconds(); @@ -111,41 +232,6 @@ describe('spanToJSON', () => { }); describe('OpenTelemetry Span', () => { - function createMockedOtelSpan({ - spanId, - traceId, - attributes, - startTime, - name, - status, - endTime, - parentSpanId, - }: { - spanId: string; - traceId: string; - attributes: SpanAttributes; - startTime: SpanTimeInput; - name: string; - status: SpanStatus; - endTime: SpanTimeInput; - parentSpanId?: string; - }): Span { - return { - spanContext: () => { - return { - spanId, - traceId, - }; - }, - attributes, - startTime, - name, - status, - endTime, - parentSpanId, - } as OpenTelemetrySdkTraceBaseSpan; - } - it('works with a simple span', () => { const span = createMockedOtelSpan({ spanId: 'SPAN-1', diff --git a/packages/core/test/utils-hoist/stacktrace.test.ts b/packages/core/test/utils-hoist/stacktrace.test.ts index 22a50f3f71de..1732c35d02bc 100644 --- a/packages/core/test/utils-hoist/stacktrace.test.ts +++ b/packages/core/test/utils-hoist/stacktrace.test.ts @@ -319,4 +319,64 @@ describe('node', () => { const result = node(line); expect(result?.in_app).toBe(true); }); + + it('parses frame filename paths with spaces and characters in file name', () => { + const input = 'at myObject.myMethod (/path/to/file with space(1).js:10:5)'; + + const expectedOutput = { + filename: '/path/to/file with space(1).js', + module: undefined, + function: 'myObject.myMethod', + lineno: 10, + colno: 5, + in_app: true, + }; + + expect(node(input)).toEqual(expectedOutput); + }); + + it('parses frame filename paths with spaces and characters in file path', () => { + const input = 'at myObject.myMethod (/path with space(1)/to/file.js:10:5)'; + + const expectedOutput = { + filename: '/path with space(1)/to/file.js', + module: undefined, + function: 'myObject.myMethod', + lineno: 10, + colno: 5, + in_app: true, + }; + + expect(node(input)).toEqual(expectedOutput); + }); + + it('parses encoded frame filename paths with spaces and characters in file name', () => { + const input = 'at myObject.myMethod (/path/to/file%20with%20space(1).js:10:5)'; + + const expectedOutput = { + filename: '/path/to/file with space(1).js', + module: undefined, + function: 'myObject.myMethod', + lineno: 10, + colno: 5, + in_app: true, + }; + + expect(node(input)).toEqual(expectedOutput); + }); + + it('parses encoded frame filename paths with spaces and characters in file path', () => { + const input = 'at myObject.myMethod (/path%20with%20space(1)/to/file.js:10:5)'; + + const expectedOutput = { + filename: '/path with space(1)/to/file.js', + module: undefined, + function: 'myObject.myMethod', + lineno: 10, + colno: 5, + in_app: true, + }; + + expect(node(input)).toEqual(expectedOutput); + }); }); diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 5a98e37acc9c..971a48c7233b 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "8.42.0", "@sentry/react": "8.42.0", - "@sentry/webpack-plugin": "2.22.6" + "@sentry/webpack-plugin": "2.22.7" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 64d57ec530b3..6f89769c2a37 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -49,6 +49,7 @@ export { extractRequestData, createGetModuleFromFilename, anrIntegration, + disableAnrDetectionForCallback, consoleIntegration, httpIntegration, nativeNodeFetchIntegration, diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index 782e113e1877..89e82588cd7a 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -9,11 +9,9 @@ import { isExpectedError } from './helpers'; */ export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => { return (target: unknown, propertyKey, descriptor: PropertyDescriptor) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalMethod = descriptor.value as (...args: any[]) => Promise; + const originalMethod = descriptor.value as (...args: unknown[]) => Promise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { + descriptor.value = function (...args: unknown[]) { return Sentry.withMonitor( monitorSlug, () => { @@ -31,11 +29,9 @@ export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): */ export function SentryTraced(op: string = 'function') { return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalMethod = descriptor.value as (...args: any[]) => Promise | any; // function can be sync or async + const originalMethod = descriptor.value as (...args: unknown[]) => Promise | unknown; // function can be sync or async - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { + descriptor.value = function (...args: unknown[]) { return startSpan( { op: op, @@ -64,11 +60,9 @@ export function SentryTraced(op: string = 'function') { */ export function SentryExceptionCaptured() { return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const originalCatch = descriptor.value as (exception: unknown, host: unknown, ...args: any[]) => void; + const originalCatch = descriptor.value as (exception: unknown, host: unknown, ...args: unknown[]) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (exception: unknown, host: unknown, ...args: any[]) { + descriptor.value = function (exception: unknown, host: unknown, ...args: unknown[]) { if (isExpectedError(exception)) { return originalCatch.apply(this, [exception, host, ...args]); } diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 55e168c53963..e75054d3391d 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -23,6 +23,23 @@ import { import type { Observable } from 'rxjs'; import { isExpectedError } from './helpers'; +// Partial extract of FastifyRequest interface +// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41 +interface FastifyRequest { + routeOptions?: { + method?: string; + url?: string; + }; +} + +// Partial extract of ExpressRequest interface +interface ExpressRequest { + route?: { + path?: string; + }; + method?: string; +} + /** * Note: We cannot use @ syntax to add the decorators, so we add them directly below the classes as function wrappers. */ @@ -52,11 +69,15 @@ class SentryTracingInterceptor implements NestInterceptor { } if (context.getType() === 'http') { - const req = context.switchToHttp().getRequest(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (req.route) { - // eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining,@typescript-eslint/no-unsafe-member-access - getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); + const req = context.switchToHttp().getRequest() as FastifyRequest | ExpressRequest; + if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { + // fastify case + getIsolationScope().setTransactionName( + `${(req.routeOptions.method || 'GET').toUpperCase()} ${req.routeOptions.url}`, + ); + } else if ('route' in req && req.route && req.route.path) { + // express case + getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.route.path}`); } } diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 44c3950bad62..74cdfc546a24 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -77,8 +77,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/instrumentation-http": "0.53.0", - "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "8.42.0", "@sentry/core": "8.42.0", @@ -86,7 +85,7 @@ "@sentry/opentelemetry": "8.42.0", "@sentry/react": "8.42.0", "@sentry/vercel-edge": "8.42.0", - "@sentry/webpack-plugin": "2.22.6", + "@sentry/webpack-plugin": "2.22.7", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "3.29.5", diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts index 10f783b9e9e6..bc0f9ff170c6 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts @@ -41,32 +41,29 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI sentryTrace, baggage, }: { - data: { - pageProps: { - _sentryTraceData?: string; - _sentryBaggage?: string; - }; - }; + data?: unknown; sentryTrace?: string; baggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call - // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per - // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. - // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. - if (!appGetInitialProps.pageProps) { - appGetInitialProps.pageProps = {}; - } + if (typeof appGetInitialProps === 'object' && appGetInitialProps !== null) { + // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call + // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per + // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. + // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. + if (!(appGetInitialProps as Record).pageProps) { + (appGetInitialProps as Record).pageProps = {}; + } - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (sentryTrace) { - appGetInitialProps.pageProps._sentryTraceData = sentryTrace; - } + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + if (sentryTrace) { + (appGetInitialProps as { pageProps: Record }).pageProps._sentryTraceData = sentryTrace; + } - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (baggage) { - appGetInitialProps.pageProps._sentryBaggage = baggage; + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + if (baggage) { + (appGetInitialProps as { pageProps: Record }).pageProps._sentryBaggage = baggage; + } } return appGetInitialProps; diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts index 731d3fe1e24a..0a0ee1e2867f 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts @@ -43,22 +43,21 @@ export function wrapErrorGetInitialPropsWithSentry( baggage, sentryTrace, }: { - data: ErrorProps & { - _sentryTraceData?: string; - _sentryBaggage?: string; - }; + data?: unknown; baggage?: string; sentryTrace?: string; } = await tracedGetInitialProps.apply(thisArg, args); - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (sentryTrace) { - errorGetInitialProps._sentryTraceData = sentryTrace; - } + if (typeof errorGetInitialProps === 'object' && errorGetInitialProps !== null) { + if (sentryTrace) { + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + (errorGetInitialProps as Record)._sentryTraceData = sentryTrace; + } - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (baggage) { - errorGetInitialProps._sentryBaggage = baggage; + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + if (baggage) { + (errorGetInitialProps as Record)._sentryBaggage = baggage; + } } return errorGetInitialProps; diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts index 97246ec9d122..2ba9bbb3156d 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts @@ -39,22 +39,21 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro baggage, sentryTrace, }: { - data: { - _sentryTraceData?: string; - _sentryBaggage?: string; - }; + data?: unknown; baggage?: string; sentryTrace?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (sentryTrace) { - initialProps._sentryTraceData = sentryTrace; - } + if (typeof initialProps === 'object' && initialProps !== null) { + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + if (sentryTrace) { + (initialProps as Record)._sentryTraceData = sentryTrace; + } - // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - if (baggage) { - initialProps._sentryBaggage = baggage; + // The Next.js serializer throws on undefined values so we need to guard for it (#12102) + if (baggage) { + (initialProps as Record)._sentryBaggage = baggage; + } } return initialProps; diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts index 7c4b4101d80e..328f35d8b350 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts @@ -34,9 +34,13 @@ export function wrapGetServerSidePropsWithSentry( data: serverSideProps, baggage, sentryTrace, + }: { + data?: unknown; + baggage?: string; + sentryTrace?: string; } = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType); - if (serverSideProps && 'props' in serverSideProps) { + if (typeof serverSideProps === 'object' && serverSideProps !== null && 'props' in serverSideProps) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { (serverSideProps.props as Record)._sentryTraceData = sentryTrace; diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 521e45df4e27..4b764aeeed8e 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -6,6 +6,8 @@ import { SPAN_STATUS_OK, Scope, captureException, + generateSpanId, + generateTraceId, getActiveSpan, getCapturedScopesOnSpan, getClient, @@ -14,7 +16,6 @@ import { propagationContextFromHeaders, setCapturedScopesOnSpan, startSpanManual, - uuid4, winterCGHeadersToDict, withIsolationScope, withScope, @@ -88,8 +89,8 @@ export function wrapGenerationFunctionWithSentry a headersDict?.['sentry-trace'] ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage']) : { - traceId: requestTraceId || uuid4(), - spanId: uuid4().substring(16), + traceId: requestTraceId || generateTraceId(), + spanId: generateSpanId(), }, ); scope.setPropagationContext(propagationContext); diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index ff91823faf43..0d4ffe5f32d1 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -6,6 +6,8 @@ import { SPAN_STATUS_OK, Scope, captureException, + generateSpanId, + generateTraceId, getActiveSpan, getCapturedScopesOnSpan, getRootSpan, @@ -13,7 +15,6 @@ import { propagationContextFromHeaders, setCapturedScopesOnSpan, startSpanManual, - uuid4, vercelWaitUntil, winterCGHeadersToDict, withIsolationScope, @@ -67,8 +68,8 @@ export function wrapServerComponentWithSentry any> headersDict?.['sentry-trace'] ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage']) : { - traceId: requestTraceId || uuid4(), - spanId: uuid4().substring(16), + traceId: requestTraceId || generateTraceId(), + spanId: generateSpanId(), }, ); diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 75ce7258fae5..44dfb544654f 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -376,7 +376,10 @@ export type SentryBuildOptions = { /** * Use `hidden-source-map` for webpack `devtool` option, which strips the `sourceMappingURL` from the bottom of built * JS files. + * + * @deprecated This is deprecated. The SDK emits chunks without `sourceMappingURL` for client bundles by default. */ + // TODO(v9): Remove option hideSourceMaps?: boolean; /** diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 20f1a00ce3c0..9fb3bef49e67 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -336,20 +336,11 @@ export function constructWebpackConfigFunction( if (sentryWebpackPlugin) { if (!userSentryOptions.sourcemaps?.disable) { - // TODO(v9): Remove this warning - if (newConfig.devtool === false) { - const runtimePrefix = !isServer ? 'Client' : runtime === 'edge' ? 'Edge' : 'Node.js'; - // eslint-disable-next-line no-console - console.warn( - `[@sentry/nextjs - ${runtimePrefix}] You disabled sourcemaps with the Webpack \`devtool\` option. Currently, the Sentry SDK will override this option to generate sourcemaps. In future versions, the Sentry SDK will not override the \`devtool\` option if you explicitly disable it. If you want to generate and upload sourcemaps please set the \`devtool\` option to 'hidden-source-map' or undefined.`, - ); - } - // TODO(v9): Remove this warning and print warning in case source map deletion is auto configured if (!isServer && !userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload) { // eslint-disable-next-line no-console console.warn( - "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `deleteSourceMapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `deleteSourceMapsAfterUpload` will default to `true`.", + "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `deleteSourceMapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `deleteSourceMapsAfterUpload` will default to `true`. If you do not want to generate and upload sourcemaps, set the `sourcemaps.disable` option in `withSentryConfig()`.", ); } diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index a9d8b812f8a9..20af1d99f1ce 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -68,19 +68,19 @@ describe('constructWebpackConfigFunction()', () => { expect(finalWebpackConfig?.devtool).not.toEqual('source-map'); }); - it('allows for the use of `hidden-source-map` as `devtool` value for client-side builds', async () => { + it('uses `hidden-source-map` as `devtool` value for client-side builds', async () => { const finalClientWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig: exportedNextConfig, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: clientBuildContext, - sentryBuildTimeOptions: { hideSourceMaps: true }, + sentryBuildTimeOptions: {}, }); const finalServerWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig: exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, - sentryBuildTimeOptions: { hideSourceMaps: true }, + sentryBuildTimeOptions: {}, }); expect(finalClientWebpackConfig.devtool).toEqual('hidden-source-map'); diff --git a/packages/nextjs/test/types/next.config.ts b/packages/nextjs/test/types/next.config.ts index 74ea16a946db..1039c99162b2 100644 --- a/packages/nextjs/test/types/next.config.ts +++ b/packages/nextjs/test/types/next.config.ts @@ -3,7 +3,6 @@ import type { NextConfig } from 'next'; import { withSentryConfig } from '../../src/config/withSentryConfig'; const config: NextConfig = { - hideSourceMaps: true, webpack: config => ({ ...config, module: { diff --git a/packages/nitro-utils/.eslintrc.js b/packages/nitro-utils/.eslintrc.js new file mode 100644 index 000000000000..3849c1ee28a6 --- /dev/null +++ b/packages/nitro-utils/.eslintrc.js @@ -0,0 +1,21 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + env: { + node: true, + }, + overrides: [ + { + files: ['src/**'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + }, + }, + { + files: ['src/metrics/**'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, + ], +}; diff --git a/packages/nitro-utils/LICENSE b/packages/nitro-utils/LICENSE new file mode 100644 index 000000000000..5af93a5bdae5 --- /dev/null +++ b/packages/nitro-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2024 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/nitro-utils/README.md b/packages/nitro-utils/README.md new file mode 100644 index 000000000000..321352938655 --- /dev/null +++ b/packages/nitro-utils/README.md @@ -0,0 +1,19 @@ +

+ + Sentry + +

+ +# Sentry Utilities for Nitro-based SDKs + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) +- [TypeDoc](http://getsentry.github.io/sentry-node/) + +## General + +Common utilities used by Sentry SDKs that use Nitro on the server-side. + +Note: This package is only meant to be used internally, and as such is not part of our public API contract and does not +follow semver. diff --git a/packages/nitro-utils/package.json b/packages/nitro-utils/package.json new file mode 100644 index 000000000000..0e024b525f61 --- /dev/null +++ b/packages/nitro-utils/package.json @@ -0,0 +1,71 @@ +{ + "name": "@sentry-internal/nitro-utils", + "version": "8.42.0", + "description": "Utilities for all Sentry SDKs with Nitro on the server-side", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro-utils", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=16.20" + }, + "files": [ + "/build" + ], + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "typesVersions": { + "<4.9": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "8.42.0" + }, + "devDependencies": { + "rollup": "^4.24.4" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "run-p build:transpile:watch build:types:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "clean": "rimraf build coverage sentry-internal-nitro-utils-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "test": "yarn test:unit", + "test:unit": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/nitro-utils/rollup.npm.config.mjs b/packages/nitro-utils/rollup.npm.config.mjs new file mode 100644 index 000000000000..d28a7a6f54a0 --- /dev/null +++ b/packages/nitro-utils/rollup.npm.config.mjs @@ -0,0 +1,17 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, + }, + }), +); diff --git a/packages/nitro-utils/src/index.ts b/packages/nitro-utils/src/index.ts new file mode 100644 index 000000000000..92a212e16f1d --- /dev/null +++ b/packages/nitro-utils/src/index.ts @@ -0,0 +1,6 @@ +export { patchEventHandler } from './nitro/patchEventHandler'; + +export { + wrapServerEntryWithDynamicImport, + type WrapServerEntryPluginOptions, +} from './rollupPlugins/wrapServerEntryWithDynamicImport'; diff --git a/packages/nitro-utils/src/nitro/patchEventHandler.ts b/packages/nitro-utils/src/nitro/patchEventHandler.ts new file mode 100644 index 000000000000..d15da9f0db27 --- /dev/null +++ b/packages/nitro-utils/src/nitro/patchEventHandler.ts @@ -0,0 +1,39 @@ +import { getDefaultIsolationScope, getIsolationScope, logger, withIsolationScope } from '@sentry/core'; +import type { EventHandler } from 'h3'; +import { flushIfServerless } from '../util/flush'; + +/** + * A helper to patch a given `h3` event handler, ensuring that + * requests are properly isolated and data is flushed to Sentry. + */ +export function patchEventHandler(handler: EventHandler): EventHandler { + return new Proxy(handler, { + async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { + // In environments where we cannot make use of the OTel + // http instrumentation (e.g. when using top level import + // of the server instrumentation file instead of + // `--import` or dynamic import, like on vercel) + // we still need to ensure requests are properly isolated + // by comparing the current isolation scope to the default + // one. + // Requests are properly isolated if they differ. + // If that's not the case, we fork the isolation scope here. + const isolationScope = getIsolationScope(); + const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + logger.log( + `Patched h3 event handler. ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); + + return withIsolationScope(newIsolationScope, async () => { + try { + return await handlerTarget.apply(handlerThisArg, handlerArgs); + } finally { + await flushIfServerless(); + } + }); + }, + }); +} diff --git a/packages/nitro-utils/src/nitro/utils.ts b/packages/nitro-utils/src/nitro/utils.ts new file mode 100644 index 000000000000..8e8f08cf1a3b --- /dev/null +++ b/packages/nitro-utils/src/nitro/utils.ts @@ -0,0 +1,30 @@ +import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core'; + +/** + * Flushes Sentry for serverless environments. + */ +export async function flushIfServerless(): Promise { + const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY; + + // @ts-expect-error - this is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} + +/** + * Flushes Sentry. + */ +export async function flushWithTimeout(): Promise { + const isDebug = getClient()?.getOptions()?.debug; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} diff --git a/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts b/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts new file mode 100644 index 000000000000..8bd3b3d939e4 --- /dev/null +++ b/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts @@ -0,0 +1,242 @@ +import { consoleSandbox } from '@sentry/core'; +import type { InputPluginOption } from 'rollup'; + +export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; +export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions='; +export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions='; +export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END'; + +export type WrapServerEntryPluginOptions = { + serverEntrypointFileName: string; + serverConfigFileName: string; + resolvedServerConfigPath: string; + entrypointWrappedFunctions: string[]; + additionalImports?: string[]; + debug?: boolean; +}; + +/** + * A Rollup plugin which wraps the server entry with a dynamic `import()`. This makes it possible to initialize Sentry first + * by using a regular `import` and load the server after that. + * This also works with serverless `handler` functions, as it re-exports the `handler`. + * + * @param config Configuration options for the Rollup Plugin + * @param config.serverConfigFileName Name of the Sentry server config (without file extension). E.g. 'sentry.server.config' + * @param config.resolvedServerConfigPath Resolved path of the Sentry server config (based on `src` directory) + * @param config.entryPointWrappedFunctions Exported bindings of the server entry file, which are wrapped as async function. E.g. ['default', 'handler', 'server'] + * @param config.additionalImports Adds additional imports to the entry file. Can be e.g. 'import-in-the-middle/hook.mjs' + * @param config.debug Whether debug logs are enabled in the build time environment + */ +export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOptions): InputPluginOption { + const { + serverEntrypointFileName, + serverConfigFileName, + resolvedServerConfigPath, + entrypointWrappedFunctions, + additionalImports, + debug, + } = config; + + // In order to correctly import the server config file + // and dynamically import the nitro runtime, we need to + // mark the resolutionId with '\0raw' to fall into the + // raw chunk group, c.f. https://github.com/nitrojs/nitro/commit/8b4a408231bdc222569a32ce109796a41eac4aa6#diff-e58102d2230f95ddeef2662957b48d847a6e891e354cfd0ae6e2e03ce848d1a2R142 + const resolutionIdPrefix = '\0raw'; + + return { + name: 'sentry-wrap-server-entry-with-dynamic-import', + async resolveId(source, importer, options) { + if (source.includes(`/${serverConfigFileName}`)) { + return { id: source, moduleSideEffects: true }; + } + + if (additionalImports && additionalImports.includes(source)) { + // When importing additional imports like "import-in-the-middle/hook.mjs" in the returned code of the `load()` function below: + // By setting `moduleSideEffects` to `true`, the import is added to the bundle, although nothing is imported from it + // By importing "import-in-the-middle/hook.mjs", we can make sure this file is included, as not all node builders are including files imported with `module.register()`. + // Prevents the error "Failed to register ESM hook Error: Cannot find module 'import-in-the-middle/hook.mjs'" + return { id: source, moduleSideEffects: true, external: true }; + } + + if ( + options.isEntry && + source.includes(serverEntrypointFileName) && + source.includes('.mjs') && + !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + ) { + const resolution = await this.resolve(source, importer, options); + + // If it cannot be resolved or is external, just return it so that Rollup can display an error + if (!resolution || (resolution && resolution.external)) return resolution; + + const moduleInfo = await this.load(resolution); + + moduleInfo.moduleSideEffects = true; + + // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix + return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + ? resolution.id + : `${resolutionIdPrefix}${resolution.id + // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) + .concat(SENTRY_WRAPPED_ENTRY) + .concat( + constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug), + ) + .concat(QUERY_END_INDICATOR)}`; + } + return null; + }, + load(id: string) { + if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length); + + // Mostly useful for serverless `handler` functions + const reExportedFunctions = + id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) + ? constructFunctionReExport(id, entryId) + : ''; + + return ( + // Regular `import` of the Sentry config + `import ${JSON.stringify(resolvedServerConfigPath)};\n` + + // Dynamic `import()` for the previous, actual entry point. + // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling) + `import(${JSON.stringify(entryId)});\n` + + // By importing additional imports like "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`. + `${additionalImports ? additionalImports.map(importPath => `import "${importPath}";\n`) : ''}` + + `${reExportedFunctions}\n` + ); + } + + return null; + }, + }; +} + +/** + * Strips the Sentry query part from a path. + * Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path + * + * **Only exported for testing** + */ +export function removeSentryQueryFromPath(url: string): string { + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`); + return url.replace(regex, ''); +} + +/** + * Extracts and sanitizes function re-export and function wrap query parameters from a query string. + * If it is a default export, it is not considered for re-exporting. + * + * **Only exported for testing** + */ +export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } { + // Regex matches the comma-separated params between the functions query + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const wrapRegex = new RegExp( + `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`, + ); + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`); + + const wrapMatch = query.match(wrapRegex); + const reexportMatch = query.match(reexportRegex); + + const wrap = + wrapMatch && wrapMatch[1] + ? wrapMatch[1] + .split(',') + .filter(param => param !== '') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + const reexport = + reexportMatch && reexportMatch[1] + ? reexportMatch[1] + .split(',') + .filter(param => param !== '' && param !== 'default') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + return { wrap, reexport }; +} + +/** + * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry. + * It uses Rollup's `exportedBindings` to determine the functions to re-export. Functions which should be wrapped + * (e.g. serverless handlers) are wrapped by Sentry. + * + * **Only exported for testing** + */ +export function constructWrappedFunctionExportQuery( + exportedBindings: Record | null, + entrypointWrappedFunctions: string[], + debug?: boolean, +): string { + const functionsToExport: { wrap: string[]; reexport: string[] } = { + wrap: [], + reexport: [], + }; + + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` + // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. + Object.values(exportedBindings || {}).forEach(functions => + functions.forEach(fn => { + if (entrypointWrappedFunctions.includes(fn)) { + functionsToExport.wrap.push(fn); + } else { + functionsToExport.reexport.push(fn); + } + }), + ); + + if (debug && functionsToExport.wrap.length === 0) { + consoleSandbox(() => + // eslint-disable-next-line no-console + console.warn( + '[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to `entrypointWrappedFunctions`.', + ), + ); + } + + const wrapQuery = functionsToExport.wrap.length + ? `${SENTRY_WRAPPED_FUNCTIONS}${functionsToExport.wrap.join(',')}` + : ''; + const reexportQuery = functionsToExport.reexport.length + ? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}` + : ''; + + return [wrapQuery, reexportQuery].join(''); +} + +/** + * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`) + * + * **Only exported for testing** + */ +export function constructFunctionReExport(pathWithQuery: string, entryId: string): string { + const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery); + + return wrapFunctions + .reduce( + (functionsCode, currFunctionName) => + functionsCode.concat( + `async function ${currFunctionName}_sentryWrapped(...args) {\n` + + ` const res = await import(${JSON.stringify(entryId)});\n` + + ` return res.${currFunctionName}.call(this, ...args);\n` + + '}\n' + + `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`, + ), + '', + ) + .concat( + reexportFunctions.reduce( + (functionsCode, currFunctionName) => + functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`), + '', + ), + ); +} diff --git a/packages/nitro-utils/src/util/flush.ts b/packages/nitro-utils/src/util/flush.ts new file mode 100644 index 000000000000..8e8f08cf1a3b --- /dev/null +++ b/packages/nitro-utils/src/util/flush.ts @@ -0,0 +1,30 @@ +import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core'; + +/** + * Flushes Sentry for serverless environments. + */ +export async function flushIfServerless(): Promise { + const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY; + + // @ts-expect-error - this is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} + +/** + * Flushes Sentry. + */ +export async function flushWithTimeout(): Promise { + const isDebug = getClient()?.getOptions()?.debug; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} diff --git a/packages/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts b/packages/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts new file mode 100644 index 000000000000..c13973cc4afe --- /dev/null +++ b/packages/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts @@ -0,0 +1,193 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + QUERY_END_INDICATOR, + SENTRY_REEXPORTED_FUNCTIONS, + SENTRY_WRAPPED_ENTRY, + SENTRY_WRAPPED_FUNCTIONS, + constructFunctionReExport, + constructWrappedFunctionExportQuery, + extractFunctionReexportQueryParameters, + removeSentryQueryFromPath, +} from '../../src/rollupPlugins/wrapServerEntryWithDynamicImport'; + +describe('removeSentryQueryFromPath', () => { + it('strips the Sentry query part from the path', () => { + const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_WRAPPED_FUNCTIONS}foo,${QUERY_END_INDICATOR}`; + const url2 = `/example/path${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`; + const result = removeSentryQueryFromPath(url); + const result2 = removeSentryQueryFromPath(url2); + expect(result).toBe('/example/path'); + expect(result2).toBe('/example/path'); + }); + + it('returns the same path if the specific query part is not present', () => { + const url = '/example/path?other-query=param'; + const result = removeSentryQueryFromPath(url); + expect(result).toBe(url); + }); +}); + +describe('extractFunctionReexportQueryParameters', () => { + it.each([ + [`${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }], + [ + `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,default${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar', 'default'], reexport: [] }, + ], + [ + `${SENTRY_WRAPPED_FUNCTIONS}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'], reexport: [] }, + ], + [`/example/path/${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }], + [ + `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar'], reexport: [] }, + ], + [`${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, { wrap: [], reexport: [] }], + [ + `/path${SENTRY_WRAPPED_FUNCTIONS}foo,bar${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`, + { wrap: ['foo', 'bar'], reexport: ['bar'] }, + ], + ['?other-query=param', { wrap: [], reexport: [] }], + ])('extracts parameters from the query string: %s', (query, expected) => { + const result = extractFunctionReexportQueryParameters(query); + expect(result).toEqual(expected); + }); +}); + +describe('constructWrappedFunctionExportQuery', () => { + it.each([ + [{ '.': ['handler'] }, ['handler'], `${SENTRY_WRAPPED_FUNCTIONS}handler`], + [{ '.': ['handler'], './module': ['server'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}handler,server`], + [ + { '.': ['handler'], './module': ['server'] }, + ['server'], + `${SENTRY_WRAPPED_FUNCTIONS}server${SENTRY_REEXPORTED_FUNCTIONS}handler`, + ], + [ + { '.': ['handler', 'otherFunction'] }, + ['handler'], + `${SENTRY_WRAPPED_FUNCTIONS}handler${SENTRY_REEXPORTED_FUNCTIONS}otherFunction`, + ], + [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], `${SENTRY_WRAPPED_FUNCTIONS}handler,otherFn`], + [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], `${SENTRY_WRAPPED_FUNCTIONS}bar,foo`], + [{ '.': ['foo', 'bar'] }, ['foo'], `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}bar`], + [{ '.': ['foo', 'bar'] }, ['bar'], `${SENTRY_WRAPPED_FUNCTIONS}bar${SENTRY_REEXPORTED_FUNCTIONS}foo`], + [{ '.': ['foo', 'bar'] }, ['foo', 'bar'], `${SENTRY_WRAPPED_FUNCTIONS}foo,bar`], + [{ '.': ['foo', 'bar'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}foo,bar`], + ])( + 'constructs re-export query for exportedBindings: %j and entrypointWrappedFunctions: %j', + (exportedBindings, entrypointWrappedFunctions, expected) => { + const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions); + expect(result).toBe(expected); + }, + ); + + it('logs a warning if no functions are found for re-export and debug is true', () => { + const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const exportedBindings = { '.': ['handler'] }; + const entrypointWrappedFunctions = ['nonExistentFunction']; + const debug = true; + + const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug); + expect(result).toBe('?sentry-query-reexported-functions=handler'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to `entrypointWrappedFunctions`.', + ); + + consoleWarnSpy.mockRestore(); + }); +}); + +describe('constructFunctionReExport', () => { + it('constructs re-export code for given query parameters and entry ID', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}}`; + const query2 = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}}`; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + const result2 = constructFunctionReExport(query2, entryId); + + const expected = ` +async function foo_sentryWrapped(...args) { + const res = await import("./module"); + return res.foo.call(this, ...args); +} +export { foo_sentryWrapped as foo }; +async function bar_sentryWrapped(...args) { + const res = await import("./module"); + return res.bar.call(this, ...args); +} +export { bar_sentryWrapped as bar }; +`; + expect(result.trim()).toBe(expected.trim()); + expect(result2.trim()).toBe(expected.trim()); + }); + + it('constructs re-export code for a "default" query parameters and entry ID', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`; + const entryId = './index'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function default_sentryWrapped(...args) { + const res = await import("./index"); + return res.default.call(this, ...args); +} +export { default_sentryWrapped as default }; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('constructs re-export code for a "default" query parameters and entry ID', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`; + const entryId = './index'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function default_sentryWrapped(...args) { + const res = await import("./index"); + return res.default.call(this, ...args); +} +export { default_sentryWrapped as default }; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('constructs re-export code for a mix of wrapped and re-exported functions', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function foo_sentryWrapped(...args) { + const res = await import("./module"); + return res.foo.call(this, ...args); +} +export { foo_sentryWrapped as foo }; +export { bar } from "./module"; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('does not re-export a default export for regular re-exported functions', () => { + const query = `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}default${QUERY_END_INDICATOR}`; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + + const expected = ` +async function foo_sentryWrapped(...args) { + const res = await import("./module"); + return res.foo.call(this, ...args); +} +export { foo_sentryWrapped as foo }; +`; + expect(result.trim()).toBe(expected.trim()); + }); + + it('returns an empty string if the query string is empty', () => { + const query = ''; + const entryId = './module'; + const result = constructFunctionReExport(query, entryId); + expect(result).toBe(''); + }); +}); diff --git a/packages/nitro-utils/test/tsconfig.json b/packages/nitro-utils/test/tsconfig.json new file mode 100644 index 000000000000..38ca0b13bcdd --- /dev/null +++ b/packages/nitro-utils/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.test.json" +} diff --git a/packages/nitro-utils/test/vitest.setup.ts b/packages/nitro-utils/test/vitest.setup.ts new file mode 100644 index 000000000000..7676ce96afef --- /dev/null +++ b/packages/nitro-utils/test/vitest.setup.ts @@ -0,0 +1,8 @@ +export function setup() {} + +if (!globalThis.fetch) { + // @ts-expect-error - Needed for vitest to work with our fetch instrumentation + globalThis.Request = class Request {}; + // @ts-expect-error - Needed for vitest to work with our fetch instrumentation + globalThis.Response = class Response {}; +} diff --git a/packages/nitro-utils/tsconfig.json b/packages/nitro-utils/tsconfig.json new file mode 100644 index 000000000000..425f0657515d --- /dev/null +++ b/packages/nitro-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + "lib": ["ES2018"], + } +} diff --git a/packages/nitro-utils/tsconfig.test.json b/packages/nitro-utils/tsconfig.test.json new file mode 100644 index 000000000000..3fbe012384ee --- /dev/null +++ b/packages/nitro-utils/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "vitest/globals"] + } +} diff --git a/packages/nitro-utils/tsconfig.types.json b/packages/nitro-utils/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/nitro-utils/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/packages/nitro-utils/vite.config.ts b/packages/nitro-utils/vite.config.ts new file mode 100644 index 000000000000..0229ec105e04 --- /dev/null +++ b/packages/nitro-utils/vite.config.ts @@ -0,0 +1,9 @@ +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, + test: { + environment: 'jsdom', + setupFiles: ['./test/vitest.setup.ts'], + }, +}; diff --git a/packages/node/package.json b/packages/node/package.json index 697aa00a9751..84be66e1ce2c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -66,36 +66,36 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.25.1", - "@opentelemetry/core": "^1.25.1", - "@opentelemetry/instrumentation": "^0.54.0", - "@opentelemetry/instrumentation-amqplib": "^0.43.0", - "@opentelemetry/instrumentation-connect": "0.40.0", - "@opentelemetry/instrumentation-dataloader": "0.12.0", - "@opentelemetry/instrumentation-express": "0.44.0", - "@opentelemetry/instrumentation-fastify": "0.41.0", - "@opentelemetry/instrumentation-fs": "0.16.0", - "@opentelemetry/instrumentation-generic-pool": "0.39.0", - "@opentelemetry/instrumentation-graphql": "0.44.0", - "@opentelemetry/instrumentation-hapi": "0.41.0", - "@opentelemetry/instrumentation-http": "0.53.0", - "@opentelemetry/instrumentation-ioredis": "0.43.0", - "@opentelemetry/instrumentation-kafkajs": "0.4.0", - "@opentelemetry/instrumentation-knex": "0.41.0", - "@opentelemetry/instrumentation-koa": "0.43.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.40.0", - "@opentelemetry/instrumentation-mongodb": "0.48.0", - "@opentelemetry/instrumentation-mongoose": "0.42.0", - "@opentelemetry/instrumentation-mysql": "0.41.0", - "@opentelemetry/instrumentation-mysql2": "0.41.0", - "@opentelemetry/instrumentation-nestjs-core": "0.40.0", - "@opentelemetry/instrumentation-pg": "0.44.0", - "@opentelemetry/instrumentation-redis-4": "0.42.0", - "@opentelemetry/instrumentation-tedious": "0.15.0", - "@opentelemetry/instrumentation-undici": "0.6.0", - "@opentelemetry/resources": "^1.26.0", - "@opentelemetry/sdk-trace-base": "^1.26.0", - "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/context-async-hooks": "^1.29.0", + "@opentelemetry/core": "^1.29.0", + "@opentelemetry/instrumentation": "^0.56.0", + "@opentelemetry/instrumentation-amqplib": "^0.45.0", + "@opentelemetry/instrumentation-connect": "0.42.0", + "@opentelemetry/instrumentation-dataloader": "0.15.0", + "@opentelemetry/instrumentation-express": "0.46.0", + "@opentelemetry/instrumentation-fastify": "0.43.0", + "@opentelemetry/instrumentation-fs": "0.18.0", + "@opentelemetry/instrumentation-generic-pool": "0.42.0", + "@opentelemetry/instrumentation-graphql": "0.46.0", + "@opentelemetry/instrumentation-hapi": "0.44.0", + "@opentelemetry/instrumentation-http": "0.56.0", + "@opentelemetry/instrumentation-ioredis": "0.46.0", + "@opentelemetry/instrumentation-kafkajs": "0.6.0", + "@opentelemetry/instrumentation-knex": "0.43.0", + "@opentelemetry/instrumentation-koa": "0.46.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.43.0", + "@opentelemetry/instrumentation-mongodb": "0.50.0", + "@opentelemetry/instrumentation-mongoose": "0.45.0", + "@opentelemetry/instrumentation-mysql": "0.44.0", + "@opentelemetry/instrumentation-mysql2": "0.44.0", + "@opentelemetry/instrumentation-nestjs-core": "0.43.0", + "@opentelemetry/instrumentation-pg": "0.49.0", + "@opentelemetry/instrumentation-redis-4": "0.45.0", + "@opentelemetry/instrumentation-tedious": "0.17.0", + "@opentelemetry/instrumentation-undici": "0.9.0", + "@opentelemetry/resources": "^1.29.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "@prisma/instrumentation": "5.19.1", "@sentry/core": "8.42.0", "@sentry/opentelemetry": "8.42.0", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4572cf65b9ce..fa16ac4e6b3d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -9,7 +9,7 @@ export { localVariablesIntegration } from './integrations/local-variables'; export { modulesIntegration } from './integrations/modules'; export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; -export { anrIntegration } from './integrations/anr'; +export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr'; export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 9e979f64e5c3..b93fcfd66612 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,8 +1,10 @@ +import { types } from 'node:util'; import { Worker } from 'node:worker_threads'; import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/core'; import { GLOBAL_OBJ, defineIntegration, + getClient, getCurrentScope, getFilenameToDebugIdMap, getGlobalScope, @@ -14,6 +16,8 @@ import { NODE_VERSION } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; import type { AnrIntegrationOptions, WorkerStartData } from './common'; +const { isPromise } = types; + // This string is a placeholder that gets overwritten with the worker code. export const base64WorkerScript = '###AnrWorkerScript###'; @@ -213,3 +217,26 @@ async function _startWorker( clearInterval(timer); }; } + +export function disableAnrDetectionForCallback(callback: () => T): T; +export function disableAnrDetectionForCallback(callback: () => Promise): Promise; +/** + * Disables ANR detection for the duration of the callback + */ +export function disableAnrDetectionForCallback(callback: () => T | Promise): T | Promise { + const integration = getClient()?.getIntegrationByName(INTEGRATION_NAME) as AnrInternal | undefined; + + if (!integration) { + return callback(); + } + + integration.stopWorker(); + + const result = callback(); + if (isPromise(result)) { + return result.finally(() => integration.startWorker()); + } + + integration.startWorker(); + return result; +} diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 382e5bde8a0a..30421e66a345 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -4,7 +4,6 @@ import type * as https from 'node:https'; import { VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { getRequestInfo } from '@opentelemetry/instrumentation-http'; import type { RequestEventData, SanitizedRequestData, Scope } from '@sentry/core'; import { addBreadcrumb, @@ -21,7 +20,7 @@ import { import { DEBUG_BUILD } from '../../debug-build'; import type { NodeClient } from '../../sdk/client'; import { getRequestUrl } from '../../utils/getRequestUrl'; - +import { getRequestInfo } from './vendor/getRequestInfo'; type Http = typeof http; type Https = typeof https; @@ -149,6 +148,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase(); if (client && client.getOptions().autoSessionTracking) { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); } @@ -196,7 +196,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase; diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 12b9564737a0..10f0583b5ac6 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -2,7 +2,7 @@ import type { ClientRequest, IncomingMessage, RequestOptions, ServerResponse } f import { diag } from '@opentelemetry/api'; import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import type { IntegrationFn, Span } from '@sentry/core'; +import type { Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { getClient } from '@sentry/opentelemetry'; import { generateInstrumentOnce } from '../../otel/instrument'; @@ -30,6 +30,16 @@ interface HttpOptions { */ spans?: boolean; + /** + * Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry. + * Read more about Release Health: https://docs.sentry.io/product/releases/health/ + * + * Defaults to `true`. + * + * Note: If `autoSessionTracking` is set to `false` in `Sentry.init()` or the Client owning this integration, this option will be ignored. + */ + trackIncomingRequestsAsSessions?: boolean; + /** * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. @@ -123,20 +133,18 @@ const instrumentHttp = (options: HttpOptions = {}): void => { instrumentSentryHttp(options); }; -const _httpIntegration = ((options: HttpOptions = {}) => { +/** + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. + */ +export const httpIntegration = defineIntegration((options: HttpOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { instrumentHttp(options); }, }; -}) satisfies IntegrationFn; - -/** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. - */ -export const httpIntegration = defineIntegration(_httpIntegration); +}); /** * Determines if @param req is a ClientRequest, meaning the request was created within the express app @@ -207,7 +215,12 @@ function getConfigWithDefaults(options: Partial = {}): HttpInstrume }, responseHook: (span, res) => { const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { + + if ( + client && + client.getOptions().autoSessionTracking !== false && + options.trackIncomingRequestsAsSessions !== false + ) { setImmediate(() => { client['_captureRequestSession'](); }); diff --git a/packages/node/src/integrations/http/vendor/getRequestInfo.ts b/packages/node/src/integrations/http/vendor/getRequestInfo.ts new file mode 100644 index 000000000000..cbe167caa455 --- /dev/null +++ b/packages/node/src/integrations/http/vendor/getRequestInfo.ts @@ -0,0 +1,157 @@ +/* eslint-disable complexity */ + +/** + * Vendored in from https://github.com/open-telemetry/opentelemetry-js/commit/87bd98edd24c98a5fbb9a56fed4b673b7f17a724 + */ + +/* + * 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. + */ +import type { RequestOptions } from 'node:http'; +import * as url from 'url'; +import type { DiagLogger } from '@opentelemetry/api'; + +/** + * Makes sure options is an url object + * return an object with default value and parsed options + * @param logger component logger + * @param options original options for the request + * @param [extraOptions] additional options for the request + */ +export const getRequestInfo = ( + logger: DiagLogger, + options: url.URL | RequestOptions | string, + extraOptions?: RequestOptions, +): { + origin: string; + pathname: string; + method: string; + invalidUrl: boolean; + optionsParsed: RequestOptions; +} => { + let pathname: string; + let origin: string; + let optionsParsed: RequestOptions; + let invalidUrl = false; + if (typeof options === 'string') { + try { + const convertedOptions = stringUrlToHttpOptions(options); + optionsParsed = convertedOptions; + pathname = convertedOptions.pathname || '/'; + } catch (e) { + invalidUrl = true; + logger.verbose( + 'Unable to parse URL provided to HTTP request, using fallback to determine path. Original error:', + e, + ); + // for backward compatibility with how url.parse() behaved. + optionsParsed = { + path: options, + }; + pathname = optionsParsed.path || '/'; + } + + origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`; + if (extraOptions !== undefined) { + Object.assign(optionsParsed, extraOptions); + } + } else if (options instanceof url.URL) { + optionsParsed = { + protocol: options.protocol, + hostname: + typeof options.hostname === 'string' && options.hostname.startsWith('[') + ? options.hostname.slice(1, -1) + : options.hostname, + path: `${options.pathname || ''}${options.search || ''}`, + }; + if (options.port !== '') { + optionsParsed.port = Number(options.port); + } + if (options.username || options.password) { + optionsParsed.auth = `${options.username}:${options.password}`; + } + pathname = options.pathname; + origin = options.origin; + if (extraOptions !== undefined) { + Object.assign(optionsParsed, extraOptions); + } + } else { + optionsParsed = Object.assign({ protocol: options.host ? 'http:' : undefined }, options); + + const hostname = + optionsParsed.host || + (optionsParsed.port != null ? `${optionsParsed.hostname}${optionsParsed.port}` : optionsParsed.hostname); + origin = `${optionsParsed.protocol || 'http:'}//${hostname}`; + + pathname = (options as url.URL).pathname; + if (!pathname && optionsParsed.path) { + try { + const parsedUrl = new URL(optionsParsed.path, origin); + pathname = parsedUrl.pathname || '/'; + } catch (e) { + pathname = '/'; + } + } + } + + // some packages return method in lowercase.. + // ensure upperCase for consistency + const method = optionsParsed.method ? optionsParsed.method.toUpperCase() : 'GET'; + + return { origin, pathname, method, optionsParsed, invalidUrl }; +}; + +/** + * Mimics Node.js conversion of URL strings to RequestOptions expected by + * `http.request` and `https.request` APIs. + * + * See https://github.com/nodejs/node/blob/2505e217bba05fc581b572c685c5cf280a16c5a3/lib/internal/url.js#L1415-L1437 + * + * @param stringUrl + * @throws TypeError if the URL is not valid. + */ +function stringUrlToHttpOptions(stringUrl: string): RequestOptions & { pathname: string } { + // This is heavily inspired by Node.js handling of the same situation, trying + // to follow it as closely as possible while keeping in mind that we only + // deal with string URLs, not URL objects. + const { hostname, pathname, port, username, password, search, protocol, hash, href, origin, host } = new URL( + stringUrl, + ); + + const options: RequestOptions & { + pathname: string; + hash: string; + search: string; + href: string; + origin: string; + } = { + protocol: protocol, + hostname: hostname && hostname[0] === '[' ? hostname.slice(1, -1) : hostname, + hash: hash, + search: search, + pathname: pathname, + path: `${pathname || ''}${search || ''}`, + href: href, + origin: origin, + host: host, + }; + if (port !== '') { + options.port = Number(port); + } + if (username || password) { + options.auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`; + } + return options; +} diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index ecf89960cbd6..4a7c7a1fe83d 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -63,13 +63,8 @@ export function makeUnhandledPromiseHandler( /** * Handler for `mode` option - */ -function handleRejection( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reason: any, - options: OnUnhandledRejectionOptions, -): void { +function handleRejection(reason: unknown, options: OnUnhandledRejectionOptions): void { // https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240 const rejectionWarning = 'This error originated either by ' + @@ -81,8 +76,7 @@ function handleRejection( if (options.mode === 'warn') { consoleSandbox(() => { console.warn(rejectionWarning); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - console.error(reason && reason.stack ? reason.stack : reason); + console.error(reason && typeof reason === 'object' && 'stack' in reason ? reason.stack : reason); }); } else if (options.mode === 'strict') { consoleSandbox(() => { diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index 73312353da46..bcb668d52f80 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -129,6 +129,7 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid // running in SessionAggregates mode const isSessionAggregatesMode = client['_sessionFlusher'] !== undefined; if (isSessionAggregatesMode) { + // eslint-disable-next-line deprecation/deprecation const requestSession = getIsolationScope().getRequestSession(); // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 1d6a7e9fdb8f..6faf403511b9 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -19,6 +19,7 @@ import { instrumentNest, nestIntegration } from './nest/nest'; import { instrumentPostgres, postgresIntegration } from './postgres'; import { instrumentRedis, redisIntegration } from './redis'; import { instrumentTedious, tediousIntegration } from './tedious'; +import { instrumentVercelAi, vercelAIIntegration } from './vercelai'; /** * With OTEL, all performance integrations will be added, as OTEL only initializes them when the patched package is actually required. @@ -48,6 +49,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { kafkaIntegration(), amqplibIntegration(), lruMemoizerIntegration(), + vercelAIIntegration(), ]; } @@ -78,5 +80,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentTedious, instrumentGenericPool, instrumentAmqplib, + instrumentVercelAi, ]; } diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 1c63c22783aa..b5c9ea4bb61f 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -87,8 +87,15 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE } if (context.getType() === 'http') { + // getRequest() returns either a FastifyRequest or ExpressRequest, depending on the used adapter const req = context.switchToHttp().getRequest(); - if (req.route) { + if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { + // fastify case + getIsolationScope().setTransactionName( + `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, + ); + } else if ('route' in req && req.route && req.route.path) { + // express case getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); } } diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index ed7e968a9600..a983832ac8c6 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -1,5 +1,22 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +// Partial extract of FastifyRequest interface +// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41 +interface FastifyRequest { + routeOptions?: { + method?: string; + url?: string; + }; +} + +// Partial extract of ExpressRequest interface +interface ExpressRequest { + route?: { + path?: string; + }; + method?: string; +} + export interface MinimalNestJsExecutionContext { getType: () => string; @@ -7,12 +24,7 @@ export interface MinimalNestJsExecutionContext { // minimal request object // according to official types, all properties are required but // let's play it safe and assume they're optional - getRequest: () => { - route?: { - path?: string; - }; - method?: string; - }; + getRequest: () => FastifyRequest | ExpressRequest; }; _sentryInterceptorInstrumented?: boolean; diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts new file mode 100644 index 000000000000..d3fafc33bb02 --- /dev/null +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -0,0 +1,192 @@ +/* eslint-disable complexity */ +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, defineIntegration, spanToJSON } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core'; +import { generateInstrumentOnce } from '../../../otel/instrument'; +import { addOriginToSpan } from '../../../utils/addOriginToSpan'; +import { SentryVercelAiInstrumentation, sentryVercelAiPatched } from './instrumentation'; + +export const instrumentVercelAi = generateInstrumentOnce('vercelAI', () => new SentryVercelAiInstrumentation({})); + +const _vercelAIIntegration = (() => { + return { + name: 'vercelAI', + setupOnce() { + instrumentVercelAi(); + }, + processEvent(event) { + if (event.type === 'transaction' && event.spans?.length) { + for (const span of event.spans) { + const { data: attributes, description: name } = span; + + if (!attributes || !name || span.origin !== 'auto.vercelai.otel') { + continue; + } + + // attributes around token usage can only be set on span finish + span.data = span.data || {}; + + if (attributes['ai.usage.completionTokens'] != undefined) { + span.data['ai.completion_tokens.used'] = attributes['ai.usage.completionTokens']; + } + if (attributes['ai.usage.promptTokens'] != undefined) { + span.data['ai.prompt_tokens.used'] = attributes['ai.usage.promptTokens']; + } + if ( + attributes['ai.usage.completionTokens'] != undefined && + attributes['ai.usage.promptTokens'] != undefined + ) { + span.data['ai.total_tokens.used'] = + attributes['ai.usage.completionTokens'] + attributes['ai.usage.promptTokens']; + } + } + } + + return event; + }, + setup(client) { + client.on('spanStart', span => { + if (!sentryVercelAiPatched) { + return; + } + + const { data: attributes, description: name } = spanToJSON(span); + + if (!attributes || !name) { + return; + } + + // The id of the model + const aiModelId: string | undefined = attributes['ai.model.id']; + + // the provider of the model + const aiModelProvider: string | undefined = attributes['ai.model.provider']; + + // both of these must be defined for the integration to work + if (!aiModelId || !aiModelProvider) { + return; + } + + let isPipelineSpan = false; + + switch (name) { + case 'ai.generateText': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generateText'); + isPipelineSpan = true; + break; + } + case 'ai.generateText.doGenerate': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doGenerate'); + break; + } + case 'ai.streamText': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.streamText'); + isPipelineSpan = true; + break; + } + case 'ai.streamText.doStream': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doStream'); + break; + } + case 'ai.generateObject': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.generateObject'); + isPipelineSpan = true; + break; + } + case 'ai.generateObject.doGenerate': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doGenerate'); + break; + } + case 'ai.streamObject': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.streamObject'); + isPipelineSpan = true; + break; + } + case 'ai.streamObject.doStream': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run.doStream'); + break; + } + case 'ai.embed': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embed'); + isPipelineSpan = true; + break; + } + case 'ai.embed.doEmbed': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.embeddings'); + break; + } + case 'ai.embedMany': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.pipeline.embedMany'); + isPipelineSpan = true; + break; + } + case 'ai.embedMany.doEmbed': { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.embeddings'); + break; + } + case 'ai.toolCall': + case 'ai.stream.firstChunk': + case 'ai.stream.finish': + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run'); + break; + } + + addOriginToSpan(span, 'auto.vercelai.otel'); + + const nameWthoutAi = name.replace('ai.', ''); + span.setAttribute('ai.pipeline.name', nameWthoutAi); + span.updateName(nameWthoutAi); + + // If a Telemetry name is set and it is a pipeline span, use that as the operation name + if (attributes['ai.telemetry.functionId'] && isPipelineSpan) { + span.updateName(attributes['ai.telemetry.functionId']); + span.setAttribute('ai.pipeline.name', attributes['ai.telemetry.functionId']); + } + + if (attributes['ai.prompt']) { + span.setAttribute('ai.input_messages', attributes['ai.prompt']); + } + if (attributes['ai.model.id']) { + span.setAttribute('ai.model_id', attributes['ai.model.id']); + } + span.setAttribute('ai.streaming', name.includes('stream')); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library. + * + * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry). + * + * @example + * ```javascript + * const Sentry = require('@sentry/node'); + * + * Sentry.init({ + * integrations: [Sentry.vercelAIIntegration()], + * }); + * ``` + * + * By default this integration adds tracing support to all `ai` function calls. If you need to disable + * collecting spans for a specific call, you can do so by setting `experimental_telemetry.isEnabled` to + * `false` in the first argument of the function call. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: false }, + * }); + * ``` + * + * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each + * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs` + * to `true`. + * + * ```javascript + * const result = await generateText({ + * model: openai('gpt-4-turbo'), + * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true }, + * }); + */ +export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); diff --git a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts new file mode 100644 index 000000000000..97721eaee15d --- /dev/null +++ b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts @@ -0,0 +1,81 @@ +import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; +import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation'; +import { SDK_VERSION } from '@sentry/core'; +import type { TelemetrySettings } from './types'; + +// List of patched methods +// From: https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#collected-data +const INSTRUMENTED_METHODS = [ + 'generateText', + 'streamText', + 'generateObject', + 'streamObject', + 'embed', + 'embedMany', +] as const; + +interface MethodFirstArg extends Record { + experimental_telemetry?: TelemetrySettings; +} + +type MethodArgs = [MethodFirstArg, ...unknown[]]; + +type PatchedModuleExports = Record<(typeof INSTRUMENTED_METHODS)[number], (...args: MethodArgs) => unknown> & + Record; + +export let sentryVercelAiPatched = false; + +/** + * This detects is added by the Sentry Vercel AI Integration to detect if the integration should + * be enabled. + * + * It also patches the `ai` module to enable Vercel AI telemetry automatically for all methods. + */ +export class SentryVercelAiInstrumentation extends InstrumentationBase { + public constructor(config: InstrumentationConfig = {}) { + super('@sentry/instrumentation-vercel-ai', SDK_VERSION, config); + } + + /** + * Initializes the instrumentation by defining the modules to be patched. + */ + public init(): InstrumentationModuleDefinition { + const module = new InstrumentationNodeModuleDefinition('ai', ['>=3.0.0 <5'], this._patch.bind(this)); + return module; + } + + /** + * Patches module exports to enable Vercel AI telemetry. + */ + private _patch(moduleExports: PatchedModuleExports): unknown { + sentryVercelAiPatched = true; + + function generatePatch(name: string) { + return (...args: MethodArgs) => { + const existingExperimentalTelemetry = args[0].experimental_telemetry || {}; + const isEnabled = existingExperimentalTelemetry.isEnabled; + + // if `isEnabled` is not explicitly set to `true` or `false`, enable telemetry + // but disable capturing inputs and outputs by default + if (isEnabled === undefined) { + args[0].experimental_telemetry = { + isEnabled: true, + recordInputs: false, + recordOutputs: false, + ...existingExperimentalTelemetry, + }; + } + + // @ts-expect-error we know that the method exists + return moduleExports[name].apply(this, args); + }; + } + + const patchedModuleExports = INSTRUMENTED_METHODS.reduce((acc, curr) => { + acc[curr] = generatePatch(curr); + return acc; + }, {} as PatchedModuleExports); + + return { ...moduleExports, ...patchedModuleExports }; + } +} diff --git a/packages/node/src/integrations/tracing/vercelai/types.ts b/packages/node/src/integrations/tracing/vercelai/types.ts new file mode 100644 index 000000000000..8773f84d52c6 --- /dev/null +++ b/packages/node/src/integrations/tracing/vercelai/types.ts @@ -0,0 +1,44 @@ +/** + * Telemetry configuration. + */ +export type TelemetrySettings = { + /** + * Enable or disable telemetry. Disabled by default while experimental. + */ + isEnabled?: boolean; + /** + * Enable or disable input recording. Enabled by default. + * + * You might want to disable input recording to avoid recording sensitive + * information, to reduce data transfers, or to increase performance. + */ + recordInputs?: boolean; + /** + * Enable or disable output recording. Enabled by default. + * + * You might want to disable output recording to avoid recording sensitive + * information, to reduce data transfers, or to increase performance. + */ + recordOutputs?: boolean; + /** + * Identifier for this function. Used to group telemetry data by function. + */ + functionId?: string; + /** + * Additional information to include in the telemetry data. + */ + metadata?: Record; +}; + +/** + * Attribute values may be any non-nullish primitive value except an object. + * + * null or undefined attribute values are invalid and will result in undefined behavior. + */ +export declare type AttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 1104386fb2ca..8cc3b5ff8609 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -156,6 +156,7 @@ function _init( logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); + // TODO(V9): Remove this code since all of the logic should be in an integration if (options.autoSessionTracking) { startSessionTracking(); } diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index eb62cef6ac65..4f0bb444d83d 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -10,6 +10,7 @@ import { import { GLOBAL_OBJ, SDK_VERSION, consoleSandbox, logger } from '@sentry/core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { createAddHookMessageChannel } from 'import-in-the-middle'; +import { DEBUG_BUILD } from '../debug-build'; import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; import { SentryContextManager } from '../otel/contextManager'; import type { EsmLoaderHookOptions } from '../types'; @@ -18,6 +19,9 @@ import type { NodeClient } from './client'; declare const __IMPORT_META_URL_REPLACEMENT__: string; +// About 277h - this must fit into new Array(len)! +const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000; + /** * Initialize OpenTelemetry for Node. */ @@ -136,12 +140,12 @@ export function setupOtel(client: NodeClient): BasicTracerProvider { [ATTR_SERVICE_VERSION]: SDK_VERSION, }), forceFlushTimeoutMillis: 500, + spanProcessors: [ + new SentrySpanProcessor({ + timeout: _clampSpanProcessorTimeout(client.getOptions().maxSpanWaitDuration), + }), + ], }); - provider.addSpanProcessor( - new SentrySpanProcessor({ - timeout: client.getOptions().maxSpanWaitDuration, - }), - ); // Initialize the provider provider.register({ @@ -152,6 +156,26 @@ export function setupOtel(client: NodeClient): BasicTracerProvider { return provider; } +/** Just exported for tests. */ +export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): number | undefined { + if (maxSpanWaitDuration == null) { + return undefined; + } + + // We guard for a max. value here, because we create an array with this length + // So if this value is too large, this would fail + if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) { + DEBUG_BUILD && + logger.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); + return MAX_MAX_SPAN_WAIT_DURATION; + } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) { + DEBUG_BUILD && logger.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); + return undefined; + } + + return maxSpanWaitDuration; +} + /** * Setup the OTEL logger to use our own logger. */ diff --git a/packages/node/src/utils/errorhandling.ts b/packages/node/src/utils/errorhandling.ts index 8eda429ba38e..c99da5c0d04f 100644 --- a/packages/node/src/utils/errorhandling.ts +++ b/packages/node/src/utils/errorhandling.ts @@ -7,7 +7,7 @@ const DEFAULT_SHUTDOWN_TIMEOUT = 2000; /** * @hidden */ -export function logAndExitProcess(error: Error): void { +export function logAndExitProcess(error: unknown): void { consoleSandbox(() => { // eslint-disable-next-line no-console console.error(error); diff --git a/packages/node/src/utils/module.ts b/packages/node/src/utils/module.ts index b5470269b921..c7ea0ffee30a 100644 --- a/packages/node/src/utils/module.ts +++ b/packages/node/src/utils/module.ts @@ -29,6 +29,10 @@ export function createGetModuleFromFilename( file = file.slice(0, ext.length * -1); } + // The file name might be URI-encoded which we want to decode to + // the original file name. + const decodedFile = decodeURIComponent(file); + if (!dir) { // No dirname whatsoever dir = '.'; @@ -36,22 +40,16 @@ export function createGetModuleFromFilename( const n = dir.lastIndexOf('/node_modules'); if (n > -1) { - return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; + return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`; } // Let's see if it's a part of the main module // To be a part of main module, it has to share the same base if (dir.startsWith(normalizedBase)) { - let moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.'); - - if (moduleName) { - moduleName += ':'; - } - moduleName += file; - - return moduleName; + const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.'); + return moduleName ? `${moduleName}:${decodedFile}` : decodedFile; } - return file; + return decodedFile; }; } diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index 3affbe37fb2c..443590601281 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -680,7 +680,7 @@ describe('Integration | Transactions', () => { expect(spans).toHaveLength(2); - expect(spans[0]!.description).toEqual('inner span 1'); - expect(spans[1]!.description).toEqual('inner span 2'); + expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 1' })); + expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 2' })); }); }); diff --git a/packages/node/test/integrations/express.test.ts b/packages/node/test/integrations/express.test.ts index 417c3b783216..1213e7bb38cf 100644 --- a/packages/node/test/integrations/express.test.ts +++ b/packages/node/test/integrations/express.test.ts @@ -59,6 +59,7 @@ describe('expressErrorHandler()', () => { jest.spyOn(client, '_captureRequestSession'); + // eslint-disable-next-line deprecation/deprecation getIsolationScope().setRequestSession({ status: 'ok' }); let isolationScope: Scope; @@ -68,6 +69,7 @@ describe('expressErrorHandler()', () => { }); setImmediate(() => { + // eslint-disable-next-line deprecation/deprecation expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); done(); }); @@ -80,6 +82,7 @@ describe('expressErrorHandler()', () => { jest.spyOn(client, '_captureRequestSession'); + // eslint-disable-next-line deprecation/deprecation getIsolationScope().setRequestSession({ status: 'ok' }); let isolationScope: Scope; @@ -89,6 +92,7 @@ describe('expressErrorHandler()', () => { }); setImmediate(() => { + // eslint-disable-next-line deprecation/deprecation expect(isolationScope.getRequestSession()).toEqual({ status: 'ok' }); done(); }); @@ -106,8 +110,10 @@ describe('expressErrorHandler()', () => { jest.spyOn(client, '_captureRequestSession'); withScope(() => { + // eslint-disable-next-line deprecation/deprecation getIsolationScope().setRequestSession({ status: 'ok' }); sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { + // eslint-disable-next-line deprecation/deprecation expect(getIsolationScope().getRequestSession()).toEqual({ status: 'crashed' }); }); }); @@ -130,6 +136,7 @@ describe('expressErrorHandler()', () => { }); setImmediate(() => { + // eslint-disable-next-line deprecation/deprecation expect(isolationScope.getRequestSession()).toEqual(undefined); done(); }); diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 5f351ff3e990..5add93731c5e 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -82,10 +82,12 @@ describe('NodeClient', () => { initOpenTelemetry(client); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureException(new Error('test exception')); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -103,10 +105,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureException(new Error('test exception')); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -124,10 +128,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'crashed' }); client.captureException(new Error('test exception')); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('crashed'); }); @@ -145,10 +151,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureException(new Error('test exception')); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('errored'); }); @@ -167,6 +175,7 @@ describe('NodeClient', () => { let isolationScope: Scope; withIsolationScope(_isolationScope => { + // eslint-disable-next-line deprecation/deprecation _isolationScope.setRequestSession({ status: 'ok' }); isolationScope = _isolationScope; }); @@ -174,6 +183,7 @@ describe('NodeClient', () => { client.captureException(new Error('test exception')); setImmediate(() => { + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession).toEqual({ status: 'ok' }); done(); @@ -194,8 +204,10 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -209,10 +221,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('errored'); }); @@ -230,10 +244,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureEvent({ message: 'message' }); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -254,6 +270,7 @@ describe('NodeClient', () => { isolationScope.clear(); client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); + // eslint-disable-next-line deprecation/deprecation expect(isolationScope.getRequestSession()).toEqual(undefined); }); }); @@ -270,10 +287,12 @@ describe('NodeClient', () => { client.initSessionFlusher(); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureEvent({ message: 'message', type: 'transaction' }); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -287,10 +306,12 @@ describe('NodeClient', () => { initOpenTelemetry(client); withIsolationScope(isolationScope => { + // eslint-disable-next-line deprecation/deprecation isolationScope.setRequestSession({ status: 'ok' }); client.captureEvent({ message: 'message', exception: { values: [{ type: 'exception type 1' }] } }); + // eslint-disable-next-line deprecation/deprecation const requestSession = isolationScope.getRequestSession(); expect(requestSession!.status).toEqual('ok'); }); @@ -527,6 +548,7 @@ describe('flush/close', () => { // not due to the interval running every 60s clearInterval(client['_sessionFlusher']!['_intervalId']); + // eslint-disable-next-line deprecation/deprecation const sessionFlusherFlushFunc = jest.spyOn(SessionFlusher.prototype, 'flush'); const delay = 1; diff --git a/packages/node/test/sdk/initOtel.test.ts b/packages/node/test/sdk/initOtel.test.ts new file mode 100644 index 000000000000..bb3c3a29c919 --- /dev/null +++ b/packages/node/test/sdk/initOtel.test.ts @@ -0,0 +1,78 @@ +import { logger } from '@sentry/core'; +import { _clampSpanProcessorTimeout } from '../../src/sdk/initOtel'; + +describe('_clampSpanProcessorTimeout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('works with undefined', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(undefined); + expect(timeout).toBe(undefined); + expect(loggerWarnSpy).not.toHaveBeenCalled(); + }); + + it('works with positive number', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(10); + expect(timeout).toBe(10); + expect(loggerWarnSpy).not.toHaveBeenCalled(); + }); + + it('works with 0', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(0); + expect(timeout).toBe(undefined); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith( + '`maxSpanWaitDuration` must be a positive number, using default value instead.', + ); + }); + + it('works with negative number', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(-10); + expect(timeout).toBe(undefined); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith( + '`maxSpanWaitDuration` must be a positive number, using default value instead.', + ); + }); + + it('works with -Infinity', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(-Infinity); + expect(timeout).toBe(undefined); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith( + '`maxSpanWaitDuration` must be a positive number, using default value instead.', + ); + }); + + it('works with Infinity', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(Infinity); + expect(timeout).toBe(1_000_000); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); + }); + + it('works with large number', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(1_000_000_000); + expect(timeout).toBe(1_000_000); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); + }); + + it('works with NaN', () => { + const loggerWarnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {}); + const timeout = _clampSpanProcessorTimeout(NaN); + expect(timeout).toBe(undefined); + expect(loggerWarnSpy).toHaveBeenCalledTimes(1); + expect(loggerWarnSpy).toHaveBeenCalledWith( + '`maxSpanWaitDuration` must be a positive number, using default value instead.', + ); + }); +}); diff --git a/packages/node/test/utils/module.test.ts b/packages/node/test/utils/module.test.ts new file mode 100644 index 000000000000..1616bc7482d9 --- /dev/null +++ b/packages/node/test/utils/module.test.ts @@ -0,0 +1,46 @@ +import { createGetModuleFromFilename } from '../../src'; + +describe('createGetModuleFromFilename', () => { + it.each([ + ['/path/to/file.js', 'file'], + ['/path/to/file.mjs', 'file'], + ['/path/to/file.cjs', 'file'], + ['file.js', 'file'], + ])('returns the module name from a filename %s', (filename, expected) => { + const getModule = createGetModuleFromFilename(); + expect(getModule(filename)).toBe(expected); + }); + + it('applies the given base path', () => { + const getModule = createGetModuleFromFilename('/path/to/base'); + expect(getModule('/path/to/base/file.js')).toBe('file'); + }); + + it('decodes URI-encoded file names', () => { + const getModule = createGetModuleFromFilename(); + expect(getModule('/path%20with%space/file%20with%20spaces(1).js')).toBe('file with spaces(1)'); + }); + + it('returns undefined if no filename is provided', () => { + const getModule = createGetModuleFromFilename(); + expect(getModule(undefined)).toBeUndefined(); + }); + + it.each([ + ['/path/to/base/node_modules/@sentry/test/file.js', '@sentry.test:file'], + ['/path/to/base/node_modules/somePkg/file.js', 'somePkg:file'], + ])('handles node_modules file paths %s', (filename, expected) => { + const getModule = createGetModuleFromFilename(); + expect(getModule(filename)).toBe(expected); + }); + + it('handles windows paths with passed basePath and node_modules', () => { + const getModule = createGetModuleFromFilename('C:\\path\\to\\base', true); + expect(getModule('C:\\path\\to\\base\\node_modules\\somePkg\\file.js')).toBe('somePkg:file'); + }); + + it('handles windows paths with default basePath', () => { + const getModule = createGetModuleFromFilename(undefined, true); + expect(getModule('C:\\path\\to\\base\\somePkg\\file.js')).toBe('file'); + }); +}); diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 1fc8a05b6b3f..76bb33737972 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -47,12 +47,13 @@ "@sentry/core": "8.42.0", "@sentry/node": "8.42.0", "@sentry/opentelemetry": "8.42.0", - "@sentry/rollup-plugin": "2.22.6", + "@sentry/rollup-plugin": "2.22.7", "@sentry/vite-plugin": "2.22.6", "@sentry/vue": "8.42.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", + "@sentry-internal/nitro-utils": "8.42.0", "nuxt": "^3.13.2" }, "scripts": { diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 46f390120cfe..93ca94016924 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -103,22 +103,35 @@ export type SentryNuxtModuleOptions = { debug?: boolean; /** - * Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register - * necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling) * - * If this option is `false`, the Sentry SDK won't wrap the server entry file with `import()`. Not wrapping the - * server entry file will disable Sentry on the server-side. When you set this option to `false`, make sure - * to add the Sentry server config with the node `--import` CLI flag to enable Sentry on the server-side. + * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible. * - * **DO NOT** add the node CLI flag `--import` in your node start script, when `dynamicImportForServerEntry` is set to `true` (default). + * **DO NOT** add the node CLI flag `--import` in your node start script, when auto-injecting Sentry. * This would initialize Sentry twice on the server-side and this leads to unexpected issues. * - * @default true + * --- + * + * **"top-level-import"** + * + * Enabling basic server tracing with top-level import can be used for environments where modifying the node option `--import` is not possible. + * However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.). + * + * If `"top-level-import"` is enabled, the Sentry SDK will import the Sentry server config at the top of the server entry file to load the SDK on the server. + * + * --- + * **"experimental_dynamic-import"** + * + * Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register + * necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling) + * + * If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`. + * + * @default undefined */ - dynamicImportForServerEntry?: boolean; + autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import'; /** - * By defaultβ€”unless you configure `dynamicImportForServerEntry: false`β€”the SDK will try to wrap your Nitro server entrypoint + * When `autoInjectServerSentry` is set to `"experimental_dynamic-import"`, the SDK will wrap your Nitro server entrypoint * with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported. * Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is. * @@ -128,7 +141,7 @@ export type SentryNuxtModuleOptions = { * * @default ['default', 'handler', 'server'] */ - entrypointWrappedFunctions?: string[]; + experimental_entrypointWrappedFunctions?: string[]; /** * Options to be passed directly to the Sentry Rollup Plugin (`@sentry/rollup-plugin`) and Sentry Vite Plugin (`@sentry/vite-plugin`) that ship with the Sentry Nuxt SDK. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index bd6cb96122de..e246430f69d6 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'; import { consoleSandbox } from '@sentry/core'; import type { SentryNuxtModuleOptions } from './common/types'; -import { addDynamicImportEntryFileWrapper, addServerConfigToBuild } from './vite/addServerConfig'; +import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig'; import { setupSourceMaps } from './vite/sourceMaps'; import { findDefaultSdkInitFile } from './vite/utils'; @@ -20,8 +20,12 @@ export default defineNuxtModule({ setup(moduleOptionsParam, nuxt) { const moduleOptions = { ...moduleOptionsParam, - dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true - entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'], + autoInjectServerSentry: moduleOptionsParam.autoInjectServerSentry, + experimental_entrypointWrappedFunctions: moduleOptionsParam.experimental_entrypointWrappedFunctions || [ + 'default', + 'handler', + 'server', + ], }; const moduleDirResolver = createResolver(import.meta.url); @@ -54,15 +58,15 @@ export default defineNuxtModule({ const serverConfigFile = findDefaultSdkInitFile('server'); if (serverConfigFile) { - if (moduleOptions.dynamicImportForServerEntry === false) { - // Inject the server-side Sentry config file with a side effect import + if (moduleOptions.autoInjectServerSentry !== 'experimental_dynamic-import') { addPluginTemplate({ mode: 'server', filename: 'sentry-server-config.mjs', getContents: () => - `import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` + - 'import { defineNuxtPlugin } from "#imports"\n' + - 'export default defineNuxtPlugin(() => {})', + // This won't actually import the server config in the build output (so no double init call). The import here is only needed for correctly resolving the Sentry release injection. + `import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"; + import { defineNuxtPlugin } from "#imports"; + export default defineNuxtPlugin(() => {});`, }); } @@ -79,12 +83,32 @@ export default defineNuxtModule({ consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - '[Sentry] Your application is running in development mode. Note: @sentry/nuxt is in beta and may not work as expected on the server-side (Nitro). Errors are reported, but tracing does not work.', + '[Sentry] Your application is running in development mode. Note: @sentry/nuxt does not work as expected on the server-side (Nitro). Errors are reported, but tracing does not work.', ); }); } - if (moduleOptions.dynamicImportForServerEntry === false) { + consoleSandbox(() => { + const serverDir = nitro.options.output.serverDir; + + // Netlify env: https://docs.netlify.com/configure-builds/environment-variables/#build-metadata + if (serverDir.includes('.netlify') || !!process.env.NETLIFY) { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Warning: The Sentry SDK detected a Netlify build. Server-side support for the Sentry Nuxt SDK on Netlify is currently unreliable due to technical limitations of serverless functions. Traces are not collected, and errors may occasionally not be reported. For more information on setting up Sentry on the Nuxt server-side, please refer to the documentation: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/', + ); + } + + // Vercel env: https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL + if (serverDir.includes('.vercel') || !!process.env.VERCEL) { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Warning: The Sentry SDK detected a Vercel build. The Sentry Nuxt SDK currently does not support tracing on Vercel. For more information on setting up Sentry on the Nuxt server-side, please refer to the documentation: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/', + ); + } + }); + + if (moduleOptions.autoInjectServerSentry !== 'experimental_dynamic-import') { addServerConfigToBuild(moduleOptions, nuxt, nitro, serverConfigFile); if (moduleOptions.debug) { @@ -101,7 +125,13 @@ export default defineNuxtModule({ ); }); } - } else { + } + + if (moduleOptions.autoInjectServerSentry === 'top-level-import') { + addSentryTopImport(moduleOptions, nitro); + } + + if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') { addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions); if (moduleOptions.debug) { diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 1159a6d427ff..f85e69883bb8 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,3 +1,5 @@ +import { patchEventHandler } from '@sentry-internal/nitro-utils'; +import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core'; import * as Sentry from '@sentry/node'; import { H3Error } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; @@ -5,7 +7,9 @@ import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; export default defineNitroPlugin(nitroApp => { - nitroApp.hooks.hook('error', (error, errorContext) => { + nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); + + nitroApp.hooks.hook('error', async (error, errorContext) => { // Do not handle 404 and 422 if (error instanceof H3Error) { // Do not report if status code is 3xx or 4xx @@ -29,6 +33,8 @@ export default defineNitroPlugin(nitroApp => { captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); + + await flushIfServerless(); }); // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context @@ -36,3 +42,27 @@ export default defineNitroPlugin(nitroApp => { addSentryTracingMetaTags(html.head); }); }); + +async function flushIfServerless(): Promise { + const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY; + + // @ts-expect-error This is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} + +async function flushWithTimeout(): Promise { + const sentryClient = getClient(); + const isDebug = sentryClient ? sentryClient.getOptions().debug : false; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 5ac673b3dd20..b5577830396b 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -12,6 +12,7 @@ import { SENTRY_WRAPPED_FUNCTIONS, constructFunctionReExport, constructWrappedFunctionExportQuery, + getFilenameFromNodeStartCommand, removeSentryQueryFromPath, } from './utils'; @@ -38,41 +39,90 @@ export function addServerConfigToBuild( (viteInlineConfig.build.rollupOptions.input as { [entryName: string]: string })[SERVER_CONFIG_FILENAME] = createResolver(nuxt.options.srcDir).resolve(`/${serverConfigFile}`); } + }); + + /** + * When the build process is finished, copy the `sentry.server.config` file to the `.output` directory. + * This is necessary because we need to reference this file path in the node --import option. + */ + nitro.hooks.hook('close', async () => { + const buildDirResolver = createResolver(nitro.options.buildDir); + const serverDirResolver = createResolver(nitro.options.output.serverDir); + const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`); + const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`); + + try { + await fs.promises.access(source, fs.constants.F_OK); + await fs.promises.copyFile(source, destination); + + if (moduleOptions.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``, + ); + }); + } + } catch (error) { + if (moduleOptions.debug) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`, + error, + ); + }); + } + } + }); +} - /** - * When the build process is finished, copy the `sentry.server.config` file to the `.output` directory. - * This is necessary because we need to reference this file path in the node --import option. - */ - nitro.hooks.hook('close', async () => { - const buildDirResolver = createResolver(nitro.options.buildDir); - const serverDirResolver = createResolver(nitro.options.output.serverDir); - const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`); - const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`); - - try { - await fs.promises.access(source, fs.constants.F_OK); - await fs.promises.copyFile(source, destination); - - if (moduleOptions.debug) { - consoleSandbox(() => { +/** + * Adds the Sentry server config import at the top of the server entry file to load the SDK on the server. + * This is necessary for environments where modifying the node option `--import` is not possible. + * However, only limited tracing instrumentation is supported when doing this. + */ +export function addSentryTopImport(moduleOptions: SentryNuxtModuleOptions, nitro: Nitro): void { + nitro.hooks.hook('close', async () => { + const fileNameFromCommand = + nitro.options.commands.preview && getFilenameFromNodeStartCommand(nitro.options.commands.preview); + + // other presets ('node-server' or 'vercel') have an index.mjs + const presetsWithServerFile = ['netlify']; + + const entryFileName = fileNameFromCommand + ? fileNameFromCommand + : typeof nitro.options.rollupConfig?.output.entryFileNames === 'string' + ? nitro.options.rollupConfig?.output.entryFileNames + : presetsWithServerFile.includes(nitro.options.preset) + ? 'server.mjs' + : 'index.mjs'; + + const serverDirResolver = createResolver(nitro.options.output.serverDir); + const entryFilePath = serverDirResolver.resolve(entryFileName); + + try { + fs.readFile(entryFilePath, 'utf8', (err, data) => { + const updatedContent = `import './${SERVER_CONFIG_FILENAME}.mjs';\n${data}`; + + fs.writeFile(entryFilePath, updatedContent, 'utf8', () => { + if (moduleOptions.debug) { // eslint-disable-next-line no-console console.log( - `[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``, - ); - }); - } - } catch (error) { - if (moduleOptions.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`, - error, + `[Sentry] Successfully added the Sentry import to the server entry file "\`${entryFilePath}\`"`, ); - }); - } + } + }); + }); + } catch (err) { + if (moduleOptions.debug) { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] An error occurred when trying to add the Sentry import to the server entry file "\`${entryFilePath}\`":`, + err, + ); } - }); + } }); } @@ -86,8 +136,8 @@ export function addServerConfigToBuild( export function addDynamicImportEntryFileWrapper( nitro: Nitro, serverConfigFile: string, - moduleOptions: Omit & - Required>, + moduleOptions: Omit & + Required>, ): void { if (!nitro.options.rollupConfig) { nitro.options.rollupConfig = { output: {} }; @@ -103,7 +153,7 @@ export function addDynamicImportEntryFileWrapper( nitro.options.rollupConfig.plugins.push( wrapEntryWithDynamicImport({ resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`), - entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions, + experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions, }), ); } @@ -115,9 +165,13 @@ export function addDynamicImportEntryFileWrapper( */ function wrapEntryWithDynamicImport({ resolvedSentryConfigPath, - entrypointWrappedFunctions, + experimental_entrypointWrappedFunctions, debug, -}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption { +}: { + resolvedSentryConfigPath: string; + experimental_entrypointWrappedFunctions: string[]; + debug?: boolean; +}): InputPluginOption { // In order to correctly import the server config file // and dynamically import the nitro runtime, we need to // mark the resolutionId with '\0raw' to fall into the @@ -156,7 +210,11 @@ function wrapEntryWithDynamicImport({ // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) .concat(SENTRY_WRAPPED_ENTRY) .concat( - constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug), + constructWrappedFunctionExportQuery( + moduleInfo.exportedBindings, + experimental_entrypointWrappedFunctions, + debug, + ), ) .concat(QUERY_END_INDICATOR)}`; } diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index fff676a6ede1..85696f76f6ae 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -26,6 +26,15 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde return filePaths.find(filename => fs.existsSync(filename)); } +/** + * Extracts the filename from a node command with a path. + */ +export function getFilenameFromNodeStartCommand(nodeCommand: string): string | null { + const regex = /[^/\\]+$/; + const match = nodeCommand.match(regex); + return match ? match[0] : null; +} + export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions='; export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions='; @@ -113,7 +122,7 @@ export function constructWrappedFunctionExportQuery( consoleSandbox(() => // eslint-disable-next-line no-console console.warn( - "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.experimental_entrypointWrappedFunctions` in `nuxt.config.ts`.", ), ); } diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index a35f9cf8ca34..f2f6b2b23c8d 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -9,6 +9,7 @@ import { constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, + getFilenameFromNodeStartCommand, removeSentryQueryFromPath, } from '../../src/vite/utils'; @@ -70,6 +71,44 @@ describe('findDefaultSdkInitFile', () => { }); }); +describe('getFilenameFromPath', () => { + it('should return the filename from a simple path', () => { + const path = 'node ./server/index.mjs'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBe('index.mjs'); + }); + + it('should return the filename from a nested path', () => { + const path = 'node ./.output/whatever/path/server.js'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBe('server.js'); + }); + + it('should return the filename from a Windows-style path', () => { + const path = '.\\Projects\\my-app\\src\\main.js'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBe('main.js'); + }); + + it('should return null for an empty path', () => { + const path = ''; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBeNull(); + }); + + it('should return the filename when there are no directory separators', () => { + const path = 'index.mjs'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBe('index.mjs'); + }); + + it('should return null for paths with trailing slashes', () => { + const path = 'node ./server/'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBeNull(); + }); +}); + describe('removeSentryQueryFromPath', () => { it('strips the Sentry query part from the path', () => { const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_WRAPPED_FUNCTIONS}foo,${QUERY_END_INDICATOR}`; @@ -152,7 +191,7 @@ describe('constructWrappedFunctionExportQuery', () => { const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug); expect(result).toBe('?sentry-query-reexported-functions=handler'); expect(consoleWarnSpy).toHaveBeenCalledWith( - "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.", + "[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.experimental_entrypointWrappedFunctions` in `nuxt.config.ts`.", ); consoleWarnSpy.mockRestore(); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 167c3c0b2a5d..fb35e612f0f2 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -43,17 +43,17 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.25.1", - "@opentelemetry/instrumentation": "^0.54.0", - "@opentelemetry/sdk-trace-base": "^1.26.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/core": "^1.29.0", + "@opentelemetry/instrumentation": "^0.56.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.28.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.25.1", - "@opentelemetry/core": "^1.25.1", - "@opentelemetry/sdk-trace-base": "^1.26.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/context-async-hooks": "^1.29.0", + "@opentelemetry/core": "^1.29.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.28.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index 0f056d09a4ea..22e3d01c46a8 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -4,7 +4,6 @@ export const SENTRY_TRACE_HEADER = 'sentry-trace'; export const SENTRY_BAGGAGE_HEADER = 'baggage'; export const SENTRY_TRACE_STATE_DSC = 'sentry.dsc'; -export const SENTRY_TRACE_STATE_PARENT_SPAN_ID = 'sentry.parent_span_id'; export const SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING = 'sentry.sampled_not_recording'; export const SENTRY_TRACE_STATE_URL = 'sentry.url'; diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 74f5452960df..09ba10a173a4 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -8,6 +8,7 @@ import { SENTRY_BAGGAGE_KEY_PREFIX, baggageHeaderToDynamicSamplingContext, generateSentryTraceHeader, + generateSpanId, getClient, getCurrentScope, getDynamicSamplingContextFromScope, @@ -24,7 +25,6 @@ import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC, - SENTRY_TRACE_STATE_PARENT_SPAN_ID, SENTRY_TRACE_STATE_URL, } from './constants'; import { DEBUG_BUILD } from './debug-build'; @@ -32,6 +32,7 @@ import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { makeTraceState } from './utils/makeTraceState'; import { setIsSetup } from './utils/setupCheck'; +import { spanHasParentId } from './utils/spanTypes'; /** Get the Sentry propagation context from a span context. */ export function getPropagationContextFromSpan(span: Span): PropagationContext { @@ -43,8 +44,7 @@ export function getPropagationContextFromSpan(span: Span): PropagationContext { const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; const traceStateDsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; - const parentSpanId = traceState ? traceState.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID) || undefined : undefined; - + const parentSpanId = spanHasParentId(span) ? span.parentSpanId : undefined; const sampled = getSamplingDecision(spanContext); // No trace state? --> Take DSC from root span @@ -141,20 +141,9 @@ export class SentryPropagator extends W3CBaggagePropagator { : maybeSentryTraceHeader : undefined; - const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); - // Add remote parent span context - const ctxWithSpanContext = getContextWithRemoteActiveSpan(context, { sentryTrace, baggage }); - - // Also update the scope on the context (to be sure this is picked up everywhere) - const scopes = getScopesFromContext(ctxWithSpanContext); - const newScopes = { - scope: scopes ? scopes.scope.clone() : getCurrentScope().clone(), - isolationScope: scopes ? scopes.isolationScope : getIsolationScope(), - }; - newScopes.scope.setPropagationContext(propagationContext); - - return setScopesOnContext(ctxWithSpanContext, newScopes); + // If there is no incoming trace, this will return the context as-is + return ensureScopesOnContext(getContextWithRemoteActiveSpan(context, { sentryTrace, baggage })); } /** @@ -205,13 +194,28 @@ export function getInjectionData(context: Context): { sampled: boolean | undefined; } { const span = trace.getSpan(context); - const spanIsRemote = span?.spanContext().isRemote; - // If we have a local span, we can just pick everything from it - if (span && !spanIsRemote) { + // If we have a remote span, the spanId should be considered as the parentSpanId, not spanId itself + // Instead, we use a virtual (generated) spanId for propagation + if (span && span.spanContext().isRemote) { const spanContext = span.spanContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); + return { + dynamicSamplingContext, + traceId: spanContext.traceId, + // Because this is a remote span, we do not want to propagate this directly + // As otherwise things may be attached "directly" to an unrelated span + spanId: generateSpanId(), + sampled: getSamplingDecision(spanContext), + }; + } + + // If we have a local span, we just use this + if (span) { + const spanContext = span.spanContext(); const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); + return { dynamicSamplingContext, traceId: spanContext.traceId, @@ -221,6 +225,7 @@ export function getInjectionData(context: Context): { } // Else we try to use the propagation context from the scope + // The only scenario where this should happen is when we neither have a span, nor an incoming trace const scope = getScopesFromContext(context)?.scope || getCurrentScope(); const client = getClient(); @@ -242,7 +247,21 @@ function getContextWithRemoteActiveSpan( ): Context { const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); - const spanContext = generateSpanContextForPropagationContext(propagationContext); + const { traceId, parentSpanId, sampled, dsc } = propagationContext; + + // We only want to set the virtual span if we are continuing a concrete trace + // Otherwise, we ignore the incoming trace here, e.g. if we have no trace headers + if (!parentSpanId) { + return ctx; + } + + const spanContext = generateRemoteSpanContext({ + traceId, + spanId: parentSpanId, + sampled, + dsc, + }); + return trace.setSpanContext(ctx, spanContext); } @@ -255,11 +274,24 @@ export function continueTraceAsRemoteSpan( options: Parameters[0], callback: () => T, ): T { - const ctxWithSpanContext = getContextWithRemoteActiveSpan(ctx, options); + const ctxWithSpanContext = ensureScopesOnContext(getContextWithRemoteActiveSpan(ctx, options)); return context.with(ctxWithSpanContext, callback); } +function ensureScopesOnContext(ctx: Context): Context { + // If there are no scopes yet on the context, ensure we have them + const scopes = getScopesFromContext(ctx); + const newScopes = { + // If we have no scope here, this is most likely either the root context or a context manually derived from it + // In this case, we want to fork the current scope, to ensure we do not pollute the root scope + scope: scopes ? scopes.scope : getCurrentScope().clone(), + isolationScope: scopes ? scopes.isolationScope : getIsolationScope(), + }; + + return setScopesOnContext(ctx, newScopes); +} + /** Try to get the existing baggage header so we can merge this in. */ function getExistingBaggage(carrier: unknown): string | undefined { try { @@ -297,20 +329,28 @@ function getCurrentURL(span: Span): string | undefined { return undefined; } -// TODO: Adjust this behavior to avoid invalid spans -function generateSpanContextForPropagationContext(propagationContext: PropagationContext): SpanContext { +function generateRemoteSpanContext({ + spanId, + traceId, + sampled, + dsc, +}: { + spanId: string; + traceId: string; + sampled: boolean | undefined; + dsc?: Partial; +}): SpanContext { // We store the DSC as OTEL trace state on the span context const traceState = makeTraceState({ - parentSpanId: propagationContext.parentSpanId, - dsc: propagationContext.dsc, - sampled: propagationContext.sampled, + dsc, + sampled, }); const spanContext: SpanContext = { - traceId: propagationContext.traceId, - spanId: propagationContext.parentSpanId || '', + traceId, + spanId, isRemote: true, - traceFlags: propagationContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, + traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, traceState, }; diff --git a/packages/opentelemetry/src/setupEventContextTrace.ts b/packages/opentelemetry/src/setupEventContextTrace.ts index 43d4344a8ce7..1bf9bcb961be 100644 --- a/packages/opentelemetry/src/setupEventContextTrace.ts +++ b/packages/opentelemetry/src/setupEventContextTrace.ts @@ -1,8 +1,6 @@ import type { Client } from '@sentry/core'; -import { dropUndefinedKeys, getDynamicSamplingContextFromSpan, getRootSpan } from '@sentry/core'; -import { SENTRY_TRACE_STATE_PARENT_SPAN_ID } from './constants'; +import { getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceContext } from '@sentry/core'; import { getActiveSpan } from './utils/getActiveSpan'; -import { spanHasParentId } from './utils/spanTypes'; /** Ensure the `trace` context is set on all events. */ export function setupEventContextTrace(client: Client): void { @@ -14,25 +12,9 @@ export function setupEventContextTrace(client: Client): void { return; } - const spanContext = span.spanContext(); - - // If we have a parent span id from trace state, use that ('' means no parent should be used) - // Else, pick the one from the span - const parentSpanIdFromTraceState = spanContext.traceState?.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID); - const parent_span_id = - typeof parentSpanIdFromTraceState === 'string' - ? parentSpanIdFromTraceState || undefined - : spanHasParentId(span) - ? span.parentSpanId - : undefined; - // If event has already set `trace` context, use that one. event.contexts = { - trace: dropUndefinedKeys({ - trace_id: spanContext.traceId, - span_id: spanContext.spanId, - parent_span_id, - }), + trace: spanToTraceContext(span), ...event.contexts, }; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 126efed460b6..ad044372e0df 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -18,7 +18,6 @@ import { spanTimeInputToSeconds, timedEventsToMeasurements, } from '@sentry/core'; -import { SENTRY_TRACE_STATE_PARENT_SPAN_ID } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; import { getRequestSpanData } from './utils/getRequestSpanData'; @@ -239,15 +238,12 @@ function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { const { traceId: trace_id, spanId: span_id } = span.spanContext(); - const parentSpanIdFromTraceState = span.spanContext().traceState?.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID); - // If parentSpanIdFromTraceState is defined at all, we want it to take precedence // In that case, an empty string should be interpreted as "no parent span id", // even if `span.parentSpanId` is set // this is the case when we are starting a new trace, where we have a virtual span based on the propagationContext // We only want to continue the traceId in this case, but ignore the parent span - const parent_span_id = - typeof parentSpanIdFromTraceState === 'string' ? parentSpanIdFromTraceState || undefined : span.parentSpanId; + const parent_span_id = span.parentSpanId; const status = mapStatus(span); diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 1c1710b94793..e1f082c1d424 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,5 +1,5 @@ import type { Context, Span, SpanContext, SpanOptions, Tracer } from '@opentelemetry/api'; -import { INVALID_SPANID, SpanStatusCode, TraceFlags, context, trace } from '@opentelemetry/api'; +import { SpanStatusCode, TraceFlags, context, trace } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import type { Client, DynamicSamplingContext, Scope, Span as SentrySpan, TraceContext } from '@sentry/core'; import { @@ -18,7 +18,7 @@ import { } from '@sentry/core'; import { continueTraceAsRemoteSpan } from './propagator'; import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; -import { getContextFromScope, getScopesFromContext } from './utils/contextData'; +import { getContextFromScope } from './utils/contextData'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { makeTraceState } from './utils/makeTraceState'; @@ -178,40 +178,11 @@ function ensureTimestampInMilliseconds(timestamp: number): number { function getContext(scope: Scope | undefined, forceTransaction: boolean | undefined): Context { const ctx = getContextForScope(scope); - // Note: If the context is the ROOT_CONTEXT, no scope is attached - // Thus we will not use the propagation context in this case, which is desired - const actualScope = getScopesFromContext(ctx)?.scope; const parentSpan = trace.getSpan(ctx); - // In the case that we have no parent span, we need to "simulate" one to ensure the propagation context is correct + // In the case that we have no parent span, we start a new trace + // Note that if we continue a trace, we'll always have a remote parent span here anyhow if (!parentSpan) { - const client = getClient(); - - if (actualScope && client) { - const propagationContext = actualScope.getPropagationContext(); - - // We store the DSC as OTEL trace state on the span context - const traceState = makeTraceState({ - parentSpanId: propagationContext.parentSpanId, - // Not defined yet, we want to pick this up on-demand only - dsc: undefined, - sampled: propagationContext.sampled, - }); - - const spanOptions: SpanContext = { - traceId: propagationContext.traceId, - // eslint-disable-next-line deprecation/deprecation - spanId: propagationContext.parentSpanId || propagationContext.spanId, - isRemote: true, - traceFlags: propagationContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, - traceState, - }; - - // Add remote parent span context, - return trace.setSpanContext(ctx, spanOptions); - } - - // if we have no scope or client, we just return the context as-is return ctx; } @@ -237,7 +208,6 @@ function getContext(scope: Scope | undefined, forceTransaction: boolean | undefi const traceState = makeTraceState({ dsc, - parentSpanId: spanId !== INVALID_SPANID ? spanId : undefined, sampled, }); diff --git a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts index 307f25364c5e..4aa9ecfdb8b6 100644 --- a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts +++ b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts @@ -1,5 +1,6 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, hasTracingEnabled, spanToJSON } from '@sentry/core'; import type { Client } from '@sentry/core'; +import { getSamplingDecision } from './getSamplingDecision'; import { parseSpanDescription } from './parseSpanDescription'; import { spanHasName } from './spanTypes'; @@ -9,20 +10,31 @@ import { spanHasName } from './spanTypes'; */ export function enhanceDscWithOpenTelemetryRootSpanName(client: Client): void { client.on('createDsc', (dsc, rootSpan) => { + if (!rootSpan) { + return; + } + // We want to overwrite the transaction on the DSC that is created by default in core // The reason for this is that we want to infer the span name, not use the initial one // Otherwise, we'll get names like "GET" instead of e.g. "GET /foo" // `parseSpanDescription` takes the attributes of the span into account for the name // This mutates the passed-in DSC - if (rootSpan) { - const jsonSpan = spanToJSON(rootSpan); - const attributes = jsonSpan.data || {}; - const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: undefined }; - if (source !== 'url' && description) { - dsc.transaction = description; - } + const jsonSpan = spanToJSON(rootSpan); + const attributes = jsonSpan.data || {}; + const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + + const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: undefined }; + if (source !== 'url' && description) { + dsc.transaction = description; + } + + // Also ensure sampling decision is correctly inferred + // In core, we use `spanIsSampled`, which just looks at the trace flags + // but in OTEL, we use a slightly more complex logic to be able to differntiate between unsampled and deferred sampling + if (hasTracingEnabled()) { + const sampled = getSamplingDecision(rootSpan.spanContext()); + dsc.sampled = sampled == undefined ? undefined : String(sampled); } }); } diff --git a/packages/opentelemetry/src/utils/generateSpanContextForPropagationContext.ts b/packages/opentelemetry/src/utils/generateSpanContextForPropagationContext.ts index 8f5f0b41b2b9..bf0af49b1200 100644 --- a/packages/opentelemetry/src/utils/generateSpanContextForPropagationContext.ts +++ b/packages/opentelemetry/src/utils/generateSpanContextForPropagationContext.ts @@ -12,7 +12,6 @@ import { makeTraceState } from './makeTraceState'; export function generateSpanContextForPropagationContext(propagationContext: PropagationContext): SpanContext { // We store the DSC as OTEL trace state on the span context const traceState = makeTraceState({ - parentSpanId: propagationContext.parentSpanId, dsc: propagationContext.dsc, sampled: propagationContext.sampled, }); diff --git a/packages/opentelemetry/src/utils/makeTraceState.ts b/packages/opentelemetry/src/utils/makeTraceState.ts index 6175ffd982c1..c232c981bb41 100644 --- a/packages/opentelemetry/src/utils/makeTraceState.ts +++ b/packages/opentelemetry/src/utils/makeTraceState.ts @@ -1,32 +1,22 @@ import { TraceState } from '@opentelemetry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/core'; import type { DynamicSamplingContext } from '@sentry/core'; -import { - SENTRY_TRACE_STATE_DSC, - SENTRY_TRACE_STATE_PARENT_SPAN_ID, - SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, -} from '../constants'; +import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from '../constants'; /** * Generate a TraceState for the given data. */ export function makeTraceState({ - parentSpanId, dsc, sampled, }: { - parentSpanId?: string; dsc?: Partial; sampled?: boolean; }): TraceState { // We store the DSC as OTEL trace state on the span context const dscString = dsc ? dynamicSamplingContextToSentryBaggageHeader(dsc) : undefined; - // We _always_ set the parent span ID, even if it is empty - // If we'd set this to 'undefined' we could not know if the trace state was set, but there was no parentSpanId, - // vs the trace state was not set at all (in which case we want to do fallback handling) - // If `''`, it should be considered "no parent" - const traceStateBase = new TraceState().set(SENTRY_TRACE_STATE_PARENT_SPAN_ID, parentSpanId || ''); + const traceStateBase = new TraceState(); const traceStateWithDsc = dscString ? traceStateBase.set(SENTRY_TRACE_STATE_DSC, dscString) : traceStateBase; diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts index 7e59400cc1b1..18e7f1e3d867 100644 --- a/packages/opentelemetry/test/helpers/initOtel.ts +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -16,6 +16,7 @@ import { SentryPropagator } from '../../src/propagator'; import { SentrySampler } from '../../src/sampler'; import { setupEventContextTrace } from '../../src/setupEventContextTrace'; import { SentrySpanProcessor } from '../../src/spanProcessor'; +import { enhanceDscWithOpenTelemetryRootSpanName } from '../../src/utils/enhanceDscWithOpenTelemetryRootSpanName'; import type { TestClientInterface } from './TestClient'; /** @@ -44,6 +45,7 @@ export function initOtel(): void { } setupEventContextTrace(client); + enhanceDscWithOpenTelemetryRootSpanName(client); const provider = setupOtel(client); client.traceProvider = provider; @@ -61,8 +63,8 @@ export function setupOtel(client: TestClientInterface): BasicTracerProvider { [ATTR_SERVICE_VERSION]: SDK_VERSION, }), forceFlushTimeoutMillis: 500, + spanProcessors: [new SentrySpanProcessor()], }); - provider.addSpanProcessor(new SentrySpanProcessor()); // We use a custom context manager to keep context in sync with sentry scope const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 49d4a30b6780..bc0179b55e38 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -327,7 +327,6 @@ describe('Integration | Transactions', () => { const parentSpanId = '6e0c63257de34c92'; const traceState = makeTraceState({ - parentSpanId, dsc: undefined, sampled: true, }); diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index d3b9483674a1..56aed551353e 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -12,7 +12,6 @@ import { getCurrentScope, withScope } from '@sentry/core'; import { SENTRY_BAGGAGE_HEADER, SENTRY_SCOPES_CONTEXT_KEY, SENTRY_TRACE_HEADER } from '../src/constants'; import { SentryPropagator } from '../src/propagator'; -import { getScopesFromContext } from '../src/utils/contextData'; import { getSamplingDecision } from '../src/utils/getSamplingDecision'; import { makeTraceState } from '../src/utils/makeTraceState'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; @@ -101,39 +100,6 @@ describe('SentryPropagator', () => { }); }); - it('uses scope propagation context over remote spanContext', () => { - context.with( - trace.setSpanContext(ROOT_CONTEXT, { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.NONE, - isRemote: true, - }), - () => { - withScope(scope => { - scope.setPropagationContext({ - traceId: 'TRACE_ID', - parentSpanId: 'PARENT_SPAN_ID', - spanId: 'SPAN_ID', - sampled: true, - }); - - propagator.inject(context.active(), carrier, defaultTextMapSetter); - - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( - [ - 'sentry-environment=production', - 'sentry-release=1.0.0', - 'sentry-public_key=abc', - 'sentry-trace_id=TRACE_ID', - ].sort(), - ); - expect(carrier[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); - }); - }, - ); - }); - it('uses propagation data from current scope if no scope & span is found', () => { const scope = getCurrentScope(); const traceId = scope.getPropagationContext().traceId; @@ -181,7 +147,6 @@ describe('SentryPropagator', () => { traceFlags: TraceFlags.SAMPLED, isRemote: true, traceState: makeTraceState({ - parentSpanId: '6e0c63257de34c92', dsc: { transaction: 'sampled-transaction', sampled: 'true', @@ -256,7 +221,6 @@ describe('SentryPropagator', () => { traceFlags: TraceFlags.NONE, isRemote: true, traceState: makeTraceState({ - parentSpanId: '6e0c63257de34c92', dsc: { transaction: 'sampled-transaction', sampled: 'false', @@ -291,7 +255,6 @@ describe('SentryPropagator', () => { isRemote: true, traceState: makeTraceState({ sampled: false, - parentSpanId: '6e0c63257de34c92', dsc: { transaction: 'sampled-transaction', trace_id: 'dsc_trace_id', @@ -383,14 +346,53 @@ describe('SentryPropagator', () => { }); }, ); + }); + + it('uses remote span with deferred sampling decision over propagation context', () => { + const carrier: Record = {}; + context.with( + trace.setSpanContext(ROOT_CONTEXT, { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + isRemote: true, + }), + () => { + withScope(scope => { + scope.setPropagationContext({ + traceId: 'TRACE_ID', + parentSpanId: 'PARENT_SPAN_ID', + spanId: 'SPAN_ID', + sampled: true, + }); + + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ].sort(), + ); + // Used spanId is a random ID, not from the remote span + expect(carrier[SENTRY_TRACE_HEADER]).toMatch(/d4cda95b652f4a1592b449d5929fda1b-[a-f0-9]{16}/); + expect(carrier[SENTRY_TRACE_HEADER]).not.toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92'); + }); + }, + ); + }); - const carrier2: Record = {}; + it('uses remote span over propagation context', () => { + const carrier: Record = {}; context.with( trace.setSpanContext(ROOT_CONTEXT, { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, isRemote: true, + traceState: makeTraceState({ sampled: false }), }), () => { withScope(scope => { @@ -401,17 +403,20 @@ describe('SentryPropagator', () => { sampled: true, }); - propagator.inject(context.active(), carrier2, defaultTextMapSetter); + propagator.inject(context.active(), carrier, defaultTextMapSetter); - expect(baggageToArray(carrier2[SENTRY_BAGGAGE_HEADER])).toEqual( + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( [ 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', - 'sentry-trace_id=TRACE_ID', + 'sentry-sampled=false', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', ].sort(), ); - expect(carrier2[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); + // Used spanId is a random ID, not from the remote span + expect(carrier[SENTRY_TRACE_HEADER]).toMatch(/d4cda95b652f4a1592b449d5929fda1b-[a-f0-9]{16}-0/); + expect(carrier[SENTRY_TRACE_HEADER]).not.toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0'); }); }, ); @@ -557,7 +562,7 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, traceId: 'd4cda95b652f4a1592b449d5929fda1b', - traceState: makeTraceState({ parentSpanId: '6e0c63257de34c92' }), + traceState: makeTraceState({}), }); expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(true); }); @@ -571,7 +576,7 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, traceId: 'd4cda95b652f4a1592b449d5929fda1b', - traceState: makeTraceState({ parentSpanId: '6e0c63257de34c92', sampled: false }), + traceState: makeTraceState({ sampled: false }), }); expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(false); }); @@ -585,41 +590,20 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, traceId: 'd4cda95b652f4a1592b449d5929fda1b', - traceState: makeTraceState({ parentSpanId: '6e0c63257de34c92' }), + traceState: makeTraceState({}), }); expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(undefined); }); - it('sets data from sentry trace header on scope', () => { - const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; - carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; - const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - - const scopes = getScopesFromContext(context); - - expect(scopes).toBeDefined(); - expect(scopes?.scope.getPropagationContext()).toEqual({ - spanId: expect.any(String), - sampled: true, - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - parentSpanId: '6e0c63257de34c92', - dsc: {}, - }); - expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(true); - }); - it('handles undefined sentry trace header', () => { const sentryTraceHeader = undefined; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(trace.getSpanContext(context)).toEqual({ - isRemote: true, - spanId: expect.any(String), - traceFlags: TraceFlags.NONE, - traceId: expect.any(String), - traceState: makeTraceState({}), + expect(trace.getSpanContext(context)).toEqual(undefined); + expect(getCurrentScope().getPropagationContext()).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), }); - expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(undefined); }); it('sets data from baggage header on span context', () => { @@ -635,7 +619,6 @@ describe('SentryPropagator', () => { traceFlags: TraceFlags.SAMPLED, traceId: 'd4cda95b652f4a1592b449d5929fda1b', traceState: makeTraceState({ - parentSpanId: '6e0c63257de34c92', dsc: { environment: 'production', release: '1.0.0', @@ -648,55 +631,29 @@ describe('SentryPropagator', () => { expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(true); }); - it('sets data from baggage header on scope', () => { - const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; - const baggage = - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; - carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; - carrier[SENTRY_BAGGAGE_HEADER] = baggage; - const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - - const scopes = getScopesFromContext(context); - - expect(scopes).toBeDefined(); - expect(scopes?.scope.getPropagationContext()).toEqual({ - spanId: expect.any(String), - sampled: true, - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - parentSpanId: '6e0c63257de34c92', - dsc: { - environment: 'production', - release: '1.0.0', - public_key: 'abc', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - transaction: 'dsc-transaction', - }, - }); - }); - it('handles empty dsc baggage header', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; const baggage = ''; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); expect(trace.getSpanContext(context)).toEqual({ isRemote: true, - spanId: expect.any(String), - traceFlags: TraceFlags.NONE, - traceId: expect.any(String), + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', traceState: makeTraceState({}), }); - expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(undefined); + expect(getSamplingDecision(trace.getSpanContext(context)!)).toBe(true); }); it('handles when sentry-trace is an empty array', () => { carrier[SENTRY_TRACE_HEADER] = []; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(trace.getSpanContext(context)).toEqual({ - isRemote: true, - spanId: expect.any(String), - traceFlags: TraceFlags.NONE, - traceId: expect.any(String), - traceState: makeTraceState({}), + expect(trace.getSpanContext(context)).toEqual(undefined); + expect(getCurrentScope().getPropagationContext()).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), }); }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 95edc98dfd81..2c22318ec977 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -329,9 +329,7 @@ describe('trace', () => { it('allows to pass parentSpan=null', () => { startSpan({ name: 'GET users/[id' }, () => { startSpan({ name: 'child', parentSpan: null }, span => { - // Due to the way we propagate the scope in OTEL, - // the parent_span_id is not actually undefined here, but comes from the propagation context - expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId); + expect(spanToJSON(span).parent_span_id).toBe(undefined); }); }); }); @@ -591,10 +589,7 @@ describe('trace', () => { it('allows to pass parentSpan=null', () => { startSpan({ name: 'outer' }, () => { const span = startInactiveSpan({ name: 'test span', parentSpan: null }); - - // Due to the way we propagate the scope in OTEL, - // the parent_span_id is not actually undefined here, but comes from the propagation context - expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId); + expect(spanToJSON(span).parent_span_id).toBe(undefined); span.end(); }); }); @@ -881,9 +876,7 @@ describe('trace', () => { it('allows to pass parentSpan=null', () => { startSpan({ name: 'outer' }, () => { startSpanManual({ name: 'GET users/[id]', parentSpan: null }, span => { - // Due to the way we propagate the scope in OTEL, - // the parent_span_id is not actually undefined here, but comes from the propagation context - expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId); + expect(spanToJSON(span).parent_span_id).toBe(undefined); span.end(); }); }); @@ -1016,17 +1009,21 @@ describe('trace', () => { }); describe('propagation', () => { - it('picks up the trace context from the scope, if there is no parent', () => { + it('starts new trace, if there is no parent', () => { withScope(scope => { const propagationContext = scope.getPropagationContext(); const span = startInactiveSpan({ name: 'test span' }); expect(span).toBeDefined(); - expect(spanToJSON(span).trace_id).toEqual(propagationContext.traceId); - expect(spanToJSON(span).parent_span_id).toEqual(propagationContext.spanId); + const traceId = spanToJSON(span).trace_id; + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanToJSON(span).parent_span_id).toBe(undefined); + expect(spanToJSON(span).trace_id).not.toEqual(propagationContext.traceId); expect(getDynamicSamplingContextFromSpan(span)).toEqual({ - ...getDynamicSamplingContextFromClient(propagationContext.traceId, getClient()!), + trace_id: traceId, + environment: 'production', + public_key: 'username', sample_rate: '1', sampled: 'true', transaction: 'test span', @@ -1034,18 +1031,23 @@ describe('trace', () => { }); }); - it('picks up the trace context from the scope, including parentSpanId, if there is no parent', () => { + // Note: This _should_ never happen, when we have an incoming trace, we should always have a parent span + it('starts new trace, ignoring parentSpanId, if there is no parent', () => { withScope(scope => { const propagationContext = scope.getPropagationContext(); propagationContext.parentSpanId = '1121201211212012'; const span = startInactiveSpan({ name: 'test span' }); expect(span).toBeDefined(); - expect(spanToJSON(span).trace_id).toEqual(propagationContext.traceId); - expect(spanToJSON(span).parent_span_id).toEqual('1121201211212012'); + const traceId = spanToJSON(span).trace_id; + expect(traceId).toMatch(/[a-f0-9]{32}/); + expect(spanToJSON(span).parent_span_id).toBe(undefined); + expect(spanToJSON(span).trace_id).not.toEqual(propagationContext.traceId); expect(getDynamicSamplingContextFromSpan(span)).toEqual({ - ...getDynamicSamplingContextFromClient(propagationContext.traceId, getClient()!), + environment: 'production', + public_key: 'username', + trace_id: traceId, sample_rate: '1', sampled: 'true', transaction: 'test span', @@ -1082,7 +1084,6 @@ describe('trace', () => { isRemote: false, traceFlags: TraceFlags.SAMPLED, traceState: makeTraceState({ - parentSpanId: '1121201211212011', dsc: { release: '1.0', environment: 'production', @@ -1112,7 +1113,6 @@ describe('trace', () => { isRemote: true, traceFlags: TraceFlags.SAMPLED, traceState: makeTraceState({ - parentSpanId: '1121201211212011', dsc: { release: '1.0', environment: 'production', @@ -1540,14 +1540,7 @@ describe('continueTrace', () => { it('works without trace & baggage data', () => { const scope = continueTrace({ sentryTrace: undefined, baggage: undefined }, () => { const span = getActiveSpan()!; - expect(span).toBeDefined(); - expect(spanToJSON(span)).toEqual({ - span_id: '', - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); - expect(getSamplingDecision(span.spanContext())).toBe(undefined); - expect(spanIsSampled(span)).toBe(false); - + expect(span).toBeUndefined(); return getCurrentScope(); }); diff --git a/packages/opentelemetry/test/utils/setupCheck.test.ts b/packages/opentelemetry/test/utils/setupCheck.test.ts index 86eca3688731..8ac8bcb8e844 100644 --- a/packages/opentelemetry/test/utils/setupCheck.test.ts +++ b/packages/opentelemetry/test/utils/setupCheck.test.ts @@ -36,6 +36,8 @@ describe('openTelemetrySetupCheck', () => { provider = new BasicTracerProvider({ sampler: new SentrySampler(client), }); + // We want to test this deprecated case also works + // eslint-disable-next-line deprecation/deprecation provider.addSpanProcessor(new SentrySpanProcessor()); const setup = openTelemetrySetupCheck(); diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 5a3f068d10b2..8833a51235cc 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -85,8 +85,7 @@ "@types/node-abi": "^3.0.3", "clang-format": "^1.8.0", "cross-env": "^7.0.3", - "node-gyp": "^9.4.1", - "typescript": "^4.9.5" + "node-gyp": "^9.4.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/profiling-node/scripts/prune-profiler-binaries.js b/packages/profiling-node/scripts/prune-profiler-binaries.js index 22fc13ce28b6..a0c5ce7fc33d 100755 --- a/packages/profiling-node/scripts/prune-profiler-binaries.js +++ b/packages/profiling-node/scripts/prune-profiler-binaries.js @@ -62,6 +62,7 @@ const NODE_TO_ABI = { 16: '93', 18: '108', 20: '115', + 22: '127', }; if (NODE) { @@ -73,9 +74,13 @@ if (NODE) { NODE = NODE_TO_ABI['18']; } else if (NODE.startsWith('20')) { NODE = NODE_TO_ABI['20']; + } else if (NODE.startsWith('22')) { + NODE = NODE_TO_ABI['22']; } else { ARGV_ERRORS.push( - '❌ Sentry: Invalid node version passed as argument, please make sure --target_node is a valid major node version. Supported versions are 16, 18 and 20.', + `❌ Sentry: Invalid node version passed as argument, please make sure --target_node is a valid major node version. Supported versions are ${Object.keys( + NODE_TO_ABI, + ).join(', ')}.`, ); } } diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts index 76fad9a286a6..4897745ededa 100644 --- a/packages/profiling-node/src/cpu_profiler.ts +++ b/packages/profiling-node/src/cpu_profiler.ts @@ -143,6 +143,7 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { return require('../sentry_cpu_profiler-linux-arm64-musl-127.node'); } } + if (stdlib === 'glibc') { if (abi === '93') { return require('../sentry_cpu_profiler-linux-arm64-glibc-93.node'); diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 48c4ccb56888..2f5467ee1640 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -44,7 +44,7 @@ export function tanstackRouterBrowserTracingIntegration( if (instrumentPageLoad && initialWindowLocation) { const matchedRoutes = castRouterInstance.matchRoutes( initialWindowLocation.pathname, - initialWindowLocation.search, + castRouterInstance.options.parseSearch(initialWindowLocation.search), { preload: false, throwOnError: false }, ); diff --git a/packages/react/src/vendor/tanstackrouter-types.ts b/packages/react/src/vendor/tanstackrouter-types.ts index 99a4510228a3..e5eeba71aa87 100644 --- a/packages/react/src/vendor/tanstackrouter-types.ts +++ b/packages/react/src/vendor/tanstackrouter-types.ts @@ -29,6 +29,10 @@ SOFTWARE. export interface VendoredTanstackRouter { history: VendoredTanstackRouterHistory; state: VendoredTanstackRouterState; + options: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseSearch: (search: string) => Record; + }; matchRoutes: ( pathname: string, // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/packages/remix/package.json b/packages/remix/package.json index 3a0d716edf93..8f3e2d1f46ca 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -52,14 +52,15 @@ "access": "public" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.38.2", + "@sentry/cli": "^2.39.1", "@sentry/core": "8.42.0", "@sentry/node": "8.42.0", "@sentry/opentelemetry": "8.42.0", "@sentry/react": "8.42.0", "glob": "^10.3.4", - "opentelemetry-instrumentation-remix": "0.7.1", + "opentelemetry-instrumentation-remix": "0.8.0", "yargs": "^17.6.0" }, "devDependencies": { diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index a92d1d94ca08..f6a5f5060dd9 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -21,6 +21,7 @@ export { addRequestDataToEvent, amqplibIntegration, anrIntegration, + disableAnrDetectionForCallback, captureCheckIn, captureConsoleIntegration, captureEvent, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 82c4b8e75b2d..5b9cbdc2ca63 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -65,7 +65,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry-internal/rrweb": "2.29.0" + "@sentry-internal/rrweb": "2.30.0" }, "dependencies": { "@sentry-internal/replay": "8.42.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index bbdf55c68ee9..78b42bccc501 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -69,8 +69,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "8.42.0", - "@sentry-internal/rrweb": "2.29.0", - "@sentry-internal/rrweb-snapshot": "2.29.0", + "@sentry-internal/rrweb": "2.30.0", + "@sentry-internal/rrweb-snapshot": "2.30.0", "fflate": "^0.8.1", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.2.1" diff --git a/packages/solid/src/errorboundary.ts b/packages/solid/src/errorboundary.ts index 8a1ad0efa902..df75f9da80f9 100644 --- a/packages/solid/src/errorboundary.ts +++ b/packages/solid/src/errorboundary.ts @@ -16,8 +16,7 @@ export function withSentryErrorBoundary(ErrorBoundary: Component { const [local, others] = splitProps(props, ['fallback']); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fallback = (error: any, reset: () => void): JSX.Element => { + const fallback = (error: unknown, reset: () => void): JSX.Element => { captureException(error); const f = local.fallback; diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 277870c98c8a..d39d12b2c963 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -66,7 +66,6 @@ } }, "dependencies": { - "@opentelemetry/instrumentation": "^0.54.0", "@sentry/core": "8.42.0", "@sentry/node": "8.42.0", "@sentry/opentelemetry": "8.42.0", diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 6ff657695081..450420a2b586 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -13,6 +13,7 @@ export { addRequestDataToEvent, amqplibIntegration, anrIntegration, + disableAnrDetectionForCallback, captureCheckIn, captureConsoleIntegration, captureEvent, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f1c00d2f5e3a..bb88e121244f 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -13,6 +13,7 @@ export { addRequestDataToEvent, amqplibIntegration, anrIntegration, + disableAnrDetectionForCallback, captureCheckIn, captureConsoleIntegration, captureEvent, diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 9a5656f3f78f..b664e2d23db5 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -269,7 +269,10 @@ function getFiles(dir: string): string[] { function detectSentryRelease(): string { let releaseFallback: string; try { - releaseFallback = child_process.execSync('git rev-parse HEAD', { stdio: 'ignore' }).toString().trim(); + releaseFallback = child_process + .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }) + .toString() + .trim(); } catch (_) { // the command can throw for various reasons. Most importantly: // - git is not installed diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 372e5e854a87..565589bc0fbb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -205,7 +205,8 @@ export type FetchBreadcrumbHint = FetchBreadcrumbHint_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type XhrBreadcrumbHint = XhrBreadcrumbHint_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type Client = Client_imported; +// eslint-disable-next-line deprecation/deprecation +export type Client> = Client_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type ClientReport = ClientReport_imported; /** @deprecated This type has been moved to `@sentry/core`. */ @@ -322,7 +323,8 @@ export type Integration = Integration_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type IntegrationClass = IntegrationClass_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type IntegrationFn = IntegrationFn_imported; +// eslint-disable-next-line deprecation/deprecation +export type IntegrationFn = IntegrationFn_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type Mechanism = Mechanism_imported; /** @deprecated This type has been moved to `@sentry/core`. */ @@ -334,9 +336,11 @@ export type Primitive = Primitive_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type WorkerLocation = WorkerLocation_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type ClientOptions = ClientOptions_imported; +// eslint-disable-next-line deprecation/deprecation +export type ClientOptions = ClientOptions_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type Options = Options_imported; +// eslint-disable-next-line deprecation/deprecation +export type Options = Options_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type Package = Package_imported; /** @deprecated This type has been moved to `@sentry/core`. */ @@ -419,10 +423,13 @@ export type SessionContext = SessionContext_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type SessionStatus = SessionStatus_imported; /** @deprecated This type has been moved to `@sentry/core`. */ +// eslint-disable-next-line deprecation/deprecation export type RequestSession = RequestSession_imported; /** @deprecated This type has been moved to `@sentry/core`. */ +// eslint-disable-next-line deprecation/deprecation export type RequestSessionStatus = RequestSessionStatus_imported; /** @deprecated This type has been moved to `@sentry/core`. */ +// eslint-disable-next-line deprecation/deprecation export type SessionFlusherLike = SessionFlusherLike_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type SerializedSession = SerializedSession_imported; @@ -511,7 +518,8 @@ export type WebFetchHeaders = WebFetchHeaders_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type WebFetchRequest = WebFetchRequest_imported; /** @deprecated This type has been moved to `@sentry/core`. */ -export type WrappedFunction = WrappedFunction_imported; +// eslint-disable-next-line @typescript-eslint/ban-types +export type WrappedFunction = WrappedFunction_imported; /** @deprecated This type has been moved to `@sentry/core`. */ export type HandlerDataFetch = HandlerDataFetch_imported; /** @deprecated This type has been moved to `@sentry/core`. */ diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 54fcde136c90..bb206f8df6fb 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -44,10 +44,10 @@ }, "devDependencies": { "@edge-runtime/types": "3.0.1", - "@opentelemetry/core": "^1.25.1", - "@opentelemetry/resources": "^1.26.0", - "@opentelemetry/sdk-trace-base": "^1.26.0", - "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/core": "^1.29.0", + "@opentelemetry/resources": "^1.29.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "@sentry/opentelemetry": "8.42.0" }, "scripts": { diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 2ddfa96fcd45..6fc0e6196de6 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -167,14 +167,13 @@ export function setupOtel(client: VercelEdgeClient): void { [ATTR_SERVICE_VERSION]: SDK_VERSION, }), forceFlushTimeoutMillis: 500, + spanProcessors: [ + new SentrySpanProcessor({ + timeout: client.getOptions().maxSpanWaitDuration, + }), + ], }); - provider.addSpanProcessor( - new SentrySpanProcessor({ - timeout: client.getOptions().maxSpanWaitDuration, - }), - ); - const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); // Initialize the provider diff --git a/scripts/ci-unit-tests.ts b/scripts/ci-unit-tests.ts index ea771b29a957..08459e9eabba 100644 --- a/scripts/ci-unit-tests.ts +++ b/scripts/ci-unit-tests.ts @@ -47,6 +47,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/nuxt', '@sentry/nestjs', '@sentry-internal/eslint-plugin-sdk', + '@sentry-internal/nitro-utils', ], }, '16': { diff --git a/yarn.lock b/yarn.lock index 4fe2dc0d7aa0..47d5996cf30a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -94,6 +94,42 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== +"@ai-sdk/provider-utils@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.0.2.tgz#ea9d510be442b38bd40ae50dbf5b64ffc396952b" + integrity sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA== + dependencies: + "@ai-sdk/provider" "1.0.1" + eventsource-parser "^3.0.0" + nanoid "^3.3.7" + secure-json-parse "^2.7.0" + +"@ai-sdk/provider@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.0.1.tgz#8172a3cbbfa61bb40b88512165f70fe3c186cb60" + integrity sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg== + dependencies: + json-schema "^0.4.0" + +"@ai-sdk/react@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-1.0.3.tgz#b9bc24e20bdc5768cbb0d9c65471fb60ab2675ec" + integrity sha512-Mak7qIRlbgtP4I7EFoNKRIQTlABJHhgwrN8SV2WKKdmsfWK2RwcubQWz1hp88cQ0bpF6KxxjSY1UUnS/S9oR5g== + dependencies: + "@ai-sdk/provider-utils" "2.0.2" + "@ai-sdk/ui-utils" "1.0.2" + swr "^2.2.5" + throttleit "2.1.0" + +"@ai-sdk/ui-utils@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-1.0.2.tgz#2b5ad527f821b055663ddc60f2c45a82956091a0" + integrity sha512-hHrUdeThGHu/rsGZBWQ9PjrAU9Htxgbo9MFyR5B/aWoNbBeXn1HLMY1+uMEnXL5pRPlmyVRjgIavWg7UgeNDOw== + dependencies: + "@ai-sdk/provider" "1.0.1" + "@ai-sdk/provider-utils" "2.0.2" + zod-to-json-schema "^3.23.5" + "@ampproject/remapping@2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -7506,20 +7542,18 @@ dependencies: "@opentelemetry/api" "^1.0.0" -"@opentelemetry/api-logs@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be" - integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw== - dependencies: - "@opentelemetry/api" "^1.0.0" - -"@opentelemetry/api-logs@0.54.0": - version "0.54.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.54.0.tgz#a8e09ae22f6d318b6202765dbc2cc0b05e3377be" - integrity sha512-9HhEh5GqFrassUndqJsyW7a0PzfyWr2eV2xwzHLIS+wX3125+9HE9FMRAKmJRwxZhgZGwH3HNQQjoMGZqmOeVA== +"@opentelemetry/api-logs@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz#68f8c51ca905c260b610c8a3c67d3f9fa3d59a45" + integrity sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g== dependencies: "@opentelemetry/api" "^1.3.0" +"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@opentelemetry/api@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.12.0.tgz#0359c3926e8f16fdcd8c78f196bd1e9fc4e66777" @@ -7527,34 +7561,22 @@ dependencies: "@opentelemetry/context-base" "^0.12.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" - integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== - -"@opentelemetry/context-async-hooks@^1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz#810bff2fcab84ec51f4684aff2d21f6c057d9e73" - integrity sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ== +"@opentelemetry/context-async-hooks@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.29.0.tgz#3b3836c913834afa7720fdcf9687620f49b2cf37" + integrity sha512-TKT91jcFXgHyIDF1lgJF3BHGIakn6x0Xp7Tq3zoS3TMPzT9IlP0xEavWP8C1zGjU9UmZP2VR1tJhW9Az1A3w8Q== "@opentelemetry/context-base@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550" integrity sha512-UXwSsXo3F3yZ1dIBOG9ID8v2r9e+bqLWoizCtTb8rXtwF+N5TM7hzzvQz72o3nBU+zrI/D5e+OqAYK8ZgDd3DA== -"@opentelemetry/core@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.1.tgz#ff667d939d128adfc7c793edae2f6bca177f829d" - integrity sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ== - dependencies: - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.26.0.tgz#7d84265aaa850ed0ca5813f97d831155be42b328" - integrity sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ== +"@opentelemetry/core@1.29.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.29.0", "@opentelemetry/core@^1.8.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.29.0.tgz#a9397dfd9a8b37b2435b5e44be16d39ec1c82bd9" + integrity sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA== dependencies: - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/semantic-conventions" "1.28.0" "@opentelemetry/core@^0.12.0": version "0.12.0" @@ -7565,257 +7587,246 @@ "@opentelemetry/context-base" "^0.12.0" semver "^7.1.3" -"@opentelemetry/instrumentation-amqplib@^0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.43.0.tgz#e18b7d763b69c605a7abf9869e1c278f9bfdc1eb" - integrity sha512-ALjfQC+0dnIEcvNYsbZl/VLh7D2P1HhFF4vicRKHhHFIUV3Shpg4kXgiek5PLhmeKSIPiUB25IYH5RIneclL4A== +"@opentelemetry/instrumentation-amqplib@^0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.45.0.tgz#747d72e38ff89266670e730ead90b85b6edc62d3" + integrity sha512-SlKLsOS65NGMIBG1Lh/hLrMDU9WzTUF25apnV6ZmWZB1bBmUwan7qrwwrTu1cL5LzJWCXOdZPuTaxP7pC9qxnQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-aws-lambda@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.44.0.tgz#9b82bd6cc86f572be837578b29ef6bf242eb1a39" - integrity sha512-6vmr7FJIuopZzsQjEQTp4xWtF6kBp7DhD7pPIN8FN0dKUKyuVraABIpgWjMfelaUPy4rTYUGkYqPluhG0wx8Dw== +"@opentelemetry/instrumentation-aws-lambda@0.49.0": + version "0.49.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.49.0.tgz#dbc27f899b4c9148b6608f6584673c517bbf439c" + integrity sha512-FIKQSzX/MSzfARqgm7lX9p/QUj7USyicioBYI5BFGuOOoLefxBlJINAcRs3EvCh1taEnJ7/LpbrhlcF7r4Yqvg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" - "@opentelemetry/propagator-aws-xray" "^1.3.1" - "@opentelemetry/resources" "^1.8.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/aws-lambda" "8.10.143" -"@opentelemetry/instrumentation-aws-sdk@0.45.0": - version "0.45.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.45.0.tgz#269371282ba95877937a11170843cf9d9436706f" - integrity sha512-3EGgC0LFZuFfXcOeslhXHhsiInVhhN046YQsYIPflsicAk7v0wN946sZKWuerEfmqx/kFXOsbOeI1SkkTRmqWQ== +"@opentelemetry/instrumentation-aws-sdk@0.48.0": + version "0.48.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.48.0.tgz#af28488661382443de017f6c69b97fdc7cbdbdc8" + integrity sha512-Bl4geb9DS5Zxr5mOsDcDTLjwrfipQ4KDl1ZT5gmoOvVuZPp308reGdtnO1QmqbvMwcgMxD2aBdWUoYgtx1WgWw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" - "@opentelemetry/propagation-utils" "^0.30.12" + "@opentelemetry/instrumentation" "^0.56.0" + "@opentelemetry/propagation-utils" "^0.30.14" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-connect@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.40.0.tgz#cb151b860ad8a711ebce4d7e025dcde95e4ba2c5" - integrity sha512-3aR/3YBQ160siitwwRLjwqrv2KBT16897+bo6yz8wIfel6nWOxTZBJudcbsK3p42pTC7qrbotJ9t/1wRLpv79Q== +"@opentelemetry/instrumentation-connect@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.42.0.tgz#daebedbe65068746c9db0eee6e3a636a0912d251" + integrity sha512-bOoYHBmbnq/jFaLHmXJ55VQ6jrH5fHDMAPjFM0d3JvR0dvIqW7anEoNC33QqYGFYUfVJ50S0d/eoyF61ALqQuA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/connect" "3.4.36" -"@opentelemetry/instrumentation-dataloader@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.12.0.tgz#de03a3948dec4f15fed80aa424d6bd5d6a8d10c7" - integrity sha512-pnPxatoFE0OXIZDQhL2okF//dmbiWFzcSc8pUg9TqofCLYZySSxDCgQc69CJBo5JnI3Gz1KP+mOjS4WAeRIH4g== +"@opentelemetry/instrumentation-dataloader@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.15.0.tgz#c3ac6f41672961a489080edd2c59aceebe412798" + integrity sha512-5fP35A2jUPk4SerVcduEkpbRAIoqa2PaP5rWumn01T1uSbavXNccAr3Xvx1N6xFtZxXpLJq4FYqGFnMgDWgVng== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-express@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.44.0.tgz#51dc11e3152ffbee1c4e389298aac30231c8270a" - integrity sha512-GWgibp6Q0wxyFaaU8ERIgMMYgzcHmGrw3ILUtGchLtLncHNOKk0SNoWGqiylXWWT4HTn5XdV8MGawUgpZh80cA== +"@opentelemetry/instrumentation-express@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.46.0.tgz#8dfbc9dc567e2e864a00a6a7edfbec2dd8482056" + integrity sha512-BCEClDj/HPq/1xYRAlOr6z+OUnbp2eFp18DSrgyQz4IT9pkdYk8eWHnMi9oZSqlC6J5mQzkFmaW5RrKb1GLQhg== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fastify@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.41.0.tgz#5e1d00383756f3a8cc2ea4a9d15f9f7510cec571" - integrity sha512-pNRjFvf0mvqfJueaeL/qEkuGJwgtE5pgjIHGYwjc2rMViNCrtY9/Sf+Nu8ww6dDd/Oyk2fwZZP7i0XZfCnETrA== +"@opentelemetry/instrumentation-fastify@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.43.0.tgz#855e259733bd75e21cb54cc110a7910861b200a4" + integrity sha512-Lmdsg7tYiV+K3/NKVAQfnnLNGmakUOFdB0PhoTh2aXuSyCmyNnnDvhn2MsArAPTZ68wnD5Llh5HtmiuTkf+DyQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fs@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.16.0.tgz#aa1cc3aa81011ad9843a0156b200f06f31ffa03e" - integrity sha512-hMDRUxV38ln1R3lNz6osj3YjlO32ykbHqVrzG7gEhGXFQfu7LJUx8t9tEwE4r2h3CD4D0Rw4YGDU4yF4mP3ilg== +"@opentelemetry/instrumentation-fs@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.18.0.tgz#6ef0e58cda3212ce2cd17bddc4dd74f768fd74c0" + integrity sha512-kC40y6CEMONm8/MWwoF5GHWIC7gOdF+g3sgsjfwJaUkgD6bdWV+FgG0XApqSbTQndICKzw3RonVk8i7s6mHqhA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-generic-pool@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz#2b9af16ad82d5cbe67125c0125753cecd162a728" - integrity sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw== +"@opentelemetry/instrumentation-generic-pool@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.42.0.tgz#6c6c6dcf2300e803acb22b2b914c6053acb80bf3" + integrity sha512-J4QxqiQ1imtB9ogzsOnHra0g3dmmLAx4JCeoK3o0rFes1OirljNHnO8Hsj4s1jAir8WmWvnEEQO1y8yk6j2tog== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-graphql@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.44.0.tgz#6fce8e2f303d16810bf8a03148cad6e8e6119de1" - integrity sha512-FYXTe3Bv96aNpYktqm86BFUTpjglKD0kWI5T5bxYkLUPEPvFn38vWGMJTGrDMVou/i55E4jlWvcm6hFIqLsMbg== +"@opentelemetry/instrumentation-graphql@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.46.0.tgz#fbcf0844656c759294c03c30c471fc4862209a01" + integrity sha512-tplk0YWINSECcK89PGM7IVtOYenXyoOuhOQlN0X0YrcDUfMS4tZMKkVc0vyhNWYYrexnUHwNry2YNBNugSpjlQ== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-hapi@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz#de8711907256d8fae1b5faf71fc825cef4a7ddbb" - integrity sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ== +"@opentelemetry/instrumentation-hapi@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.44.0.tgz#5b4524bef636209ba6cc95cfbb976b605c2946cd" + integrity sha512-4HdNIMNXWK1O6nsaQOrACo83QWEVoyNODTdVDbUqtqXiv2peDfD0RAPhSQlSGWLPw3S4d9UoOmrV7s2HYj6T2A== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-http@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz#0d806adf1b3aba036bc46e16162e3c0dbb8a6b60" - integrity sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ== +"@opentelemetry/instrumentation-http@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.56.0.tgz#f7a9e1bb4126c0d918775c1368a42b8afd5a48a2" + integrity sha512-/bWHBUAq8VoATnH9iLk5w8CE9+gj+RgYSUphe7hry472n6fYl7+4PvuScoQMdmSUTprKq/gyr2kOWL6zrC7FkQ== dependencies: - "@opentelemetry/core" "1.26.0" - "@opentelemetry/instrumentation" "0.53.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/instrumentation" "0.56.0" + "@opentelemetry/semantic-conventions" "1.28.0" + forwarded-parse "2.1.2" semver "^7.5.2" -"@opentelemetry/instrumentation-ioredis@0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz#dbadabaeefc4cb47c406f878444f1bcac774fa89" - integrity sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig== +"@opentelemetry/instrumentation-ioredis@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.46.0.tgz#ec230466813f8ce82eb9ca9b23308ccfa460ce2b" + integrity sha512-sOdsq8oGi29V58p1AkefHvuB3l2ymP1IbxRIX3y4lZesQWKL8fLhBmy8xYjINSQ5gHzWul2yoz7pe7boxhZcqQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-kafkajs@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.4.0.tgz#c1fe0de45a65a66581be0d7422f6828cc806b3bb" - integrity sha512-I9VwDG314g7SDL4t8kD/7+1ytaDBRbZQjhVaQaVIDR8K+mlsoBhLsWH79yHxhHQKvwCSZwqXF+TiTOhoQVUt7A== +"@opentelemetry/instrumentation-kafkajs@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.6.0.tgz#5d1c6738da8e270acde9259521a9c6e0f421490c" + integrity sha512-MGQrzqEUAl0tacKJUFpuNHJesyTi51oUzSVizn7FdvJplkRIdS11FukyZBZJEscofSEdk7Ycmg+kNMLi5QHUFg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-knex@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.41.0.tgz#74d611489e823003a825097bac019c6c2ad061a5" - integrity sha512-OhI1SlLv5qnsnm2dOVrian/x3431P75GngSpnR7c4fcVFv7prXGYu29Z6ILRWJf/NJt6fkbySmwdfUUnFnHCTg== +"@opentelemetry/instrumentation-knex@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.43.0.tgz#1f45cfea69212bd579e4fa95c6d5cccdd9626b8e" + integrity sha512-mOp0TRQNFFSBj5am0WF67fRO7UZMUmsF3/7HSDja9g3H4pnj+4YNvWWyZn4+q0rGrPtywminAXe0rxtgaGYIqg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-koa@0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz#963fd192a1b5f6cbae5dabf4ec82e3105cbb23b1" - integrity sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ== +"@opentelemetry/instrumentation-koa@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.46.0.tgz#bcdfb29f3b41be45355a9aa278fb231e19eb02e5" + integrity sha512-RcWXMQdJQANnPUaXbHY5G0Fg6gmleZ/ZtZeSsekWPaZmQq12FGk0L1UwodIgs31OlYfviAZ4yTeytoSUkgo5vQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-lru-memoizer@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.40.0.tgz#dc60d7fdfd2a0c681cb23e7ed4f314d1506ccdc0" - integrity sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A== +"@opentelemetry/instrumentation-lru-memoizer@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.43.0.tgz#7d3f524a10715d9f681e8d4ee6bfe91be80c82cf" + integrity sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-mongodb@0.48.0": - version "0.48.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.48.0.tgz#40fb8c705cb4bf8d8c5bf8752c60c5a0aaaaf617" - integrity sha512-9YWvaGvrrcrydMsYGLu0w+RgmosLMKe3kv/UNlsPy8RLnCkN2z+bhhbjjjuxtUmvEuKZMCoXFluABVuBr1yhjw== +"@opentelemetry/instrumentation-mongodb@0.50.0": + version "0.50.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.50.0.tgz#e5c60ad0bfbdd8ac3238c255a0662b7430083303" + integrity sha512-DtwJMjYFXFT5auAvv8aGrBj1h3ciA/dXQom11rxL7B1+Oy3FopSpanvwYxJ+z0qmBrQ1/iMuWELitYqU4LnlkQ== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-mongoose@0.42.0": - version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz#375afd21adfcd897a8f521c1ffd2d91e6a428705" - integrity sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg== +"@opentelemetry/instrumentation-mongoose@0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.45.0.tgz#c8179827769fac8528b681da5888ae1779bd844b" + integrity sha512-zHgNh+A01C5baI2mb5dAGyMC7DWmUpOfwpV8axtC0Hd5Uzqv+oqKgKbVDIVhOaDkPxjgVJwYF9YQZl2pw2qxIA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-mysql2@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz#6377b6e2d2487fd88e1d79aa03658db6c8d51651" - integrity sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g== +"@opentelemetry/instrumentation-mysql2@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.44.0.tgz#309d3fa452d4fcb632c4facb68ed7ea74b6738f9" + integrity sha512-e9QY4AGsjGFwmfHd6kBa4yPaQZjAq2FuxMb0BbKlXCAjG+jwqw+sr9xWdJGR60jMsTq52hx3mAlE3dUJ9BipxQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@opentelemetry/sql-common" "^0.40.1" -"@opentelemetry/instrumentation-mysql@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz#2d50691ead5219774bd36d66c35d5b4681485dd7" - integrity sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw== +"@opentelemetry/instrumentation-mysql@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.44.0.tgz#a29af4432d4289ed9d147d9c30038c57031d950c" + integrity sha512-al7jbXvT/uT1KV8gdNDzaWd5/WXf+mrjrsF0/NtbnqLa0UUFGgQnoK3cyborgny7I+KxWhL8h7YPTf6Zq4nKsg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/mysql" "2.15.26" -"@opentelemetry/instrumentation-nestjs-core@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz#2c0e6405b56caaec32747d55c57ff9a034668ea8" - integrity sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg== +"@opentelemetry/instrumentation-nestjs-core@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.43.0.tgz#c176409ab5ebfac862298e31a6a149126e278700" + integrity sha512-NEo4RU7HTjiaXk3curqXUvCb9alRiFWxQY//+hvDXwWLlADX2vB6QEmVCeEZrKO+6I/tBrI4vNdAnbCY9ldZVg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-pg@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz#1e97a0aeb2dca068ee23ce75884a0a0063a7ce3f" - integrity sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ== +"@opentelemetry/instrumentation-pg@0.49.0": + version "0.49.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.49.0.tgz#47a6a461099fae8e1ffbb97b715a0c34f0aec0b6" + integrity sha512-3alvNNjPXVdAPdY1G7nGRVINbDxRK02+KAugDiEpzw0jFQfU8IzFkSWA4jyU4/GbMxKvHD+XIOEfSjpieSodKw== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/core" "^1.26.0" + "@opentelemetry/instrumentation" "^0.56.0" + "@opentelemetry/semantic-conventions" "1.27.0" "@opentelemetry/sql-common" "^0.40.1" "@types/pg" "8.6.1" "@types/pg-pool" "2.0.6" -"@opentelemetry/instrumentation-redis-4@0.42.0": - version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz#fc01104cfe884c7546385eaae03c57a47edd19d1" - integrity sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg== +"@opentelemetry/instrumentation-redis-4@0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.45.0.tgz#34115d39f7050b8576344d9e7f7cb8ceebf85067" + integrity sha512-Sjgym1xn3mdxPRH5CNZtoz+bFd3E3NlGIu7FoYr4YrQouCc9PbnmoBcmSkEdDy5LYgzNildPgsjx9l0EKNjKTQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-tedious@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.15.0.tgz#da82f4d153fb6ff7d1f85d39872ac40bf9db12ea" - integrity sha512-Kb7yo8Zsq2TUwBbmwYgTAMPK0VbhoS8ikJ6Bup9KrDtCx2JC01nCb+M0VJWXt7tl0+5jARUbKWh5jRSoImxdCw== +"@opentelemetry/instrumentation-tedious@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.17.0.tgz#689b7c87346f11b73488b3aa91661d15e8fa830c" + integrity sha512-yRBz2409an03uVd1Q2jWMt3SqwZqRFyKoWYYX3hBAtPDazJ4w5L+1VOij71TKwgZxZZNdDBXImTQjii+VeuzLg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/tedious" "^4.0.14" -"@opentelemetry/instrumentation-undici@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz#9436ee155c8dcb0b760b66947c0e0f347688a5ef" - integrity sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ== +"@opentelemetry/instrumentation-undici@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.9.0.tgz#c0be1854a90a5002d2345f8bc939d659a9ad76b1" + integrity sha512-lxc3cpUZ28CqbrWcUHxGW/ObDpMOYbuxF/ZOzeFZq54P9uJ2Cpa8gcrC9F716mtuiMaekwk8D6n34vg/JtkkxQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" - integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A== +"@opentelemetry/instrumentation@0.56.0", "@opentelemetry/instrumentation@^0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz#3330ce16d9235a548efa1019a4a7f01414edd44a" + integrity sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w== dependencies: - "@opentelemetry/api-logs" "0.53.0" + "@opentelemetry/api-logs" "0.56.0" "@types/shimmer" "^1.2.0" import-in-the-middle "^1.8.1" require-in-the-middle "^7.1.1" semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" - integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== - dependencies: - "@types/shimmer" "^1.0.2" - import-in-the-middle "1.4.2" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - -"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0": +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.1": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== @@ -7827,50 +7838,23 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.54.0": - version "0.54.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.54.0.tgz#3fa9df964d3b157ea7ef2270168d343331d6448e" - integrity sha512-B0Ydo9g9ehgNHwtpc97XivEzjz0XBKR6iQ83NTENIxEEf5NHE0otZQuZLgDdey1XNk+bP1cfRpIkSFWM5YlSyg== - dependencies: - "@opentelemetry/api-logs" "0.54.0" - "@types/shimmer" "^1.2.0" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - -"@opentelemetry/propagation-utils@^0.30.12": - version "0.30.12" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.12.tgz#58200cfd085e791bab5e3c4d36d77b2c60fc2b6b" - integrity sha512-bgab3q/4dYUutUpQCEaSDa+mLoQJG3vJKeSiGuhM4iZaSpkz8ov0fs1MGil5PfxCo6Hhw3bB3bFYhUtnsfT/Pg== - -"@opentelemetry/propagator-aws-xray@^1.3.1": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-1.26.0.tgz#becd2b9208eb554cc606529f68e58dfd03a20f4d" - integrity sha512-Sex+JyEZ/xX328TArBqQjh1NZSfNyw5NdASUIi9hnPsnMBMSBaDe7B9JRnXv0swz7niNyAnXa6MY7yOCV76EvA== - dependencies: - "@opentelemetry/core" "1.26.0" +"@opentelemetry/propagation-utils@^0.30.14": + version "0.30.14" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.14.tgz#ba8454e3337e03a79282ea4e210819ae4cab7de2" + integrity sha512-RsdKGFd0PYG5Aop9aq8khYbR8Oq+lYTQBX/9/pk7b+8+0WwdFqrvGDmRxpBAH9hgIvtUgETeshlYctwjo2l9SQ== "@opentelemetry/redis-common@^0.36.2": version "0.36.2" resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47" integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g== -"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.8.0": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.1.tgz#bb9a674af25a1a6c30840b755bc69da2796fefbb" - integrity sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/resources@1.26.0", "@opentelemetry/resources@^1.26.0": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.26.0.tgz#da4c7366018bd8add1f3aa9c91c6ac59fd503cef" - integrity sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw== +"@opentelemetry/resources@1.29.0", "@opentelemetry/resources@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.29.0.tgz#d170f39b2ac93d61b53d13dfcd96795181bdc372" + integrity sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ== dependencies: - "@opentelemetry/core" "1.26.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" "@opentelemetry/resources@^0.12.0": version "0.12.0" @@ -7880,34 +7864,25 @@ "@opentelemetry/api" "^0.12.0" "@opentelemetry/core" "^0.12.0" -"@opentelemetry/sdk-trace-base@^1.22": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz#cbc1e60af255655d2020aa14cde17b37bd13df37" - integrity sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/sdk-trace-base@^1.26.0": - version "1.26.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz#0c913bc6d2cfafd901de330e4540952269ae579c" - integrity sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw== +"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz#f48d95dae0e58e601d0596bd2e482122d2688fb8" + integrity sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ== dependencies: - "@opentelemetry/core" "1.26.0" - "@opentelemetry/resources" "1.26.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" -"@opentelemetry/semantic-conventions@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e" - integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ== - -"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.27.0": +"@opentelemetry/semantic-conventions@1.27.0": version "1.27.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== +"@opentelemetry/semantic-conventions@1.28.0", "@opentelemetry/semantic-conventions@^1.25.1", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" + integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== + "@opentelemetry/semantic-conventions@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.12.0.tgz#7e392aecdbdbd5d737d3995998b120dc17589ab0" @@ -7931,74 +7906,79 @@ "@opentelemetry/resources" "^0.12.0" "@opentelemetry/semantic-conventions" "^0.12.0" -"@parcel/watcher-android-arm64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" - integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== +"@parcel/watcher-android-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a" + integrity sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ== -"@parcel/watcher-darwin-arm64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz#c817c7a3b4f3a79c1535bfe54a1c2818d9ffdc34" - integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== +"@parcel/watcher-darwin-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz#0d9e680b7e9ec1c8f54944f1b945aa8755afb12f" + integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw== -"@parcel/watcher-darwin-x64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" - integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== +"@parcel/watcher-darwin-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz#f9f1d5ce9d5878d344f14ef1856b7a830c59d1bb" + integrity sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA== -"@parcel/watcher-freebsd-x64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" - integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== +"@parcel/watcher-freebsd-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz#2b77f0c82d19e84ff4c21de6da7f7d096b1a7e82" + integrity sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw== -"@parcel/watcher-linux-arm-glibc@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" - integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== +"@parcel/watcher-linux-arm-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz#92ed322c56dbafa3d2545dcf2803334aee131e42" + integrity sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA== -"@parcel/watcher-linux-arm64-glibc@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" - integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== +"@parcel/watcher-linux-arm-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz#cd48e9bfde0cdbbd2ecd9accfc52967e22f849a4" + integrity sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA== -"@parcel/watcher-linux-arm64-musl@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" - integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== +"@parcel/watcher-linux-arm64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz#7b81f6d5a442bb89fbabaf6c13573e94a46feb03" + integrity sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA== -"@parcel/watcher-linux-x64-glibc@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" - integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== +"@parcel/watcher-linux-arm64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz#dcb8ff01077cdf59a18d9e0a4dff7a0cfe5fd732" + integrity sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q== -"@parcel/watcher-linux-x64-musl@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" - integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== +"@parcel/watcher-linux-x64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz#2e254600fda4e32d83942384d1106e1eed84494d" + integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw== + +"@parcel/watcher-linux-x64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz#01fcea60fedbb3225af808d3f0a7b11229792eef" + integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA== "@parcel/watcher-wasm@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz#c4353e4fdb96ee14389856f7f6f6d21b7dcef9e1" - integrity sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.5.0.tgz#81fad1e10957f08a532eb4fc0d4c353cd8901a50" + integrity sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ== dependencies: is-glob "^4.0.3" micromatch "^4.0.5" napi-wasm "^1.1.0" -"@parcel/watcher-win32-arm64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" - integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== +"@parcel/watcher-win32-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz#87cdb16e0783e770197e52fb1dc027bb0c847154" + integrity sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig== -"@parcel/watcher-win32-ia32@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" - integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== +"@parcel/watcher-win32-ia32@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz#778c39b56da33e045ba21c678c31a9f9d7c6b220" + integrity sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA== -"@parcel/watcher-win32-x64@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" - integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== +"@parcel/watcher-win32-x64@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz#33873876d0bbc588aacce38e90d1d7480ce81cb7" + integrity sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw== "@parcel/watcher@2.0.4": version "2.0.4" @@ -8009,27 +7989,28 @@ node-gyp-build "^4.3.0" "@parcel/watcher@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.4.1.tgz#a50275151a1bb110879c6123589dba90c19f1bf8" - integrity sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.0.tgz#5c88818b12b8de4307a9d3e6dc3e28eba0dfbd10" + integrity sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ== dependencies: detect-libc "^1.0.3" is-glob "^4.0.3" micromatch "^4.0.5" node-addon-api "^7.0.0" optionalDependencies: - "@parcel/watcher-android-arm64" "2.4.1" - "@parcel/watcher-darwin-arm64" "2.4.1" - "@parcel/watcher-darwin-x64" "2.4.1" - "@parcel/watcher-freebsd-x64" "2.4.1" - "@parcel/watcher-linux-arm-glibc" "2.4.1" - "@parcel/watcher-linux-arm64-glibc" "2.4.1" - "@parcel/watcher-linux-arm64-musl" "2.4.1" - "@parcel/watcher-linux-x64-glibc" "2.4.1" - "@parcel/watcher-linux-x64-musl" "2.4.1" - "@parcel/watcher-win32-arm64" "2.4.1" - "@parcel/watcher-win32-ia32" "2.4.1" - "@parcel/watcher-win32-x64" "2.4.1" + "@parcel/watcher-android-arm64" "2.5.0" + "@parcel/watcher-darwin-arm64" "2.5.0" + "@parcel/watcher-darwin-x64" "2.5.0" + "@parcel/watcher-freebsd-x64" "2.5.0" + "@parcel/watcher-linux-arm-glibc" "2.5.0" + "@parcel/watcher-linux-arm-musl" "2.5.0" + "@parcel/watcher-linux-arm64-glibc" "2.5.0" + "@parcel/watcher-linux-arm64-musl" "2.5.0" + "@parcel/watcher-linux-x64-glibc" "2.5.0" + "@parcel/watcher-linux-x64-musl" "2.5.0" + "@parcel/watcher-win32-arm64" "2.5.0" + "@parcel/watcher-win32-ia32" "2.5.0" + "@parcel/watcher-win32-x64" "2.5.0" "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -8043,15 +8024,10 @@ dependencies: playwright "1.44.1" -"@polka/url@^1.0.0-next.20": - version "1.0.0-next.21" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" - integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== - -"@polka/url@^1.0.0-next.24": - version "1.0.0-next.25" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" - integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== +"@polka/url@^1.0.0-next.20", "@polka/url@^1.0.0-next.24": + version "1.0.0-next.28" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" + integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== "@prisma/client@5.9.1": version "5.9.1" @@ -8473,91 +8449,181 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz#07db37fcd9d401aae165f662c0069efd61d4ffcc" integrity sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA== +"@rollup/rollup-android-arm-eabi@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz#c460b54c50d42f27f8254c435a4f3b3e01910bc8" + integrity sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw== + "@rollup/rollup-android-arm64@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz#160975402adf85ecd58a0721ad60ae1779a68147" integrity sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA== +"@rollup/rollup-android-arm64@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz#96e01f3a04675d8d5973ab8d3fd6bc3be21fa5e1" + integrity sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA== + "@rollup/rollup-darwin-arm64@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz#2b126f0aa4349694fe2941bcbcc4b0982b7f1a49" integrity sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg== +"@rollup/rollup-darwin-arm64@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz#9b2ec23b17b47cbb2f771b81f86ede3ac6730bce" + integrity sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ== + "@rollup/rollup-darwin-x64@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz#3f4987eff6195532037c50b8db92736e326b5bb2" integrity sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA== +"@rollup/rollup-darwin-x64@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz#f30e4ee6929e048190cf10e0daa8e8ae035b6e46" + integrity sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg== + "@rollup/rollup-freebsd-arm64@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz#15fe184ecfafc635879500f6985c954e57697c44" integrity sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw== +"@rollup/rollup-freebsd-arm64@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz#c54b2373ec5bcf71f08c4519c7ae80a0b6c8e03b" + integrity sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw== + "@rollup/rollup-freebsd-x64@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz#c72d37315d36b6e0763b7aabb6ae53c361b45e05" integrity sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg== +"@rollup/rollup-freebsd-x64@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz#3bc53aa29d5a34c28ba8e00def76aa612368458e" + integrity sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g== + "@rollup/rollup-linux-arm-gnueabihf@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz#f274f81abf845dcca5f1f40d434a09a79a3a73a0" integrity sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig== +"@rollup/rollup-linux-arm-gnueabihf@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz#c85aedd1710c9e267ee86b6d1ce355ecf7d9e8d9" + integrity sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA== + "@rollup/rollup-linux-arm-musleabihf@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz#9edaeb1a9fa7d4469917cb0614f665f1cf050625" integrity sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw== +"@rollup/rollup-linux-arm-musleabihf@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz#e77313408bf13995aecde281aec0cceb08747e42" + integrity sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw== + "@rollup/rollup-linux-arm64-gnu@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz#6eb6851f594336bfa00f074f58a00a61e9751493" integrity sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ== +"@rollup/rollup-linux-arm64-gnu@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz#633f632397b3662108cfaa1abca2a80b85f51102" + integrity sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg== + "@rollup/rollup-linux-arm64-musl@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz#9d8dc8e80df8f156d2888ecb8d6c96d653580731" integrity sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA== +"@rollup/rollup-linux-arm64-musl@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz#63edd72b29c4cced93e16113a68e1be9fef88907" + integrity sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA== + "@rollup/rollup-linux-powerpc64le-gnu@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz#358e3e7dda2d60c46ff7c74f7075045736df5b50" integrity sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw== +"@rollup/rollup-linux-powerpc64le-gnu@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz#a9418a4173df80848c0d47df0426a0bf183c4e75" + integrity sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA== + "@rollup/rollup-linux-riscv64-gnu@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz#b08461ace599c3f0b5f27051f1756b6cf1c78259" integrity sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg== +"@rollup/rollup-linux-riscv64-gnu@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz#bc9c195db036a27e5e3339b02f51526b4ce1e988" + integrity sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw== + "@rollup/rollup-linux-s390x-gnu@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz#daab36c9b5c8ac4bfe5a9c4c39ad711464b7dfee" integrity sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q== +"@rollup/rollup-linux-s390x-gnu@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz#1651fdf8144ae89326c01da5d52c60be63e71a82" + integrity sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ== + "@rollup/rollup-linux-x64-gnu@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz#4cc3a4f31920bdb028dbfd7ce0e972a17424a63c" integrity sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ== +"@rollup/rollup-linux-x64-gnu@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz#e473de5e4acb95fcf930a35cbb7d3e8080e57a6f" + integrity sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA== + "@rollup/rollup-linux-x64-musl@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz#59800e26c538517ee05f4645315d9e1aded93200" integrity sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q== +"@rollup/rollup-linux-x64-musl@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz#0af12dd2578c29af4037f0c834b4321429dd5b01" + integrity sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q== + "@rollup/rollup-win32-arm64-msvc@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz#c80e2c33c952b6b171fa6ad9a97dfbb2e4ebee44" integrity sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw== +"@rollup/rollup-win32-arm64-msvc@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz#e48e78cdd45313b977c1390f4bfde7ab79be8871" + integrity sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA== + "@rollup/rollup-win32-ia32-msvc@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz#a1e9d275cb16f6d5feb9c20aee7e897b1e193359" integrity sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw== +"@rollup/rollup-win32-ia32-msvc@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz#a3fc8536d243fe161c796acb93eba43c250f311c" + integrity sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg== + "@rollup/rollup-win32-x64-msvc@4.24.2": version "4.24.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz#0610af0fb8fec52be779d5b163bbbd6930150467" integrity sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA== +"@rollup/rollup-win32-x64-msvc@4.24.4": + version "4.24.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz#e2a9d1fd56524103a6cc8a54404d9d3ebc73c454" + integrity sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg== + "@schematics/angular@14.2.13": version "14.2.13" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-14.2.13.tgz#35ee9120a3ac07077bad169fa74fdf4ce4e193d7" @@ -8567,34 +8633,34 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry-internal/rrdom@2.29.0": - version "2.29.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.29.0.tgz#df60564466718ae7ada376cf1bd483b8ee07831a" - integrity sha512-TXhujPMt0Iq4l/sjm+rdU/CI6yR8K9+NheKPbCrs3UBzQHbu2VglrlEmhyx57mJY2GwRBrvLcCr5NokX7v1eBA== +"@sentry-internal/rrdom@2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.30.0.tgz#b0d0455be62db08a196d22c3f99e063489634223" + integrity sha512-u5f38j3y7esGSoJfblgQETX2sWC2+jM3nkzhqPP0nOEKoIb0GPA+m1fa2D949BXrk20e98qEUPzW32dpF4ka/w== dependencies: - "@sentry-internal/rrweb-snapshot" "2.29.0" + "@sentry-internal/rrweb-snapshot" "2.30.0" -"@sentry-internal/rrweb-snapshot@2.29.0": - version "2.29.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.29.0.tgz#b0bb64ccffbd486bb739c87d481aa8cdcd7d5c05" - integrity sha512-nIf593YObUzdmEilT3LEXBTpcVGXRYlYTgxiESeJgXrEmNoeB1BolKh4OJa5KpEmwmHcfe3zl15GdzhjxOIwAA== +"@sentry-internal/rrweb-snapshot@2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.30.0.tgz#d4e1974e32e068db0bd3e3cfd5f0d700f5c4d414" + integrity sha512-rR6KRcE0UZfrh1taBO1KLVzfDaQ2iWW879LBMa94HEH/xUUSG3vRF7t55rmKpxIam1v2Ib6iiCMMTAZoZxzE0Q== -"@sentry-internal/rrweb-types@2.29.0": - version "2.29.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.29.0.tgz#71b20e6dd452f005ff37f059df2dacad98f6e0ea" - integrity sha512-0x1aT+ifDjX3JKd4kmGzbofkI6qWYAOZmd5tPX07OmVnT3aIoecBqBCUagx15ewm0kMRv5Pl53is0EWzHIDvlA== +"@sentry-internal/rrweb-types@2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.30.0.tgz#50ab65034fab3d8243b601ff9925fe45ad141f48" + integrity sha512-Wb6RM5SnnWdCpHB6nxEGFV4bqQwMMDOGryMj8QyZf7fK6lkxtEBXOWiEhxUgAUPjMBqQDZm/2DzKO+bW4NHLZg== dependencies: - "@sentry-internal/rrweb-snapshot" "2.29.0" + "@sentry-internal/rrweb-snapshot" "2.30.0" "@types/css-font-loading-module" "0.0.7" -"@sentry-internal/rrweb@2.29.0": - version "2.29.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.29.0.tgz#1019bee52be0ed4bd3112a3e1a1c50adfb6bab78" - integrity sha512-UmEtyfo3yCdJsIdt0m7OLLmg9CeNmGlkmGSa91nResZVIC1+rd4RA+PmmqkwAV/WOljCXHZHs7ezlW1Mjjm2vQ== +"@sentry-internal/rrweb@2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.30.0.tgz#9af028b80a0081c75ff410817fe2fcda010f9cf6" + integrity sha512-ZOf4RmxX29LgQDW5sy9D/JfwmQbgMzF6DfA00rlFTtQYht56gbgtmWfqeWMDxG9tas71BnMTOz6eF28t7MoykQ== dependencies: - "@sentry-internal/rrdom" "2.29.0" - "@sentry-internal/rrweb-snapshot" "2.29.0" - "@sentry-internal/rrweb-types" "2.29.0" + "@sentry-internal/rrdom" "2.30.0" + "@sentry-internal/rrweb-snapshot" "2.30.0" + "@sentry-internal/rrweb-types" "2.30.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -8606,6 +8672,11 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== +"@sentry/babel-plugin-component-annotate@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd" + integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ== + "@sentry/bundler-plugin-core@2.22.6": version "2.22.6" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" @@ -8620,45 +8691,59 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.38.2.tgz#2a53028e143d0cfed607588b87e04906ef5317e7" - integrity sha512-21ywIcJCCFrCTyiF1o1PaT7rbelFC2fWmayKYgFElnQ55IzNYkcn8BYhbh/QknE0l1NBRaeWMXwTTdeoqETCCg== - -"@sentry/cli-linux-arm64@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.38.2.tgz#1b45de7e4f5e1a953b88b0b811d789de1fc708aa" - integrity sha512-4Fp/jjQpNZj4Th+ZckMQvldAuuP0ZcyJ9tJCP1CCOn5poIKPYtY6zcbTP036R7Te14PS4ALOcDNX3VNKfpsifA== - -"@sentry/cli-linux-arm@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.38.2.tgz#91f73c251f1d4b591fa98af10ee3889c9b93d208" - integrity sha512-+AiKDBQKIdQe4NhBiHSHGl0KR+b//HHTrnfK1SaTrOm9HtM4ELXAkjkRF3bmbpSzSQCS5WzcbIxxCJOeaUaO0A== - -"@sentry/cli-linux-i686@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.38.2.tgz#26e30a9bc358f910e21d812359294dd4c6103fda" - integrity sha512-6zVJN10dHIn4R1v+fxuzlblzVBhIVwsaN/S7aBED6Vn1HhAyAcNG2tIzeCLGeDfieYjXlE2sCI82sZkQBCbAGw== - -"@sentry/cli-linux-x64@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.38.2.tgz#18728bbb20e28315c4368baded677786f2dba70a" - integrity sha512-4UiLu9zdVtqPeltELR5MDGKcuqAdQY9xz3emISuA6bm+MXGbt2W1WgX+XY3GElwjZbmH8qpyLUEd34sw6sdcbQ== - -"@sentry/cli-win32-i686@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.38.2.tgz#dfe268b041c3e3db556290dba745455d0b2c0d72" - integrity sha512-DYfSvd5qLPerLpIxj3Xu2rRe3CIlpGOOfGSNI6xvJ5D8j6hqbOHlCzvfC4oBWYVYGtxnwQLMeDGJ7o7RMYulig== - -"@sentry/cli-win32-x64@2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.38.2.tgz#e7b5744026ff5f7e84971512bee228620ba5857d" - integrity sha512-W5UX58PKY1hNUHo9YJxWNhGvgvv2uOYHI27KchRiUvFYBIqlUUcIdPZDfyzetDfd8qBCxlAsFnkL2VJSNdpA9A== - -"@sentry/cli@^2.36.1", "@sentry/cli@^2.38.2": - version "2.38.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.38.2.tgz#e9a7a9bbeaaade4557de91704d50d131760345d3" - integrity sha512-CR0oujpAnhegK2pBAv6ZReMqbFTuNJLDZLvoD1B+syrKZX+R+oxkgT2e1htsBbht+wGxAsluVWsIAydSws1GAA== +"@sentry/bundler-plugin-core@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.7.tgz#28204a224cd1fef58d157e5beeb2493947a9bc35" + integrity sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "2.22.7" + "@sentry/cli" "2.39.1" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + +"@sentry/cli-darwin@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781" + integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ== + +"@sentry/cli-linux-arm64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6" + integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw== + +"@sentry/cli-linux-arm@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c" + integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ== + +"@sentry/cli-linux-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201" + integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg== + +"@sentry/cli-linux-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889" + integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA== + +"@sentry/cli-win32-i686@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35" + integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q== + +"@sentry/cli-win32-x64@2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb" + integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw== + +"@sentry/cli@2.39.1", "@sentry/cli@^2.36.1", "@sentry/cli@^2.39.1": + version "2.39.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29" + integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -8666,20 +8751,20 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.38.2" - "@sentry/cli-linux-arm" "2.38.2" - "@sentry/cli-linux-arm64" "2.38.2" - "@sentry/cli-linux-i686" "2.38.2" - "@sentry/cli-linux-x64" "2.38.2" - "@sentry/cli-win32-i686" "2.38.2" - "@sentry/cli-win32-x64" "2.38.2" - -"@sentry/rollup-plugin@2.22.6": - version "2.22.6" - resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-2.22.6.tgz#74e9ab69729ee024a497b21b66be3b1992e786d5" - integrity sha512-UmTT4kLytwDJkmfwFCOXIgS6pBi2+ZeM/zU/xJ2R4jE0+s1VvYP3DBGYsUhp4Uf/zDanCawpKJqYZMZtq9EyMA== - dependencies: - "@sentry/bundler-plugin-core" "2.22.6" + "@sentry/cli-darwin" "2.39.1" + "@sentry/cli-linux-arm" "2.39.1" + "@sentry/cli-linux-arm64" "2.39.1" + "@sentry/cli-linux-i686" "2.39.1" + "@sentry/cli-linux-x64" "2.39.1" + "@sentry/cli-win32-i686" "2.39.1" + "@sentry/cli-win32-x64" "2.39.1" + +"@sentry/rollup-plugin@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-2.22.7.tgz#994bb859437eb1e5fd34c485aaa79ba14354778f" + integrity sha512-7rgIsne8Ghb/CrfFJG5DMLcHyMqrUaw4yifq7sgYCdDyUBQ5Ox0eWVQ/autK/NYLDxDsT2r8FNttpM2hpEsUxg== + dependencies: + "@sentry/bundler-plugin-core" "2.22.7" unplugin "1.0.1" "@sentry/vite-plugin@2.22.6", "@sentry/vite-plugin@^2.22.6": @@ -8690,12 +8775,12 @@ "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/webpack-plugin@2.22.6": - version "2.22.6" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.6.tgz#8c9d27d5cd89153a5b6e08cc9dcb3048b122ffbc" - integrity sha512-BiLhAzQYAz/9kCXKj2LeUKWf/9GBVn2dD0DeYK89s+sjDEaxjbcLBBiLlLrzT7eC9QVj2tUZRKOi6puCfc8ysw== +"@sentry/webpack-plugin@2.22.7": + version "2.22.7" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.7.tgz#992c6c782c736f22e72eb318745e28cc24aabad7" + integrity sha512-j5h5LZHWDlm/FQCCmEghQ9FzYXwfZdlOf3FE/X6rK6lrtx0JCAkq+uhMSasoyP4XYKL4P4vRS6WFSos4jxf/UA== dependencies: - "@sentry/bundler-plugin-core" "2.22.6" + "@sentry/bundler-plugin-core" "2.22.7" unplugin "1.0.1" uuid "^9.0.0" @@ -9637,6 +9722,11 @@ dependencies: "@types/ms" "*" +"@types/diff-match-patch@^1.0.36": + version "1.0.36" + resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af" + integrity sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg== + "@types/duplexify@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8" @@ -9964,8 +10054,17 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": - name "@types/history-4" +"@types/history-4@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history-5@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -10292,7 +10391,15 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14": + version "5.1.14" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" + integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -11515,11 +11622,6 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== - acorn-import-attributes@^1.9.2, acorn-import-attributes@^1.9.5: version "1.9.5" resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" @@ -11653,6 +11755,19 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ai@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ai/-/ai-4.0.6.tgz#94ef793df8525c01043e15a60030ce88d7b5c7d5" + integrity sha512-TD7fH0LymjIYWmdQViB5SoBb1iuuDPOZ7RMU3W9r4SeUf68RzWyixz118QHQTENNqPiGA6vs5NDVAmZOnhzqYA== + dependencies: + "@ai-sdk/provider" "1.0.1" + "@ai-sdk/provider-utils" "2.0.2" + "@ai-sdk/react" "1.0.3" + "@ai-sdk/ui-utils" "1.0.2" + "@opentelemetry/api" "1.9.0" + jsondiffpatch "0.6.0" + zod-to-json-schema "^3.23.5" + ajv-formats@2.1.1, ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -14419,7 +14534,7 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -client-only@0.0.1: +client-only@0.0.1, client-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== @@ -15985,6 +16100,11 @@ devlop@^1.0.0: dependencies: dequal "^2.0.0" +diff-match-patch@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -18120,6 +18240,11 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57" + integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA== + exec-sh@^0.3.2, exec-sh@^0.3.4: version "0.3.6" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" @@ -18899,6 +19024,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +forwarded-parse@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -20718,20 +20848,10 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.4.2.tgz#2a266676e3495e72c04bbaa5ec14756ba168391b" - integrity sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw== - dependencies: - acorn "^8.8.2" - acorn-import-assertions "^1.9.0" - cjs-module-lexer "^1.2.2" - module-details-from-path "^1.0.3" - import-in-the-middle@^1.11.2, import-in-the-middle@^1.8.1: - version "1.11.2" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz#dd848e72b63ca6cd7c34df8b8d97fc9baee6174f" - integrity sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA== + version "1.11.3" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.3.tgz#08559f2c05fd65ba7062e747af056ed18a80120c" + integrity sha512-tNpKEb4AjZrCyrxi+Eyu43h5ig0O8ZRFSXPHh/00/o+4P4pKzVEW/m5lsVtsAT7fCIgmQOAPjdqecGDsBXRxsw== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5" @@ -22449,6 +22569,11 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -22498,6 +22623,15 @@ jsonc-parser@3.2.0, jsonc-parser@^3.0.0, jsonc-parser@^3.2.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== +jsondiffpatch@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz#daa6a25bedf0830974c81545568d5f671c82551f" + integrity sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ== + dependencies: + "@types/diff-match-patch" "^1.0.36" + chalk "^5.3.0" + diff-match-patch "^1.0.5" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -26273,13 +26407,13 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -opentelemetry-instrumentation-remix@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.7.1.tgz#ef90ede718612786f7015e5496bd25cac8c49ce3" - integrity sha512-zzJ8CAsf4Pem+B1zY0NEKQcQjXIORgylPBIQMO2x1OdRc9HzBPLUp7JIlGt/RAfPsp5qaEs7QqvX9xtsrZgtFQ== +opentelemetry-instrumentation-remix@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.8.0.tgz#cf917395f82b2c995ee46068d85d9fa1c95eb36f" + integrity sha512-2XhIEWfzHeQmxnzv9HzklwkgYMx4NuWwloZuVIwjUb9R28gH5j3rJPqjErTvYSyz0fLbw0gyI+gfYHKHn/v/1Q== dependencies: - "@opentelemetry/instrumentation" "^0.43.0" - "@opentelemetry/semantic-conventions" "^1.17.0" + "@opentelemetry/instrumentation" "^0.52.1" + "@opentelemetry/semantic-conventions" "^1.25.1" optional-require@1.0.x: version "1.0.3" @@ -28654,8 +28788,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: - name react-router-6 +"react-router-6@npm:react-router@6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -28670,6 +28803,13 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -29740,6 +29880,33 @@ rollup@^4.13.0, rollup@^4.18.0, rollup@^4.20.0, rollup@^4.24.2: "@rollup/rollup-win32-x64-msvc" "4.24.2" fsevents "~2.3.2" +rollup@^4.24.4: + version "4.24.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.4.tgz#fdc76918de02213c95447c9ffff5e35dddb1d058" + integrity sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.24.4" + "@rollup/rollup-android-arm64" "4.24.4" + "@rollup/rollup-darwin-arm64" "4.24.4" + "@rollup/rollup-darwin-x64" "4.24.4" + "@rollup/rollup-freebsd-arm64" "4.24.4" + "@rollup/rollup-freebsd-x64" "4.24.4" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.4" + "@rollup/rollup-linux-arm-musleabihf" "4.24.4" + "@rollup/rollup-linux-arm64-gnu" "4.24.4" + "@rollup/rollup-linux-arm64-musl" "4.24.4" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.4" + "@rollup/rollup-linux-riscv64-gnu" "4.24.4" + "@rollup/rollup-linux-s390x-gnu" "4.24.4" + "@rollup/rollup-linux-x64-gnu" "4.24.4" + "@rollup/rollup-linux-x64-musl" "4.24.4" + "@rollup/rollup-win32-arm64-msvc" "4.24.4" + "@rollup/rollup-win32-ia32-msvc" "4.24.4" + "@rollup/rollup-win32-x64-msvc" "4.24.4" + fsevents "~2.3.2" + rrweb-cssom@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" @@ -30004,6 +30171,11 @@ section-matter@^1.0.0: extend-shallow "^2.0.1" kind-of "^6.0.0" +secure-json-parse@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -31068,7 +31240,16 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -31180,7 +31361,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -31476,6 +31664,14 @@ svgo@^3.3.2: csso "^5.0.5" picocolors "^1.0.0" +swr@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" + integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" @@ -31828,6 +32024,11 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +throttleit@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4" + integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw== + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -32392,7 +32593,7 @@ typescript@4.6.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== -typescript@4.9.5, typescript@^4.9.5: +typescript@4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -32913,17 +33114,7 @@ unplugin@1.0.1: webpack-sources "^3.2.3" webpack-virtual-modules "^0.5.0" -unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.3.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.10.1.tgz#8ceda065dc71bc67d923dea0920f05c67f2cd68c" - integrity sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg== - dependencies: - acorn "^8.11.3" - chokidar "^3.6.0" - webpack-sources "^3.2.3" - webpack-virtual-modules "^0.6.1" - -unplugin@^1.12.2, unplugin@^1.14.1: +unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.12.2, unplugin@^1.14.1, unplugin@^1.3.1, unplugin@^1.8.3: version "1.14.1" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.14.1.tgz#c76d6155a661e43e6a897bce6b767a1ecc344c1a" integrity sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w== @@ -32931,16 +33122,6 @@ unplugin@^1.12.2, unplugin@^1.14.1: acorn "^8.12.1" webpack-virtual-modules "^0.6.2" -unplugin@^1.8.3: - version "1.12.0" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.12.0.tgz#a11d3eb565602190748b1f95ecc8590b0f7dcbb4" - integrity sha512-KeczzHl2sATPQUx1gzo+EnUkmN4VmGBYRRVOZSGvGITE9rGHRDGqft6ONceP3vgXcyJ2XjX5axG5jMWUwNCYLw== - dependencies: - acorn "^8.12.1" - chokidar "^3.6.0" - webpack-sources "^3.2.3" - webpack-virtual-modules "^0.6.2" - unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -33137,6 +33318,11 @@ urlpattern-polyfill@8.0.2: resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz#99f096e35eff8bf4b5a2aa7d58a1523d6ebc7ce5" integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ== +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -33195,12 +33381,7 @@ uuid@8.3.2, uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - -uuid@^9.0.1: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -33879,11 +34060,6 @@ webpack-virtual-modules@^0.5.0: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== -webpack-virtual-modules@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz#ac6fdb9c5adb8caecd82ec241c9631b7a3681b6f" - integrity sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg== - webpack-virtual-modules@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" @@ -34221,7 +34397,16 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -34526,6 +34711,11 @@ zip-stream@^6.0.1: compress-commons "^6.0.2" readable-stream "^4.0.0" +zod-to-json-schema@^3.23.5: + version "3.23.5" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz#ec23def47dcafe3a4d640eba6a346b34f9a693a5" + integrity sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA== + zod@^3.22.3: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"