diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c17b81246e79..cd358158b2ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,6 @@ env: ${{ github.workspace }}/packages/*/build ${{ github.workspace }}/packages/ember/*.d.ts ${{ github.workspace }}/packages/gatsby/*.d.ts - ${{ github.workspace }}/packages/core/src/version.ts ${{ github.workspace }}/packages/utils/cjs ${{ github.workspace }}/packages/utils/esm @@ -801,7 +800,15 @@ jobs: needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] runs-on: ubuntu-20.04-large-js timeout-minutes: 15 + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + matrix-optional: ${{ steps.matrix-optional.outputs.matrix }} steps: + - name: Check out base commit (${{ github.event.pull_request.base.sha }}) + uses: actions/checkout@v4 + if: github.event_name == 'pull_request' + with: + ref: ${{ github.event.pull_request.base.sha }} - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 with: @@ -851,11 +858,21 @@ jobs: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + - name: Determine which E2E test applications should be run + id: matrix + run: yarn --silent ci:build-matrix --base=${{ (github.event_name == 'pull_request' && github.event.pull_request.base.sha) || '' }} >> $GITHUB_OUTPUT + working-directory: dev-packages/e2e-tests + + - name: Determine which optional E2E test applications should be run + id: matrix-optional + run: yarn --silent ci:build-matrix-optional --base=${{ (github.event_name == 'pull_request' && github.event.pull_request.base.sha) || '' }} >> $GITHUB_OUTPUT + working-directory: dev-packages/e2e-tests + job_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test # We need to add the `always()` check here because the previous step has this as well :( # See: https://github.com/actions/runner/issues/2205 - if: always() && needs.job_e2e_prepare.result == 'success' + if: always() && needs.job_e2e_prepare.result == 'success' && needs.job_e2e_prepare.outputs.matrix != '{"include":[]}' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 timeout-minutes: 15 @@ -870,103 +887,7 @@ jobs: E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: fail-fast: false - matrix: - is_dependabot: - - ${{ github.actor == 'dependabot[bot]' }} - test-application: - [ - 'angular-17', - 'angular-18', - 'astro-4', - 'aws-lambda-layer-cjs', - 'aws-serverless-esm', - 'node-express', - 'create-react-app', - 'create-next-app', - 'create-remix-app', - 'create-remix-app-legacy', - 'create-remix-app-v2', - 'create-remix-app-v2-legacy', - 'create-remix-app-express', - 'create-remix-app-express-legacy', - 'create-remix-app-express-vite-dev', - 'default-browser', - 'node-express-esm-loader', - 'node-express-esm-preload', - 'node-express-esm-without-loader', - 'node-express-cjs-preload', - 'node-otel-sdk-node', - 'node-otel-custom-sampler', - 'node-otel-without-tracing', - 'ember-classic', - 'ember-embroider', - 'nextjs-app-dir', - 'nextjs-13', - 'nextjs-14', - 'nextjs-15', - 'nextjs-turbo', - 'nextjs-t3', - 'react-17', - 'react-19', - 'react-create-hash-router', - 'react-router-6-use-routes', - 'react-router-5', - 'react-router-6', - 'solid', - 'solidstart', - 'solidstart-spa', - 'svelte-5', - 'sveltekit', - 'sveltekit-2', - 'sveltekit-2-svelte-5', - 'sveltekit-2-twp', - 'tanstack-router', - 'generic-ts3.8', - 'node-fastify', - 'node-fastify-5', - 'node-hapi', - 'node-nestjs-basic', - 'node-nestjs-distributed-tracing', - 'nestjs-basic', - 'nestjs-distributed-tracing', - 'nestjs-with-submodules', - 'nestjs-with-submodules-decorator', - 'nestjs-basic-with-graphql', - 'nestjs-graphql', - 'node-exports-test-app', - 'node-koa', - 'node-connect', - 'nuxt-3', - 'nuxt-4', - 'vue-3', - 'webpack-4', - 'webpack-5' - ] - build-command: - - false - label: - - false - # Add any variations of a test app here - # You should provide an alternate build-command as well as a matching label - include: - - test-application: 'create-react-app' - build-command: 'test:build-ts3.8' - label: 'create-react-app (TS 3.8)' - - test-application: 'react-router-6' - build-command: 'test:build-ts3.8' - label: 'react-router-6 (TS 3.8)' - - test-application: 'create-next-app' - build-command: 'test:build-13' - label: 'create-next-app (next@13)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-13' - label: 'nextjs-app-dir (next@13)' - exclude: - - is_dependabot: true - test-application: 'cloudflare-astro' - - is_dependabot: true - test-application: 'cloudflare-workers' - + matrix: ${{ fromJson(needs.job_e2e_prepare.outputs.matrix) }} steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -1067,6 +988,7 @@ jobs: # See: https://github.com/actions/runner/issues/2205 if: always() && needs.job_e2e_prepare.result == 'success' && + needs.job_e2e_prepare.outputs.matrix-optional != '{"include":[]}' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_e2e_prepare] @@ -1083,58 +1005,7 @@ jobs: E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: fail-fast: false - matrix: - test-application: - [ - 'cloudflare-astro', - 'cloudflare-workers', - 'react-send-to-sentry', - 'node-express-send-to-sentry', - 'debug-id-sourcemaps', - ] - build-command: - - false - assert-command: - - false - label: - - false - include: - - test-application: 'create-remix-app' - assert-command: 'test:assert-sourcemaps' - label: 'create-remix-app (sourcemaps)' - - test-application: 'create-remix-app-legacy' - assert-command: 'test:assert-sourcemaps' - label: 'create-remix-app-legacy (sourcemaps)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-canary' - label: 'nextjs-app-dir (canary)' - - test-application: 'nextjs-app-dir' - build-command: 'test:build-latest' - label: 'nextjs-app-dir (latest)' - - test-application: 'nextjs-13' - build-command: 'test:build-canary' - label: 'nextjs-13 (canary)' - - test-application: 'nextjs-13' - build-command: 'test:build-latest' - label: 'nextjs-13 (latest)' - - test-application: 'nextjs-14' - build-command: 'test:build-canary' - label: 'nextjs-14 (canary)' - - test-application: 'nextjs-14' - build-command: 'test:build-latest' - label: 'nextjs-14 (latest)' - - test-application: 'nextjs-15' - build-command: 'test:build-canary' - label: 'nextjs-15 (canary)' - - test-application: 'nextjs-15' - build-command: 'test:build-latest' - label: 'nextjs-15 (latest)' - - test-application: 'nextjs-turbo' - build-command: 'test:build-canary' - label: 'nextjs-turbo (canary)' - - test-application: 'nextjs-turbo' - build-command: 'test:build-latest' - label: 'nextjs-turbo (latest)' + matrix: ${{ fromJson(needs.job_e2e_prepare.outputs.matrix-optional) }} steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -1234,7 +1105,7 @@ jobs: (needs.job_get_metadata.outputs.is_release == 'true') ) needs: [job_get_metadata, job_build, job_e2e_prepare] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 15 env: E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} @@ -1254,19 +1125,24 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} + - uses: pnpm/action-setup@v4 with: version: 9.4.0 + - name: Set up Node uses: actions/setup-node@v4 with: node-version: 22 + - name: Restore caches uses: ./.github/actions/restore-cache with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Build Profiling Node run: yarn lerna run build:lib --scope @sentry/profiling-node + - name: Extract Profiling Node Prebuilt Binaries uses: actions/download-artifact@v4 with: @@ -1305,6 +1181,18 @@ jobs: env: E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }} + - name: Setup xvfb and update ubuntu dependencies + run: | + sudo apt-get install xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps + sudo apt-get install build-essential clang libdbus-1-dev libgtk2.0-dev \ + libnotify-dev libgconf2-dev \ + libasound2-dev libcap-dev libcups2-dev libxtst-dev \ + libxss1 libnss3-dev gcc-multilib g++-multilib + + - name: Install dependencies + working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + run: yarn install --ignore-engines --frozen-lockfile + - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 7 @@ -1313,7 +1201,7 @@ jobs: - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 - run: yarn test:assert + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert job_required_jobs_passed: name: All required jobs passed or were skipped diff --git a/.size-limit.js b/.size-limit.js index 4903d38fef62..3f1be5b9c140 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -139,7 +139,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '38 KB', + limit: '38.5 KB', }, // Svelte SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 23375eb92a2b..1eea88c2fc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.39.0 + +### Important Changes + +- **feat(nestjs): Instrument event handlers ([#14307](https://github.com/getsentry/sentry-javascript/pull/14307))** + +The `@sentry/nestjs` SDK will now capture performance data for [NestJS Events (`@nestjs/event-emitter`)](https://docs.nestjs.com/techniques/events) + +### Other Changes + +- feat(nestjs): Add alias `@SentryExceptionCaptured` for `@WithSentry` ([#14322](https://github.com/getsentry/sentry-javascript/pull/14322)) +- feat(nestjs): Duplicate `SentryService` behaviour into `@sentry/nestjs` SDK `init()` ([#14321](https://github.com/getsentry/sentry-javascript/pull/14321)) +- feat(nestjs): Handle GraphQL contexts in `SentryGlobalFilter` ([#14320](https://github.com/getsentry/sentry-javascript/pull/14320)) +- feat(node): Add alias `childProcessIntegration` for `processThreadBreadcrumbIntegration` and deprecate it ([#14334](https://github.com/getsentry/sentry-javascript/pull/14334)) +- feat(node): Ensure request bodies are reliably captured for http requests ([#13746](https://github.com/getsentry/sentry-javascript/pull/13746)) +- feat(replay): Upgrade rrweb packages to 2.29.0 ([#14160](https://github.com/getsentry/sentry-javascript/pull/14160)) +- fix(cdn): Ensure `_sentryModuleMetadata` is not mangled ([#14344](https://github.com/getsentry/sentry-javascript/pull/14344)) +- fix(core): Set `sentry.source` attribute to `custom` when calling `span.updateName` on `SentrySpan` ([#14251](https://github.com/getsentry/sentry-javascript/pull/14251)) +- fix(mongo): rewrite Buffer as ? during serialization ([#14071](https://github.com/getsentry/sentry-javascript/pull/14071)) +- fix(replay): Remove replay id from DSC on expired sessions ([#14342](https://github.com/getsentry/sentry-javascript/pull/14342)) +- ref(profiling) Fix electron crash ([#14216](https://github.com/getsentry/sentry-javascript/pull/14216)) +- ref(types): Deprecate `Request` type in favor of `RequestEventData` ([#14317](https://github.com/getsentry/sentry-javascript/pull/14317)) +- ref(utils): Stop setting `transaction` in `requestDataIntegration` ([#14306](https://github.com/getsentry/sentry-javascript/pull/14306)) +- ref(vue): Reduce bundle size for starting application render span ([#14275](https://github.com/getsentry/sentry-javascript/pull/14275)) + ## 8.38.0 - docs: Improve docstrings for node otel integrations ([#14217](https://github.com/getsentry/sentry-javascript/pull/14217)) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b2c913488d42..5dfd72c4dcdf 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.11.0", + "@sentry-internal/rrweb": "2.29.0", "@sentry/browser": "8.38.0", "axios": "1.7.7", "babel-loader": "^8.2.2", diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js index 207f9d1d58f6..3e9014dabf47 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/subject.js @@ -1,5 +1 @@ -window.addEventListener('error', function (event) { - Sentry.captureException(event); -}); - -window.thisDoesNotExist(); +Sentry.captureException(new ErrorEvent('something', { message: 'test error' })); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts index 9c09ba374e78..5e8cbc0dd9c1 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/test.ts @@ -4,15 +4,15 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; -sentryTest('should capture an ErrorEvent', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); +sentryTest('should capture an ErrorEvent', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ type: 'ErrorEvent', - value: 'Event `ErrorEvent` captured as exception with message `Script error.`', + value: 'Event `ErrorEvent` captured as exception with message `test error`', mechanism: { type: 'generic', handled: true, diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js index f88672f09214..a51740976b6a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/subject.js @@ -1,6 +1,6 @@ window.calls = {}; const xhr = new XMLHttpRequest(); -xhr.open('GET', 'test'); +xhr.open('GET', 'http://example.com'); xhr.onreadystatechange = function wat() { window.calls[xhr.readyState] = window.calls[xhr.readyState] ? window.calls[xhr.readyState] + 1 : 1; }; diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts index faec510f8f47..f9b1816c6f2d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/onreadystatechange/test.ts @@ -4,16 +4,28 @@ import { sentryTest } from '../../../../../utils/fixtures'; sentryTest( 'should not call XMLHttpRequest onreadystatechange more than once per state', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('http://example.com/', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); await page.goto(url); - const calls = await page.evaluate(() => { - // @ts-expect-error window.calls defined in subject.js - return window.calls; - }); + // Wait until XHR is done + await page.waitForFunction('window.calls["4"]'); - expect(calls).toEqual({ '4': 1 }); + const calls = await page.evaluate('window.calls'); + + expect(calls).toEqual({ + '2': 1, + '3': 1, + '4': 1, + }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index f1152bde21da..3b64a1230a5b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,5 +1,10 @@ import { expect } from '@playwright/test'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -7,18 +12,30 @@ import { waitForTransactionRequestOnUrl, } from '../../../../utils/helpers'; -sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestPath({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - - expect(transaction.transaction).toBe('parent_span'); - expect(transaction.spans).toBeDefined(); -}); +sentryTest( + 'sends a transaction in an envelope with manual origin and custom source', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + const attributes = transaction.contexts?.trace?.data; + expect(attributes).toEqual({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }); + + expect(transaction.transaction_info?.source).toBe('custom'); + + expect(transaction.transaction).toBe('parent_span'); + expect(transaction.spans).toBeDefined(); + }, +); sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestPath, page }) => { if (shouldSkipTracingTest()) { 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 b2cd4196643b..85353801980d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -1,10 +1,10 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/browser'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestPath, page }) => { +sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -12,7 +12,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const replayEvent0 = getReplayEvent(await reqPromise0); @@ -26,7 +26,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT timestamp: expect.any(Number), error_ids: [], trace_ids: [], - urls: [expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/)], + urls: [`${TEST_HOST}/index.html`], replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, @@ -49,7 +49,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, @@ -86,7 +86,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, 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 e9db4c92343c..82bbe104ab98 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -1,10 +1,10 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/browser'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays (@sentry-internal/replay export)', async ({ getLocalTestPath, page }) => { +sentryTest('should capture replays (@sentry-internal/replay export)', async ({ getLocalTestUrl, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -12,7 +12,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const replayEvent0 = getReplayEvent(await reqPromise0); @@ -26,7 +26,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g timestamp: expect.any(Number), error_ids: [], trace_ids: [], - urls: [expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/)], + urls: [`${TEST_HOST}/index.html`], replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, @@ -49,7 +49,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, @@ -86,7 +86,7 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g name: 'sentry.javascript.browser', }, request: { - url: expect.stringMatching(/\/dist\/([\w-]+)\/index\.html$/), + url: `${TEST_HOST}/index.html`, headers: { 'User-Agent': expect.stringContaining(''), }, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index 3ee84086cc37..4f1ba066f5e4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; import { expectedCLSPerformanceSpan, expectedClickBreadcrumb, @@ -30,7 +30,7 @@ well as the correct DOM snapshots and updates are recorded and sent. */ sentryTest( 'record page navigations and performance entries across multiple pages', - async ({ getLocalTestPath, page, browserName }) => { + async ({ getLocalTestUrl, page, browserName }) => { // We only test this against the NPM package and replay bundles // and only on chromium as most performance entries are only available in chromium if (shouldSkipReplayTest() || browserName !== 'chromium') { @@ -48,7 +48,7 @@ sentryTest( const reqPromise8 = waitForReplayRequest(page, 8); const reqPromise9 = waitForReplayRequest(page, 9); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); const [req0] = await Promise.all([reqPromise0, page.goto(url)]); const replayEvent0 = getReplayEvent(req0); @@ -72,7 +72,7 @@ sentryTest( const collectedPerformanceSpans = [...recording0.performanceSpans, ...recording1.performanceSpans]; const collectedBreadcrumbs = [...recording0.breadcrumbs, ...recording1.breadcrumbs]; - expect(collectedPerformanceSpans.length).toEqual(8); + expect(collectedPerformanceSpans.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpans).toEqual( expect.arrayContaining([ expectedNavigationPerformanceSpan, @@ -112,7 +112,7 @@ sentryTest( const collectedPerformanceSpansAfterReload = [...recording2.performanceSpans, ...recording3.performanceSpans]; const collectedBreadcrumbsAdterReload = [...recording2.breadcrumbs, ...recording3.breadcrumbs]; - expect(collectedPerformanceSpansAfterReload.length).toEqual(8); + expect(collectedPerformanceSpansAfterReload.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpansAfterReload).toEqual( expect.arrayContaining([ expectedReloadPerformanceSpan, @@ -146,7 +146,8 @@ sentryTest( url: expect.stringContaining('page-0.html'), headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -168,7 +169,8 @@ sentryTest( url: expect.stringContaining('page-0.html'), headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -210,13 +212,12 @@ sentryTest( getExpectedReplayEvent({ segment_id: 6, urls: ['/spa'], - request: { - // @ts-expect-error this is fine - url: expect.stringContaining('page-0.html'), + url: `${TEST_HOST}/spa`, headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -235,11 +236,11 @@ sentryTest( urls: [], request: { - // @ts-expect-error this is fine - url: expect.stringContaining('page-0.html'), + url: `${TEST_HOST}/spa`, headers: { // @ts-expect-error this is fine - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/index.html`, }, }, }), @@ -279,6 +280,14 @@ sentryTest( expect(replayEvent8).toEqual( getExpectedReplayEvent({ segment_id: 8, + request: { + url: `${TEST_HOST}/index.html`, + headers: { + // @ts-expect-error this is fine + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/spa`, + }, + }, }), ); expect(normalize(recording8.fullSnapshots)).toMatchSnapshot('seg-8-snap-full'); @@ -293,6 +302,14 @@ sentryTest( getExpectedReplayEvent({ segment_id: 9, urls: [], + request: { + url: `${TEST_HOST}/index.html`, + headers: { + // @ts-expect-error this is fine + 'User-Agent': expect.any(String), + Referer: `${TEST_HOST}/spa`, + }, + }, }), ); expect(recording9.fullSnapshots.length).toEqual(0); @@ -304,7 +321,7 @@ sentryTest( ]; const collectedBreadcrumbsAfterIndexNavigation = [...recording8.breadcrumbs, ...recording9.breadcrumbs]; - expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(8); + expect(collectedPerformanceSpansAfterIndexNavigation.length).toBeGreaterThanOrEqual(6); expect(collectedPerformanceSpansAfterIndexNavigation).toEqual( expect.arrayContaining([ expectedNavigationPerformanceSpan, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-0-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-2-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full index b0aeb348b388..1c3d1f22aeba 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full @@ -116,7 +116,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/index.html" + "href": "http://sentry-test.io/index.html" }, "childNodes": [ { @@ -153,4 +153,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium index b0aeb348b388..2e7bfb9bd2d2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-4-snap-full-chromium @@ -116,7 +116,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/index.html" + "href": "http://sentry-test.io/index.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full index fdccbb1b9387..0d77b67cb862 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { @@ -110,4 +110,4 @@ }, "timestamp": [timestamp] } -] \ No newline at end of file +] diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium index fdccbb1b9387..40b05fcbe191 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-8-snap-full-chromium @@ -73,7 +73,7 @@ "type": 2, "tagName": "a", "attributes": { - "href": "/page-0.html" + "href": "http://sentry-test.io/page-0.html" }, "childNodes": [ { diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts index 4650c6b94f3c..884dea9c618c 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers that contain protocol names correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -52,14 +52,14 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` - Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(7).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts index c0c813058128..a78c15963814 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers that are protocol names correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -56,8 +56,8 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); @@ -65,7 +65,7 @@ sentryTest( expect(eventData.exception?.values).toBeDefined(); expect(eventData.exception?.values?.[0].stacktrace).toBeDefined(); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( - Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(7).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts b/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts index 6ba6e15e6bd2..7585a21521e0 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts +++ b/dev-packages/browser-integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts @@ -6,8 +6,8 @@ import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; sentryTest( 'should parse function identifiers correctly @firefox', - async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const frames = eventData.exception?.values?.[0].stacktrace?.frames; @@ -56,14 +56,14 @@ sentryTest( sentryTest( 'should not add any part of the function identifier to beginning of filename', - async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` - Array(8).fill({ filename: expect.stringMatching(/^file:\/?/) }), + Array(8).fill({ filename: expect.stringMatching(/^http:\/?/) }), ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js new file mode 100644 index 000000000000..1f0b64911a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js new file mode 100644 index 000000000000..6e93018cc063 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/subject.js @@ -0,0 +1,3 @@ +const activeSpan = Sentry.getActiveSpan(); +const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan); +rootSpan?.updateName('new name'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts new file mode 100644 index 000000000000..ff47f1a2d238 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('sets the source to custom when updating the transaction name', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const traceContextData = eventData.contexts?.trace?.data; + + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(eventData.transaction).toBe('new name'); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index 6a186b63b02a..70f719d8dbbf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,10 +1,16 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should create a pageload transaction', async ({ getLocalTestPath, page }) => { +sentryTest('creates a pageload transaction with url as source', async ({ getLocalTestPath, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -16,8 +22,17 @@ sentryTest('should create a pageload transaction', async ({ getLocalTestPath, pa const { start_timestamp: startTimestamp } = eventData; + const traceContextData = eventData.contexts?.trace?.data; + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + expect(eventData.contexts?.trace?.op).toBe('pageload'); expect(eventData.spans?.length).toBeGreaterThan(0); expect(eventData.transaction_info?.source).toEqual('url'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts index e8c21a66647f..41003a133b34 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -170,7 +170,7 @@ async function makeRequestAndGetBaggageItems(page: Page): Promise { return baggage?.split(',').sort() ?? []; } -async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise { +async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise | undefined> { const errorEventPromise = getMultipleSentryEnvelopeRequests( page, 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index fc74fa685bc7..152cadf80418 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -5,35 +5,47 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestPath, page, browser }) => { +sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('**/path/to/image.svg', (route: Route) => route.fulfill({ path: `${__dirname}/assets/image.svg` })); - await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); - await page.route('**/path/to/style.css', (route: Route) => route.fulfill({ path: `${__dirname}/assets/style.css` })); + await page.route('https://example.com/path/to/image.svg', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/image.svg` }), + ); + await page.route('https://example.com/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + await page.route('https://example.com/path/to/style.css', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/style.css` }), + ); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); - // Webkit 16.0 (which is linked to Playwright 1.27.1) consistently creates 2 consecutive spans for `css`, - // so we need to check for 3 or 4 spans. - if (browser.browserType().name() === 'webkit') { - expect(resourceSpans?.length).toBeGreaterThanOrEqual(3); - } else { - expect(resourceSpans?.length).toBe(3); + const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); + const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link'); + const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img'); + + expect(imgSpans).toHaveLength(1); + expect(linkSpans).toHaveLength(1); + + const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); + + const expectedScripts = ['/init.bundle.js', '/subject.bundle.js', 'https://example.com/path/to/script.js']; + if (hasCdnBundle) { + expectedScripts.unshift('/cdn.bundle.js'); } - ['resource.img', 'resource.script', 'resource.link'].forEach(op => - expect(resourceSpans).toContainEqual( - expect.objectContaining({ - op: op, - parent_span_id: eventData.contexts?.trace?.span_id, - }), - ), - ); + expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); + + const spanId = eventData.contexts?.trace?.span_id; + + expect(spanId).toBeDefined(); + expect(imgSpans?.[0].parent_span_id).toBe(spanId); + expect(linkSpans?.[0].parent_span_id).toBe(spanId); + expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); }); diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts index 51760544d868..40e303e9b0cb 100644 --- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -33,7 +33,7 @@ const DEFAULT_REPLAY_EVENT = { request: { url: expect.stringContaining('/index.html'), headers: { - 'User-Agent': expect.stringContaining(''), + 'User-Agent': expect.any(String), }, }, platform: 'javascript', diff --git a/dev-packages/e2e-tests/lib/getTestMatrix.ts b/dev-packages/e2e-tests/lib/getTestMatrix.ts new file mode 100644 index 000000000000..342f20cf9820 --- /dev/null +++ b/dev-packages/e2e-tests/lib/getTestMatrix.ts @@ -0,0 +1,155 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { dirname } from 'path'; +import { parseArgs } from 'util'; +import { sync as globSync } from 'glob'; + +interface MatrixInclude { + /** The test application (directory) name. */ + 'test-application': string; + /** Optional override for the build command to run. */ + 'build-command'?: string; + /** Optional override for the assert command to run. */ + 'assert-command'?: string; + /** Optional label for the test run. If not set, defaults to value of `test-application`. */ + label?: string; +} + +interface PackageJsonSentryTestConfig { + /** If this is true, the test app is optional. */ + optional?: boolean; + /** Variant configs that should be run in non-optional test runs. */ + variants?: Partial[]; + /** Variant configs that should be run in optional test runs. */ + optionalVariants?: Partial[]; + /** Skip this test app for matrix generation. */ + skip?: boolean; +} + +/** + * This methods generates a matrix for the GitHub Actions workflow to run the E2E tests. + * It checks which test applications are affected by the current changes in the PR and then generates a matrix + * including all test apps that have at least one dependency that was changed in the PR. + * If no `--base=xxx` is provided, it will output all test applications. + * + * If `--optional=true` is set, it will generate a matrix of optional test applications only. + * Otherwise, these will be skipped. + */ +function run(): void { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + base: { type: 'string' }, + head: { type: 'string' }, + optional: { type: 'string', default: 'false' }, + }, + }); + + const { base, head, optional } = values; + + const testApplications = globSync('*/package.json', { + cwd: `${__dirname}/../test-applications`, + }).map(filePath => dirname(filePath)); + + // If `--base=xxx` is defined, we only want to get test applications changed since that base + // Else, we take all test applications (e.g. on push) + const includedTestApplications = base + ? getAffectedTestApplications(testApplications, { base, head }) + : testApplications; + + const optionalMode = optional === 'true'; + const includes: MatrixInclude[] = []; + + includedTestApplications.forEach(testApp => { + addIncludesForTestApp(testApp, includes, { optionalMode }); + }); + + // We print this to the output, so the GHA can use it for the matrix + // eslint-disable-next-line no-console + console.log(`matrix=${JSON.stringify({ include: includes })}`); +} + +function addIncludesForTestApp( + testApp: string, + includes: MatrixInclude[], + { optionalMode }: { optionalMode: boolean }, +): void { + const packageJson = getPackageJson(testApp); + + const shouldSkip = packageJson.sentryTest?.skip || false; + const isOptional = packageJson.sentryTest?.optional || false; + const variants = (optionalMode ? packageJson.sentryTest?.optionalVariants : packageJson.sentryTest?.variants) || []; + + if (shouldSkip) { + return; + } + + // Add the basic test-application itself, if it is in the current mode + if (optionalMode === isOptional) { + includes.push({ + 'test-application': testApp, + }); + } + + variants.forEach(variant => { + includes.push({ + 'test-application': testApp, + ...variant, + }); + }); +} + +function getSentryDependencies(appName: string): string[] { + const packageJson = getPackageJson(appName) || {}; + + const dependencies = { + ...packageJson.devDependencies, + ...packageJson.dependencies, + }; + + return Object.keys(dependencies).filter(key => key.startsWith('@sentry')); +} + +function getPackageJson(appName: string): { + dependencies?: { [key: string]: string }; + devDependencies?: { [key: string]: string }; + sentryTest?: PackageJsonSentryTestConfig; +} { + const fullPath = path.resolve(__dirname, '..', 'test-applications', appName, 'package.json'); + + if (!fs.existsSync(fullPath)) { + throw new Error(`Could not find package.json for ${appName}`); + } + + return JSON.parse(fs.readFileSync(fullPath, 'utf8')); +} + +run(); + +function getAffectedTestApplications( + testApplications: string[], + { base = 'develop', head }: { base?: string; head?: string }, +): string[] { + const additionalArgs = [`--base=${base}`]; + + if (head) { + additionalArgs.push(`--head=${head}`); + } + + const affectedProjects = execSync(`yarn --silent nx show projects --affected ${additionalArgs.join(' ')}`) + .toString() + .split('\n') + .map(line => line.trim()) + .filter(Boolean); + + // If something in e2e tests themselves are changed, just run everything + if (affectedProjects.includes('@sentry-internal/e2e-tests')) { + return testApplications; + } + + return testApplications.filter(testApp => { + const sentryDependencies = getSentryDependencies(testApp); + return sentryDependencies.some(dep => affectedProjects.includes(dep)); + }); +} diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index fdb5958462ee..ccf59ef38f9d 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -14,11 +14,13 @@ "test:prepare": "ts-node prepare.ts", "test:validate": "run-s test:validate-configuration test:validate-test-app-setups", "clean": "rimraf tmp node_modules pnpm-lock.yaml && yarn clean:test-applications", + "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", + "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml} .last-run.json && pnpm store prune" }, "devDependencies": { "@types/glob": "8.0.0", - "@types/node": "^14.18.0", + "@types/node": "^18.0.0", "dotenv": "16.0.3", "esbuild": "0.20.0", "glob": "8.0.3", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json index fd636122d590..d2fc66736b4f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-astro/package.json @@ -26,5 +26,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index bb01c0b8a8ad..f7fd08df85f9 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -24,5 +24,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } 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 9c240942b3b7..316fb561cdf3 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 @@ -27,5 +27,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-13", + "label": "create-next-app (next@13)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/package.json b/dev-packages/e2e-tests/test-applications/create-react-app/package.json index ce3471d2a7d1..916a17260a2a 100644 --- a/dev-packages/e2e-tests/test-applications/create-react-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-react-app/package.json @@ -43,5 +43,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-ts3.8", + "label": "create-react-app (TS 3.8)" + } + ] } } 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 4b7c2c162b86..6b50ddc96b4a 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 @@ -36,5 +36,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "assert-command": "test:assert-sourcemaps", + "label": "create-remix-app-legacy (sourcemaps)" + } + ] } } 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 db5c5b474ef0..4850fedf1e5d 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 @@ -36,5 +36,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "assert-command": "test:assert-sourcemaps", + "label": "create-remix-app (sourcemaps)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index 9295b7997ee6..6451610ffe86 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -19,5 +19,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json index d13bf86e7c64..80875e5a2d0f 100644 --- a/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json +++ b/dev-packages/e2e-tests/test-applications/generic-ts3.8/package.json @@ -10,7 +10,8 @@ "test:assert": "pnpm -v" }, "devDependencies": { - "typescript": "3.8.3" + "typescript": "3.8.3", + "@types/node": "^14.18.0" }, "dependencies": { "@sentry/browser": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-8/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/.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-8/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-8/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/.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-8/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-8/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json new file mode 100644 index 000000000000..9cf681c33ef5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json @@ -0,0 +1,49 @@ +{ + "name": "nestjs-8", + "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": "^8.0.0", + "@nestjs/core": "^8.0.0", + "@nestjs/microservices": "^8.0.0", + "@nestjs/schedule": "^4.1.0", + "@nestjs/platform-express": "^8.0.0", + "@sentry/nestjs": "latest || *", + "@sentry/types": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.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/express": "^4.17.17", + "@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": "^4.9.5" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-8/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.controller.ts new file mode 100644 index 000000000000..77e25a72dad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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') + async killTestCron() { + this.appService.killTestCron(); + } + + @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-8/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.module.ts new file mode 100644 index 000000000000..3de3c82dc925 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts new file mode 100644 index 000000000000..72aef6947a6c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/app.service.ts @@ -0,0 +1,102 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; +import { Cron, SchedulerRegistry } from '@nestjs/schedule'; +import * as Sentry from '@sentry/nestjs'; +import { SentryCron, SentryTraced } from '@sentry/nestjs'; +import type { MonitorConfig } from '@sentry/types'; + +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!'); + } + + async killTestCron() { + this.schedulerRegistry.deleteCronJob('test-cron-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-8/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-2.interceptor.ts new file mode 100644 index 000000000000..2cf9dfb9e043 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example-global-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global-filter.exception.ts new file mode 100644 index 000000000000..41981ba748fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts new file mode 100644 index 000000000000..988696d0e13d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-global.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { Request, Response } from 'express'; +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).json({ + 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-8/src/example-local-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local-filter.exception.ts new file mode 100644 index 000000000000..8f76520a3b94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example-local.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts new file mode 100644 index 000000000000..505217f5dcbd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example-local.filter.ts @@ -0,0 +1,19 @@ +import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; +import { Request, Response } from 'express'; +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).json({ + 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-8/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.guard.ts new file mode 100644 index 000000000000..e12bbdc4e994 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/example.middleware.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts new file mode 100644 index 000000000000..31d15c9372ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/example.middleware.ts @@ -0,0 +1,12 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; +import { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class ExampleMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + // 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-8/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/instrument.ts new file mode 100644 index 000000000000..4f16ebb36d11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts new file mode 100644 index 000000000000..71ce685f4d61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/src/main.ts @@ -0,0 +1,15 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +const PORT = 3030; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(PORT); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs new file mode 100644 index 000000000000..e771eb5dbc4b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-8', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts new file mode 100644 index 000000000000..dee95c1f1b01 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/cron-decorator.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; + +test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-8', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs-8', envelope => { + return envelope[0].type === 'check_in' && 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.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + + 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.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron`); +}); 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 new file mode 100644 index 000000000000..72570f43efc4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/errors.test.ts @@ -0,0 +1,166 @@ +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-8', 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({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); + +test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-8', 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-8', 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-8', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + const transactionEventPromise500 = waitForTransaction('nestjs-8', 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-8', 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-8', 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-8', 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-8', 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-8', 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-8', 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-8/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/span-decorator.test.ts new file mode 100644 index 000000000000..2aa097d5262c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'wait and return a string', + }, + description: 'wait', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'return a string', + }, + description: 'getString', + parent_span_id: expect.any(String), + 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-8/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts new file mode 100644 index 000000000000..dc72d6c639e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts @@ -0,0 +1,729 @@ +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-8', 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.any(String), + status: 'ok', + trace_id: expect.any(String), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + data: { + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + 'http.route': '/test-transaction', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + }, + op: 'request_handler.express', + description: '/test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.express', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + 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.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'auto.http.otel.nestjs', + op: 'handler.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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleMiddleware', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-middleware-span', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleGuard', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-guard-span', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor1', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor2', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-1', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-2', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span-after-route', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'AsyncInterceptor', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-async-interceptor-span', + parent_span_id: expect.any(String), + 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-8', 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.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'Interceptors - After Route', + parent_span_id: expect.any(String), + 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.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-async-interceptor-span-after-route', + parent_span_id: expect.any(String), + 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-8/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tsconfig.json @@ -0,0 +1,22 @@ +{ + "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, + "moduleResolution": "Node16" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json index b4d0ead875f9..efc52a8a4db9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json @@ -18,6 +18,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/event-emitter": "^2.0.0", "@sentry/nestjs": "latest || *", "@sentry/types": "latest || *", "reflect-metadata": "^0.2.0", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts new file mode 100644 index 000000000000..cb5ddebcc3ae --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get } from '@nestjs/common'; +import { EventsService } from './events.service'; + +@Controller('events') +export class EventsController { + constructor(private readonly eventsService: EventsService) {} + + @Get('emit') + async emitEvents() { + await this.eventsService.emitEvents(); + + return { message: 'Events emitted' }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts new file mode 100644 index 000000000000..b92995e323eb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { EventsController } from './events.controller'; +import { EventsService } from './events.service'; +import { TestEventListener } from './listeners/test-event.listener'; + +@Module({ + imports: [SentryModule.forRoot(), EventEmitterModule.forRoot()], + controllers: [EventsController], + providers: [ + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + EventsService, + TestEventListener, + ], +}) +export class EventsModule {} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts new file mode 100644 index 000000000000..4a9f36ddaf5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +@Injectable() +export class EventsService { + constructor(private readonly eventEmitter: EventEmitter2) {} + + async emitEvents() { + await this.eventEmitter.emit('myEvent.pass', { data: 'test' }); + await this.eventEmitter.emit('myEvent.throw'); + + return { message: 'Events emitted' }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts new file mode 100644 index 000000000000..c1a3237f1f0c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class TestEventListener { + @OnEvent('myEvent.pass') + async handlePassEvent(payload: any): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + @OnEvent('myEvent.throw') + async handleThrowEvent(): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + throw new Error('Test error from event handler'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts index 5aad5748b244..a18877460852 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/main.ts @@ -3,11 +3,13 @@ import './instrument'; // Import other modules import { NestFactory } from '@nestjs/core'; +import { EventsModule } from './events.module'; import { TraceInitiatorModule } from './trace-initiator.module'; import { TraceReceiverModule } from './trace-receiver.module'; const TRACE_INITIATOR_PORT = 3030; const TRACE_RECEIVER_PORT = 3040; +const EVENTS_PORT = 3050; async function bootstrap() { const trace_initiator_app = await NestFactory.create(TraceInitiatorModule); @@ -15,6 +17,9 @@ async function bootstrap() { const trace_receiver_app = await NestFactory.create(TraceReceiverModule); await trace_receiver_app.listen(TRACE_RECEIVER_PORT); + + const events_app = await NestFactory.create(EventsModule); + await events_app.listen(EVENTS_PORT); } bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts new file mode 100644 index 000000000000..b09eabb38980 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Event emitter', async () => { + const eventErrorPromise = waitForError('nestjs-distributed-tracing', errorEvent => { + return errorEvent.exception.values[0].value === 'Test error from event handler'; + }); + const successEventTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { + return transactionEvent.transaction === 'event myEvent.pass'; + }); + + const eventsUrl = `http://localhost:3050/events/emit`; + await fetch(eventsUrl); + + const eventError = await eventErrorPromise; + const successEventTransaction = await successEventTransactionPromise; + + expect(eventError.exception).toEqual({ + values: [ + { + type: 'Error', + value: 'Test error from event handler', + stacktrace: expect.any(Object), + mechanism: expect.any(Object), + }, + ], + }); + + expect(successEventTransaction.contexts.trace).toEqual({ + parent_span_id: expect.any(String), + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.source': 'custom', + 'sentry.sample_rate': 1, + 'sentry.op': 'event.nestjs', + 'sentry.origin': 'auto.event.nestjs', + }, + origin: 'auto.event.nestjs', + op: 'event.nestjs', + status: 'ok', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts index cee50d0d2c7c..a2afcff4dc1b 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/src/example-global.filter.ts @@ -1,10 +1,10 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { WithSentry } from '@sentry/nestjs'; +import { SentryExceptionCaptured } from '@sentry/nestjs'; import { Request, Response } from 'express'; @Catch() export class ExampleWrappedGlobalFilter implements ExceptionFilter { - @WithSentry() + @SentryExceptionCaptured() catch(exception: BadRequestException, host: ArgumentsHost): void { const ctx = host.switchToHttp(); const response = ctx.getResponse(); 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 3e7a0ac88266..c56d7c6ed204 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -41,5 +41,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-13 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-13 (latest)" + } + ] } } 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 bbda1b0144cc..c8fcba03410d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -41,5 +41,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-14 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-14 (latest)" + } + ] } } 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 04033e0362b2..1c5754bd66da 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -42,5 +42,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-15 (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-15 (latest)" + } + ] } } 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 8ccad25e6ab4..b0bc898d9bd1 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 @@ -44,5 +44,23 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-13", + "label": "nextjs-app-dir (next@13)" + } + ], + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-app-dir (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-app-dir (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 900e0b5b2efc..9cf05720fc28 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -45,5 +45,17 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-turbo (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-turbo (latest)" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 546639e8a766..83f9c1639cdc 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [ 'NodeClient', // Bun doesn't emit the required diagnostics_channel events 'processThreadBreadcrumbIntegration', + 'childProcessIntegration', ], }, { diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json index 96f61837c597..5a1b9b7d8300 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/package.json @@ -24,5 +24,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/e2e-tests/test-applications/node-koa/index.js b/dev-packages/e2e-tests/test-applications/node-koa/index.js index ddc17f62e6f7..9e800a4fcc99 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/index.js +++ b/dev-packages/e2e-tests/test-applications/node-koa/index.js @@ -14,10 +14,12 @@ const port1 = 3030; const port2 = 3040; const Koa = require('koa'); +const { bodyParser } = require('@koa/bodyparser'); const Router = require('@koa/router'); const http = require('http'); const app1 = new Koa(); +app1.use(bodyParser()); Sentry.setupKoaErrorHandler(app1); @@ -109,6 +111,10 @@ router1.get('/test-assert/:condition', async ctx => { ctx.assert(condition, 400, 'ctx.assert failed'); }); +router1.post('/test-post', async ctx => { + ctx.body = { status: 'ok', body: ctx.request.body }; +}); + app1.use(router1.routes()).use(router1.allowedMethods()); app1.listen(port1); diff --git a/dev-packages/e2e-tests/test-applications/node-koa/package.json b/dev-packages/e2e-tests/test-applications/node-koa/package.json index 79a4e540c089..dd8a17d0f4b5 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/package.json +++ b/dev-packages/e2e-tests/test-applications/node-koa/package.json @@ -10,6 +10,7 @@ "test:assert": "pnpm test" }, "dependencies": { + "@koa/bodyparser": "^5.1.1", "@koa/router": "^12.0.1", "@sentry/node": "latest || *", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts index 4c52c932e7b4..1197575d1a96 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts @@ -46,76 +46,129 @@ test('Sends an API route transaction', async ({ baseURL }) => { origin: 'auto.http.otel.http', }); - expect(transactionEvent).toEqual( - expect.objectContaining({ - spans: [ - { - data: { - 'koa.name': '', - 'koa.type': 'middleware', - 'sentry.origin': 'auto.http.otel.koa', - 'sentry.op': 'middleware.koa', - }, - op: 'middleware.koa', - origin: 'auto.http.otel.koa', - description: '< unknown >', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - }, - { - data: { - 'http.route': '/test-transaction', - 'koa.name': '/test-transaction', - 'koa.type': 'router', - 'sentry.origin': 'auto.http.otel.koa', - 'sentry.op': 'router.koa', - }, - op: 'router.koa', - description: '/test-transaction', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'auto.http.otel.koa', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'test-span', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'manual', - }, - { - data: { - 'sentry.origin': 'manual', - }, - description: 'child-span', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.any(String), - origin: 'manual', - }, - ], - transaction: 'GET /test-transaction', - type: 'transaction', - transaction_info: { - source: 'route', + expect(transactionEvent).toMatchObject({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }); + + expect(transactionEvent.spans).toEqual([ + { + data: { + 'koa.name': 'bodyParser', + 'koa.type': 'middleware', + 'sentry.op': 'middleware.koa', + 'sentry.origin': 'auto.http.otel.koa', + }, + description: 'bodyParser', + op: 'middleware.koa', + origin: 'auto.http.otel.koa', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { + 'koa.name': '', + 'koa.type': 'middleware', + 'sentry.origin': 'auto.http.otel.koa', + 'sentry.op': 'middleware.koa', + }, + op: 'middleware.koa', + origin: 'auto.http.otel.koa', + description: '< unknown >', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { + 'http.route': '/test-transaction', + 'koa.name': '/test-transaction', + 'koa.type': 'router', + 'sentry.origin': 'auto.http.otel.koa', + 'sentry.op': 'router.koa', }, + op: 'router.koa', + description: '/test-transaction', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.koa', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + ]); +}); + +test('Captures request metadata', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-koa', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'POST /test-post' + ); + }); + + const res = await fetch(`${baseURL}/test-post`, { + method: 'POST', + body: JSON.stringify({ foo: 'bar', other: 1 }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const resBody = await res.json(); + + expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.request).toEqual({ + cookies: {}, + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: expect.objectContaining({ + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', }), - ); + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }); + + expect(transactionEvent.user).toEqual(undefined); }); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js b/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js new file mode 100644 index 000000000000..4519220008d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/__tests__/electron.spec.js @@ -0,0 +1,24 @@ +const process = require('process'); +const { test, expect, _electron: electron } = require('@playwright/test'); + +test('an h1 contains hello world"', async () => { + const electronApp = await electron.launch({ + args: ['./index.electron.js'], + process: { + env: { + ...process.env, + }, + }, + }); + + // Wait for the first BrowserWindow to open + const window = await electronApp.firstWindow(); + + // Check for the presence of an h1 element with the text "hello" + const headerElement = await window.$('h1'); + const headerText = await headerElement.textContent(); + expect(headerText).toBe('Hello From Profiled Electron App'); + + // Close the app + await electronApp.close(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js b/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js new file mode 100644 index 000000000000..d08ac4ecc142 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.electron.js @@ -0,0 +1,54 @@ +// Modules to control application life and create native browser window +const { app, BrowserWindow } = require('electron'); +const Sentry = require('@sentry/electron/main'); +const path = require('node:path'); + +Sentry.init({ + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + debug: true, + tracesSampleRate: 1.0, +}); + +// Hog the cpu for a second +function block() { + const start = Date.now(); + while (start + 1000 > Date.now()) {} +} + +const createWindow = () => { + block(); + // Create the browser window. + const mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }); + + // and load the index.html of the app. + mainWindow.loadFile('index.html'); + block(); +}; + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + Sentry.profiler.startProfiler(); + createWindow(); + Sentry.profiler.stopProfiler(); + + app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.html b/dev-packages/e2e-tests/test-applications/node-profiling/index.html new file mode 100644 index 000000000000..97022c9b265e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.html @@ -0,0 +1,6 @@ + + + +

Hello From Profiled Electron App

+ + diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json index a4c4bf1284fe..8aede827a1f3 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -7,15 +7,23 @@ "build": "node build.mjs && node build.shimmed.mjs", "test": "node dist/index.js && node --experimental-require-module dist/index.js && node dist/index.shimmed.mjs", "clean": "npx rimraf node_modules dist", - "test:build": "npm run typecheck && npm run build", - "test:assert": "npm run test" + "test:electron": "$(pnpm bin)/electron-rebuild && playwright test", + "test:build": "pnpm run typecheck && pnpm run build", + "test:assert": "pnpm run test && pnpm run test:electron" }, "dependencies": { + "@electron/rebuild": "^3.7.0", + "@playwright/test": "^1.48.2", + "@sentry/electron": "latest || *", "@sentry/node": "latest || *", - "@sentry/profiling-node": "latest || *" + "@sentry/profiling-node": "latest || *", + "electron": "^33.2.0" }, "devDependencies": {}, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "skip": true } } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.gitignore new file mode 100644 index 000000000000..4a7f73a2ed0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.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-min/.npmrc b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/.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-min/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue new file mode 100644 index 000000000000..23283a522546 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/app.vue @@ -0,0 +1,17 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue new file mode 100644 index 000000000000..92ea714ae489 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/components/ErrorButton.vue @@ -0,0 +1,22 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash b/dev-packages/e2e-tests/test-applications/nuxt-3-min/copyIITM.bash new file mode 100644 index 000000000000..0e04d001c968 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts new file mode 100644 index 000000000000..87e046ed39e9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts @@ -0,0 +1,20 @@ +// 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\/.*/], + }, + }, +}); 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 new file mode 100644 index 000000000000..18f798f89246 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -0,0 +1,30 @@ +{ + "name": "nuxt-3-min", + "description": "E2E test app for the minimum nuxt 3 version our nuxt SDK supports.", + "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 && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/nuxt": "latest || *", + "nuxt": "3.13.2" + }, + "devDependencies": { + "@nuxt/test-utils": "^3.14.1", + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "overrides": { + "nitropack": "2.9.7", + "@vercel/nft": "^0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue new file mode 100644 index 000000000000..d244ef773140 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/client-error.vue @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue new file mode 100644 index 000000000000..8cb2a9997e58 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/fetch-server-error.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue new file mode 100644 index 000000000000..74513c5697f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue new file mode 100644 index 000000000000..e83392b37b5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/test-param/[param].vue @@ -0,0 +1,23 @@ + + + + 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 new file mode 100644 index 000000000000..aa1ff8e9743c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nuxt-3-min/public/favicon.ico new file mode 100644 index 000000000000..18993ad91cfd Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nuxt-3-min/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts new file mode 100644 index 000000000000..7547bafa6618 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts @@ -0,0 +1,10 @@ +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, + trackComponents: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts new file mode 100644 index 000000000000..729b2296c683 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/server/api/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/param-error/[param].ts new file mode 100644 index 000000000000..389d8ac4d633 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/server-error.ts new file mode 100644 index 000000000000..ec961a010510 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/server/api/test-param/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/api/test-param/[param].ts new file mode 100644 index 000000000000..1867874cd494 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min/server/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json new file mode 100644 index 000000000000..b9ed69c19eaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs new file mode 100644 index 000000000000..f7e78ea06418 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nuxt-3-min', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts new file mode 100644 index 000000000000..66f86755218e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3-min 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-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); + + test('shows parametrized route on button error', async ({ page }) => { + const errorPromise = waitForError('nuxt-3-min', 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-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Nuxt-3-min E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + + const error1 = await error1Promise; + + const error2Promise = waitForError('nuxt-3-min', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Another Error thrown from Nuxt-3-min 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-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + + expect(error2).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Another Error thrown from Nuxt-3-min E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts new file mode 100644 index 000000000000..8f20aa938893 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min', 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-min', 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-min/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts new file mode 100644 index 000000000000..9d0b3c694a1c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min', 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-min', 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.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.ui.vue', + }; + + expect(errorButtonSpan).toMatchObject(expected); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts new file mode 100644 index 000000000000..6f2085e38cd7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min', 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-min', 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-min', 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-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts new file mode 100644 index 000000000000..b110f27843e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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-min', txnEvent => { + return txnEvent.transaction === '/test-param/:param()'; + }); + + const serverTxnEventPromise = waitForTransaction('nuxt-3-min', 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-min/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tsconfig.json new file mode 100644 index 000000000000..a746f2a70c28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/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 6c8eb1fcdd95..0b9654108d48 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.13.1" + "nuxt": "^3.13.1" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/package.json b/dev-packages/e2e-tests/test-applications/react-router-6/package.json index 5171a89eadb3..b3ef37f6bc4a 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 @@ -51,5 +51,13 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-ts3.8", + "label": "react-router-6 (TS 3.8)" + } + ] } } 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 95b9c3bd78b4..a6ba509bc09a 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 @@ -47,5 +47,8 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "optional": true } } diff --git a/dev-packages/node-integration-tests/README.md b/dev-packages/node-integration-tests/README.md index ab1ce5e834de..3f7abd7b5727 100644 --- a/dev-packages/node-integration-tests/README.md +++ b/dev-packages/node-integration-tests/README.md @@ -14,20 +14,11 @@ suites/ |---- scenario_2.ts [optional extra test scenario] |---- server_with_mongo.ts [optional custom server] |---- server_with_postgres.ts [optional custom server] -utils/ -|---- defaults/ - |---- server.ts [default Express server configuration] ``` The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple folders containing test scenarios and assertions. -Tests run on Express servers (a server instance per test). By default, a simple server template inside -`utils/defaults/server.ts` is used. Every server instance runs on a different port. - -A custom server configuration can be used, supplying a script that exports a valid express server instance as default. -`runServer` utility function accepts an optional `serverPath` argument for this purpose. - `scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but `runServer` also accepts an optional `scenarioPath` argument for non-standard usage. diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0690206e6b51..5b62e3a8e996 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -27,9 +27,9 @@ "dependencies": { "@aws-sdk/client-s3": "^3.552.0", "@hapi/hapi": "^21.3.10", - "@nestjs/common": "^10.3.7", - "@nestjs/core": "^10.3.3", - "@nestjs/platform-express": "^10.4.6", + "@nestjs/common": "10.4.6", + "@nestjs/core": "10.4.6", + "@nestjs/platform-express": "10.4.6", "@prisma/client": "5.9.1", "@sentry/aws-serverless": "8.38.0", "@sentry/node": "8.38.0", @@ -41,6 +41,7 @@ "amqplib": "^0.10.4", "apollo-server": "^3.11.1", "axios": "^1.7.7", + "body-parser": "^1.20.3", "connect": "^3.7.0", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts index ecf69671b9f4..58d4a299174c 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts @@ -14,7 +14,7 @@ afterAll(() => { * This test nevertheless covers the behavior so that we're aware. */ test('withScope scope is NOT applied to thrown error caught by global handler', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .expect({ event: { exception: { @@ -42,16 +42,15 @@ test('withScope scope is NOT applied to thrown error caught by global handler', tags: expect.not.objectContaining({ local: expect.anything() }), }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/withScope')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/withScope', { expectError: true }); }); /** * This test shows that the isolation scope set tags are applied correctly to the error. */ test('isolation scope is applied to thrown error caught by global handler', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .expect({ event: { exception: { @@ -81,7 +80,6 @@ test('isolation scope is applied to thrown error caught by global handler', done }, }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/isolationScope')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/isolationScope', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts index 955d725ae0c5..3ad6a3d2068f 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .ignore('transaction') .expect({ event: { @@ -33,7 +33,6 @@ test('should capture and send Express controller error with txn name if tracesSa transaction: 'GET /test/express/:id', }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts index cb43073fa994..b02d74016ad4 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); test('should capture and send Express controller error if tracesSampleRate is not set.', done => { - const runner = createRunner(__dirname, 'server.ts') + createRunner(__dirname, 'server.ts') .ignore('transaction') .expect({ event: { @@ -32,7 +32,6 @@ test('should capture and send Express controller error if tracesSampleRate is no }, }, }) - .start(done); - - expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow(); + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index 4ec29414868c..0ee5ca2204f5 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -9,7 +9,9 @@ test('Should overwrite baggage if the incoming request already has Sentry baggag const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + headers: { + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); @@ -25,8 +27,10 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }, }); expect(response).toBeDefined(); @@ -42,8 +46,10 @@ test('Should not propagate baggage data from an incoming to an outgoing request const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + headers: { + 'sentry-trace': '', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }, }); expect(response).toBeDefined(); @@ -59,7 +65,9 @@ test('Should not propagate baggage if sentry-trace header is present in incoming const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + }, }); expect(response).toBeDefined(); @@ -74,8 +82,10 @@ test('Should not propagate baggage and ignore original 3rd party baggage entries const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'foo=bar', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'foo=bar', + }, }); expect(response).toBeDefined(); @@ -107,7 +117,9 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - baggage: 'foo=bar,bar=baz', + headers: { + baggage: 'foo=bar,bar=baz', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index 9af5d4456c89..0e083f5c2dc6 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -9,8 +9,10 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index dd3c0f8cddd7..2403da850d9d 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -9,8 +9,10 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', - baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts index 071f02f83647..1ef9c11aff70 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -10,7 +10,9 @@ test('Should assign `sentry-trace` header which sets parent trace id of an outgo const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + }, }); expect(response).toBeDefined(); diff --git a/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts b/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts index 97ff6e3fa769..ffc702d63057 100644 --- a/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts +++ b/dev-packages/node-integration-tests/suites/express/setupExpressErrorHandler/test.ts @@ -22,9 +22,9 @@ describe('express setupExpressErrorHandler', () => { .start(done); // this error is filtered & ignored - expect(() => runner.makeRequest('get', '/test1')).rejects.toThrow(); + runner.makeRequest('get', '/test1', { expectError: true }); // this error is actually captured - expect(() => runner.makeRequest('get', '/test2')).rejects.toThrow(); + runner.makeRequest('get', '/test2', { expectError: true }); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/server.js b/dev-packages/node-integration-tests/suites/express/tracing/server.js index 81560806097e..f9b4ae24b339 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/server.js +++ b/dev-packages/node-integration-tests/suites/express/tracing/server.js @@ -13,11 +13,15 @@ Sentry.init({ // express must be required after Sentry is initialized const express = require('express'); const cors = require('cors'); +const bodyParser = require('body-parser'); const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); const app = express(); app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); app.get('/test/express', (_req, res) => { res.send({ response: 'response 1' }); @@ -35,6 +39,10 @@ app.get(['/test/arr/:id', /\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/ res.send({ response: 'response 4' }); }); +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/test.ts index 44852233ed67..0b56d354759c 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/test.ts @@ -137,5 +137,106 @@ describe('express tracing', () => { .start(done) .makeRequest('get', `/test/${segment}`); }) as any); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'text/plain' }, + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: body, + }); + }); + }); }); }); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts index 2a85d39b83b8..5b96e8b1a2a3 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/server.ts @@ -8,10 +8,15 @@ Sentry.init({ }); import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import bodyParser from 'body-parser'; import express from 'express'; const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + Sentry.setTag('global', 'tag'); app.get('/test/isolationScope/:id', (req, res) => { @@ -24,6 +29,12 @@ app.get('/test/isolationScope/:id', (req, res) => { res.send({}); }); +app.post('/test-post', function (req, res) { + Sentry.captureException(new Error('This is an exception')); + + res.send({ status: 'ok', body: req.body }); +}); + Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts index 7c304062bc22..fdd63ad4aa4b 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts @@ -4,26 +4,129 @@ afterAll(() => { cleanupChildProcesses(); }); -test('correctly applies isolation scope even without tracing', done => { - const runner = createRunner(__dirname, 'server.ts') - .expect({ - event: { - transaction: 'GET /test/isolationScope/1', - tags: { - global: 'tag', - 'isolation-scope': 'tag', - 'isolation-scope-1': '1', +describe('express without tracing', () => { + test('correctly applies isolation scope even without tracing', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'GET /test/isolationScope/1', + tags: { + global: 'tag', + 'isolation-scope': 'tag', + 'isolation-scope-1': '1', + }, + // Request is correctly set + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test\/isolationScope\/1$/), + method: 'GET', + headers: { + 'user-agent': expect.stringContaining(''), + }, + }, }, - // Request is correctly set - request: { - url: expect.stringContaining('/test/isolationScope/1'), - headers: { - 'user-agent': expect.stringContaining(''), + }) + .start(done); + + runner.makeRequest('get', '/test/isolationScope/1'); + }); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { + 'Content-Type': 'text/plain', }, - }, - }) - .start(done); + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; - runner.makeRequest('get', '/test/isolationScope/1'); + runner.makeRequest('post', '/test-post', { headers: { 'Content-Type': 'application/octet-stream' }, data: body }); + }); + }); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts index f7a1678e1eb6..5cf4dc8c8c40 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts @@ -1,5 +1,6 @@ import type { SpanJSON } from '@sentry/types'; -import { assertSentryTransaction, cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import { assertSentryTransaction } from '../../../../utils/assertions'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index 6e8a86e627d9..0500c702189a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful and crashed sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful and crashed sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -25,11 +19,9 @@ test('should aggregate successful and crashed sessions', async () => { ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/error_unhandled'); - runner.makeRequest('get', '/success_next'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/error_unhandled', { expectError: true }); + runner.makeRequest('get', '/test/success_next'); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index 383bfca96062..1159d092fdd7 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful, crashed and erroneous sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful, crashed and erroneous sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -26,11 +20,9 @@ test('should aggregate successful, crashed and erroneous sessions', async () => ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/error_handled'); - runner.makeRequest('get', '/error_unhandled'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/error_handled'); + runner.makeRequest('get', '/test/error_unhandled', { expectError: true }); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index 23b80109fa43..465761e76224 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -4,16 +4,10 @@ afterEach(() => { cleanupChildProcesses(); }); -test('should aggregate successful sessions', async () => { - let _done: undefined | (() => void); - const promise = new Promise(resolve => { - _done = resolve; - }); - - const runner = createRunner(__dirname, 'server.ts') +test('should aggregate successful sessions', done => { + const runner = createRunner(__dirname, '..', 'server.ts') .ignore('transaction', 'event') .unignore('sessions') - .expectError() .expect({ sessions: { aggregates: [ @@ -24,11 +18,9 @@ test('should aggregate successful sessions', async () => { ], }, }) - .start(_done); - - runner.makeRequest('get', '/success'); - runner.makeRequest('get', '/success_next'); - runner.makeRequest('get', '/success_slow'); + .start(done); - await promise; + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/success_next'); + runner.makeRequest('get', '/test/success_slow'); }); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index e06f00ef486a..2415140b6140 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -1,26 +1,24 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; import type { SessionFlusher } from '@sentry/core'; import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + transport: loggingTransport, }); +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; const app = express(); -// ### Taken from manual tests ### -// Hack that resets the 60s default flush interval, and replaces it with just a one second interval const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; -let flusherIntervalId = flusher && flusher['_intervalId']; - -clearInterval(flusherIntervalId); - -flusherIntervalId = flusher['_intervalId'] = setInterval(() => flusher?.flush(), 2000); - -setTimeout(() => clearInterval(flusherIntervalId), 4000); +// Flush after 2 seconds (to avoid waiting for the default 60s) +setTimeout(() => { + flusher?.flush(); +}, 2000); app.get('/test/success', (_req, res) => { res.send('Success!'); @@ -52,4 +50,4 @@ app.get('/test/error_handled', (_req, res) => { Sentry.setupExpressErrorHandler(app); -export default app; +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts index dd14c2277f7b..a416656f6355 100644 --- a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts @@ -45,7 +45,6 @@ describe('connect auto-instrumentation', () => { test('CJS - should capture errors in `connect` middleware.', done => { createRunner(__dirname, 'scenario.js') .ignore('transaction') - .expectError() .expect({ event: EXPECTED_EVENT }) .start(done) .makeRequest('get', '/error'); @@ -55,7 +54,6 @@ describe('connect auto-instrumentation', () => { createRunner(__dirname, 'scenario.js') .ignore('event') .expect({ transaction: { transaction: 'GET /error' } }) - .expectError() .start(done) .makeRequest('get', '/error'); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts b/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts index 8bb3bfdb0796..4bd995777248 100644 --- a/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/hapi/test.ts @@ -50,9 +50,8 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/error'); + .makeRequest('get', '/error', { expectError: true }); }); test('CJS - should assign parameterized transactionName to error.', done => { @@ -64,9 +63,8 @@ describe('hapi auto-instrumentation', () => { }, }) .ignore('transaction') - .expectError() .start(done) - .makeRequest('get', '/error/123'); + .makeRequest('get', '/error/123', { expectError: true }); }); test('CJS - should handle returned Boom errors in routes.', done => { @@ -77,9 +75,8 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/boom-error'); + .makeRequest('get', '/boom-error', { expectError: true }); }); test('CJS - should handle promise rejections in routes.', done => { @@ -90,8 +87,7 @@ describe('hapi auto-instrumentation', () => { }, }) .expect({ event: EXPECTED_ERROR_EVENT }) - .expectError() .start(done) - .makeRequest('get', '/promise-error'); + .makeRequest('get', '/promise-error', { expectError: true }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts index ab63b1c9cb35..7c94d30b686a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/test.ts @@ -12,8 +12,10 @@ describe('getTraceMetaTags', () => { const runner = createRunner(__dirname, 'server.js').start(); const response = await runner.makeRequest('get', '/test', { - 'sentry-trace': `${traceId}-${parentSpanId}-1`, - baggage: 'sentry-environment=production', + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }, }); // @ts-ignore - response is defined, types just don't reflect it @@ -61,8 +63,10 @@ describe('getTraceMetaTags', () => { const runner = createRunner(__dirname, 'server-sdk-disabled.js').start(); const response = await runner.makeRequest('get', '/test', { - 'sentry-trace': `${traceId}-${parentSpanId}-1`, - baggage: 'sentry-environment=production', + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }, }); // @ts-ignore - response is defined, types just don't reflect it diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index 59c50d32ebdc..92fc857ed4e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -71,12 +71,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + 'db.statement': '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', 'otel.kind': 'CLIENT', }, - description: - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + description: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', op: 'db', origin: 'auto.db.otel.mongo', }), @@ -162,12 +160,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + 'db.statement': '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', 'otel.kind': 'CLIENT', }, - description: - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + description: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', op: 'db', origin: 'auto.db.otel.mongo', }), diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts index 51173004b2f8..e4f15f80fe70 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors-no-express/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -35,7 +33,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts index 11a0bb831c36..7cf65cbbbb1c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-errors/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -33,7 +31,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts index b6a6e4c0dca7..e77888ded6a3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs-no-express/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; @@ -35,7 +33,7 @@ class AppController { constructor(private readonly appService: AppService) {} @Get('test-exception/:id') - async testException(@Param('id') id: string): void { + async testException(@Param('id') id: string): Promise { Sentry.captureException(new Error(`error with id ${id}`)); } } diff --git a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts index 953619d8d437..2d4ac4e534cd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/nestjs/scenario.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck These are only tests /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; diff --git a/dev-packages/node-integration-tests/utils/assertions.ts b/dev-packages/node-integration-tests/utils/assertions.ts new file mode 100644 index 000000000000..68ce3941ff92 --- /dev/null +++ b/dev-packages/node-integration-tests/utils/assertions.ts @@ -0,0 +1,78 @@ +import type { + ClientReport, + Envelope, + Event, + SerializedCheckIn, + SerializedSession, + SessionAggregates, + TransactionEvent, +} from '@sentry/types'; +import { SDK_VERSION } from '@sentry/utils'; + +/** + * Asserts against a Sentry Event ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryEvent = (actual: Event, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +}; + +/** + * Asserts against a Sentry Transaction ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +}; + +export function assertSentrySession(actual: SerializedSession, expected: Partial): void { + expect(actual).toMatchObject({ + sid: expect.any(String), + ...expected, + }); +} + +export function assertSentrySessions(actual: SessionAggregates, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { + expect(actual).toMatchObject({ + check_in_id: expect.any(String), + ...expected, + }); +} + +export function assertSentryClientReport(actual: ClientReport, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { + expect(actual).toEqual({ + event_id: expect.any(String), + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + ...expected, + }); +} diff --git a/dev-packages/node-integration-tests/utils/defaults/server.ts b/dev-packages/node-integration-tests/utils/defaults/server.ts deleted file mode 100644 index 3cf8cadab65a..000000000000 --- a/dev-packages/node-integration-tests/utils/defaults/server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import express from 'express'; - -const app = express(); - -export default app; diff --git a/dev-packages/node-integration-tests/utils/index.ts b/dev-packages/node-integration-tests/utils/index.ts index 8c12ec72e0d2..0beedd250980 100644 --- a/dev-packages/node-integration-tests/utils/index.ts +++ b/dev-packages/node-integration-tests/utils/index.ts @@ -43,37 +43,6 @@ export const conditionalTest = (allowedVersion: { min?: number; max?: number }): : describe; }; -/** - * Asserts against a Sentry Event ignoring non-deterministic properties - * - * @param {Record} actual - * @param {Record} expected - */ -export const assertSentryEvent = (actual: Record, expected: Record): void => { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - ...expected, - }); -}; - -/** - * Asserts against a Sentry Transaction ignoring non-deterministic properties - * - * @param {Record} actual - * @param {Record} expected - */ -export const assertSentryTransaction = (actual: Record, expected: Record): void => { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - start_timestamp: expect.anything(), - spans: expect.any(Array), - type: 'transaction', - ...expected, - }); -}; - /** * Parses response body containing an Envelope * diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bde5bd06cd21..1cbd9ade2e67 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { spawn, spawnSync } from 'child_process'; +import { existsSync } from 'fs'; import { join } from 'path'; -import { SDK_VERSION } from '@sentry/node'; import type { ClientReport, Envelope, @@ -13,59 +13,19 @@ import type { SessionAggregates, TransactionEvent, } from '@sentry/types'; +import { normalize } from '@sentry/utils'; import axios from 'axios'; +import { + assertEnvelopeHeader, + assertSentryCheckIn, + assertSentryClientReport, + assertSentryEvent, + assertSentrySession, + assertSentrySessions, + assertSentryTransaction, +} from './assertions'; import { createBasicSentryServer } from './server'; -export function assertSentryEvent(actual: Event, expected: Event): void { - expect(actual).toMatchObject({ - event_id: expect.any(String), - ...expected, - }); -} - -export function assertSentrySession(actual: SerializedSession, expected: Partial): void { - expect(actual).toMatchObject({ - sid: expect.any(String), - ...expected, - }); -} - -export function assertSentryTransaction(actual: Event, expected: Partial): void { - expect(actual).toMatchObject({ - event_id: expect.any(String), - timestamp: expect.anything(), - start_timestamp: expect.anything(), - spans: expect.any(Array), - type: 'transaction', - ...expected, - }); -} - -export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { - expect(actual).toMatchObject({ - check_in_id: expect.any(String), - ...expected, - }); -} - -export function assertSentryClientReport(actual: ClientReport, expected: Partial): void { - expect(actual).toMatchObject({ - ...expected, - }); -} - -export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { - expect(actual).toEqual({ - event_id: expect.any(String), - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: SDK_VERSION, - }, - ...expected, - }); -} - const CLEANUP_STEPS = new Set(); export function cleanupChildProcesses(): void { @@ -130,8 +90,7 @@ async function runDockerCompose(options: DockerOptions): Promise { function newData(data: Buffer): void { const text = data.toString('utf8'); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log(text); + if (process.env.DEBUG) log(text); for (const match of options.readyMatches) { if (text.includes(match)) { @@ -147,24 +106,31 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } +type ExpectedEvent = Partial | ((event: Event) => void); +type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); +type ExpectedSession = Partial | ((event: SerializedSession) => void); +type ExpectedSessions = Partial | ((event: SessionAggregates) => void); +type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) => void); +type ExpectedClientReport = Partial | ((event: ClientReport) => void); + type Expected = | { - event: Partial | ((event: Event) => void); + event: ExpectedEvent; } | { - transaction: Partial | ((event: TransactionEvent) => void); + transaction: ExpectedTransaction; } | { - session: Partial | ((event: SerializedSession) => void); + session: ExpectedSession; } | { - sessions: Partial | ((event: SessionAggregates) => void); + sessions: ExpectedSessions; } | { - check_in: Partial | ((event: SerializedCheckIn) => void); + check_in: ExpectedCheckIn; } | { - client_report: Partial | ((event: ClientReport) => void); + client_report: ExpectedClientReport; }; type ExpectedEnvelopeHeader = @@ -178,16 +144,19 @@ type ExpectedEnvelopeHeader = export function createRunner(...paths: string[]) { const testPath = join(...paths); + if (!existsSync(testPath)) { + throw new Error(`Test scenario not found: ${testPath}`); + } + const expectedEnvelopes: Expected[] = []; let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined; const flags: string[] = []; // By default, we ignore session & sessions - const ignored: EnvelopeItemType[] = ['session', 'sessions']; + const ignored: Set = new Set(['session', 'sessions']); let withEnv: Record = {}; let withSentryServer = false; let dockerOptions: DockerOptions | undefined; let ensureNoErrorOutput = false; - let expectError = false; const logs: string[] = []; if (testPath.endsWith('.ts')) { @@ -207,10 +176,6 @@ export function createRunner(...paths: string[]) { expectedEnvelopeHeaders.push(expected); return this; }, - expectError: function () { - expectError = true; - return this; - }, withEnv: function (env: Record) { withEnv = env; return this; @@ -224,15 +189,12 @@ export function createRunner(...paths: string[]) { return this; }, ignore: function (...types: EnvelopeItemType[]) { - ignored.push(...types); + types.forEach(t => ignored.add(t)); return this; }, unignore: function (...types: EnvelopeItemType[]) { for (const t of types) { - const pos = ignored.indexOf(t); - if (pos > -1) { - ignored.splice(pos, 1); - } + ignored.delete(t); } return this; }, @@ -254,7 +216,7 @@ export function createRunner(...paths: string[]) { function complete(error?: Error): void { child?.kill(); - done?.(error); + done?.(normalize(error)); } /** Called after each expect callback to check if we're complete */ @@ -269,7 +231,7 @@ export function createRunner(...paths: string[]) { for (const item of envelope[1]) { const envelopeItemType = item[0].type; - if (ignored.includes(envelopeItemType)) { + if (ignored.has(envelopeItemType)) { continue; } @@ -307,58 +269,25 @@ export function createRunner(...paths: string[]) { } if ('event' in expected) { - const event = item[1] as Event; - if (typeof expected.event === 'function') { - expected.event(event); - } else { - assertSentryEvent(event, expected.event); - } - + expectErrorEvent(item[1] as Event, expected.event); expectCallbackCalled(); - } - - if ('transaction' in expected) { - const event = item[1] as TransactionEvent; - if (typeof expected.transaction === 'function') { - expected.transaction(event); - } else { - assertSentryTransaction(event, expected.transaction); - } - + } else if ('transaction' in expected) { + expectTransactionEvent(item[1] as TransactionEvent, expected.transaction); expectCallbackCalled(); - } - - if ('session' in expected) { - const session = item[1] as SerializedSession; - if (typeof expected.session === 'function') { - expected.session(session); - } else { - assertSentrySession(session, expected.session); - } - + } else if ('session' in expected) { + expectSessionEvent(item[1] as SerializedSession, expected.session); expectCallbackCalled(); - } - - if ('check_in' in expected) { - const checkIn = item[1] as SerializedCheckIn; - if (typeof expected.check_in === 'function') { - expected.check_in(checkIn); - } else { - assertSentryCheckIn(checkIn, expected.check_in); - } - + } else if ('sessions' in expected) { + expectSessionsEvent(item[1] as SessionAggregates, expected.sessions); expectCallbackCalled(); - } - - if ('client_report' in expected) { - const clientReport = item[1] as ClientReport; - if (typeof expected.client_report === 'function') { - expected.client_report(clientReport); - } else { - assertSentryClientReport(clientReport, expected.client_report); - } - + } else if ('check_in' in expected) { + expectCheckInEvent(item[1] as SerializedCheckIn, expected.check_in); + expectCallbackCalled(); + } else if ('client_report' in expected) { + expectClientReport(item[1] as ClientReport, expected.client_report); expectCallbackCalled(); + } else { + throw new Error(`Unhandled expected envelope item type: ${JSON.stringify(expected)}`); } } catch (e) { complete(e as Error); @@ -397,8 +326,7 @@ export function createRunner(...paths: string[]) { ? { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } : { ...process.env, ...withEnv }; - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); + if (process.env.DEBUG) log('starting scenario', testPath, flags, env.SENTRY_DSN); child = spawn('node', [...flags, testPath], { env }); @@ -425,8 +353,7 @@ export function createRunner(...paths: string[]) { // Pass error to done to end the test quickly child.on('error', e => { - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('scenario error', e); + if (process.env.DEBUG) log('scenario error', e); complete(e); }); @@ -465,8 +392,7 @@ export function createRunner(...paths: string[]) { logs.push(line.trim()); buffer = Buffer.from(buffer.subarray(splitIndex + 1)); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('line', line); + if (process.env.DEBUG) log('line', line); tryParseEnvelopeFromStdoutLine(line); } }); @@ -483,35 +409,95 @@ export function createRunner(...paths: string[]) { makeRequest: async function ( method: 'get' | 'post', path: string, - headers: Record = {}, - data?: any, // axios accept any as data + options: { headers?: Record; data?: unknown; expectError?: boolean } = {}, ): Promise { try { await waitFor(() => scenarioServerPort !== undefined); } catch (e) { complete(e as Error); - return undefined; + return; } const url = `http://localhost:${scenarioServerPort}${path}`; - if (expectError) { - try { - if (method === 'get') { - await axios.get(url, { headers }); - } else { - await axios.post(url, data, { headers }); - } - } catch (e) { + const data = options.data; + const headers = options.headers || {}; + const expectError = options.expectError || false; + + if (process.env.DEBUG) log('making request', method, url, headers, data); + + try { + const res = + method === 'post' ? await axios.post(url, data, { headers }) : await axios.get(url, { headers }); + + if (expectError) { + complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + return; + } + + return res.data; + } catch (e) { + if (expectError) { return; } + + complete(e as Error); return; - } else if (method === 'get') { - return (await axios.get(url, { headers })).data; - } else { - return (await axios.post(url, data, { headers })).data; } }, }; }, }; } + +function log(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args.map(arg => normalize(arg))); +} + +function expectErrorEvent(item: Event, expected: ExpectedEvent): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryEvent(item, expected); + } +} + +function expectTransactionEvent(item: TransactionEvent, expected: ExpectedTransaction): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryTransaction(item, expected); + } +} + +function expectSessionEvent(item: SerializedSession, expected: ExpectedSession): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySession(item, expected); + } +} + +function expectSessionsEvent(item: SessionAggregates, expected: ExpectedSessions): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySessions(item, expected); + } +} + +function expectCheckInEvent(item: SerializedCheckIn, expected: ExpectedCheckIn): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryCheckIn(item, expected); + } +} + +function expectClientReport(item: ClientReport, expected: ExpectedClientReport): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryClientReport(item, expected); + } +} diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index a3e25c232479..dce0ca15bf35 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -138,6 +138,8 @@ export function makeTerserPlugin() { '_resolveFilename', // Set on e.g. the shim feedbackIntegration to be able to detect it '_isShim', + // This is used in metadata integration + '_sentryModuleMetadata', ], }, }, diff --git a/dev-packages/size-limit-gh-action/index.mjs b/dev-packages/size-limit-gh-action/index.mjs index 1b8daa867e82..c12f263f9ea9 100644 --- a/dev-packages/size-limit-gh-action/index.mjs +++ b/dev-packages/size-limit-gh-action/index.mjs @@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import * as artifact from '@actions/artifact'; +import { DefaultArtifactClient } from '@actions/artifact'; import * as core from '@actions/core'; import { exec } from '@actions/exec'; import { context, getOctokit } from '@actions/github'; @@ -195,7 +195,7 @@ async function runSizeLimitOnComparisonBranch() { const resultsFilePath = getResultsFilePath(); const limit = new SizeLimitFormatter(); - const artifactClient = artifact.create(); + const artifactClient = new DefaultArtifactClient(); const { output: baseOutput } = await execSizeLimit(); diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index ff7d7001625a..985e50ef37be 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -14,7 +14,7 @@ "fix": "eslint . --format stylish --fix" }, "dependencies": { - "@actions/artifact": "1.1.2", + "@actions/artifact": "2.1.11", "@actions/core": "1.10.1", "@actions/exec": "1.1.1", "@actions/github": "^5.0.0", diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md new file mode 100644 index 000000000000..8b0cebd7e51d --- /dev/null +++ b/docs/migration/draft-v9-migration-guide.md @@ -0,0 +1,21 @@ + + +# Deprecations + +## `@sentry/utils` + +- Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will + be removed in v9. +- Deprecated `TransactionNamingScheme` type. + +## `@sentry/core` + +- Deprecated `transactionNamingScheme` option in `requestDataIntegration`. + +## `@sentry/types` + +- Deprecated `Request` in favor of `RequestEventData`. + +## Server-side SDKs (`@sentry/node` and all dependents) + +- Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. diff --git a/jest/jest.config.js b/jest/jest.config.js index 6bb8d30df35e..ce564a640f2d 100644 --- a/jest/jest.config.js +++ b/jest/jest.config.js @@ -3,8 +3,7 @@ module.exports = { rootDir: process.cwd(), collectCoverage: true, transform: { - '^.+\\.ts$': 'ts-jest', - '^.+\\.tsx$': 'ts-jest', + '^.+\\.(ts|tsx)$': 'ts-jest', }, coverageDirectory: '/coverage', moduleFileExtensions: ['js', 'ts', 'tsx'], @@ -15,6 +14,10 @@ module.exports = { globals: { 'ts-jest': { tsconfig: '/tsconfig.test.json', + diagnostics: { + // Ignore this warning for tests, we do not care about this + ignoreCodes: ['TS151001'], + }, }, __DEBUG_BUILD__: true, }, diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index e4c871ec74ea..853623abbc8a 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -91,7 +91,9 @@ export { parameterize, postgresIntegration, prismaIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, redisIntegration, requestDataIntegration, rewriteFramesIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 060dddd51787..8341b01719c1 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -105,7 +105,9 @@ export { setupNestErrorHandler, postgresIntegration, prismaIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, hapiIntegration, setupHapiErrorHandler, spotlightIntegration, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index fe5179f77661..ebea58f1fa7c 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -1,7 +1,9 @@ export type { Breadcrumb, BreadcrumbHint, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 5688d1007769..d8c97d6e8246 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index fa0d76a54521..4115874aa5e5 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index f7846dec6fea..15be450cf27e 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,5 +1,6 @@ import type { IntegrationFn } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; +import { addNormalizedRequestDataToEvent } from '@sentry/utils'; import { addRequestDataToEvent } from '@sentry/utils'; import { defineIntegration } from '../integration'; @@ -23,7 +24,11 @@ export type RequestDataIntegrationOptions = { }; }; - /** Whether to identify transactions by parameterized path, parameterized path with method, or handler name */ + /** + * Whether to identify transactions by parameterized path, parameterized path with method, or handler name. + * @deprecated This option does not do anything anymore, and will be removed in v9. + */ + // eslint-disable-next-line deprecation/deprecation transactionNamingScheme?: TransactionNamingScheme; }; @@ -73,15 +78,26 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = // that's happened, it will be easier to add this logic in without worrying about unexpected side effects.) const { sdkProcessingMetadata = {} } = event; - const req = sdkProcessingMetadata.request; + const { request, normalizedRequest } = sdkProcessingMetadata; + + const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options); - if (!req) { + // If this is set, it takes precedence over the plain request object + if (normalizedRequest) { + // Some other data is not available in standard HTTP requests, but can sometimes be augmented by e.g. Express or Next.js + const ipAddress = request ? request.ip || (request.socket && request.socket.remoteAddress) : undefined; + const user = request ? request.user : undefined; + + addNormalizedRequestDataToEvent(event, normalizedRequest, { ipAddress, user }, addRequestDataOptions); return event; } - const addRequestDataOptions = convertReqDataIntegrationOptsToAddReqDataOpts(_options); + // TODO(v9): Eventually we can remove this fallback branch and only rely on the normalizedRequest above + if (!request) { + return event; + } - return addRequestDataToEvent(event, req, addRequestDataOptions); + return addRequestDataToEvent(event, request, addRequestDataOptions); }, }; }) satisfies IntegrationFn; @@ -98,6 +114,7 @@ function convertReqDataIntegrationOptsToAddReqDataOpts( integrationOptions: Required, ): AddRequestDataToEventOptions { const { + // eslint-disable-next-line deprecation/deprecation transactionNamingScheme, include: { ip, user, ...requestOptions }, } = integrationOptions; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 54f59386ff17..4f0096b29773 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -193,6 +193,7 @@ export class SentrySpan implements Span { */ public updateName(name: string): this { this._name = name; + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); return this; } diff --git a/packages/core/test/lib/carrier.test.ts b/packages/core/test/lib/carrier.test.ts index 3c94c96b98c1..6e846d496ea5 100644 --- a/packages/core/test/lib/carrier.test.ts +++ b/packages/core/test/lib/carrier.test.ts @@ -42,14 +42,13 @@ describe('getSentryCarrier', () => { describe('multiple (older) SDKs', () => { it("returns the version of the sentry carrier object of the SDK's version rather than the one set in .version", () => { const sentryCarrier = getSentryCarrier({ + // @ts-expect-error - this is just a test object __SENTRY__: { - version: '8.0.0' as const, // another SDK set this + version: '8.0.0', // another SDK set this '8.0.0': { - // @ts-expect-error - this is just a test object, not passing a full stack stack: {}, }, [SDK_VERSION]: { - // @ts-expect-error - this is just a test object, not passing a full ACS acs: {}, }, hub: {}, diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index 74e764c1d938..253ac07d96c2 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -95,6 +95,13 @@ describe('createSpanEnvelope', () => { client = new TestClient(options); setCurrentClient(client); client.init(); + + // We want to avoid console errors in the tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.resetAllMocks(); }); it('creates a span envelope', () => { diff --git a/packages/core/test/lib/metrics/browser-aggregator.test.ts b/packages/core/test/lib/metrics/browser-aggregator.test.ts index 669959a03e05..e5ed6b3f8296 100644 --- a/packages/core/test/lib/metrics/browser-aggregator.test.ts +++ b/packages/core/test/lib/metrics/browser-aggregator.test.ts @@ -3,6 +3,10 @@ import { CounterMetric } from '../../../src/metrics/instance'; import { serializeMetricBuckets } from '../../../src/metrics/utils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; +function _cleanupAggregator(aggregator: BrowserMetricsAggregator): void { + clearInterval(aggregator['_interval']); +} + describe('BrowserMetricsAggregator', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); const testClient = new TestClient(options); @@ -21,6 +25,8 @@ describe('BrowserMetricsAggregator', () => { timestamp: expect.any(Number), unit: 'none', }); + + _cleanupAggregator(aggregator); }); it('groups same items together', () => { @@ -40,6 +46,8 @@ describe('BrowserMetricsAggregator', () => { unit: 'none', }); expect(firstValue.metric._value).toEqual(2); + + _cleanupAggregator(aggregator); }); it('differentiates based on tag value', () => { @@ -48,6 +56,8 @@ describe('BrowserMetricsAggregator', () => { expect(aggregator['_buckets'].size).toEqual(1); aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); expect(aggregator['_buckets'].size).toEqual(2); + + _cleanupAggregator(aggregator); }); describe('serializeBuckets', () => { @@ -69,6 +79,8 @@ describe('BrowserMetricsAggregator', () => { expect(serializedBuckets).toContain('cpu@none:52:50:55:157:3|g|T'); expect(serializedBuckets).toContain('lcp@second:1:1.2|d|#a:value,b:anothervalue|T'); expect(serializedBuckets).toContain('important_people@none:97:98|s|#numericKey:2|T'); + + _cleanupAggregator(aggregator); }); }); }); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 9698ab5e3398..8e43123e3f3c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -1,5 +1,5 @@ import { timestampInSeconds } from '@sentry/utils'; -import { setCurrentClient } from '../../../src'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCurrentClient } from '../../../src'; import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanToJSON } from '../../../src/utils/spanUtils'; @@ -20,6 +20,19 @@ describe('SentrySpan', () => { expect(spanToJSON(span).description).toEqual('new name'); }); + + it('sets the source to custom when calling updateName', () => { + const span = new SentrySpan({ + name: 'original name', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }); + + span.updateName('new name'); + + const spanJson = spanToJSON(span); + expect(spanJson.description).toEqual('new name'); + expect(spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); + }); }); describe('setters', () => { diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index c7328c810f92..3531074793f3 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 5e1c2bba5bc1..53cf4c026868 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -117,7 +117,9 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + // eslint-disable-next-line deprecation/deprecation processThreadBreadcrumbIntegration, + childProcessIntegration, } from '@sentry/node'; export { diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index 0cdb832a75f6..749e3d4efd6c 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -10,9 +10,6 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/nestjs.svg)](https://www.npmjs.com/package/@sentry/nestjs) [![npm dt](https://img.shields.io/npm/dt/@sentry/nestjs.svg)](https://www.npmjs.com/package/@sentry/nestjs) -This SDK is in **Beta**. The API is stable but updates may include minor changes in behavior. Please reach out on -[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns. - ## Installation ```bash @@ -72,8 +69,8 @@ export class AppModule {} In case you are using a global catch-all exception filter (which is either a filter registered with `app.useGlobalFilters()` or a filter registered in your app module providers annotated with an empty `@Catch()` -decorator), add a `@WithSentry()` decorator to the `catch()` method of this global error filter. This decorator will -report all unexpected errors that are received by your global error filter to Sentry: +decorator), add a `@SentryExceptionCaptured()` decorator to the `catch()` method of this global error filter. This +decorator will report all unexpected errors that are received by your global error filter to Sentry: ```typescript import { Catch, ExceptionFilter } from '@nestjs/common'; @@ -81,7 +78,7 @@ import { WithSentry } from '@sentry/nestjs'; @Catch() export class YourCatchAllExceptionFilter implements ExceptionFilter { - @WithSentry() + @SentryExceptionCaptured() catch(exception, host): void { // your implementation here } diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts new file mode 100644 index 000000000000..60e1049b3fd2 --- /dev/null +++ b/packages/nestjs/src/decorators.ts @@ -0,0 +1,87 @@ +import { captureException } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { startSpan } from '@sentry/node'; +import type { MonitorConfig } from '@sentry/types'; +import { isExpectedError } from './helpers'; + +/** + * A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry. + */ +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; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return Sentry.withMonitor( + monitorSlug, + () => { + return originalMethod.apply(this, args); + }, + monitorConfig, + ); + }; + return descriptor; + }; +}; + +/** + * A decorator usable to wrap arbitrary functions with spans. + */ +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 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return startSpan( + { + op: op, + name: propertyKey, + }, + () => { + return originalMethod.apply(this, args); + }, + ); + }; + + // preserve the original name on the decorated function + Object.defineProperty(descriptor.value, 'name', { + value: originalMethod.name, + configurable: true, + enumerable: true, + writable: true, + }); + + return descriptor; + }; +} + +/** + * A decorator to wrap user-defined exception filters and add Sentry error reporting. + */ +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; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (exception: unknown, host: unknown, ...args: any[]) { + if (isExpectedError(exception)) { + return originalCatch.apply(this, [exception, host, ...args]); + } + + captureException(exception); + return originalCatch.apply(this, [exception, host, ...args]); + }; + + return descriptor; + }; +} + +/** + * A decorator to wrap user-defined exception filters and add Sentry error reporting. + */ +export const WithSentry = SentryExceptionCaptured; diff --git a/packages/nestjs/src/decorators/sentry-cron.ts b/packages/nestjs/src/decorators/sentry-cron.ts deleted file mode 100644 index 8cb86c6d66cc..000000000000 --- a/packages/nestjs/src/decorators/sentry-cron.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from '@sentry/node'; -import type { MonitorConfig } from '@sentry/types'; - -/** - * A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry. - */ -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; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { - return Sentry.withMonitor( - monitorSlug, - () => { - return originalMethod.apply(this, args); - }, - monitorConfig, - ); - }; - return descriptor; - }; -}; diff --git a/packages/nestjs/src/decorators/sentry-traced.ts b/packages/nestjs/src/decorators/sentry-traced.ts deleted file mode 100644 index 2f90e4dab5d9..000000000000 --- a/packages/nestjs/src/decorators/sentry-traced.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { startSpan } from '@sentry/node'; - -/** - * A decorator usable to wrap arbitrary functions with spans. - */ -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 - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (...args: any[]) { - return startSpan( - { - op: op, - name: propertyKey, - }, - () => { - return originalMethod.apply(this, args); - }, - ); - }; - - // preserve the original name on the decorated function - Object.defineProperty(descriptor.value, 'name', { - value: originalMethod.name, - configurable: true, - enumerable: true, - writable: true, - }); - - return descriptor; - }; -} diff --git a/packages/nestjs/src/decorators/with-sentry.ts b/packages/nestjs/src/decorators/with-sentry.ts deleted file mode 100644 index cf86ea6e7cc5..000000000000 --- a/packages/nestjs/src/decorators/with-sentry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { captureException } from '@sentry/core'; -import { isExpectedError } from '../helpers'; - -/** - * A decorator to wrap user-defined exception filters and add Sentry error reporting. - */ -export function WithSentry() { - 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; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = function (exception: unknown, host: unknown, ...args: any[]) { - if (isExpectedError(exception)) { - return originalCatch.apply(this, [exception, host, ...args]); - } - - captureException(exception); - return originalCatch.apply(this, [exception, host, ...args]); - }; - - return descriptor; - }; -} diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index 71fb1ae4f78c..d99f491c1f6c 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -2,6 +2,9 @@ export * from '@sentry/node'; export { init } from './sdk'; -export { SentryTraced } from './decorators/sentry-traced'; -export { SentryCron } from './decorators/sentry-cron'; -export { WithSentry } from './decorators/with-sentry'; +export { + SentryTraced, + SentryCron, + WithSentry, + SentryExceptionCaptured, +} from './decorators'; diff --git a/packages/nestjs/src/sdk.ts b/packages/nestjs/src/sdk.ts index 8d5ca21b1706..b4789e2d01c2 100644 --- a/packages/nestjs/src/sdk.ts +++ b/packages/nestjs/src/sdk.ts @@ -1,5 +1,10 @@ -import { applySdkMetadata } from '@sentry/core'; -import type { NodeClient, NodeOptions } from '@sentry/node'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + applySdkMetadata, + spanToJSON, +} from '@sentry/core'; +import type { NodeClient, NodeOptions, Span } from '@sentry/node'; import { init as nodeInit } from '@sentry/node'; /** @@ -12,5 +17,29 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi applySdkMetadata(opts, 'nestjs'); - return nodeInit(opts); + const client = nodeInit(opts); + + if (client) { + client.on('spanStart', span => { + // The NestInstrumentation has no requestHook, so we add NestJS-specific attributes here + addNestSpanAttributes(span); + }); + } + + return client; +} + +function addNestSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data || {}; + + // this is one of: app_creation, request_context, handler + const type = attributes['nestjs.type']; + + // Only set the NestJS attributes for spans that are created by the NestJS instrumentation and for spans that do not have an op already. + if (type && !attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]) { + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, + }); + } } diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index a18f95417f11..0d7ccdb2c4dc 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -67,21 +67,39 @@ export { SentryTracingInterceptor }; */ class SentryGlobalFilter extends BaseExceptionFilter { public readonly __SENTRY_INTERNAL__: boolean; + private readonly _logger: Logger; public constructor(applicationRef?: HttpServer) { super(applicationRef); this.__SENTRY_INTERNAL__ = true; + this._logger = new Logger('ExceptionsHandler'); } /** * Catches exceptions and reports them to Sentry unless they are expected errors. */ public catch(exception: unknown, host: ArgumentsHost): void { - if (isExpectedError(exception)) { - return super.catch(exception, host); + // The BaseExceptionFilter does not work well in GraphQL applications. + // By default, Nest GraphQL applications use the ExternalExceptionFilter, which just rethrows the error: + // https://github.com/nestjs/nest/blob/master/packages/core/exceptions/external-exception-filter.ts + if (host.getType<'graphql'>() === 'graphql') { + // neither report nor log HttpExceptions + if (exception instanceof HttpException) { + throw exception; + } + + if (exception instanceof Error) { + this._logger.error(exception.message, exception.stack); + } + + captureException(exception); + throw exception; + } + + if (!isExpectedError(exception)) { + captureException(exception); } - captureException(exception); return super.catch(exception, host); } } @@ -89,13 +107,7 @@ Catch()(SentryGlobalFilter); export { SentryGlobalFilter }; /** - * Global filter to handle exceptions and report them to Sentry. - * - * The BaseExceptionFilter does not work well in GraphQL applications. - * By default, Nest GraphQL applications use the ExternalExceptionFilter, which just rethrows the error: - * https://github.com/nestjs/nest/blob/master/packages/core/exceptions/external-exception-filter.ts - * - * The ExternalExceptinFilter is not exported, so we reimplement this filter here. + * Global filter to handle exceptions in NestJS + GraphQL applications and report them to Sentry. */ class SentryGlobalGraphQLFilter { private static readonly _logger = new Logger('ExceptionsHandler'); @@ -129,29 +141,7 @@ export { SentryGlobalGraphQLFilter }; * * This filter is a generic filter that can handle both HTTP and GraphQL exceptions. */ -class SentryGlobalGenericFilter extends SentryGlobalFilter { - public readonly __SENTRY_INTERNAL__: boolean; - private readonly _graphqlFilter: SentryGlobalGraphQLFilter; - - public constructor(applicationRef?: HttpServer) { - super(applicationRef); - this.__SENTRY_INTERNAL__ = true; - this._graphqlFilter = new SentryGlobalGraphQLFilter(); - } - - /** - * Catches exceptions and forwards them to the according error filter. - */ - public catch(exception: unknown, host: ArgumentsHost): void { - if (host.getType<'graphql'>() === 'graphql') { - return this._graphqlFilter.catch(exception, host); - } - - super.catch(exception, host); - } -} -Catch()(SentryGlobalGenericFilter); -export { SentryGlobalGenericFilter }; +export const SentryGlobalGenericFilter = SentryGlobalFilter; /** * Service to set up Sentry performance tracing for Nest.js applications. diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 5ee59e1dae29..086ec4ecf2b3 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -79,7 +79,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation-http": "0.53.0", "@opentelemetry/semantic-conventions": "^1.27.0", - "@rollup/plugin-commonjs": "26.0.1", + "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "8.38.0", "@sentry/core": "8.38.0", "@sentry/node": "8.38.0", diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index c44ef444fdf7..4c92e0999f57 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -61,6 +61,7 @@ export function appRouterInstrumentNavigation(client: Client): void { WINDOW.addEventListener('popstate', () => { if (currentNavigationSpan && currentNavigationSpan.isRecording()) { currentNavigationSpan.updateName(WINDOW.location.pathname); + currentNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); } else { currentNavigationSpan = startBrowserTracingNavigationSpan(client, { name: WINDOW.location.pathname, @@ -105,9 +106,11 @@ export function appRouterInstrumentNavigation(client: Client): void { if (routerFunctionName === 'push') { span?.updateName(transactionNameifyRouterArgument(argArray[0])); + span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); span?.setAttribute('navigation.type', 'router.push'); } else if (routerFunctionName === 'replace') { span?.updateName(transactionNameifyRouterArgument(argArray[0])); + span?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); span?.setAttribute('navigation.type', 'router.replace'); } else if (routerFunctionName === 'back') { span?.setAttribute('navigation.type', 'router.back'); diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 05aa992f39e6..3a33130a3220 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -34,6 +34,9 @@ describe('applyTunnelRouteOption()', () => { }); it("Doesn't apply `tunnelRoute` when DSN is invalid", () => { + // Avoid polluting the test output with error messages + const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route'; const options: any = { dsn: 'invalidDsn', @@ -42,6 +45,8 @@ describe('applyTunnelRouteOption()', () => { applyTunnelRouteOption(options); expect(options.tunnel).toBeUndefined(); + + mockConsoleError.mockRestore(); }); it("Doesn't apply `tunnelRoute` option when `tunnelRoute` option wasn't injected", () => { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6ab536034894..cc81dce37577 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -33,7 +33,8 @@ export { tediousIntegration } from './integrations/tracing/tedious'; export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { amqplibIntegration } from './integrations/tracing/amqplib'; -export { processThreadBreadcrumbIntegration } from './integrations/processThread'; +// eslint-disable-next-line deprecation/deprecation +export { processThreadBreadcrumbIntegration, childProcessIntegration } from './integrations/childProcess'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; @@ -144,7 +145,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/node/src/integrations/processThread.ts b/packages/node/src/integrations/childProcess.ts similarity index 85% rename from packages/node/src/integrations/processThread.ts rename to packages/node/src/integrations/childProcess.ts index 870a0dc6df64..99525b4092b4 100644 --- a/packages/node/src/integrations/processThread.ts +++ b/packages/node/src/integrations/childProcess.ts @@ -2,7 +2,6 @@ import type { ChildProcess } from 'node:child_process'; import * as diagnosticsChannel from 'node:diagnostics_channel'; import type { Worker } from 'node:worker_threads'; import { addBreadcrumb, defineIntegration } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; interface Options { /** @@ -13,9 +12,13 @@ interface Options { includeChildProcessArgs?: boolean; } +// TODO(v9): Update this name and mention in migration docs. const INTEGRATION_NAME = 'ProcessAndThreadBreadcrumbs'; -const _processThreadBreadcrumbIntegration = ((options: Options = {}) => { +/** + * Capture breadcrumbs for child processes and worker threads. + */ +export const childProcessIntegration = defineIntegration((options: Options = {}) => { return { name: INTEGRATION_NAME, setup(_client) { @@ -34,12 +37,14 @@ const _processThreadBreadcrumbIntegration = ((options: Options = {}) => { }); }, }; -}) satisfies IntegrationFn; +}); /** * Capture breadcrumbs for child processes and worker threads. + * + * @deprecated Use `childProcessIntegration` integration instead. Functionally they are the same. `processThreadBreadcrumbIntegration` will be removed in the next major version. */ -export const processThreadBreadcrumbIntegration = defineIntegration(_processThreadBreadcrumbIntegration); +export const processThreadBreadcrumbIntegration = childProcessIntegration; function captureChildProcessEvents(child: ChildProcess, options: Options): void { let hasExited = false; diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 090c0783507a..b17810adb601 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -1,18 +1,20 @@ import type * as http from 'node:http'; -import type { RequestOptions } from 'node:http'; +import type { IncomingMessage, RequestOptions } from 'node:http'; 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 { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core'; -import type { SanitizedRequestData } from '@sentry/types'; +import type { PolymorphicRequest, RequestEventData, SanitizedRequestData } from '@sentry/types'; import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, + logger, parseUrl, stripUrlQueryAndFragment, } from '@sentry/utils'; +import { DEBUG_BUILD } from '../../debug-build'; import type { NodeClient } from '../../sdk/client'; import { getRequestUrl } from '../../utils/getRequestUrl'; @@ -39,6 +41,9 @@ type SentryHttpInstrumentationOptions = InstrumentationConfig & { ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; }; +// We only want to capture request bodies up to 1mb. +const MAX_BODY_BYTE_LENGTH = 1024 * 1024; + /** * This custom HTTP instrumentation is used to isolate incoming requests and annotate them with additional information. * It does not emit any spans. @@ -128,8 +133,27 @@ export class SentryHttpInstrumentation extends InstrumentationBase'; + const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http'; + const originalUrl = request.url || ''; + const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`; + + // This is non-standard, but may be set on e.g. Next.js or Express requests + const cookies = (request as PolymorphicRequest).cookies; + + const normalizedRequest: RequestEventData = { + url: absoluteUrl, + method: request.method, + query_string: extractQueryParams(request), + headers: headersToDict(request.headers), + cookies, + }; + + patchRequestToCaptureBody(request, normalizedRequest); + // Update the isolation scope, isolate this request - isolationScope.setSDKProcessingMetadata({ request }); + isolationScope.setSDKProcessingMetadata({ request, normalizedRequest }); const client = getClient(); if (client && client.getOptions().autoSessionTracking) { @@ -316,3 +340,135 @@ function getBreadcrumbData(request: http.ClientRequest): Partial acc + chunk.byteLength, 0); + } + + /** + * We need to keep track of the original callbacks, in order to be able to remove listeners again. + * Since `off` depends on having the exact same function reference passed in, we need to be able to map + * original listeners to our wrapped ones. + */ + const callbackMap = new WeakMap(); + + try { + // eslint-disable-next-line @typescript-eslint/unbound-method + req.on = new Proxy(req.on, { + apply: (target, thisArg, args: Parameters) => { + const [event, listener, ...restArgs] = args; + + if (event === 'data') { + const callback = new Proxy(listener, { + apply: (target, thisArg, args: Parameters) => { + // If we have already read more than the max body length, we stop addiing chunks + // To avoid growing the memory indefinitely if a respons is e.g. streamed + if (getChunksSize() < MAX_BODY_BYTE_LENGTH) { + const chunk = args[0] as Buffer; + chunks.push(chunk); + } else if (DEBUG_BUILD) { + logger.log( + `Dropping request body chunk because it maximum body length of ${MAX_BODY_BYTE_LENGTH}b is exceeded.`, + ); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + callbackMap.set(listener, callback); + + return Reflect.apply(target, thisArg, [event, callback, ...restArgs]); + } + + if (event === 'end') { + const callback = new Proxy(listener, { + apply: (target, thisArg, args) => { + try { + const body = Buffer.concat(chunks).toString('utf-8'); + + // We mutate the passed in normalizedRequest and add the body to it + if (body) { + normalizedRequest.data = body; + } + } catch { + // ignore errors here + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + callbackMap.set(listener, callback); + + return Reflect.apply(target, thisArg, [event, callback, ...restArgs]); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + + // Ensure we also remove callbacks correctly + // eslint-disable-next-line @typescript-eslint/unbound-method + req.off = new Proxy(req.off, { + apply: (target, thisArg, args: Parameters) => { + const [, listener] = args; + + const callback = callbackMap.get(listener); + if (callback) { + callbackMap.delete(listener); + + const modifiedArgs = args.slice(); + modifiedArgs[1] = callback; + return Reflect.apply(target, thisArg, modifiedArgs); + } + + return Reflect.apply(target, thisArg, args); + }, + }); + } catch { + // ignore errors if we can't patch stuff + } +} + +function extractQueryParams(req: IncomingMessage): string | undefined { + // req.url is path and query string + if (!req.url) { + return; + } + + try { + // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and + // hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use. + const queryParams = new URL(req.url, 'http://dogs.are.great').search.slice(1); + return queryParams.length ? queryParams : undefined; + } catch { + return undefined; + } +} + +function headersToDict(reqHeaders: Record): Record { + const headers: Record = Object.create(null); + + try { + Object.entries(reqHeaders).forEach(([key, value]) => { + if (typeof value === 'string') { + headers[key] = value; + } + }); + } catch (e) { + DEBUG_BUILD && + logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); + } + + return headers; +} diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts index 5e42f5611db8..5f4d4e66a8a6 100644 --- a/packages/node/src/integrations/tracing/mongo.ts +++ b/packages/node/src/integrations/tracing/mongo.ts @@ -11,12 +11,58 @@ export const instrumentMongo = generateInstrumentOnce( INTEGRATION_NAME, () => new MongoDBInstrumentation({ + dbStatementSerializer: _defaultDbStatementSerializer, responseHook(span) { addOriginToSpan(span, 'auto.db.otel.mongo'); }, }), ); +/** + * Replaces values in document with '?', hiding PII and helping grouping. + */ +export function _defaultDbStatementSerializer(commandObj: Record): string { + const resultObj = _scrubStatement(commandObj); + return JSON.stringify(resultObj); +} + +function _scrubStatement(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(element => _scrubStatement(element)); + } + + if (isCommandObj(value)) { + const initial: Record = {}; + return Object.entries(value) + .map(([key, element]) => [key, _scrubStatement(element)]) + .reduce((prev, current) => { + if (isCommandEntry(current)) { + prev[current[0]] = current[1]; + } + return prev; + }, initial); + } + + // A value like string or number, possible contains PII, scrub it + return '?'; +} + +function isCommandObj(value: Record | unknown): value is Record { + return typeof value === 'object' && value !== null && !isBuffer(value); +} + +function isBuffer(value: unknown): boolean { + let isBuffer = false; + if (typeof Buffer !== 'undefined') { + isBuffer = Buffer.isBuffer(value); + } + return isBuffer; +} + +function isCommandEntry(value: [string, unknown] | unknown): value is [string, unknown] { + return Array.isArray(value); +} + const _mongoIntegration = (() => { return { name: INTEGRATION_NAME, diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts index cc83dda3855d..04dab67f65b0 100644 --- a/packages/node/src/integrations/tracing/nest/helpers.ts +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -36,6 +36,24 @@ export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget, }; } +/** + * Returns span options for nest event spans. + */ +export function getEventSpanOptions(event: string): { + name: string; + attributes: Record; + forceTransaction: boolean; +} { + return { + name: `event ${event}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'event.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs', + }, + forceTransaction: true, + }; +} + /** * Adds instrumentation to a js observable and attaches the span to an active parent span. */ diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 4f8d88fa8f86..2520367d1361 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -12,6 +12,7 @@ import { import type { IntegrationFn, Span } from '@sentry/types'; import { logger } from '@sentry/utils'; import { generateInstrumentOnce } from '../../../otel/instrument'; +import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; import type { MinimalNestJsApp, NestJsErrorFilter } from './types'; @@ -25,10 +26,15 @@ const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { return new SentryNestInstrumentation(); }); +const instrumentNestEvent = generateInstrumentOnce('Nest-Event', () => { + return new SentryNestEventInstrumentation(); +}); + export const instrumentNest = Object.assign( (): void => { instrumentNestCore(); instrumentNestCommon(); + instrumentNestEvent(); }, { id: INTEGRATION_NAME }, ); diff --git a/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts b/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts new file mode 100644 index 000000000000..16333c7fc6c3 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts @@ -0,0 +1,119 @@ +import { isWrapped } from '@opentelemetry/core'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, +} from '@opentelemetry/instrumentation'; +import { captureException, startSpan } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/utils'; +import { getEventSpanOptions } from './helpers'; +import type { OnEventTarget } from './types'; + +const supportedVersions = ['>=2.0.0']; + +/** + * Custom instrumentation for nestjs event-emitter + * + * This hooks into the `OnEvent` decorator, which is applied on event handlers. + */ +export class SentryNestEventInstrumentation extends InstrumentationBase { + public static readonly COMPONENT = '@nestjs/event-emitter'; + public static readonly COMMON_ATTRIBUTES = { + component: SentryNestEventInstrumentation.COMPONENT, + }; + + public constructor(config: InstrumentationConfig = {}) { + super('sentry-nestjs-event', SDK_VERSION, config); + } + + /** + * Initializes the instrumentation by defining the modules to be patched. + */ + public init(): InstrumentationNodeModuleDefinition { + const moduleDef = new InstrumentationNodeModuleDefinition( + SentryNestEventInstrumentation.COMPONENT, + supportedVersions, + ); + + moduleDef.files.push(this._getOnEventFileInstrumentation(supportedVersions)); + return moduleDef; + } + + /** + * Wraps the @OnEvent decorator. + */ + private _getOnEventFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { + return new InstrumentationNodeModuleFile( + '@nestjs/event-emitter/dist/decorators/on-event.decorator.js', + versions, + (moduleExports: { OnEvent: OnEventTarget }) => { + if (isWrapped(moduleExports.OnEvent)) { + this._unwrap(moduleExports, 'OnEvent'); + } + this._wrap(moduleExports, 'OnEvent', this._createWrapOnEvent()); + return moduleExports; + }, + (moduleExports: { OnEvent: OnEventTarget }) => { + this._unwrap(moduleExports, 'OnEvent'); + }, + ); + } + + /** + * Creates a wrapper function for the @OnEvent decorator. + */ + private _createWrapOnEvent() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function wrapOnEvent(original: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function wrappedOnEvent(event: any, options?: any) { + const eventName = Array.isArray(event) + ? event.join(',') + : typeof event === 'string' || typeof event === 'symbol' + ? event.toString() + : ''; + + // Get the original decorator result + const decoratorResult = original(event, options); + + // Return a new decorator function that wraps the handler + return function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) { + if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) { + return decoratorResult(target, propertyKey, descriptor); + } + + // Get the original handler + const originalHandler = descriptor.value; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const handlerName = originalHandler.name || propertyKey; + + // Instrument the handler + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = async function (...args: any[]) { + return startSpan(getEventSpanOptions(eventName), async () => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const result = await originalHandler.apply(this, args); + return result; + } catch (error) { + // exceptions from event handlers are not caught by global error filter + captureException(error); + throw error; + } + }); + }; + + // Preserve the original function name + Object.defineProperty(descriptor.value, 'name', { + value: handlerName, + configurable: true, + }); + + // Apply the original decorator + return decoratorResult(target, propertyKey, descriptor); + }; + }; + }; + } +} diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index 0590462c09d5..ed7e968a9600 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -74,6 +74,15 @@ export interface CatchTarget { }; } +/** + * Represents a target method in NestJS annotated with @OnEvent. + */ +export interface OnEventTarget { + name: string; + sentryPatched?: boolean; + __SENTRY_INTERNAL__?: boolean; +} + /** * Represents an express NextFunction. */ diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 87d61cc908bc..edebeea384db 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -30,13 +30,13 @@ import { consoleIntegration } from '../integrations/console'; import { nodeContextIntegration } from '../integrations/context'; import { contextLinesIntegration } from '../integrations/contextlines'; +import { childProcessIntegration } from '../integrations/childProcess'; import { httpIntegration } from '../integrations/http'; import { localVariablesIntegration } from '../integrations/local-variables'; import { modulesIntegration } from '../integrations/modules'; import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; -import { processThreadBreadcrumbIntegration } from '../integrations/processThread'; import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; import { getAutoPerformanceIntegrations } from '../integrations/tracing'; import { makeNodeTransport } from '../transports'; @@ -72,7 +72,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { contextLinesIntegration(), localVariablesIntegration(), nodeContextIntegration(), - processThreadBreadcrumbIntegration(), + childProcessIntegration(), ...getCjsOnlyIntegrations(), ]; } diff --git a/packages/node/src/transports/http-module.ts b/packages/node/src/transports/http-module.ts index f5cbe6fd35f9..65bf99349b10 100644 --- a/packages/node/src/transports/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -10,7 +10,8 @@ export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions export interface HTTPModuleRequestIncomingMessage { headers: IncomingHttpHeaders; statusCode?: number; - on(event: 'data' | 'end', listener: () => void): void; + on(event: 'data' | 'end', listener: (chunk: Buffer) => void): void; + off(event: 'data' | 'end', listener: (chunk: Buffer) => void): void; setEncoding(encoding: string): void; } diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts new file mode 100644 index 000000000000..29571c07babe --- /dev/null +++ b/packages/node/test/integrations/tracing/mongo.test.ts @@ -0,0 +1,72 @@ +import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; + +import { + _defaultDbStatementSerializer, + instrumentMongo, + mongoIntegration, +} from '../../../src/integrations/tracing/mongo'; +import { INSTRUMENTED } from '../../../src/otel/instrument'; + +jest.mock('@opentelemetry/instrumentation-mongodb'); + +describe('Mongo', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete INSTRUMENTED.Mongo; + + (MongoDBInstrumentation as unknown as jest.SpyInstance).mockImplementation(() => { + return { + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: () => ({}), + enable: () => undefined, + }; + }); + }); + + it('defaults are correct for instrumentMongo', () => { + instrumentMongo(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + dbStatementSerializer: expect.any(Function), + responseHook: expect.any(Function), + }); + }); + + it('defaults are correct for mongoIntegration', () => { + mongoIntegration().setupOnce!(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + responseHook: expect.any(Function), + dbStatementSerializer: expect.any(Function), + }); + }); + + describe('_defaultDbStatementSerializer', () => { + it('rewrites strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: 'foo', + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + + it('rewrites nested strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: { + inner: 'foo', + }, + }); + expect(JSON.parse(serialized).find.inner).toBe('?'); + }); + + it('rewrites Buffer as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: Buffer.from('foo', 'utf8'), + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + }); +}); diff --git a/packages/node/test/integrations/tracing/nest.test.ts b/packages/node/test/integrations/tracing/nest.test.ts index 3837e3e4ee3d..7f592a93f341 100644 --- a/packages/node/test/integrations/tracing/nest.test.ts +++ b/packages/node/test/integrations/tracing/nest.test.ts @@ -1,5 +1,8 @@ +import * as core from '@sentry/core'; import { isPatched } from '../../../src/integrations/tracing/nest/helpers'; +import { SentryNestEventInstrumentation } from '../../../src/integrations/tracing/nest/sentry-nest-event-instrumentation'; import type { InjectableTarget } from '../../../src/integrations/tracing/nest/types'; +import type { OnEventTarget } from '../../../src/integrations/tracing/nest/types'; describe('Nest', () => { describe('isPatched', () => { @@ -14,4 +17,99 @@ describe('Nest', () => { expect(target.sentryPatched).toBe(true); }); }); + + describe('EventInstrumentation', () => { + let instrumentation: SentryNestEventInstrumentation; + let mockOnEvent: jest.Mock; + let mockTarget: OnEventTarget; + + beforeEach(() => { + instrumentation = new SentryNestEventInstrumentation(); + // Mock OnEvent to return a function that applies the descriptor + mockOnEvent = jest.fn().mockImplementation(() => { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + return descriptor; + }; + }); + mockTarget = { + name: 'TestClass', + prototype: {}, + } as OnEventTarget; + jest.spyOn(core, 'startSpan'); + jest.spyOn(core, 'captureException'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('init()', () => { + it('should return module definition with correct component name', () => { + const moduleDef = instrumentation.init(); + expect(moduleDef.name).toBe('@nestjs/event-emitter'); + }); + }); + + describe('OnEvent decorator wrapping', () => { + let wrappedOnEvent: any; + let descriptor: PropertyDescriptor; + let originalHandler: jest.Mock; + + beforeEach(() => { + originalHandler = jest.fn().mockResolvedValue('result'); + descriptor = { + value: originalHandler, + }; + + const moduleDef = instrumentation.init(); + const onEventFile = moduleDef.files[0]; + const moduleExports = { OnEvent: mockOnEvent }; + onEventFile?.patch(moduleExports); + wrappedOnEvent = moduleExports.OnEvent; + }); + + it('should wrap string event handlers', async () => { + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.startSpan).toHaveBeenCalled(); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap array event handlers', async () => { + const decorated = wrappedOnEvent(['test.event1', 'test.event2']); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.startSpan).toHaveBeenCalled(); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should capture exceptions and rethrow', async () => { + const error = new Error('Test error'); + originalHandler.mockRejectedValue(error); + + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await expect(descriptor.value()).rejects.toThrow(error); + expect(core.captureException).toHaveBeenCalledWith(error); + }); + + it('should skip wrapping for internal Sentry handlers', () => { + const internalTarget = { + ...mockTarget, + __SENTRY_INTERNAL__: true, + }; + + const decorated = wrappedOnEvent('test.event'); + decorated(internalTarget, 'testMethod', descriptor); + + expect(descriptor.value).toBe(originalHandler); + }); + }); + }); }); diff --git a/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node b/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node new file mode 100755 index 000000000000..65e97eca7e48 Binary files /dev/null and b/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node differ diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc index d51a1e747e93..bf3762867769 100644 --- a/packages/profiling-node/bindings/cpu_profiler.cc +++ b/packages/profiling-node/bindings/cpu_profiler.cc @@ -333,9 +333,8 @@ void SentryProfile::Start(Profiler *profiler) { // Initialize the CPU Profiler profiler->cpu_profiler->StartProfiling( - profile_title, - {v8::CpuProfilingMode::kCallerLineNumbers, - v8::CpuProfilingOptions::kNoSampleLimit, kSamplingInterval}); + profile_title, v8::CpuProfilingMode::kCallerLineNumbers, true, + v8::CpuProfilingOptions::kNoSampleLimit); // listen for memory sample ticks profiler->measurements_ticker.add_cpu_listener(id, cpu_sampler_cb); @@ -1169,6 +1168,7 @@ napi_value Init(napi_env env, napi_value exports) { } Profiler *profiler = new Profiler(env, isolate); + profiler->cpu_profiler->SetSamplingInterval(kSamplingInterval); if (napi_set_instance_data(env, profiler, FreeAddonData, NULL) != napi_ok) { napi_throw_error(env, nullptr, "Failed to set instance data for profiler."); diff --git a/packages/remix/src/utils/errors.ts b/packages/remix/src/utils/errors.ts index 92958c2c3eb3..100dac496c75 100644 --- a/packages/remix/src/utils/errors.ts +++ b/packages/remix/src/utils/errors.ts @@ -1,12 +1,5 @@ import type { AppData, DataFunctionArgs, EntryContext, HandleDocumentRequestFunction } from '@remix-run/node'; -import { - captureException, - getActiveSpan, - getClient, - getRootSpan, - handleCallbackErrors, - spanToJSON, -} from '@sentry/core'; +import { captureException, getClient, handleCallbackErrors } from '@sentry/core'; import type { Span } from '@sentry/types'; import { addExceptionMechanism, isPrimitive, logger, objectify } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -61,19 +54,9 @@ export async function captureRemixServerException( const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - const activeRootSpanName = rootSpan ? spanToJSON(rootSpan).description : undefined; - scope.setSDKProcessingMetadata({ request: { ...normalizedRequest, - // When `route` is not defined, `RequestData` integration uses the full URL - route: activeRootSpanName - ? { - path: activeRootSpanName, - } - : undefined, }, }); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index e83c14dfbbc4..666c332afa04 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -314,9 +314,6 @@ function wrapRequestHandler( isolationScope.setSDKProcessingMetadata({ request: { ...normalizedRequest, - route: { - path: name, - }, }, }); diff --git a/packages/remix/test/integration/test/server/utils/helpers.ts b/packages/remix/test/integration/test/server/utils/helpers.ts index 981be12f314a..909d8d1671ae 100644 --- a/packages/remix/test/integration/test/server/utils/helpers.ts +++ b/packages/remix/test/integration/test/server/utils/helpers.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { createRequestHandler } from '@remix-run/express'; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as Sentry from '@sentry/node'; -import type { EnvelopeItemType } from '@sentry/types'; +import type { EnvelopeItemType, Event, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { AxiosRequestConfig } from 'axios'; import axios from 'axios'; @@ -14,8 +14,6 @@ import type { HttpTerminator } from 'http-terminator'; import { createHttpTerminator } from 'http-terminator'; import nock from 'nock'; -export * from '../../../../../../../dev-packages/node-integration-tests/utils'; - type DataCollectorOptions = { // Optional custom URL url?: string; @@ -284,3 +282,33 @@ export class RemixTestEnv extends TestEnv { const parseEnvelope = (body: string): Array> => { return body.split('\n').map(e => JSON.parse(e)); }; + +/** + * Asserts against a Sentry Event ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryEvent = (actual: Event, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +}; + +/** + * Asserts against a Sentry Transaction ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +}; diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index e68e89ba8fe8..34bdabc15cb8 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.28.0" + "@sentry-internal/rrweb": "2.29.0" }, "dependencies": { "@sentry-internal/replay": "8.38.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 7ccdeb4f62e2..50cebfab92e2 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.38.0", - "@sentry-internal/rrweb": "2.28.0", - "@sentry-internal/rrweb-snapshot": "2.28.0", + "@sentry-internal/rrweb": "2.29.0", + "@sentry-internal/rrweb-snapshot": "2.29.0", "fflate": "^0.8.1", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.2.1" diff --git a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts index d0ea607e1c06..6ba64244fddf 100644 --- a/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleGlobalEvent.ts @@ -5,6 +5,7 @@ import type { ReplayContainer } from '../types'; import { isErrorEvent, isFeedbackEvent, isReplayEvent, isTransactionEvent } from '../util/eventUtils'; import { isRrwebError } from '../util/isRrwebError'; import { logger } from '../util/logger'; +import { resetReplayIdOnDynamicSamplingContext } from '../util/resetReplayIdOnDynamicSamplingContext'; import { addFeedbackBreadcrumb } from './util/addFeedbackBreadcrumb'; import { shouldSampleForBufferEvent } from './util/shouldSampleForBufferEvent'; @@ -34,6 +35,8 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even // Ensure we do not add replay_id if the session is expired const isSessionActive = replay.checkAndHandleExpiredSession(); if (!isSessionActive) { + // prevent exceeding replay durations by removing the expired replayId from the DSC + resetReplayIdOnDynamicSamplingContext(); return event; } diff --git a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts index 9e888568d04d..a3ad967e1586 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -11,6 +11,7 @@ import { REPLAY_EVENT_NAME, SESSION_IDLE_EXPIRE_DURATION } from '../../../src/co import { handleGlobalEventListener } from '../../../src/coreHandlers/handleGlobalEvent'; import type { ReplayContainer } from '../../../src/replay'; import { makeSession } from '../../../src/session/Session'; +import * as resetReplayIdOnDynamicSamplingContextModule from '../../../src/util/resetReplayIdOnDynamicSamplingContext'; import { Error } from '../../fixtures/error'; import { Transaction } from '../../fixtures/transaction'; import { resetSdkMock } from '../../mocks/resetSdkMock'; @@ -416,4 +417,21 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { }), ); }); + + it('resets replayId on DSC when session expires', () => { + const errorEvent = Error(); + const txEvent = Transaction(); + + vi.spyOn(replay, 'checkAndHandleExpiredSession').mockReturnValue(false); + + const resetReplayIdSpy = vi.spyOn( + resetReplayIdOnDynamicSamplingContextModule, + 'resetReplayIdOnDynamicSamplingContext', + ); + + handleGlobalEventListener(replay)(errorEvent, {}); + handleGlobalEventListener(replay)(txEvent, {}); + + expect(resetReplayIdSpy).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index da0391dea35e..b4c0972decff 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -89,6 +89,7 @@ function withSentryRouterRoot(Root: Component): Component; export type ProfileChunkItem = BaseEnvelopeItem; export type SpanItem = BaseEnvelopeItem>; -export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; +export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial }; type SessionEnvelopeHeaders = { sent_at: string }; type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index c8c16b8ce514..ecfa5ad14559 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -2,13 +2,15 @@ import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Contexts } from './context'; import type { DebugMeta } from './debugMeta'; +import type { DynamicSamplingContext } from './envelope'; import type { Exception } from './exception'; import type { Extras } from './extra'; import type { Measurements } from './measurement'; import type { Mechanism } from './mechanism'; import type { Primitive } from './misc'; -import type { Request } from './request'; -import type { CaptureContext } from './scope'; +import type { PolymorphicRequest } from './polymorphics'; +import type { RequestEventData } from './request'; +import type { CaptureContext, Scope } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { SeverityLevel } from './severity'; import type { MetricSummary, SpanJSON } from './span'; @@ -34,7 +36,7 @@ export interface Event { dist?: string; environment?: string; sdk?: SdkInfo; - request?: Request; + request?: RequestEventData; transaction?: string; modules?: { [key: string]: string }; fingerprint?: string[]; @@ -51,7 +53,15 @@ export interface Event { measurements?: Measurements; debug_meta?: DebugMeta; // A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get sent to Sentry - sdkProcessingMetadata?: { [key: string]: any }; + // Note: This is considered internal and is subject to change in minors + sdkProcessingMetadata?: { [key: string]: unknown } & { + request?: PolymorphicRequest; + normalizedRequest?: RequestEventData; + dynamicSamplingContext?: Partial; + capturedSpanScope?: Scope; + capturedSpanIsolationScope?: Scope; + spanCountBeforeProcessing?: number; + }; transaction_info?: { source: TransactionSource; }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b100c1e9c26a..5dd1839aeba7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -87,7 +87,13 @@ export type { SendFeedbackParams, UserFeedback, } from './feedback'; -export type { QueryParams, Request, SanitizedRequestData } from './request'; +export type { + QueryParams, + RequestEventData, + // eslint-disable-next-line deprecation/deprecation + Request, + SanitizedRequestData, +} from './request'; export type { Runtime } from './runtime'; export type { CaptureContext, Scope, ScopeContext, ScopeData } from './scope'; export type { SdkInfo } from './sdkinfo'; diff --git a/packages/types/src/request.ts b/packages/types/src/request.ts index 3c04a788ded9..6ba060219dfd 100644 --- a/packages/types/src/request.ts +++ b/packages/types/src/request.ts @@ -1,5 +1,7 @@ -/** Request data included in an event as sent to Sentry */ -export interface Request { +/** + * Request data included in an event as sent to Sentry. + */ +export interface RequestEventData { url?: string; method?: string; data?: any; @@ -9,6 +11,12 @@ export interface Request { headers?: { [key: string]: string }; } +/** + * Request data included in an event as sent to Sentry. + * @deprecated: This type will be removed in v9. Use `RequestEventData` instead. + */ +export type Request = RequestEventData; + export type QueryParams = string | { [key: string]: string } | Array<[string, string]>; /** diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js index 35c6aab563f5..604db95b9dbe 100644 --- a/packages/utils/.eslintrc.js +++ b/packages/utils/.eslintrc.js @@ -15,5 +15,5 @@ module.exports = { }, ], // symlinks to the folders inside of `build`, created to simulate what's in the npm package - ignorePatterns: ['cjs/**', 'esm/**'], + ignorePatterns: ['cjs/**', 'esm/**', 'rollup.npm.config.mjs'], }; diff --git a/packages/utils/package.json b/packages/utils/package.json index ed4493a2a98b..3ef330009ccc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -63,7 +63,6 @@ "lint": "eslint . --format stylish", "test": "jest", "test:watch": "jest --watch", - "version": "node ../../scripts/versionbump.js src/version.ts", "yalc:publish": "yalc publish --push --sig" }, "volta": { diff --git a/packages/utils/rollup.npm.config.mjs b/packages/utils/rollup.npm.config.mjs index d28a7a6f54a0..cc3ad4064820 100644 --- a/packages/utils/rollup.npm.config.mjs +++ b/packages/utils/rollup.npm.config.mjs @@ -1,5 +1,19 @@ +// @ts-check + +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf-8')); + +if (!packageJson.version) { + throw new Error('invariant: package version not found'); +} + +const packageVersion = packageJson.version; + export default makeNPMConfigVariants( makeBaseNPMConfig({ packageSpecificConfig: { @@ -12,6 +26,14 @@ export default makeNPMConfigVariants( ? true : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), }, + plugins: [ + replace({ + preventAssignment: true, + values: { + __SENTRY_SDK_VERSION__: JSON.stringify(packageVersion), + }, + }), + ], }, }), ); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 2a89826313e8..e60bc3bec409 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,43 +1,168 @@ -export * from './aggregate-errors'; -export * from './array'; -export * from './breadcrumb-log-level'; -export * from './browser'; -export * from './dsn'; -export * from './error'; -export * from './worldwide'; -export * from './instrument'; -export * from './is'; -export * from './isBrowser'; -export * from './logger'; -export * from './memo'; -export * from './misc'; -export * from './node'; -export * from './normalize'; -export * from './object'; -export * from './path'; -export * from './promisebuffer'; +export { applyAggregateErrorsToEvent } from './aggregate-errors'; +export { flatten } from './array'; +export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; +export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; +export { dsnFromString, dsnToString, makeDsn } from './dsn'; +export { SentryError } from './error'; +export { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; +export type { InternalGlobal } from './worldwide'; +export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; +export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; +export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; +export { + addHandler, + maybeInstrument, + resetInstrumentationHandlers, + triggerHandlers, +} from './instrument/handlers'; +export { + isDOMError, + isDOMException, + isElement, + isError, + isErrorEvent, + isEvent, + isInstanceOf, + isParameterizedString, + isPlainObject, + isPrimitive, + isRegExp, + isString, + isSyntheticEvent, + isThenable, + isVueViewModel, +} from './is'; +export { isBrowser } from './isBrowser'; +export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './logger'; +export { memoBuilder } from './memo'; +export { + addContextToFrame, + addExceptionMechanism, + addExceptionTypeValue, + arrayify, + checkOrSetAlreadyCaught, + getEventDescription, + parseSemver, + uuid4, +} from './misc'; +export { dynamicRequire, isNodeEnv, loadModule } from './node'; +export { normalize, normalizeToSize, normalizeUrlToBase } from './normalize'; +export { + addNonEnumerableProperty, + convertToPlainObject, + dropUndefinedKeys, + extractExceptionKeysForMessage, + fill, + getOriginalFunction, + markFunctionWrapped, + objectify, + urlEncode, +} from './object'; +export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './path'; +export { makePromiseBuffer } from './promisebuffer'; +export type { PromiseBuffer } from './promisebuffer'; + // TODO: Remove requestdata export once equivalent integration is used everywhere -export * from './requestdata'; -export * from './severity'; -export * from './stacktrace'; -export * from './node-stack-trace'; -export * from './string'; -export * from './supports'; -export * from './syncpromise'; -export * from './time'; -export * from './tracing'; -export * from './env'; -export * from './envelope'; -export * from './clientreport'; -export * from './ratelimit'; -export * from './baggage'; -export * from './url'; -export * from './cache'; -export * from './eventbuilder'; -export * from './anr'; -export * from './lru'; -export * from './buildPolyfills'; -export * from './propagationContext'; -export * from './vercelWaitUntil'; -export * from './version'; -export * from './debug-ids'; +export { + DEFAULT_USER_INCLUDES, + addNormalizedRequestDataToEvent, + addRequestDataToEvent, + // eslint-disable-next-line deprecation/deprecation + extractPathForTransaction, + extractRequestData, + winterCGHeadersToDict, + winterCGRequestToRequestData, +} from './requestdata'; +export type { + AddRequestDataToEventOptions, + // eslint-disable-next-line deprecation/deprecation + TransactionNamingScheme, +} from './requestdata'; + +export { severityLevelFromString, validSeverityLevels } from './severity'; +export { + UNKNOWN_FUNCTION, + createStackParser, + getFramesFromEvent, + getFunctionName, + stackParserFromStackParserOptions, + stripSentryFramesAndReverse, +} from './stacktrace'; +export { filenameIsInApp, node, nodeStackLineParser } from './node-stack-trace'; +export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './string'; +export { + isNativeFunction, + supportsDOMError, + supportsDOMException, + supportsErrorEvent, + supportsFetch, + supportsNativeFetch, + supportsReferrerPolicy, + supportsReportingObserver, +} from './supports'; +export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './syncpromise'; +export { + _browserPerformanceTimeOriginMode, + browserPerformanceTimeOrigin, + dateTimestampInSeconds, + timestampInSeconds, +} from './time'; +export { + TRACEPARENT_REGEXP, + extractTraceparentData, + generateSentryTraceHeader, + propagationContextFromHeaders, +} from './tracing'; +export { getSDKSource, isBrowserBundle } from './env'; +export type { SdkSource } from './env'; +export { + addItemToEnvelope, + createAttachmentEnvelopeItem, + createEnvelope, + createEventEnvelopeHeaders, + createSpanEnvelopeItem, + envelopeContainsItemType, + envelopeItemTypeToDataCategory, + forEachEnvelopeItem, + getSdkMetadataForEnvelopeHeader, + parseEnvelope, + serializeEnvelope, +} from './envelope'; +export { createClientReportEnvelope } from './clientreport'; +export { + DEFAULT_RETRY_AFTER, + disabledUntil, + isRateLimited, + parseRetryAfterHeader, + updateRateLimits, +} from './ratelimit'; +export type { RateLimits } from './ratelimit'; +export { + BAGGAGE_HEADER_NAME, + MAX_BAGGAGE_STRING_LENGTH, + SENTRY_BAGGAGE_KEY_PREFIX, + SENTRY_BAGGAGE_KEY_PREFIX_REGEX, + baggageHeaderToDynamicSamplingContext, + dynamicSamplingContextToSentryBaggageHeader, + parseBaggageHeader, +} from './baggage'; + +export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; +export { makeFifoCache } from './cache'; +export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; +export { callFrameToStackFrame, watchdogTimer } from './anr'; +export { LRUMap } from './lru'; +export { generatePropagationContext } from './propagationContext'; +export { vercelWaitUntil } from './vercelWaitUntil'; +export { SDK_VERSION } from './version'; +export { getDebugImagesForResources, getFilenameToDebugIdMap } from './debug-ids'; +export { escapeStringForRegex } from './vendor/escapeStringForRegex'; +export { supportsHistory } from './vendor/supportsHistory'; + +export { _asyncNullishCoalesce } from './buildPolyfills/_asyncNullishCoalesce'; +export { _asyncOptionalChain } from './buildPolyfills/_asyncOptionalChain'; +export { _asyncOptionalChainDelete } from './buildPolyfills/_asyncOptionalChainDelete'; +export { _nullishCoalesce } from './buildPolyfills/_nullishCoalesce'; +export { _optionalChain } from './buildPolyfills/_optionalChain'; +export { _optionalChainDelete } from './buildPolyfills/_optionalChainDelete'; diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index a4eae547edb1..13ec367addda 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -1,7 +1,9 @@ +/* eslint-disable max-lines */ import type { Event, ExtractedNodeRequestData, PolymorphicRequest, + RequestEventData, TransactionSource, WebFetchHeaders, WebFetchRequest, @@ -12,13 +14,13 @@ import { DEBUG_BUILD } from './debug-build'; import { isPlainObject, isString } from './is'; import { logger } from './logger'; import { normalize } from './normalize'; +import { truncate } from './string'; import { stripUrlQueryAndFragment } from './url'; import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress'; const DEFAULT_INCLUDES = { ip: false, request: true, - transaction: true, user: true, }; const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; @@ -32,6 +34,8 @@ export type AddRequestDataToEventOptions = { include?: { ip?: boolean; request?: boolean | Array<(typeof DEFAULT_REQUEST_INCLUDES)[number]>; + /** @deprecated This option will be removed in v9. It does not do anything anymore, the `transcation` is set in other places. */ + // eslint-disable-next-line deprecation/deprecation transaction?: boolean | TransactionNamingScheme; user?: boolean | Array<(typeof DEFAULT_USER_INCLUDES)[number]>; }; @@ -49,6 +53,9 @@ export type AddRequestDataToEventOptions = { }; }; +/** + * @deprecated This type will be removed in v9. It is not in use anymore. + */ export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; /** @@ -64,6 +71,7 @@ export type TransactionNamingScheme = 'path' | 'methodPath' | 'handler'; * used instead of the request's route) * * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url') + * @deprecated This method will be removed in v9. It is not in use anymore. */ export function extractPathForTransaction( req: PolymorphicRequest, @@ -99,23 +107,6 @@ export function extractPathForTransaction( return [name, source]; } -function extractTransaction(req: PolymorphicRequest, type: boolean | TransactionNamingScheme): string { - switch (type) { - case 'path': { - return extractPathForTransaction(req, { path: true })[0]; - } - case 'handler': { - return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || ''; - } - case 'methodPath': - default: { - // if exist _reconstructedRoute return that path instead of route.path - const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined; - return extractPathForTransaction(req, { path: true, method: true, customRoute })[0]; - } - } -} - function extractUserData( user: { [key: string]: unknown; @@ -228,14 +219,27 @@ export function extractRequestData( if (method === 'GET' || method === 'HEAD') { break; } + // NOTE: As of v8, request is (unless a user sets this manually) ALWAYS a http request + // Which does not have a body by default + // However, in our http instrumentation, we patch the request to capture the body and store it on the + // request as `.body` anyhow + // In v9, we may update requestData to only work with plain http requests // body data: // express, koa, nextjs: req.body // // when using node by itself, you have to read the incoming stream(see // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know // where they're going to store the final result, so they'll have to capture this data themselves - if (req.body !== undefined) { - requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body)); + const body = req.body; + if (body !== undefined) { + const stringBody: string = isString(body) + ? body + : isPlainObject(body) + ? JSON.stringify(normalize(body)) + : truncate(`${body}`, 1024); + if (stringBody) { + requestData.data = stringBody; + } } break; } @@ -250,6 +254,61 @@ export function extractRequestData( return requestData; } +/** + * Add already normalized request data to an event. + * This mutates the passed in event. + */ +export function addNormalizedRequestDataToEvent( + event: Event, + req: RequestEventData, + // This is non-standard data that is not part of the regular HTTP request + additionalData: { ipAddress?: string; user?: Record }, + options: AddRequestDataToEventOptions, +): void { + const include = { + ...DEFAULT_INCLUDES, + ...(options && options.include), + }; + + if (include.request) { + const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES]; + if (include.ip) { + includeRequest.push('ip'); + } + + const extractedRequestData = extractNormalizedRequestData(req, { include: includeRequest }); + + event.request = { + ...event.request, + ...extractedRequestData, + }; + } + + if (include.user) { + const extractedUser = + additionalData.user && isPlainObject(additionalData.user) + ? extractUserData(additionalData.user, include.user) + : {}; + + if (Object.keys(extractedUser).length) { + event.user = { + ...event.user, + ...extractedUser, + }; + } + } + + if (include.ip) { + const ip = (req.headers && getClientIPAddress(req.headers)) || additionalData.ipAddress; + if (ip) { + event.user = { + ...event.user, + ip_address: ip, + }; + } + } +} + /** * Add data from the given request to the given event * @@ -308,12 +367,6 @@ export function addRequestDataToEvent( } } - if (include.transaction && !event.transaction && event.type === 'transaction') { - // TODO do we even need this anymore? - // TODO make this work for nextjs - event.transaction = extractTransaction(req, include.transaction); - } - return event; } @@ -374,3 +427,53 @@ export function winterCGRequestToRequestData(req: WebFetchRequest): PolymorphicR headers, }; } + +function extractNormalizedRequestData( + normalizedRequest: RequestEventData, + { include }: { include: string[] }, +): RequestEventData { + const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : []; + + const requestData: RequestEventData = {}; + const headers = { ...normalizedRequest.headers }; + + if (includeKeys.includes('headers')) { + requestData.headers = headers; + + // Remove the Cookie header in case cookie data should not be included in the event + if (!include.includes('cookies')) { + delete (headers as { cookie?: string }).cookie; + } + + // Remove IP headers in case IP data should not be included in the event + if (!include.includes('ip')) { + ipHeaderNames.forEach(ipHeaderName => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (headers as Record)[ipHeaderName]; + }); + } + } + + if (includeKeys.includes('method')) { + requestData.method = normalizedRequest.method; + } + + if (includeKeys.includes('url')) { + requestData.url = normalizedRequest.url; + } + + if (includeKeys.includes('cookies')) { + const cookies = normalizedRequest.cookies || (headers && headers.cookie ? parseCookie(headers.cookie) : undefined); + requestData.cookies = cookies || {}; + } + + if (includeKeys.includes('query_string')) { + requestData.query_string = normalizedRequest.query_string; + } + + if (includeKeys.includes('data')) { + requestData.data = normalizedRequest.data; + } + + return requestData; +} diff --git a/packages/utils/src/version.ts b/packages/utils/src/version.ts index 79596a866885..c4f3fcfb8363 100644 --- a/packages/utils/src/version.ts +++ b/packages/utils/src/version.ts @@ -1 +1,4 @@ -export const SDK_VERSION = '8.38.0'; +// This is a magic string replaced by rollup +declare const __SENTRY_SDK_VERSION__: string; + +export const SDK_VERSION = typeof __SENTRY_SDK_VERSION__ === 'string' ? __SENTRY_SDK_VERSION__ : '0.0.0-unknown.0'; diff --git a/packages/utils/test/requestdata.test.ts b/packages/utils/test/requestdata.test.ts index 570f80647b6b..90c734f23f2c 100644 --- a/packages/utils/test/requestdata.test.ts +++ b/packages/utils/test/requestdata.test.ts @@ -369,67 +369,6 @@ describe('addRequestDataToEvent', () => { } }); }); - - describe('transaction property', () => { - describe('for transaction events', () => { - beforeEach(() => { - mockEvent.type = 'transaction'; - }); - - test('extracts method and full route path by default`', () => { - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/:parameterName'); - }); - - test('extracts method and full path by default when mountpoint is `/`', () => { - mockReq.originalUrl = mockReq.originalUrl.replace('/routerMountpath', ''); - mockReq.baseUrl = ''; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - // `subpath/` is the full path here, because there's no router mount path - expect(parsedRequest.transaction).toEqual('POST /subpath/:parameterName'); - }); - - test('fallback to method and `originalUrl` if route is missing', () => { - delete mockReq.route; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/specificValue'); - }); - - test('can extract path only instead if configured', () => { - const optionsWithPathTransaction = { - include: { - transaction: 'path', - }, - } as const; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithPathTransaction); - - expect(parsedRequest.transaction).toEqual('/routerMountPath/subpath/:parameterName'); - }); - - test('can extract handler name instead if configured', () => { - const optionsWithHandlerTransaction = { - include: { - transaction: 'handler', - }, - } as const; - - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq, optionsWithHandlerTransaction); - - expect(parsedRequest.transaction).toEqual('parameterNameRouteHandler'); - }); - }); - it('transaction is not applied to non-transaction events', () => { - const parsedRequest: Event = addRequestDataToEvent(mockEvent, mockReq); - - expect(parsedRequest.transaction).toBeUndefined(); - }); - }); }); describe('extractRequestData', () => { @@ -763,6 +702,7 @@ describe('extractPathForTransaction', () => { expectedRoute: string, expectedSource: TransactionSource, ) => { + // eslint-disable-next-line deprecation/deprecation const [route, source] = extractPathForTransaction(req, options); expect(route).toEqual(expectedRoute); @@ -778,6 +718,7 @@ describe('extractPathForTransaction', () => { originalUrl: '/api/users/123/details', } as PolymorphicRequest; + // eslint-disable-next-line deprecation/deprecation const [route, source] = extractPathForTransaction(req, { path: true, method: true, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index e222d2de1ad1..4eea3f90d2d8 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -2,7 +2,9 @@ export type { Breadcrumb, BreadcrumbHint, PolymorphicRequest, + // eslint-disable-next-line deprecation/deprecation Request, + RequestEventData, SdkInfo, Event, EventHint, diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 3085e528bbf0..c00bdd184c20 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -81,18 +81,16 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { - const activeSpan = getActiveSpan(); - if (activeSpan) { - this.$_sentryRootSpan = - this.$_sentryRootSpan || - startInactiveSpan({ - name: 'Application Render', - op: `${VUE_OP}.render`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', - }, - }); - } + this.$_sentryRootSpan = + this.$_sentryRootSpan || + startInactiveSpan({ + name: 'Application Render', + op: `${VUE_OP}.render`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + }, + onlyIfParent: true, + }); } // Skip components that we don't want to track to minimize the noise and give a more granular control to the user diff --git a/scripts/versionbump.js b/scripts/versionbump.js deleted file mode 100644 index 931df2a7829c..000000000000 --- a/scripts/versionbump.js +++ /dev/null @@ -1,31 +0,0 @@ -const { readFile, writeFile } = require('node:fs').promises; -const pjson = require(`${process.cwd()}/package.json`); - -const REPLACE_REGEX = /\d+\.\d+.\d+(?:-\w+(?:\.\w+)?)?/g; - -async function run() { - const files = process.argv.slice(2); - if (files.length === 0) { - // eslint-disable-next-line no-console - console.error('[versionbump] Please provide files to bump'); - process.exit(1); - } - - try { - await Promise.all( - files.map(async file => { - const data = String(await readFile(file, 'utf8')); - await writeFile(file, data.replace(REPLACE_REGEX, pjson.version)); - }), - ); - - // eslint-disable-next-line no-console - console.log(`[versionbump] Bumped version for ${files.join(', ')}`); - } catch (error) { - // eslint-disable-next-line no-console - console.error('[versionbump] Error occurred:', error); - process.exit(1); - } -} - -run(); diff --git a/yarn.lock b/yarn.lock index da1c9aa37efc..320f57f45de4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,17 +2,26 @@ # yarn lockfile v1 -"@actions/artifact@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-1.1.2.tgz#13e796ce35214bd6486508f97b29b4b8e44f5a35" - integrity sha512-1gLONA4xw3/Q/9vGxKwkFdV9u1LE2RWGx/IpAqg28ZjprCnJFjwn4pA7LtShqg5mg5WhMek2fjpyH1leCmOlQQ== - dependencies: - "@actions/core" "^1.9.1" - "@actions/http-client" "^2.0.1" - tmp "^0.2.1" - tmp-promise "^3.0.2" +"@actions/artifact@2.1.11": + version "2.1.11" + resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-2.1.11.tgz#3dac32ea6feaa545bb99cb04bc4dd97b0c58e86a" + integrity sha512-V/N/3yM3oLxozq2dpdGqbd/39UbDOR54bF25vYsvn3QZnyZERSzPjTAAwpGzdcwESye9G7vnuhPiKQACEuBQpg== + dependencies: + "@actions/core" "^1.10.0" + "@actions/github" "^5.1.1" + "@actions/http-client" "^2.1.0" + "@azure/storage-blob" "^12.15.0" + "@octokit/core" "^3.5.1" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-retry" "^3.0.9" + "@octokit/request-error" "^5.0.0" + "@protobuf-ts/plugin" "^2.2.3-alpha.1" + archiver "^7.0.1" + jwt-decode "^3.1.2" + twirp-ts "^2.5.0" + unzip-stream "^0.3.1" -"@actions/core@1.10.1", "@actions/core@^1.9.1": +"@actions/core@1.10.1": version "1.10.1" resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.1.tgz#61108e7ac40acae95ee36da074fa5850ca4ced8a" integrity sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g== @@ -20,14 +29,22 @@ "@actions/http-client" "^2.0.1" uuid "^8.3.2" -"@actions/exec@1.1.1": +"@actions/core@^1.10.0", "@actions/core@^1.9.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.11.1.tgz#ae683aac5112438021588030efb53b1adb86f172" + integrity sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A== + dependencies: + "@actions/exec" "^1.1.1" + "@actions/http-client" "^2.0.1" + +"@actions/exec@1.1.1", "@actions/exec@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-1.1.1.tgz#2e43f28c54022537172819a7cf886c844221a611" integrity sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w== dependencies: "@actions/io" "^1.0.1" -"@actions/github@^5.0.0": +"@actions/github@^5.0.0", "@actions/github@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" integrity sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g== @@ -45,10 +62,10 @@ "@actions/core" "^1.9.1" minimatch "^3.0.4" -"@actions/http-client@^2.0.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.1.tgz#ed3fe7a5a6d317ac1d39886b0bb999ded229bb38" - integrity sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw== +"@actions/http-client@^2.0.1", "@actions/http-client@^2.1.0": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" + integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== dependencies: tunnel "^0.0.6" undici "^5.25.4" @@ -1066,7 +1083,7 @@ dependencies: tslib "^2.2.0" -"@azure/abort-controller@^2.0.0": +"@azure/abort-controller@^2.0.0", "@azure/abort-controller@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== @@ -1091,7 +1108,16 @@ "@azure/core-util" "^1.1.0" tslib "^2.6.2" -"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": +"@azure/core-auth@^1.8.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" + integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.11.0" + tslib "^2.6.2" + +"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.6.2", "@azure/core-client@^1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" integrity sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w== @@ -1104,7 +1130,7 @@ "@azure/logger" "^1.0.0" tslib "^2.6.2" -"@azure/core-http-compat@^2.0.1": +"@azure/core-http-compat@^2.0.0", "@azure/core-http-compat@^2.0.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz#d1585ada24ba750dc161d816169b33b35f762f0d" integrity sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ== @@ -1144,6 +1170,20 @@ https-proxy-agent "^7.0.0" tslib "^2.6.2" +"@azure/core-rest-pipeline@^1.10.1": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz#165f1cd9bb1060be3b6895742db3d1f1106271d3" + integrity sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.8.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + "@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.1.2.tgz#065dab4e093fb61899988a1cdbc827d9ad90b4ee" @@ -1151,6 +1191,13 @@ dependencies: tslib "^2.6.2" +"@azure/core-tracing@^1.1.2": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@azure/core-tracing/-/core-tracing-1.2.0.tgz#7be5d53c3522d639cf19042cbcdb19f71bc35ab2" + integrity sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg== + dependencies: + tslib "^2.6.2" + "@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.6.1", "@azure/core-util@^1.9.0": version "1.9.2" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.9.2.tgz#1dc37dc5b0dae34c578be62cf98905ba7c0cafe7" @@ -1159,6 +1206,14 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/core-util@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.11.0.tgz#f530fc67e738aea872fbdd1cc8416e70219fada7" + integrity sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + "@azure/core-util@^1.3.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.10.0.tgz#cf3163382d40343972848c914869864df5d44bdb" @@ -1167,6 +1222,14 @@ "@azure/abort-controller" "^2.0.0" tslib "^2.6.2" +"@azure/core-xml@^1.4.3": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@azure/core-xml/-/core-xml-1.4.4.tgz#a8656751943bf492762f758d147d33dfcd933d9e" + integrity sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ== + dependencies: + fast-xml-parser "^4.4.1" + tslib "^2.6.2" + "@azure/identity@^4.2.1": version "4.4.1" resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.4.1.tgz#490fa2ad26786229afa36411892bb53dfa3478d3" @@ -1232,6 +1295,25 @@ jsonwebtoken "^9.0.0" uuid "^8.3.0" +"@azure/storage-blob@^12.15.0": + version "12.25.0" + resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.25.0.tgz#fa9a1d2456cdf6526450a8b73059d2f2e9b1ec76" + integrity sha512-oodouhA3nCCIh843tMMbxty3WqfNT+Vgzj3Xo5jqR9UPnzq3d7mzLjlHAYz7lW+b4km3SIgz+NAgztvhm7Z6kQ== + dependencies: + "@azure/abort-controller" "^2.1.2" + "@azure/core-auth" "^1.4.0" + "@azure/core-client" "^1.6.2" + "@azure/core-http-compat" "^2.0.0" + "@azure/core-lro" "^2.2.0" + "@azure/core-paging" "^1.1.1" + "@azure/core-rest-pipeline" "^1.10.1" + "@azure/core-tracing" "^1.1.2" + "@azure/core-util" "^1.6.1" + "@azure/core-xml" "^1.4.3" + "@azure/logger" "^1.0.0" + events "^3.0.0" + tslib "^2.2.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -6623,49 +6705,49 @@ semver "^7.3.5" tar "^6.1.11" -"@nestjs/common@^10.3.7": - version "10.3.7" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.7.tgz#38ab5ff92277cf1f26f4749c264524e76962cfff" - integrity sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw== +"@nestjs/common@10.4.6": + version "10.4.6" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.6.tgz#952e8fd0ceafeffcc4eaf47effd67fb395844ae0" + integrity sha512-KkezkZvU9poWaNq4L+lNvx+386hpOxPJkfXBBeSMrcqBOx8kVr36TGN2uYkF4Ta4zNu1KbCjmZbc0rhHSg296g== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.2" + tslib "2.7.0" "@nestjs/common@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.3.10" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.3.10.tgz#d8825d55a50a04e33080c9188e6a5b03235d19f2" - integrity sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg== + version "10.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.7.tgz#076cb77c06149805cb1e193d8cdc69bbe8446c75" + integrity sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.6.3" + tslib "2.7.0" -"@nestjs/core@^10.3.3": - version "10.3.3" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.3.tgz#f957068ddda59252b7c36fcdb07a0fb323b52bcf" - integrity sha512-kxJWggQAPX3RuZx9JVec69eSLaYLNIox2emkZJpfBJ5Qq7cAq7edQIt1r4LGjTKq6kFubNTPsqhWf5y7yFRBPw== +"@nestjs/core@10.4.6": + version "10.4.6" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.6.tgz#797b381f12bd62d2e425897058fa219da4c3689d" + integrity sha512-zXVPxCNRfO6gAy0yvEDjUxE/8gfZICJFpsl2lZAUH31bPb6m+tXuhUq2mVCTEltyMYQ+DYtRe+fEYM2v152N1g== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.6.2" + path-to-regexp "3.3.0" + tslib "2.7.0" "@nestjs/core@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.3.10" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.3.10.tgz#508090c3ca36488a8e24a9e5939c2f37426e48f4" - integrity sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ== + version "10.4.7" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.7.tgz#adb27067a8c40b79f0713b417457fdfc6cf3406a" + integrity sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" - path-to-regexp "3.2.0" - tslib "2.6.3" + path-to-regexp "3.3.0" + tslib "2.7.0" -"@nestjs/platform-express@^10.4.6": +"@nestjs/platform-express@10.4.6": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" integrity sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg== @@ -7194,13 +7276,11 @@ "@octokit/types" "^6.0.3" "@octokit/auth-token@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f" - integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q== - dependencies: - "@octokit/types" "^8.0.0" + version "3.0.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" + integrity sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ== -"@octokit/core@^3.6.0": +"@octokit/core@^3.5.1", "@octokit/core@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== @@ -7236,11 +7316,11 @@ universal-user-agent "^6.0.0" "@octokit/endpoint@^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed" - integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.6.tgz#791f65d3937555141fb6c08f91d618a7d645f1e2" + integrity sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" universal-user-agent "^6.0.0" @@ -7254,12 +7334,12 @@ universal-user-agent "^6.0.0" "@octokit/graphql@^5.0.0": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8" - integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A== + version "5.0.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.6.tgz#9eac411ac4353ccc5d3fca7d76736e6888c5d248" + integrity sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw== dependencies: "@octokit/request" "^6.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" universal-user-agent "^6.0.0" "@octokit/openapi-types@^12.11.0": @@ -7267,24 +7347,19 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== -"@octokit/openapi-types@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" - integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== - -"@octokit/openapi-types@^16.0.0": - version "16.0.0" - resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz#d92838a6cd9fb4639ca875ddb3437f1045cc625e" - integrity sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA== - "@octokit/openapi-types@^18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" - integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== + version "18.1.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.1.1.tgz#09bdfdabfd8e16d16324326da5148010d765f009" + integrity sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw== + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== "@octokit/plugin-enterprise-rest@6.0.1": version "6.0.1" - resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/plugin-paginate-rest@^2.17.0": @@ -7322,6 +7397,14 @@ dependencies: "@octokit/types" "^10.0.0" +"@octokit/plugin-retry@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" + integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== + dependencies: + "@octokit/types" "^6.0.3" + bottleneck "^2.15.3" + "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" @@ -7332,11 +7415,20 @@ once "^1.4.0" "@octokit/request-error@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a" - integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.3.tgz#ef3dd08b8e964e53e55d471acfe00baa892b9c69" + integrity sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ== + dependencies: + "@octokit/types" "^9.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request-error@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" + integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== dependencies: - "@octokit/types" "^8.0.0" + "@octokit/types" "^13.1.0" deprecation "^2.0.0" once "^1.4.0" @@ -7353,13 +7445,13 @@ universal-user-agent "^6.0.0" "@octokit/request@^6.0.0": - version "6.2.2" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255" - integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw== + version "6.2.8" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.8.tgz#aaf480b32ab2b210e9dadd8271d187c93171d8eb" + integrity sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw== dependencies: "@octokit/endpoint" "^7.0.0" "@octokit/request-error" "^3.0.0" - "@octokit/types" "^8.0.0" + "@octokit/types" "^9.0.0" is-plain-object "^5.0.0" node-fetch "^2.6.7" universal-user-agent "^6.0.0" @@ -7386,6 +7478,13 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@octokit/types@^13.1.0": + version "13.6.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.1.tgz#432fc6c0aaae54318e5b2d3e15c22ac97fc9b15f" + integrity sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g== + dependencies: + "@octokit/openapi-types" "^22.2.0" + "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" @@ -7393,21 +7492,7 @@ dependencies: "@octokit/openapi-types" "^12.11.0" -"@octokit/types@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.0.0.tgz#93f0b865786c4153f0f6924da067fe0bb7426a9f" - integrity sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg== - dependencies: - "@octokit/openapi-types" "^14.0.0" - -"@octokit/types@^9.0.0": - version "9.0.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz#6050db04ddf4188ec92d60e4da1a2ce0633ff635" - integrity sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw== - dependencies: - "@octokit/openapi-types" "^16.0.0" - -"@octokit/types@^9.2.3": +"@octokit/types@^9.0.0", "@octokit/types@^9.2.3": version "9.3.2" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-9.3.2.tgz#3f5f89903b69f6a2d196d78ec35f888c0013cac5" integrity sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA== @@ -7982,6 +8067,42 @@ "@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0" "@opentelemetry/sdk-trace-base" "^1.22" +"@protobuf-ts/plugin-framework@^2.0.7", "@protobuf-ts/plugin-framework@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin-framework/-/plugin-framework-2.9.4.tgz#d7a617dedda4a12c568fdc1db5aa67d5e4da2406" + integrity sha512-9nuX1kjdMliv+Pes8dQCKyVhjKgNNfwxVHg+tx3fLXSfZZRcUHMc1PMwB9/vTvc6gBKt9QGz5ERqSqZc0++E9A== + dependencies: + "@protobuf-ts/runtime" "^2.9.4" + typescript "^3.9" + +"@protobuf-ts/plugin@^2.2.3-alpha.1": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/plugin/-/plugin-2.9.4.tgz#4e593e59013aaad313e7abbabe6e61964ef0ca28" + integrity sha512-Db5Laq5T3mc6ERZvhIhkj1rn57/p8gbWiCKxQWbZBBl20wMuqKoHbRw4tuD7FyXi+IkwTToaNVXymv5CY3E8Rw== + dependencies: + "@protobuf-ts/plugin-framework" "^2.9.4" + "@protobuf-ts/protoc" "^2.9.4" + "@protobuf-ts/runtime" "^2.9.4" + "@protobuf-ts/runtime-rpc" "^2.9.4" + typescript "^3.9" + +"@protobuf-ts/protoc@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/protoc/-/protoc-2.9.4.tgz#a92262ee64d252998540238701d2140f4ffec081" + integrity sha512-hQX+nOhFtrA+YdAXsXEDrLoGJqXHpgv4+BueYF0S9hy/Jq0VRTVlJS1Etmf4qlMt/WdigEes5LOd/LDzui4GIQ== + +"@protobuf-ts/runtime-rpc@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.9.4.tgz#d6ab2316c0ba67ce5a08863bb23203a837ff2a3b" + integrity sha512-y9L9JgnZxXFqH5vD4d7j9duWvIJ7AShyBRoNKJGhu9Q27qIbchfzli66H9RvrQNIFk5ER7z1Twe059WZGqERcA== + dependencies: + "@protobuf-ts/runtime" "^2.9.4" + +"@protobuf-ts/runtime@^2.9.4": + version "2.9.4" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.9.4.tgz#db8a78b1c409e26d258ca39464f4757d804add8f" + integrity sha512-vHRFWtJJB/SiogWDF0ypoKfRIZ41Kq+G9cEFj6Qm1eQaAhJ1LDFvgZ7Ja4tb3iLOQhz0PaoPnnOijF1qmEqTxg== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -8163,17 +8284,18 @@ dependencies: slash "^4.0.0" -"@rollup/plugin-commonjs@26.0.1": - version "26.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz#16d4d6e54fa63021249a292b50f27c0b0f1a30d8" - integrity sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ== +"@rollup/plugin-commonjs@28.0.1": + version "28.0.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz#e2138e31cc0637676dc3d5cae7739131f7cd565e" + integrity sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA== dependencies: "@rollup/pluginutils" "^5.0.1" commondir "^1.0.1" estree-walker "^2.0.2" - glob "^10.4.1" + fdir "^6.2.0" is-reference "1.2.1" magic-string "^0.30.3" + picomatch "^4.0.2" "@rollup/plugin-commonjs@^25.0.4", "@rollup/plugin-commonjs@^25.0.8": version "25.0.8" @@ -8445,67 +8567,34 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry-internal/rrdom@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.11.0.tgz#f7c8f54705ad84ece0e97e53f12e87c687749b32" - integrity sha512-BZnkTrbLm9Y3R70W1+8TnImys0RbKsgyB70WQoFdUervGvPw1kLcWJOJrPcDWgVe7nlbG+bEWb6iQrvLqldycw== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.11.0" - -"@sentry-internal/rrdom@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.28.0.tgz#91c55332e3392a8cc05b8e593ee9f6aa740cf5c3" - integrity sha512-9UqcIfy+ygCPpoXBAtlD3VxiTgaFQmYyqtvsL9b3lP1Wcj/rcd8ZZH7iFhT4AzA1bCi8Kx+VcYZxr09hZr5Qig== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.28.0" - -"@sentry-internal/rrweb-snapshot@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.11.0.tgz#1af79130604afea989d325465b209ac015b27c9a" - integrity sha512-1nP22QlplMNooSNvTh+L30NSZ+E3UcfaJyxXSMLxUjQHTGPyM1VkndxZMmxlKhyR5X+rLbxi/+RvuAcpM43VoA== - -"@sentry-internal/rrweb-snapshot@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.28.0.tgz#00e330fb0ecb569638af4b2236ed410c92dd8258" - integrity sha512-8pHeVKfmZPoWyWPOT2TbPc4fGnDMtaiHqMLLbDwUtLT9fkEq8AAv5UwfpY3utneIXuxaf1DaF7FgDSqpAWWkAw== - -"@sentry-internal/rrweb-types@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.11.0.tgz#e598c133b87be1fb04d31d09773b86142b095072" - integrity sha512-foCf9DGfN5ffzwykEtIXsV1P5d+XLDVGaQUnKF5ecGn+g5JzKTe/rPC92rL8/gEy2unL5sCTvlYL3DQvUFM4dA== +"@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== dependencies: - "@sentry-internal/rrweb-snapshot" "2.11.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" -"@sentry-internal/rrweb-types@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.28.0.tgz#6d3262879b9d97fd84adb8df7083d0a3b7dba18d" - integrity sha512-Xmyb6U3eGloFTHp6cv5KbN5cyL1fYF0GMxTSZd2/mVcSfgr09z8XVp0WWOcxhNouzhrz9OeLDotaDo45D8rROg== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.28.0" - "@types/css-font-loading-module" "0.0.7" +"@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@2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.11.0.tgz#be8e8dfff2acf64d418b625d35a20fdcd7daeb96" - integrity sha512-QuEqpKmRDb0xQe9fhJ3j/JHO6uxFMWBowADJBA4rvVU5HbExIg9gor1tZ0b3gDuChXnnx7pxFj9/QXZjQQ75zg== +"@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== dependencies: - "@sentry-internal/rrdom" "2.11.0" - "@sentry-internal/rrweb-snapshot" "2.11.0" - "@sentry-internal/rrweb-types" "2.11.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" "@types/css-font-loading-module" "0.0.7" - "@xstate/fsm" "^1.4.0" - base64-arraybuffer "^1.0.1" - fflate "^0.4.4" - mitt "^3.0.0" -"@sentry-internal/rrweb@2.28.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.28.0.tgz#353ac98e3308ce8e41a3e1e3a139a9c3db10b4eb" - integrity sha512-gX5gjE4xotHFrpqACP5jNCgmiUHb6pz8wWJnvC3lrc8aBUS1xNEIel4DkKjyGs9e9OY+MQk+nJghoIiLZwisSA== +"@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== dependencies: - "@sentry-internal/rrdom" "2.28.0" - "@sentry-internal/rrweb-snapshot" "2.28.0" - "@sentry-internal/rrweb-types" "2.28.0" + "@sentry-internal/rrdom" "2.29.0" + "@sentry-internal/rrweb-snapshot" "2.29.0" + "@sentry-internal/rrweb-types" "2.29.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -10093,10 +10182,12 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": - version "17.0.38" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" - integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=18": + version "22.9.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" + integrity sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ== + dependencies: + undici-types "~6.19.8" "@types/node@16.18.70": version "16.18.70" @@ -10108,13 +10199,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== -"@types/node@>=18": - version "22.7.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" - integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== - dependencies: - undici-types "~6.19.2" - "@types/node@^10.1.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -10125,6 +10209,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== +"@types/node@^18.0.0": + version "18.19.64" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.64.tgz#122897fb79f2a9ec9c979bded01c11461b2b1478" + integrity sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -12881,6 +12972,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + "binaryextensions@1 || 2", binaryextensions@^2.1.2: version "2.3.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" @@ -12954,7 +13053,7 @@ bluebird@^3.4.6, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0: +body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0, body-parser@^1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== @@ -13000,6 +13099,11 @@ boolean@^3.1.4: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== +bottleneck@^2.15.3: + version "2.19.5" + resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" + integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== + bower-config@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae" @@ -13681,6 +13785,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -14015,6 +14124,13 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -14539,6 +14655,11 @@ commander@^4.0.0, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^8.0.0, commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -16070,6 +16191,14 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dot-object@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/dot-object/-/dot-object-2.1.5.tgz#0ff0f1bff42c47ff06272081b208658c0a0231c2" + integrity sha512-xHF8EP4XH/Ba9fvAF2LDd5O3IITVolerVV6xvkxoM8zlGEiCUrggpAnHyOoKJKCrhvPcGATFAUwIujj7bRG5UA== + dependencies: + commander "^6.1.0" + glob "^7.1.6" + dot-prop@^5.1.0, dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -18392,6 +18521,13 @@ fast-xml-parser@4.2.5: dependencies: strnum "^1.0.5" +fast-xml-parser@^4.4.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37" + integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg== + dependencies: + strnum "^1.0.5" + fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -18433,16 +18569,16 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fdir@^6.2.0, fdir@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + fdir@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.3.0.tgz#fcca5a23ea20e767b15e081ee13b3e6488ee0bb0" integrity sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ== -fdir@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== - fflate@0.8.1, fflate@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9" @@ -19372,7 +19508,7 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2" path-scurry "^1.10.0" -glob@^10.3.10, glob@^10.4.1: +glob@^10.3.10: version "10.4.1" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== @@ -22475,6 +22611,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kafkajs@2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.4.tgz#59e6e16459d87fdf8b64be73970ed5aa42370a5b" @@ -26779,10 +26920,10 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: version "1.9.0" @@ -31828,13 +31969,6 @@ titleize@^3.0.0: resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== -tmp-promise@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" - integrity sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== - dependencies: - tmp "^0.2.0" - tmp@0.0.28: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" @@ -31856,11 +31990,6 @@ tmp@^0.1.0: dependencies: rimraf "^2.6.3" -tmp@^0.2.0: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== - tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -31977,6 +32106,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + tree-kill@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -32067,6 +32201,14 @@ ts-node@10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-poet@^4.5.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.15.0.tgz#637145fa554d3b27c56541578df0ce08cd9eb328" + integrity sha512-sLLR8yQBvHzi9d4R1F4pd+AzQxBfzOSSjfxiJxQhkUoH5bL7RsAC6wgvtVUQdGqiCsyS9rT6/8X2FI7ipdir5g== + dependencies: + lodash "^4.17.15" + prettier "^2.5.1" + tsconfck@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.0.tgz#b469f1ced12973bbec3209a55ed8de3bb04223c9" @@ -32110,16 +32252,6 @@ tslib@2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@2.6.2, tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -tslib@2.6.3, tslib@^2.2.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - tslib@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" @@ -32135,6 +32267,16 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== +tslib@^2.2.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -32163,6 +32305,18 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +twirp-ts@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/twirp-ts/-/twirp-ts-2.5.0.tgz#b43f09e95868d68ecd5c755ecbb08a7e51388504" + integrity sha512-JTKIK5Pf/+3qCrmYDFlqcPPUx+ohEWKBaZy8GL8TmvV2VvC0SXVyNYILO39+GCRbqnuP6hBIF+BVr8ZxRz+6fw== + dependencies: + "@protobuf-ts/plugin-framework" "^2.0.7" + camel-case "^4.1.2" + dot-object "^2.1.4" + path-to-regexp "^6.2.0" + ts-poet "^4.5.0" + yaml "^1.10.2" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -32287,6 +32441,11 @@ typescript@4.9.5, typescript@^4.9.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@^3.9: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + typescript@^5.0.4, typescript@^5.4.4: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" @@ -32426,7 +32585,12 @@ underscore@>=1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== -undici-types@~6.19.2: +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== @@ -32902,6 +33066,14 @@ unwasm@^0.3.9: pkg-types "^1.0.3" unplugin "^1.10.0" +unzip-stream@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.4.tgz#b4576755061809cf210b776cf26888d6a7823ead" + integrity sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw== + dependencies: + binary "^0.3.0" + mkdirp "^0.5.1" + upath@2.0.1, upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -34279,7 +34451,7 @@ yaml@2.2.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==