diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index 9de6e1a2b104..8fc0aeba7330 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -21,15 +21,6 @@ runs: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - # Only store cache on develop branch - - name: Store cached playwright binaries - uses: actions/cache/save@v4 - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' - with: - path: | - ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - # We always install all browsers, if uncached - name: Install Playwright dependencies (uncached) run: npx playwright install chromium webkit firefox --with-deps @@ -40,3 +31,12 @@ runs: run: npx playwright install-deps ${{ inputs.browsers || 'chromium webkit firefox' }} if: steps.playwright-cache.outputs.cache-hit == 'true' shell: bash + + # Only store cache on develop branch + - name: Store cached playwright binaries + uses: actions/cache/save@v4 + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + with: + path: | + ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index 848983376840..6cd63a6550e4 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -1,6 +1,14 @@ name: "Restore dependency & build cache" description: "Restore the dependency & build cache." +inputs: + dependency_cache_key: + description: "The dependency cache key" + required: true + node_version: + description: "If set, temporarily set node version to default one before installing, then revert to this version after." + required: false + runs: using: "composite" steps: @@ -9,17 +17,26 @@ runs: uses: actions/cache/restore@v4 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ env.DEPENDENCY_CACHE_KEY }} + key: ${{ inputs.dependency_cache_key }} - - name: Check build cache - uses: actions/cache/restore@v4 - id: build-cache + - name: Restore build artifacts + uses: actions/download-artifact@v4 with: - path: ${{ env.CACHED_BUILD_PATHS }} - key: ${{ env.BUILD_CACHE_KEY }} + name: build-output + + - name: Use default node version for install + if: inputs.node_version && steps.dep-cache.outputs.cache-hit != 'true' + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + + - name: Install dependencies + if: steps.dep-cache.outputs.cache-hit != 'true' + run: yarn install --ignore-engines --frozen-lockfile + shell: bash - - name: Check if caches are restored - uses: actions/github-script@v6 - if: steps.dep-cache.outputs.cache-hit != 'true' || steps.build-cache.outputs.cache-hit != 'true' + - name: Revert node version to ${{ inputs.node_version }} + if: inputs.node_version && steps.dep-cache.outputs.cache-hit != 'true' + uses: actions/setup-node@v4 with: - script: core.setFailed('Dependency or build cache could not be restored - please re-run ALL jobs.') + node-version: ${{ inputs.node_version }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 33d8c9ed27c0..652d9daa2e39 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,9 @@ updates: allow: - dependency-name: "@sentry/cli" - dependency-name: "@sentry/vite-plugin" + - dependency-name: "@sentry/webpack-plugin" + - dependency-name: "@sentry/rollup-plugin" + - dependency-name: "@sentry/esbuild-plugin" - dependency-name: "@opentelemetry/*" - dependency-name: "@prisma/instrumentation" - dependency-name: "opentelemetry-instrumentation-fetch-node" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fc43c3e08d5..4f06228f3a7e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,6 @@ env: ${{ github.workspace }}/packages/utils/cjs ${{ github.workspace }}/packages/utils/esm - BUILD_CACHE_KEY: build-cache-${{ github.event.inputs.commit || github.sha }} BUILD_CACHE_TARBALL_KEY: tarball-${{ github.event.inputs.commit || github.sha }} # GH will use the first restore-key it finds that matches @@ -160,13 +159,6 @@ jobs: base: ${{ github.event.pull_request.base.sha }} head: ${{ env.HEAD_COMMIT }} - - name: Check build cache - uses: actions/cache@v4 - id: cache_built_packages - with: - path: ${{ env.CACHED_BUILD_PATHS }} - key: ${{ env.BUILD_CACHE_KEY }} - - name: NX cache uses: actions/cache@v4 # Disable cache when: @@ -188,6 +180,15 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: yarn build + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-output + path: ${{ env.CACHED_BUILD_PATHS }} + retention-days: 4 + compression-level: 6 + overwrite: true + outputs: dependency_cache_key: ${{ steps.install_dependencies.outputs.cache_key }} changed_node_integration: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry-internal/node-integration-tests') }} @@ -232,8 +233,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Check bundle sizes uses: ./dev-packages/size-limit-gh-action with: @@ -259,8 +260,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Lint source files run: yarn lint:lerna - name: Lint C++ files @@ -305,8 +306,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run madge run: yarn circularDepCheck @@ -327,8 +328,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Extract Profiling Node Prebuilt Binaries uses: actions/download-artifact@v4 @@ -344,6 +345,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ github.sha }} + retention-days: 90 path: | ${{ github.workspace }}/packages/browser/build/bundles/** ${{ github.workspace }}/packages/replay-internal/build/bundles/** @@ -374,8 +376,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run affected tests run: yarn test:pr:browser --base=${{ github.event.pull_request.base.sha }} @@ -411,8 +413,8 @@ jobs: uses: oven-sh/setup-bun@v2 - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run tests run: | yarn test:ci:bun @@ -435,13 +437,13 @@ jobs: with: node-version-file: 'package.json' - name: Set up Deno - uses: denoland/setup-deno@v1.1.4 + uses: denoland/setup-deno@1.4.0 with: deno-version: v1.38.5 - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run tests run: | cd packages/deno @@ -474,8 +476,9 @@ jobs: node-version: ${{ matrix.node }} - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + node_version: ${{ matrix.node == 14 && '14' || '' }} - name: Run affected tests run: yarn test:pr:node --base=${{ github.event.pull_request.base.sha }} @@ -497,7 +500,10 @@ jobs: job_profiling_node_unit_tests: name: Node Profiling Unit Tests needs: [job_get_metadata, job_build] - if: needs.job_build.outputs.changed_node == 'true' || needs.job_get_metadata.outputs.changed_profiling_node == 'true' || github.event_name != 'pull_request' + if: | + needs.job_build.outputs.changed_node == 'true' || + needs.job_get_metadata.outputs.changed_profiling_node == 'true' || + github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -513,8 +519,8 @@ jobs: python-version: '3.11.7' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Build Configure node-gyp run: yarn lerna run build:bindings:configure --scope @sentry/profiling-node - name: Build Bindings for Current Environment @@ -581,8 +587,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Install Playwright uses: ./.github/actions/install-playwright @@ -596,11 +602,13 @@ jobs: run: yarn test:ci${{ matrix.project && format(' --project={0}', matrix.project) || '' }}${{ matrix.shard && format(' --shard={0}/{1}', matrix.shard, matrix.shards) || '' }} - name: Upload Playwright Traces - uses: actions/upload-artifact@v3 - if: always() + uses: actions/upload-artifact@v4 + if: failure() with: - name: playwright-traces + name: playwright-traces-job_browser_playwright_tests-${{ matrix.bundle}}-${{matrix.project}}-${{matrix.shard || '0'}} path: dev-packages/browser-integration-tests/test-results + overwrite: true + retention-days: 7 job_browser_loader_tests: name: PW ${{ matrix.bundle }} Tests @@ -631,8 +639,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Install Playwright uses: ./.github/actions/install-playwright @@ -646,11 +654,13 @@ jobs: cd dev-packages/browser-integration-tests yarn test:loader - name: Upload Playwright Traces - uses: actions/upload-artifact@v3 - if: always() + uses: actions/upload-artifact@v4 + if: failure() with: - name: playwright-traces + name: playwright-traces-job_browser_loader_tests-${{ matrix.bundle}} path: dev-packages/browser-integration-tests/test-results + overwrite: true + retention-days: 7 job_check_for_faulty_dts: name: Check for faulty .d.ts files @@ -668,8 +678,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Check for dts files that reference stuff in the temporary build folder run: | if grep -r --include "*.d.ts" --exclude-dir ".nxcache" 'import("@sentry(-internal)?/[^/]*/build' .; then @@ -706,8 +716,9 @@ jobs: node-version: ${{ matrix.node }} - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + node_version: ${{ matrix.node == 14 && '14' || '' }} - name: Overwrite typescript version if: matrix.typescript @@ -747,8 +758,8 @@ jobs: node-version: ${{ matrix.node }} - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Install Playwright uses: ./.github/actions/install-playwright @@ -785,8 +796,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: NX cache uses: actions/cache/restore@v4 with: @@ -897,6 +908,8 @@ jobs: 'nestjs-basic', 'nestjs-distributed-tracing', 'nestjs-with-submodules', + 'nestjs-with-submodules-decorator', + 'nestjs-graphql', 'node-exports-test-app', 'node-koa', 'node-connect', @@ -947,15 +960,19 @@ jobs: uses: oven-sh/setup-bun@v2 - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Restore tarball cache uses: actions/cache/restore@v4 + id: restore-tarball-cache with: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} - fail-on-cache-miss: true + + - name: Build tarballs if not cached + if: steps.restore-tarball-cache.outputs.cache-hit != 'true' + run: yarn build:tarball - name: Install Playwright uses: ./.github/actions/install-playwright @@ -1047,15 +1064,19 @@ jobs: node-version-file: 'dev-packages/e2e-tests/package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Restore tarball cache uses: actions/cache/restore@v4 + id: restore-tarball-cache with: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} - fail-on-cache-miss: true + + - name: Build tarballs if not cached + if: steps.restore-tarball-cache.outputs.cache-hit != 'true' + run: yarn build:tarball - name: Install Playwright uses: ./.github/actions/install-playwright @@ -1109,8 +1130,7 @@ jobs: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && ( (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') || - (github.event_name != 'pull_request') + (needs.job_get_metadata.outputs.is_release == 'true') ) needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 @@ -1142,8 +1162,8 @@ jobs: node-version-file: 'dev-packages/e2e-tests/package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + 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 @@ -1152,12 +1172,17 @@ jobs: pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ merge-multiple: true + - name: Restore tarball cache uses: actions/cache/restore@v4 + id: restore-tarball-cache with: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} - fail-on-cache-miss : true + + - name: Build tarballs if not cached + if: steps.restore-tarball-cache.outputs.cache-hit != 'true' + run: yarn build:tarball - name: Install Playwright uses: ./.github/actions/install-playwright @@ -1238,8 +1263,8 @@ jobs: node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Collect run: yarn ci:collect @@ -1260,6 +1285,7 @@ jobs: with: name: ${{ steps.process.outputs.artifactName }} path: ${{ steps.process.outputs.artifactPath }} + retention-days: 7 job_compile_bindings_profiling_node: name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} @@ -1268,8 +1294,7 @@ jobs: # Skip precompile unless we are on a release branch as precompile slows down CI times. if: | (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') || - (github.event_name != 'pull_request') + (needs.job_get_metadata.outputs.is_release == 'true') runs-on: ${{ matrix.os }} container: ${{ matrix.container }} timeout-minutes: 30 diff --git a/.github/workflows/clear-cache.yml b/.github/workflows/clear-cache.yml index 2946723fe6b8..5c327553e3b8 100644 --- a/.github/workflows/clear-cache.yml +++ b/.github/workflows/clear-cache.yml @@ -1,11 +1,43 @@ name: "Action: Clear all GHA caches" on: workflow_dispatch: + inputs: + clear_pending_prs: + description: Delete caches of pending PR workflows + type: boolean + default: false + clear_develop: + description: Delete caches on develop branch + type: boolean + default: false + clear_branches: + description: Delete caches on non-develop branches + type: boolean + default: true + schedule: + # Run every day at midnight + - cron: '0 0 * * *' jobs: clear-caches: name: Delete all caches runs-on: ubuntu-20.04 steps: - - name: Clear caches - uses: easimon/wipe-cache@v2 + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + + # TODO: Use cached version if possible (but never store cache) + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Delete GHA caches + uses: ./dev-packages/clear-cache-gh-action + with: + clear_pending_prs: ${{ inputs.clear_pending_prs }} + clear_develop: ${{ inputs.clear_develop }} + clear_branches: ${{ inputs.clear_branches }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml index 50acb2be8e73..e01a1a66a589 100644 --- a/.github/workflows/external-contributors.yml +++ b/.github/workflows/external-contributors.yml @@ -25,7 +25,6 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: 'package.json' - cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile diff --git a/.size-limit.js b/.size-limit.js index 859ce741cc3d..d2211d91d5b0 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -22,14 +22,14 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '73 KB', + limit: '75 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags', path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '66 KB', + limit: '68 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); config.plugins.push( @@ -170,7 +170,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '111 KB', + limit: '113 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa005af46c5..015c5370a75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,46 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.28.0 + +### Important Changes + +- **Beta release of official NestJS SDK** + +This release contains the beta version of `@sentry/nestjs`! For details on how to use it, check out the +[README](https://github.com/getsentry/sentry-javascript/blob/master/packages/nestjs/README.md). Any feedback/bug reports +are greatly appreciated, please reach out on GitHub. + +- **fix(browser): Remove faulty LCP, FCP and FP normalization logic (#13502)** + +This release fixes a bug in the `@sentry/browser` package and all SDKs depending on this package (e.g. `@sentry/react` +or `@sentry/nextjs`) that caused the SDK to send incorrect web vital values for the LCP, FCP and FP vitals. The SDK +previously incorrectly processed the original values as they were reported from the browser. When updating your SDK to +this version, you might experience an increase in LCP, FCP and FP values, which potentially leads to a decrease in your +performance score in the Web Vitals Insights module in Sentry. This is because the previously reported values were +smaller than the actually measured values. We apologize for the inconvenience! + +### Other Changes + +- feat(nestjs): Add `SentryGlobalGraphQLFilter` (#13545) +- feat(nestjs): Automatic instrumentation of nestjs interceptors after route execution (#13264) +- feat(nextjs): Add `bundleSizeOptimizations` to build options (#13323) +- feat(nextjs): Stabilize `captureRequestError` (#13550) +- feat(nuxt): Wrap config in nuxt context (#13457) +- feat(profiling): Expose profiler as top level primitive (#13512) +- feat(replay): Add layout shift to CLS replay data (#13386) +- feat(replay): Upgrade rrweb packages to 2.26.0 (#13483) +- fix(cdn): Do not mangle \_metadata (#13426) +- fix(cdn): Fix SDK source for CDN bundles (#13475) +- fix(nestjs): Check arguments before instrumenting with `@Injectable` (#13544) +- fix(nestjs): Ensure exception and host are correctly passed on when using @WithSentry (#13564) +- fix(node): Suppress tracing for transport request execution rather than transport creation (#13491) +- fix(replay): Consider more things as DOM mutations for dead clicks (#13518) +- fix(vue): Correctly obtain component name (#13484) + +Work in this release was contributed by @leopoldkristjansson, @mhuggins and @filips123. Thank you for your +contributions! + ## 8.27.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts index 09a10464c22e..4404dac91364 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { SDK_VERSION } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; @@ -11,3 +12,22 @@ sentryTest('captureException works', async ({ getLocalTestUrl, page }) => { expect(eventData.message).toBe('Test exception'); }); + +sentryTest('should capture correct SDK metadata', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForErrorRequestOnUrl(page, url); + + const eventData = envelopeRequestParser(req); + + expect(eventData.sdk).toMatchObject({ + name: 'sentry.javascript.browser', + version: SDK_VERSION, + integrations: expect.any(Object), + packages: [ + { + name: 'loader:@sentry/browser', + version: SDK_VERSION, + }, + ], + }); +}); diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 079025b3ec0a..8c0ca22e0358 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -9,7 +9,7 @@ "private": true, "scripts": { "clean": "rimraf -g suites/**/dist loader-suites/**/dist tmp", - "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && yarn install-browsers || echo 'Skipping browser installation'", + "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && yarn npx playwright install --with-deps || echo 'Skipping browser installation'", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/simpleError/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/simpleError/test.ts index 7e884c6eb6dc..85f001849748 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/simpleError/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/simpleError/test.ts @@ -1,13 +1,13 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; +import { SDK_VERSION } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; -sentryTest('should capture a simple error with message', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); +sentryTest('should capture a simple error with message', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForErrorRequestOnUrl(page, url); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ @@ -22,3 +22,23 @@ sentryTest('should capture a simple error with message', async ({ getLocalTestPa }, }); }); + +sentryTest('should capture correct SDK metadata', async ({ getLocalTestUrl, page }) => { + const isCdn = (process.env.PW_BUNDLE || '').startsWith('bundle'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForErrorRequestOnUrl(page, url); + const eventData = envelopeRequestParser(req); + + expect(eventData.sdk).toEqual({ + name: 'sentry.javascript.browser', + version: SDK_VERSION, + integrations: expect.any(Object), + packages: [ + { + name: `${isCdn ? 'cdn' : 'npm'}:@sentry/browser`, + version: SDK_VERSION, + }, + ], + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html index caf4b8f2deab..502f4dde80c2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html @@ -5,7 +5,7 @@
- + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index f79505c6105a..2e215c728ecf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -11,7 +11,7 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN } page.route('**', route => route.continue()); - page.route('**/path/to/image.png', async (route: Route) => { + page.route('**/my/image.png', async (route: Route) => { return route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/assets/sentry-logo-600x179.png new file mode 100644 index 000000000000..353b7233d6bf Binary files /dev/null and b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/assets/sentry-logo-600x179.png differ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/template.html new file mode 100644 index 000000000000..d4c01b121bf7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/template.html @@ -0,0 +1,11 @@ + + + + + + +
+ + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts new file mode 100644 index 000000000000..3ff09a2862c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts @@ -0,0 +1,81 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +/** + * Bit of an odd test but we previously ran into cases where we would report TTFB > (LCP, FCP, FP) + * This should never happen and this test serves as a regression test for that. + * + * The problem is: We don't always get valid TTFB from the web-vitals library, so we skip the test if that's the case. + * Note: There is another test that covers that we actually report TTFB if it is valid (@see ../web-vitals-lcp/test.ts). + */ +sentryTest('paint web vitals values are greater than TTFB', async ({ browserName, getLocalTestPath, page }) => { + // Only run in chromium to ensure all vitals are present + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + page.route('**', route => route.continue()); + page.route('**/library/image.png', async (route: Route) => { + return route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + const [eventData] = await Promise.all([ + getFirstSentryEnvelopeRequest(page), + page.goto(url), + page.locator('button').click(), + ]); + + expect(eventData.measurements).toBeDefined(); + + const ttfbValue = eventData.measurements?.ttfb?.value; + + if (!ttfbValue) { + // TTFB is unfortunately quite flaky. Sometimes, the web-vitals library doesn't report TTFB because + // responseStart is 0. This seems to happen somewhat randomly, so we just ignore this in that case. + // @see packages/browser-utils/src/metrics/web-vitals/onTTFB + + // logging the skip reason so that we at least can check for that in CI logs + // eslint-disable-next-line no-console + console.log('SKIPPING: TTFB is not reported'); + sentryTest.skip(); + } + + const lcpValue = eventData.measurements?.lcp?.value; + const fcpValue = eventData.measurements?.fcp?.value; + const fpValue = eventData.measurements?.fp?.value; + + expect(lcpValue).toBeDefined(); + expect(fcpValue).toBeDefined(); + expect(fpValue).toBeDefined(); + + // (LCP, FCP, FP) >= TTFB + expect(lcpValue).toBeGreaterThanOrEqual(ttfbValue!); + expect(fcpValue).toBeGreaterThanOrEqual(ttfbValue!); + expect(fpValue).toBeGreaterThanOrEqual(ttfbValue!); +}); + +sentryTest('captures time origin as span attribute', async ({ getLocalTestPath, page }) => { + // Only run in chromium to ensure all vitals are present + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest(page), page.goto(url)]); + + const timeOriginAttribute = eventData.contexts?.trace?.data?.['performance.timeOrigin']; + const transactionStartTimestamp = eventData.start_timestamp; + + expect(timeOriginAttribute).toBeDefined(); + expect(transactionStartTimestamp).toBeDefined(); + + const delta = Math.abs(transactionStartTimestamp! - timeOriginAttribute); + + // The delta should be less than 1ms if this flakes, we should increase the threshold + expect(delta).toBeLessThanOrEqual(1); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 30939c40c955..9189ce63812f 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -4,7 +4,7 @@ import type { Package } from '@sentry/types'; import HtmlWebpackPlugin, { createHtmlTagObject } from 'html-webpack-plugin'; import type { Compiler } from 'webpack'; -import { addStaticAsset, addStaticAssetSymlink } from './staticAssets'; +import { addStaticAsset, symlinkAsset } from './staticAssets'; const LOADER_TEMPLATE = fs.readFileSync(path.join(__dirname, '../fixtures/loader.js'), 'utf-8'); const PACKAGES_DIR = path.join(__dirname, '..', '..', '..', 'packages'); @@ -214,7 +214,10 @@ class SentryScenarioGenerationPlugin { src: 'cdn.bundle.js', }); - addStaticAssetSymlink(this.localOutPath, path.resolve(PACKAGES_DIR, bundleName, bundlePath), 'cdn.bundle.js'); + symlinkAsset( + path.resolve(PACKAGES_DIR, bundleName, bundlePath), + path.join(this.localOutPath, 'cdn.bundle.js'), + ); if (useLoader) { const loaderConfig = LOADER_CONFIGS[bundleKey]; @@ -245,14 +248,13 @@ class SentryScenarioGenerationPlugin { const fileName = `${integration}.bundle.js`; // We add the files, but not a script tag - they are lazy-loaded - addStaticAssetSymlink( - this.localOutPath, + symlinkAsset( path.resolve( PACKAGES_DIR, 'feedback', BUNDLE_PATHS['feedback']?.[integrationBundleKey]?.replace('[INTEGRATION_NAME]', integration) || '', ), - fileName, + path.join(this.localOutPath, fileName), ); }); } @@ -262,26 +264,23 @@ class SentryScenarioGenerationPlugin { if (baseIntegrationFileName) { this.requiredIntegrations.forEach(integration => { const fileName = `${integration}.bundle.js`; - addStaticAssetSymlink( - this.localOutPath, + symlinkAsset( path.resolve( PACKAGES_DIR, 'browser', baseIntegrationFileName.replace('[INTEGRATION_NAME]', integration), ), - fileName, + path.join(this.localOutPath, fileName), ); if (integration === 'feedback') { - addStaticAssetSymlink( - this.localOutPath, + symlinkAsset( path.resolve(PACKAGES_DIR, 'feedback', 'build/bundles/feedback-modal.js'), - 'feedback-modal.bundle.js', + path.join(this.localOutPath, 'feedback-modal.bundle.js'), ); - addStaticAssetSymlink( - this.localOutPath, + symlinkAsset( path.resolve(PACKAGES_DIR, 'feedback', 'build/bundles/feedback-screenshot.js'), - 'feedback-screenshot.bundle.js', + path.join(this.localOutPath, 'feedback-screenshot.bundle.js'), ); } @@ -295,10 +294,9 @@ class SentryScenarioGenerationPlugin { const baseWasmFileName = BUNDLE_PATHS['wasm']?.[integrationBundleKey]; if (this.requiresWASMIntegration && baseWasmFileName) { - addStaticAssetSymlink( - this.localOutPath, + symlinkAsset( path.resolve(PACKAGES_DIR, 'wasm', baseWasmFileName), - 'wasm.bundle.js', + path.join(this.localOutPath, 'wasm.bundle.js'), ); const wasmObject = createHtmlTagObject('script', { diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts index f4defc27182c..e711ea3bb0bb 100644 --- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -141,6 +141,7 @@ export const expectedCLSPerformanceSpan = { data: { value: expect.any(Number), nodeIds: expect.any(Array), + attributions: expect.any(Array), rating: expect.any(String), size: expect.any(Number), }, diff --git a/dev-packages/browser-integration-tests/utils/staticAssets.ts b/dev-packages/browser-integration-tests/utils/staticAssets.ts index 447a3ad337f7..81c18eec1dcf 100644 --- a/dev-packages/browser-integration-tests/utils/staticAssets.ts +++ b/dev-packages/browser-integration-tests/utils/staticAssets.ts @@ -22,23 +22,11 @@ export function addStaticAsset(localOutPath: string, fileName: string, cb: () => symlinkAsset(newPath, path.join(localOutPath, fileName)); } -export function addStaticAssetSymlink(localOutPath: string, originalPath: string, fileName: string): void { - const newPath = path.join(STATIC_DIR, fileName); - - // Only copy files once - if (!fs.existsSync(newPath)) { - fs.symlinkSync(originalPath, newPath); - } - - symlinkAsset(newPath, path.join(localOutPath, fileName)); -} - -function symlinkAsset(originalPath: string, targetPath: string): void { +export function symlinkAsset(originalPath: string, targetPath: string): void { try { - fs.unlinkSync(targetPath); + fs.linkSync(originalPath, targetPath); } catch { - // ignore errors here + // ignore errors here, probably means the file already exists + // Since we always build into a new directory for each test, we can safely ignore this } - - fs.linkSync(originalPath, targetPath); } diff --git a/dev-packages/clear-cache-gh-action/.eslintrc.cjs b/dev-packages/clear-cache-gh-action/.eslintrc.cjs new file mode 100644 index 000000000000..8c67e0037908 --- /dev/null +++ b/dev-packages/clear-cache-gh-action/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, + + overrides: [ + { + files: ['*.mjs'], + extends: ['@sentry-internal/sdk/src/base'], + }, + ], +}; diff --git a/dev-packages/clear-cache-gh-action/action.yml b/dev-packages/clear-cache-gh-action/action.yml new file mode 100644 index 000000000000..06493534b23e --- /dev/null +++ b/dev-packages/clear-cache-gh-action/action.yml @@ -0,0 +1,25 @@ +name: 'clear-cache-gh-action' +description: 'Clear caches of the GitHub repository.' +inputs: + github_token: + required: true + description: 'a github access token' + clear_develop: + required: false + default: "" + description: "If set, also clear caches from develop branch." + clear_branches: + required: false + default: "" + description: "If set, also clear caches from non-develop branches." + clear_pending_prs: + required: false + default: "" + description: "If set, also clear caches from pending PR workflow runs." + workflow_name: + required: false + default: "CI: Build & Test" + description: The workflow to clear caches for. +runs: + using: 'node20' + main: 'index.mjs' diff --git a/dev-packages/clear-cache-gh-action/index.mjs b/dev-packages/clear-cache-gh-action/index.mjs new file mode 100644 index 000000000000..b1cb75c5a5c0 --- /dev/null +++ b/dev-packages/clear-cache-gh-action/index.mjs @@ -0,0 +1,183 @@ +import * as core from '@actions/core'; + +import { context, getOctokit } from '@actions/github'; + +async function run() { + const { getInput } = core; + + const { repo, owner } = context.repo; + + const githubToken = getInput('github_token'); + const clearDevelop = inputToBoolean(getInput('clear_develop', { type: 'boolean' })); + const clearBranches = inputToBoolean(getInput('clear_branches', { type: 'boolean', default: true })); + const clearPending = inputToBoolean(getInput('clear_pending_prs', { type: 'boolean' })); + const workflowName = getInput('workflow_name'); + + const octokit = getOctokit(githubToken); + + await clearGithubCaches(octokit, { + repo, + owner, + clearDevelop, + clearPending, + clearBranches, + workflowName, + }); +} + +/** + * Clear caches. + * + * @param {ReturnType } octokit + * @param {{repo: string, owner: string, clearDevelop: boolean, clearPending: boolean, clearBranches: boolean, workflowName: string}} options + */ +async function clearGithubCaches(octokit, { repo, owner, clearDevelop, clearPending, clearBranches, workflowName }) { + let deletedCaches = 0; + let remainingCaches = 0; + + let deletedSize = 0; + let remainingSize = 0; + + /** @type {Map>} */ + const cachedPrs = new Map(); + /** @type {Map>} */ + const cachedWorkflows = new Map(); + + /** + * Clear caches. + * + * @param {{ref: string}} options + */ + const shouldClearCache = async ({ ref }) => { + // Do not clear develop caches if clearDevelop is false. + if (!clearDevelop && ref === 'refs/heads/develop') { + core.info('> Keeping cache because it is on develop.'); + return false; + } + + // There are two fundamental paths here: + // If the cache belongs to a PR, we need to check if the PR has any pending workflows. + // Else, we assume the cache belongs to a branch, where we do not check for pending workflows + const pullNumber = /^refs\/pull\/(\d+)\/merge$/.exec(ref)?.[1]; + const isPr = !!pullNumber; + + // Case 1: This is a PR, and we do not want to clear pending PRs + // In this case, we need to fetch all PRs and workflow runs to check them + if (isPr && !clearPending) { + const pr = + cachedPrs.get(pullNumber) || + (await octokit.rest.pulls.get({ + owner, + repo, + pull_number: pullNumber, + })); + cachedPrs.set(pullNumber, pr); + + const prBranch = pr.data.head.ref; + + // Check if PR has any pending workflows + const workflowRuns = + cachedWorkflows.get(prBranch) || + (await octokit.rest.actions.listWorkflowRunsForRepo({ + repo, + owner, + branch: prBranch, + })); + cachedWorkflows.set(prBranch, workflowRuns); + + // We only care about the relevant workflow + const relevantWorkflowRuns = workflowRuns.data.workflow_runs.filter(workflow => workflow.name === workflowName); + + const latestWorkflowRun = relevantWorkflowRuns[0]; + + core.info(`> Latest relevant workflow run: ${latestWorkflowRun.html_url}`); + + // No relevant workflow? Clear caches! + if (!latestWorkflowRun) { + core.info('> Clearing cache because no relevant workflow was found.'); + return true; + } + + // If the latest run was not successful, keep caches + // as either the run may be in progress, + // or failed - in which case we may want to re-run the workflow + if (latestWorkflowRun.conclusion !== 'success') { + core.info(`> Keeping cache because latest workflow is ${latestWorkflowRun.conclusion}.`); + return false; + } + + core.info(`> Clearing cache because latest workflow run is ${latestWorkflowRun.conclusion}.`); + return true; + } + + // Case 2: This is a PR, but we do want to clear pending PRs + // In this case, this cache should always be cleared + if (isPr) { + core.info('> Clearing cache of every PR workflow run.'); + return true; + } + + // Case 3: This is not a PR, and we want to clean branches + if (clearBranches) { + core.info('> Clearing cache because it is not a PR.'); + return true; + } + + // Case 4: This is not a PR, and we do not want to clean branches + core.info('> Keeping cache for non-PR workflow run.'); + return false; + }; + + for await (const response of octokit.paginate.iterator(octokit.rest.actions.getActionsCacheList, { + owner, + repo, + })) { + if (!response.data.length) { + break; + } + + for (const { id, ref, size_in_bytes } of response.data) { + core.info(`Checking cache ${id} for ${ref}...`); + + const shouldDelete = await shouldClearCache({ ref }); + + if (shouldDelete) { + core.info(`> Clearing cache ${id}...`); + + deletedCaches++; + deletedSize += size_in_bytes; + + await octokit.rest.actions.deleteActionsCacheById({ + owner, + repo, + cache_id: id, + }); + } else { + remainingCaches++; + remainingSize += size_in_bytes; + } + } + } + + const format = new Intl.NumberFormat('en-US', { + style: 'decimal', + }); + + core.info('Summary:'); + core.info(`Deleted ${deletedCaches} caches, freeing up ~${format.format(deletedSize / 1000 / 1000)} mb.`); + core.info(`Remaining ${remainingCaches} caches, using ~${format.format(remainingSize / 1000 / 1000)} mb.`); +} + +run(); + +function inputToBoolean(input) { + if (typeof input === 'boolean') { + return input; + } + + if (typeof input === 'string') { + return input === 'true'; + } + + return false; +} diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json new file mode 100644 index 000000000000..492f4fc2b31e --- /dev/null +++ b/dev-packages/clear-cache-gh-action/package.json @@ -0,0 +1,23 @@ +{ + "name": "@sentry-internal/clear-cache-gh-action", + "description": "An internal Github Action to clear GitHub caches.", + "version": "8.26.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "main": "index.mjs", + "type": "module", + "scripts": { + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix" + }, + "dependencies": { + "@actions/core": "1.10.1", + "@actions/github": "^5.0.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts index b7b770d940dd..9dcf71e9af7f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts @@ -19,6 +19,10 @@ test('Sends parameterized transaction name to Sentry', async ({ page }) => { }); test('Sends form data with action span to Sentry', async ({ page }) => { + const formdataActionTransaction = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { + return transactionEvent?.spans?.some(span => span.op === 'function.remix.action'); + }); + await page.goto('/action-formdata'); await page.fill('input[name=text]', 'test'); @@ -30,10 +34,6 @@ test('Sends form data with action span to Sentry', async ({ page }) => { await page.locator('button[type=submit]').click(); - const formdataActionTransaction = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { - return transactionEvent?.spans?.some(span => span.op === 'function.remix.action'); - }); - const actionSpan = (await formdataActionTransaction).spans.find(span => span.op === 'function.remix.action'); expect(actionSpan).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts index 9f72da47d67f..b43d538ca683 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts @@ -19,6 +19,10 @@ test('Sends parameterized transaction name to Sentry', async ({ page }) => { }); test('Sends form data with action span', async ({ page }) => { + const formdataActionTransaction = waitForTransaction('create-remix-app-express', transactionEvent => { + return transactionEvent?.spans?.some(span => span.data && span.data['code.function'] === 'action'); + }); + await page.goto('/action-formdata'); await page.fill('input[name=text]', 'test'); @@ -30,10 +34,6 @@ test('Sends form data with action span', async ({ page }) => { await page.locator('button[type=submit]').click(); - const formdataActionTransaction = waitForTransaction('create-remix-app-express', transactionEvent => { - return transactionEvent?.spans?.some(span => span.data && span.data['code.function'] === 'action'); - }); - const actionSpan = (await formdataActionTransaction).spans.find( span => span.data && span.data['code.function'] === 'action', ); 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 ed70d21cae2b..bc61532b71fa 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.0.2", "vitest": "^0.34.6", - "@sentry/rollup-plugin": "2.14.2" + "@sentry/rollup-plugin": "2.22.3" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts index 9cda3c96f9a6..75308e8f0ea9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts @@ -1,11 +1,13 @@ 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'; -import { ExampleInterceptor } from './example.interceptor'; @Controller() @UseFilters(ExampleLocalFilter) @@ -29,11 +31,17 @@ export class AppController { } @Get('test-interceptor-instrumentation') - @UseInterceptors(ExampleInterceptor) + @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 }; @@ -88,4 +96,24 @@ export class AppController { 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(); + } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts index f1c935257013..3e4639040a7e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts @@ -78,4 +78,20 @@ export class AppService { 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-basic/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/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-basic/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/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-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example-2.interceptor.ts similarity index 65% rename from dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts rename to dev-packages/e2e-tests/test-applications/nestjs-basic/src/example-2.interceptor.ts index 75c301b4cffc..2cf9dfb9e043 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example-2.interceptor.ts @@ -2,9 +2,9 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes import * as Sentry from '@sentry/nestjs'; @Injectable() -export class ExampleInterceptor implements NestInterceptor { +export class ExampleInterceptor2 implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler) { - Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); + Sentry.startSpan({ name: 'test-interceptor-span-2' }, () => {}); return next.handle().pipe(); } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts index 555b6357ade8..a9c8ffea9117 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts @@ -341,7 +341,7 @@ test('API route transaction includes nest pipe span for invalid request', async ); }); -test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ +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-basic', transactionEvent => { @@ -356,6 +356,7 @@ test('API route transaction includes nest interceptor span. Spans created in and const transactionEvent = await pageloadTransactionEventPromise; + // check if interceptor spans before route execution exist expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ @@ -366,7 +367,22 @@ test('API route transaction includes nest interceptor span. Spans created in and 'sentry.op': 'middleware.nestjs', 'sentry.origin': 'auto.middleware.nestjs', }, - description: 'ExampleInterceptor', + 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), @@ -378,9 +394,13 @@ test('API route transaction includes nest interceptor span. Spans created in and }), ); - const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); - const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + // 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([ @@ -399,7 +419,105 @@ test('API route transaction includes nest interceptor span. Spans created in and span_id: expect.any(String), trace_id: expect.any(String), data: expect.any(Object), - description: 'test-interceptor-span', + 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-basic', 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), @@ -411,12 +529,201 @@ test('API route transaction includes nest interceptor span. Spans created in and ); // verify correct span parent-child relationships - const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-interceptor-span-after-route', + ); const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - // 'ExampleInterceptor' is the parent of 'test-interceptor-span' - expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + // '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); +}); - // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +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-basic', 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-basic', 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-graphql/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-graphql/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/.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-graphql/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-graphql/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/.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-graphql/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/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-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json new file mode 100644 index 000000000000..7981c64e0b2a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json @@ -0,0 +1,50 @@ +{ + "name": "nestjs-graphql", + "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": { + "@apollo/server": "^4.10.4", + "@nestjs/apollo": "^12.2.0", + "@nestjs/common": "^10.3.10", + "@nestjs/core": "^10.3.10", + "@nestjs/graphql": "^12.2.0", + "@nestjs/platform-express": "^10.3.10", + "@sentry/nestjs": "^8.21.0", + "graphql": "^16.9.0", + "reflect-metadata": "^0.1.13", + "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-graphql/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-graphql/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/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-graphql/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/app.module.ts new file mode 100644 index 000000000000..4cfc2ebd33e4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/app.module.ts @@ -0,0 +1,30 @@ +import { ApolloDriver } from '@nestjs/apollo'; +import { Logger, Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { GraphQLModule } from '@nestjs/graphql'; +import { SentryGlobalGraphQLFilter, SentryModule } from '@sentry/nestjs/setup'; +import { AppResolver } from './app.resolver'; + +@Module({ + imports: [ + SentryModule.forRoot(), + GraphQLModule.forRoot({ + driver: ApolloDriver, + autoSchemaFile: true, + playground: true, // sets up a playground on https://localhost:3000/graphql + }), + ], + controllers: [], + providers: [ + AppResolver, + { + provide: APP_FILTER, + useClass: SentryGlobalGraphQLFilter, + }, + { + provide: Logger, + useClass: Logger, + }, + ], +}) +export class AppModule {} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/app.resolver.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/app.resolver.ts new file mode 100644 index 000000000000..0e4dfc643918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/app.resolver.ts @@ -0,0 +1,14 @@ +import { Query, Resolver } from '@nestjs/graphql'; + +@Resolver() +export class AppResolver { + @Query(() => String) + test(): string { + return 'Test endpoint!'; + } + + @Query(() => String) + error(): string { + throw new Error('This is an exception!'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts new file mode 100644 index 000000000000..f1f4de865435 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/instrument.ts @@ -0,0 +1,8 @@ +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, +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/src/main.ts new file mode 100644 index 000000000000..71ce685f4d61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/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-graphql/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-graphql/start-event-proxy.mjs new file mode 100644 index 000000000000..62fff27d8500 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-graphql', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts new file mode 100644 index 000000000000..48e0ef8c2c9f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tests/errors.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends exception to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-graphql', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception!'; + }); + + const response = await fetch(`${baseURL}/graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `query { error }`, + }), + }); + + const json_response = await response.json(); + const errorEvent = await errorEventPromise; + + expect(json_response?.errors[0]).toEqual({ + message: 'This is an exception!', + locations: expect.any(Array), + path: ['error'], + extensions: { + code: 'INTERNAL_SERVER_ERROR', + stacktrace: expect.any(Array), + }, + }); + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception!'); + + expect(errorEvent.request).toEqual({ + method: 'POST', + cookies: {}, + data: '{"query":"query { error }"}', + headers: expect.any(Object), + url: 'http://localhost:3030/graphql', + }); + + expect(errorEvent.transaction).toEqual('POST /graphql'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tsconfig.build.json new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/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-graphql/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/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/nextjs-13/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts index 1f889238427c..180685d41b4a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts @@ -15,3 +15,5 @@ export function register() { }); } } + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts index 0999fdd8e089..2dd920dfcc73 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/instrumentation.ts @@ -19,3 +19,5 @@ export function register() { }); } } + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts index ca4a213e58ba..964f937c439a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts @@ -10,4 +10,4 @@ export async function register() { } } -export const onRequestError = Sentry.experimental_captureRequestError; +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts index cd269ab160e7..a95bb9ee95ee 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/instrumentation.ts @@ -15,3 +15,5 @@ export function register() { }); } } + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts index ec0a921da2c4..70c734c61d73 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts @@ -1,8 +1,10 @@ import { Controller, Get, Param, ParseIntPipe, 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 { ExampleGuard } from './example.guard'; -import { ExampleInterceptor } from './example.interceptor'; @Controller() export class AppController { @@ -25,11 +27,17 @@ export class AppController { } @Get('test-interceptor-instrumentation') - @UseInterceptors(ExampleInterceptor) + @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 }; @@ -74,4 +82,24 @@ export class AppController { async flush() { await flush(); } + + @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(); + } } diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts index f1c935257013..3e4639040a7e 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts @@ -78,4 +78,20 @@ export class AppService { 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/node-nestjs-basic/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/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/node-nestjs-basic/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/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/node-nestjs-basic/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example-2.interceptor.ts new file mode 100644 index 000000000000..2cf9dfb9e043 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/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/node-nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts deleted file mode 100644 index 260c1798449f..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import * as Sentry from '@sentry/nestjs'; -import { Observable } from 'rxjs'; - -@Injectable() -export class ExampleInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); - return next.handle().pipe(); - } -} diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts index cb04bc06839e..23855d1f55b8 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts @@ -339,7 +339,7 @@ test('API route transaction includes nest pipe span for invalid request', async ); }); -test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ +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('node-nestjs-basic', transactionEvent => { @@ -354,6 +354,7 @@ test('API route transaction includes nest interceptor span. Spans created in and const transactionEvent = await pageloadTransactionEventPromise; + // check if interceptor spans before route execution exist expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ @@ -364,7 +365,22 @@ test('API route transaction includes nest interceptor span. Spans created in and 'sentry.op': 'middleware.nestjs', 'sentry.origin': 'auto.middleware.nestjs', }, - description: 'ExampleInterceptor', + 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), @@ -376,9 +392,13 @@ test('API route transaction includes nest interceptor span. Spans created in and }), ); - const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); - const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + // 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([ @@ -397,7 +417,105 @@ test('API route transaction includes nest interceptor span. Spans created in and span_id: expect.any(String), trace_id: expect.any(String), data: expect.any(Object), - description: 'test-interceptor-span', + 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('node-nestjs-basic', 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), @@ -409,12 +527,201 @@ test('API route transaction includes nest interceptor span. Spans created in and ); // verify correct span parent-child relationships - const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testInterceptorSpanAfterRoute = transactionEvent.spans.find( + span => span.description === 'test-interceptor-span-after-route', + ); const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); - // 'ExampleInterceptor' is the parent of 'test-interceptor-span' - expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + // '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); +}); - // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' - expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +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('node-nestjs-basic', 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('node-nestjs-basic', 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/nuxt-3/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts index 69b31a4214ec..0fcccd560af9 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts @@ -4,4 +4,11 @@ export default defineNuxtConfig({ imports: { autoImport: false, }, + runtimeConfig: { + public: { + sentry: { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }, + }, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.client.config.ts index 5253d08c90f0..5c4e0f892ca8 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.client.config.ts @@ -1,8 +1,9 @@ import * as Sentry from '@sentry/nuxt'; +import { useRuntimeConfig } from '#imports'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions - dsn: 'https://public@dsn.ingest.sentry.io/1337', + dsn: useRuntimeConfig().public.sentry.dsn, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts index 10442a4a2bde..861b6c420fbb 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts @@ -22,6 +22,7 @@ test('Captures a pageload transaction', async ({ page }) => { 'sentry.origin': 'auto.pageload.react.reactrouter_v6', 'sentry.sample_rate': 1, 'sentry.source': 'route', + 'performance.timeOrigin': expect.any(Number), }, op: 'pageload', span_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts index e7fd943c0f08..f65efa7cca2d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts @@ -219,7 +219,7 @@ export const ReplayRecordingData = [ data: { value: expect.any(Number), size: expect.any(Number), - nodeId: 16, + nodeIds: [16], }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts index 1b054c099b3d..76abdb1fa6b5 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts @@ -240,6 +240,7 @@ export const ReplayRecordingData = [ size: expect.any(Number), rating: expect.any(String), nodeIds: expect.any(Array), + attributions: expect.any(Array), }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.client.test.ts index 52d9cb219401..2e5df36817ed 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/performance.client.test.ts @@ -46,7 +46,9 @@ test('sends a navigation transaction', async ({ page }) => { }); }); -test('updates the transaction when using the back button', async ({ page }) => { +// TODO: This test is flaky as of now, so disabling it. +// It often just times out on CI +test.skip('updates the transaction when using the back button', async ({ page }) => { // Solid Router sends a `-1` navigation when using the back button. // The sentry solidRouterBrowserTracingIntegration tries to update such // transactions with the proper name once the `useLocation` hook triggers. diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock index 67441f720060..75a09184cb81 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock @@ -150,6 +150,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.44.1": + version "1.46.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.1.tgz#a8dfdcd623c4c23bb1b7ea588058aad41055c188" + integrity sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA== + dependencies: + playwright "1.46.1" + "@rollup/rollup-android-arm-eabi@4.18.0": version "4.18.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" @@ -268,6 +275,9 @@ "@sentry/types" "8.4.0" "@sentry/utils" "8.4.0" +"@sentry-internal/test-utils@link:../../../test-utils": + version "8.26.0" + "@sentry/browser@8.4.0": version "8.4.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.4.0.tgz#f4aa381eab212432d71366884693a36c2e3a1675" @@ -398,10 +408,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.31.16.tgz#6b4947e967af3173ce4929d54d9cb97234646e32" integrity sha512-rahAZXlR879P7dngDH7BZwGYiODA9D5Hqo6nUHn9GAURcqZU5IW0ZiT54dPtV5EPES7muZZmknReYueDHs7FFQ== -"@tanstack/react-router@1.34.3": - version "1.34.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.34.3.tgz#1152ca7deaaec93f519971911a816bc42011b6ba" - integrity sha512-0pB+4qnp+5snHu5T93gvFmLcpXuIfSEhXZG7ipf3Rq8kmv4y0DGwcbvgFLiYKSZ/3x58faE8U0MD6F9vR9JUtg== +"@tanstack/react-router@1.34.5": + version "1.34.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-1.34.5.tgz#2c5bc5cd6b246f830ce586c51a87f95352481957" + integrity sha512-mOMbNHSJ1cAgRuJj9W35wteQL7zFiCNJYgg3QHkxj+obO9zQWiAwycFs0hQTRxqzGfC+jhVLJe1+cW93BhqKyA== dependencies: "@tanstack/history" "1.31.16" "@tanstack/react-store" "^0.2.1" @@ -636,6 +646,11 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -712,9 +727,9 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" @@ -756,6 +771,20 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +playwright-core@1.46.1: + version "1.46.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.1.tgz#28f3ab35312135dda75b0c92a3e5c0e7edb9cc8b" + integrity sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A== + +playwright@1.46.1: + version "1.46.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.1.tgz#ea562bc48373648e10420a10c16842f0b227c218" + integrity sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng== + dependencies: + playwright-core "1.46.1" + optionalDependencies: + fsevents "2.3.2" + postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index 8d494986ab3b..2b93c4d772bc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -1,7 +1,7 @@ import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; // When running docker compose, we need a larger timeout, as this takes some time... -jest.setTimeout(75000); +jest.setTimeout(90000); describe('redis cache auto instrumentation', () => { afterAll(() => { diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index cb4ab58347e7..76c19074ed9c 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -124,7 +124,7 @@ async function runDockerCompose(options: DockerOptions): Promise { const timeout = setTimeout(() => { close(); reject(new Error('Timed out waiting for docker-compose')); - }, 60_000); + }, 75_000); function newData(data: Buffer): void { const text = data.toString('utf8'); diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 410d0847d928..1a855e5674b7 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -19,7 +19,6 @@ import { makeImportMetaUrlReplacePlugin, makeNodeResolvePlugin, makeRrwebBuildPlugin, - makeSetSDKSourcePlugin, makeSucrasePlugin, } from './plugins/index.mjs'; import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; @@ -45,7 +44,6 @@ export function makeBaseNPMConfig(options = {}) { const importMetaUrlReplacePlugin = makeImportMetaUrlReplacePlugin(); const cleanupPlugin = makeCleanupPlugin(); const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); - const setSdkSourcePlugin = makeSetSDKSourcePlugin('npm'); const rrwebBuildPlugin = makeRrwebBuildPlugin({ excludeShadowDom: undefined, excludeIframe: undefined, @@ -106,7 +104,6 @@ export function makeBaseNPMConfig(options = {}) { plugins: [ nodeResolvePlugin, - setSdkSourcePlugin, sucrasePlugin, debugBuildStatementReplacePlugin, importMetaUrlReplacePlugin, diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index ffc24f58174a..a3e25c232479 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -63,8 +63,9 @@ export function makeIsDebugBuildPlugin(includeDebugging) { export function makeSetSDKSourcePlugin(sdkSource) { return replace({ preventAssignment: false, + delimiters: ['', ''], values: { - __SENTRY_SDK_SOURCE__: JSON.stringify(sdkSource), + '/* __SENTRY_SDK_SOURCE__ */': `return ${JSON.stringify(sdkSource)};`, }, }); } @@ -116,6 +117,8 @@ export function makeTerserPlugin() { '_integrations', // _meta is used to store metadata of replay network events '_meta', + // We store SDK metadata in the options + '_metadata', // Object we inject debug IDs into with bundler plugins '_sentryDebugIds', // These are used by instrument.ts in utils for identifying HTML elements & events diff --git a/package.json b/package.json index beba7d79d284..4b9ad0383c02 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "dev-packages/overhead-metrics", "dev-packages/test-utils", "dev-packages/size-limit-gh-action", + "dev-packages/clear-cache-gh-action", "dev-packages/external-contributor-gh-action", "dev-packages/rollup-utils" ], diff --git a/packages/astro/package.json b/packages/astro/package.json index 4b7c80993dfb..82c73d8a5a0a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -61,7 +61,7 @@ "@sentry/node": "8.27.0", "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", - "@sentry/vite-plugin": "^2.20.1" + "@sentry/vite-plugin": "^2.22.3" }, "devDependencies": { "astro": "^3.5.0", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index be3f002dcbb8..0895eb86e364 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -123,6 +123,7 @@ export { withMonitor, withScope, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 7b05f8df3a86..27f48c2da0db 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -107,6 +107,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/browser-utils/src/instrument/xhr.ts b/packages/browser-utils/src/instrument/xhr.ts index 5b7a9c261092..c46662bf7c16 100644 --- a/packages/browser-utils/src/instrument/xhr.ts +++ b/packages/browser-utils/src/instrument/xhr.ts @@ -1,6 +1,6 @@ -import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, WrappedFunction } from '@sentry/types'; +import type { HandlerDataXhr, SentryWrappedXMLHttpRequest } from '@sentry/types'; -import { addHandler, fill, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils'; +import { addHandler, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils'; import { WINDOW } from '../types'; export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__'; @@ -29,20 +29,21 @@ export function instrumentXHR(): void { const xhrproto = XMLHttpRequest.prototype; - fill(xhrproto, 'open', function (originalOpen: () => void): () => void { - return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/unbound-method + xhrproto.open = new Proxy(xhrproto.open, { + apply(originalOpen, xhrOpenThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, xhrOpenArgArray) { const startTimestamp = timestampInSeconds() * 1000; // open() should always be called with two or more arguments // But to be on the safe side, we actually validate this and bail out if we don't have a method & url - const method = isString(args[0]) ? args[0].toUpperCase() : undefined; - const url = parseUrl(args[1]); + const method = isString(xhrOpenArgArray[0]) ? xhrOpenArgArray[0].toUpperCase() : undefined; + const url = parseUrl(xhrOpenArgArray[1]); if (!method || !url) { - return originalOpen.apply(this, args); + return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray); } - this[SENTRY_XHR_DATA_KEY] = { + xhrOpenThisArg[SENTRY_XHR_DATA_KEY] = { method, url, request_headers: {}, @@ -50,22 +51,22 @@ export function instrumentXHR(): void { // if Sentry key appears in URL, don't capture it as a request if (method === 'POST' && url.match(/sentry_key/)) { - this.__sentry_own_request__ = true; + xhrOpenThisArg.__sentry_own_request__ = true; } const onreadystatechangeHandler: () => void = () => { // For whatever reason, this is not the same instance here as from the outer method - const xhrInfo = this[SENTRY_XHR_DATA_KEY]; + const xhrInfo = xhrOpenThisArg[SENTRY_XHR_DATA_KEY]; if (!xhrInfo) { return; } - if (this.readyState === 4) { + if (xhrOpenThisArg.readyState === 4) { try { // touching statusCode in some platforms throws // an exception - xhrInfo.status_code = this.status; + xhrInfo.status_code = xhrOpenThisArg.status; } catch (e) { /* do nothing */ } @@ -73,64 +74,69 @@ export function instrumentXHR(): void { const handlerData: HandlerDataXhr = { endTimestamp: timestampInSeconds() * 1000, startTimestamp, - xhr: this, + xhr: xhrOpenThisArg, }; triggerHandlers('xhr', handlerData); } }; - if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') { - fill(this, 'onreadystatechange', function (original: WrappedFunction) { - return function (this: SentryWrappedXMLHttpRequest, ...readyStateArgs: unknown[]): void { + if ('onreadystatechange' in xhrOpenThisArg && typeof xhrOpenThisArg.onreadystatechange === 'function') { + xhrOpenThisArg.onreadystatechange = new Proxy(xhrOpenThisArg.onreadystatechange, { + apply(originalOnreadystatechange, onreadystatechangeThisArg, onreadystatechangeArgArray: unknown[]) { onreadystatechangeHandler(); - return original.apply(this, readyStateArgs); - }; + return originalOnreadystatechange.apply(onreadystatechangeThisArg, onreadystatechangeArgArray); + }, }); } else { - this.addEventListener('readystatechange', onreadystatechangeHandler); + xhrOpenThisArg.addEventListener('readystatechange', onreadystatechangeHandler); } // Intercepting `setRequestHeader` to access the request headers of XHR instance. // This will only work for user/library defined headers, not for the default/browser-assigned headers. // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`. - fill(this, 'setRequestHeader', function (original: WrappedFunction) { - return function (this: SentryWrappedXMLHttpRequest, ...setRequestHeaderArgs: unknown[]): void { - const [header, value] = setRequestHeaderArgs; + xhrOpenThisArg.setRequestHeader = new Proxy(xhrOpenThisArg.setRequestHeader, { + apply( + originalSetRequestHeader, + setRequestHeaderThisArg: SentryWrappedXMLHttpRequest, + setRequestHeaderArgArray: unknown[], + ) { + const [header, value] = setRequestHeaderArgArray; - const xhrInfo = this[SENTRY_XHR_DATA_KEY]; + const xhrInfo = setRequestHeaderThisArg[SENTRY_XHR_DATA_KEY]; if (xhrInfo && isString(header) && isString(value)) { xhrInfo.request_headers[header.toLowerCase()] = value; } - return original.apply(this, setRequestHeaderArgs); - }; + return originalSetRequestHeader.apply(setRequestHeaderThisArg, setRequestHeaderArgArray); + }, }); - return originalOpen.apply(this, args); - }; + return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray); + }, }); - fill(xhrproto, 'send', function (originalSend: () => void): () => void { - return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: unknown[]): void { - const sentryXhrData = this[SENTRY_XHR_DATA_KEY]; + // eslint-disable-next-line @typescript-eslint/unbound-method + xhrproto.send = new Proxy(xhrproto.send, { + apply(originalSend, sendThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, sendArgArray: unknown[]) { + const sentryXhrData = sendThisArg[SENTRY_XHR_DATA_KEY]; if (!sentryXhrData) { - return originalSend.apply(this, args); + return originalSend.apply(sendThisArg, sendArgArray); } - if (args[0] !== undefined) { - sentryXhrData.body = args[0]; + if (sendArgArray[0] !== undefined) { + sentryXhrData.body = sendArgArray[0]; } const handlerData: HandlerDataXhr = { startTimestamp: timestampInSeconds() * 1000, - xhr: this, + xhr: sendThisArg, }; triggerHandlers('xhr', handlerData); - return originalSend.apply(this, args); - }; + return originalSend.apply(sendThisArg, sendArgArray); + }, }); } diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index b71f80df1ff2..066eba1e6839 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -354,25 +354,6 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries if (op === 'pageload') { _addTtfbRequestTimeToMeasurements(_measurements); - ['fcp', 'fp', 'lcp'].forEach(name => { - const measurement = _measurements[name]; - if (!measurement || !transactionStartTime || timeOrigin >= transactionStartTime) { - return; - } - // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin. - // Unfortunately, timeOrigin is not captured within the span span data, so these web vitals will need - // to be adjusted to be relative to span.startTimestamp. - const oldValue = measurement.value; - const measurementTimestamp = timeOrigin + msToSec(oldValue); - - // normalizedValue should be in milliseconds - const normalizedValue = Math.abs((measurementTimestamp - transactionStartTime) * 1000); - const delta = normalizedValue - oldValue; - - DEBUG_BUILD && logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`); - measurement.value = normalizedValue; - }); - const fidMark = _measurements['mark.fid']; if (fidMark && _measurements['fid']) { // create span for FID @@ -399,7 +380,10 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries setMeasurement(measurementName, measurement.value, measurement.unit); }); - _tagMetricInfo(span); + // Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on + span.setAttribute('performance.timeOrigin', timeOrigin); + + _setWebVitalAttributes(span); } _lcpEntry = undefined; @@ -604,7 +588,7 @@ function _trackNavigator(span: Span): void { } /** Add LCP / CLS data to span to allow debugging */ -function _tagMetricInfo(span: Span): void { +function _setWebVitalAttributes(span: Span): void { if (_lcpEntry) { DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data'); diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 52ba6d71154c..f9a7c258d4ff 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -47,6 +47,7 @@ export function makeFetchTransport( } try { + // TODO: This may need a `suppresTracing` call in the future when we switch the browser SDK to OTEL return nativeFetch(options.url, requestOptions).then(response => { pendingBodySize -= requestSize; pendingCount--; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e49e4163af31..d4ca06f85584 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -128,6 +128,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/transport.ts b/packages/cloudflare/src/transport.ts index fd26b217c367..4f1314d693a7 100644 --- a/packages/cloudflare/src/transport.ts +++ b/packages/cloudflare/src/transport.ts @@ -1,4 +1,4 @@ -import { createTransport } from '@sentry/core'; +import { createTransport, suppressTracing } from '@sentry/core'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { SentryError } from '@sentry/utils'; @@ -89,14 +89,16 @@ export function makeCloudflareTransport(options: CloudflareTransportOptions): Tr ...options.fetchOptions, }; - return fetch(options.url, requestOptions).then(response => { - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; + return suppressTracing(() => { + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); }); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 792bf3572934..24cea1bea7ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -100,6 +100,7 @@ export { sessionTimingIntegration } from './integrations/sessiontiming'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { metrics } from './metrics/exports'; +export { profiler } from './profiling'; export type { MetricData } from '@sentry/types'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts new file mode 100644 index 000000000000..446eebb73671 --- /dev/null +++ b/packages/core/src/profiling.ts @@ -0,0 +1,72 @@ +import type { Profiler, ProfilingIntegration } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { getClient } from './currentScopes'; +import { DEBUG_BUILD } from './debug-build'; + +function isProfilingIntegrationWithProfiler( + integration: ProfilingIntegration | undefined, +): integration is ProfilingIntegration { + return ( + !!integration && + typeof integration['_profiler'] !== 'undefined' && + typeof integration['_profiler']['start'] === 'function' && + typeof integration['_profiler']['stop'] === 'function' + ); +} +/** + * Starts the Sentry continuous profiler. + * This mode is exclusive with the transaction profiler and will only work if the profilesSampleRate is set to a falsy value. + * In continuous profiling mode, the profiler will keep reporting profile chunks to Sentry until it is stopped, which allows for continuous profiling of the application. + */ +function startProfiler(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + + if (!integration) { + DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); + return; + } + + if (!isProfilingIntegrationWithProfiler(integration)) { + DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); + return; + } + + integration._profiler.start(); +} + +/** + * Stops the Sentry continuous profiler. + * Calls to stop will stop the profiler and flush the currently collected profile data to Sentry. + */ +function stopProfiler(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); + return; + } + + if (!isProfilingIntegrationWithProfiler(integration)) { + DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); + return; + } + + integration._profiler.stop(); +} + +export const profiler: Profiler = { + startProfiler, + stopProfiler, +}; diff --git a/packages/core/test/lib/transports/offline.test.ts b/packages/core/test/lib/transports/offline.test.ts index ea3fa13ffd69..738597197178 100644 --- a/packages/core/test/lib/transports/offline.test.ts +++ b/packages/core/test/lib/transports/offline.test.ts @@ -410,7 +410,7 @@ describe('makeOfflineTransport', () => { START_DELAY + 2_000, ); - // eslint-disable-next-line jest/no-disabled-tests + // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests it.skip( 'Follows the Retry-After header', async () => { diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts index c678688c2462..1b2b3c661af9 100644 --- a/packages/deno/src/transports/index.ts +++ b/packages/deno/src/transports/index.ts @@ -1,4 +1,4 @@ -import { createTransport } from '@sentry/core'; +import { createTransport, suppressTracing } from '@sentry/core'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { consoleSandbox, logger, rejectedSyncPromise } from '@sentry/utils'; @@ -37,14 +37,16 @@ export function makeFetchTransport(options: DenoTransportOptions): Transport { }; try { - return fetch(options.url, requestOptions).then(response => { - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; + return suppressTracing(() => { + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); }); } catch (e) { return rejectedSyncPromise(e); diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index 9a6fa807e09f..6daa79eaeed8 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -184,24 +184,8 @@ module.exports = { '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', '@typescript-eslint/no-floating-promises': 'off', - }, - }, - { - // Configuration only for test files (this won't apply to utils or other files in test directories) - plugins: ['jest'], - env: { - jest: true, - }, - files: ['test.ts', '*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx'], - rules: { - // Prevent permanent usage of `it.only`, `fit`, `test.only` etc - // We want to avoid debugging leftovers making their way into the codebase - 'jest/no-focused-tests': 'error', - - // Prevent permanent usage of `it.skip`, `xit`, `test.skip` etc - // We want to avoid debugging leftovers making their way into the codebase - // If there's a good reason to skip a test (e.g. bad flakiness), just add an ignore comment - 'jest/no-disabled-tests': 'error', + '@sentry-internal/sdk/no-focused-tests': 'error', + '@sentry-internal/sdk/no-skipped-tests': 'error', }, }, { diff --git a/packages/eslint-plugin-sdk/src/index.js b/packages/eslint-plugin-sdk/src/index.js index 4390af285609..d7516c343d60 100644 --- a/packages/eslint-plugin-sdk/src/index.js +++ b/packages/eslint-plugin-sdk/src/index.js @@ -15,5 +15,7 @@ module.exports = { 'no-eq-empty': require('./rules/no-eq-empty'), 'no-class-field-initializers': require('./rules/no-class-field-initializers'), 'no-regexp-constructor': require('./rules/no-regexp-constructor'), + 'no-focused-tests': require('./rules/no-focused-tests'), + 'no-skipped-tests': require('./rules/no-skipped-tests'), }, }; diff --git a/packages/eslint-plugin-sdk/src/rules/no-focused-tests.js b/packages/eslint-plugin-sdk/src/rules/no-focused-tests.js new file mode 100644 index 000000000000..008431780da2 --- /dev/null +++ b/packages/eslint-plugin-sdk/src/rules/no-focused-tests.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * This rule was created to flag usages of the `.only` function in vitest and jest tests. + * Usually, we don't want to commit focused tests as this causes other tests to be skipped. + */ +module.exports = { + meta: { + docs: { + description: "Do not focus tests via `.only` to ensure we don't commit accidentally skip the other tests.", + }, + schema: [], + }, + create: function (context) { + return { + CallExpression(node) { + if ( + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + ['test', 'it', 'describe'].includes(node.callee.object.name) && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'only' + ) { + context.report({ + node, + message: "Do not focus tests via `.only` to ensure we don't commit accidentally skip the other tests.", + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-sdk/src/rules/no-skipped-tests.js b/packages/eslint-plugin-sdk/src/rules/no-skipped-tests.js new file mode 100644 index 000000000000..2c11e6e071a0 --- /dev/null +++ b/packages/eslint-plugin-sdk/src/rules/no-skipped-tests.js @@ -0,0 +1,33 @@ +'use strict'; + +/** + * This rule was created to flag usages of the `.skip` function in vitest and jest tests. + * Usually, we don't want to commit skipped tests as this causes other tests to be skipped. + * Sometimes, skipping is valid (e.g. flaky tests), in which case, we can simply eslint-disable the rule. + */ +module.exports = { + meta: { + docs: { + description: "Do not skip tests via `.skip` to ensure we don't commit accidentally skipped tests.", + }, + schema: [], + }, + create: function (context) { + return { + CallExpression(node) { + if ( + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + ['test', 'it', 'describe'].includes(node.callee.object.name) && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'skip' + ) { + context.report({ + node, + message: "Do not skip tests via `.skip` to ensure we don't commit accidentally skipped tests.", + }); + } + }, + }; + }, +}; diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index b588a4daa156..8cfaccb63c48 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -49,7 +49,7 @@ "@sentry/react": "8.27.0", "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", - "@sentry/webpack-plugin": "2.16.0" + "@sentry/webpack-plugin": "2.22.3" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 9a501307e79f..f58305c64f6c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -107,6 +107,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index a78a2c45a620..0cdb832a75f6 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -4,13 +4,13 @@

-# Official Sentry SDK for NestJS (EXPERIMENTAL) +# Official Sentry SDK for NestJS [![npm version](https://img.shields.io/npm/v/@sentry/nestjs.svg)](https://www.npmjs.com/package/@sentry/nestjs) [![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 considered **experimental and in an alpha state**. It may experience breaking changes. Please reach out on +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 @@ -109,6 +109,8 @@ import { SentryGlobalFilter } from '@sentry/nestjs/setup'; export class AppModule {} ``` +**Note:** In NestJS + GraphQL applications replace the `SentryGlobalFilter` with the `SentryGlobalGraphQLFilter`. + ## SentryTraced Use the `@SentryTraced()` decorator to gain additional performance insights for any function within your NestJS diff --git a/packages/nestjs/src/cron-decorator.ts b/packages/nestjs/src/decorators/sentry-cron.ts similarity index 100% rename from packages/nestjs/src/cron-decorator.ts rename to packages/nestjs/src/decorators/sentry-cron.ts diff --git a/packages/nestjs/src/span-decorator.ts b/packages/nestjs/src/decorators/sentry-traced.ts similarity index 100% rename from packages/nestjs/src/span-decorator.ts rename to packages/nestjs/src/decorators/sentry-traced.ts diff --git a/packages/nestjs/src/error-decorator.ts b/packages/nestjs/src/decorators/with-sentry.ts similarity index 79% rename from packages/nestjs/src/error-decorator.ts rename to packages/nestjs/src/decorators/with-sentry.ts index bf1fd08d8cee..cf86ea6e7cc5 100644 --- a/packages/nestjs/src/error-decorator.ts +++ b/packages/nestjs/src/decorators/with-sentry.ts @@ -1,5 +1,5 @@ import { captureException } from '@sentry/core'; -import { isExpectedError } from './helpers'; +import { isExpectedError } from '../helpers'; /** * A decorator to wrap user-defined exception filters and add Sentry error reporting. @@ -12,11 +12,11 @@ export function WithSentry() { // 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, args); + return originalCatch.apply(this, [exception, host, ...args]); } captureException(exception); - return originalCatch.apply(this, args); + return originalCatch.apply(this, [exception, host, ...args]); }; return descriptor; diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index b30fe547103b..71fb1ae4f78c 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -2,6 +2,6 @@ export * from '@sentry/node'; export { init } from './sdk'; -export { SentryTraced } from './span-decorator'; -export { SentryCron } from './cron-decorator'; -export { WithSentry } from './error-decorator'; +export { SentryTraced } from './decorators/sentry-traced'; +export { SentryCron } from './decorators/sentry-cron'; +export { WithSentry } from './decorators/with-sentry'; diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index f284c4ed7875..88d58ffea22f 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -6,7 +6,7 @@ import type { NestInterceptor, OnModuleInit, } from '@nestjs/common'; -import { Catch, Global, Injectable, Module } from '@nestjs/common'; +import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common'; import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -31,7 +31,11 @@ import { isExpectedError } from './helpers'; */ class SentryTracingInterceptor implements NestInterceptor { // used to exclude this class from being auto-instrumented - public static readonly __SENTRY_INTERNAL__ = true; + public readonly __SENTRY_INTERNAL__: boolean; + + public constructor() { + this.__SENTRY_INTERNAL__ = true; + } /** * Intercepts HTTP requests to set the transaction name for Sentry tracing. @@ -61,7 +65,12 @@ export { SentryTracingInterceptor }; * Global filter to handle exceptions and report them to Sentry. */ class SentryGlobalFilter extends BaseExceptionFilter { - public static readonly __SENTRY_INTERNAL__ = true; + public readonly __SENTRY_INTERNAL__: boolean; + + public constructor() { + super(); + this.__SENTRY_INTERNAL__ = true; + } /** * Catches exceptions and reports them to Sentry unless they are expected errors. @@ -78,11 +87,51 @@ class SentryGlobalFilter extends BaseExceptionFilter { 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. + */ +class SentryGlobalGraphQLFilter { + private static readonly _logger = new Logger('ExceptionsHandler'); + public readonly __SENTRY_INTERNAL__: boolean; + + public constructor() { + this.__SENTRY_INTERNAL__ = true; + } + + /** + * Catches exceptions and reports them to Sentry unless they are HttpExceptions. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public catch(exception: unknown, host: ArgumentsHost): void { + // neither report nor log HttpExceptions + if (exception instanceof HttpException) { + throw exception; + } + if (exception instanceof Error) { + SentryGlobalGraphQLFilter._logger.error(exception.message, exception.stack); + } + captureException(exception); + throw exception; + } +} +Catch()(SentryGlobalGraphQLFilter); +export { SentryGlobalGraphQLFilter }; + /** * Service to set up Sentry performance tracing for Nest.js applications. */ class SentryService implements OnModuleInit { - public static readonly __SENTRY_INTERNAL__ = true; + public readonly __SENTRY_INTERNAL__: boolean; + + public constructor() { + this.__SENTRY_INTERNAL__ = true; + } /** * Initializes the Sentry service and registers span attributes. diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 593a23c5d133..2d9e110b078f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -78,7 +78,7 @@ "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", "@sentry/vercel-edge": "8.27.0", - "@sentry/webpack-plugin": "2.20.1", + "@sentry/webpack-plugin": "2.22.3", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "3.29.4", diff --git a/packages/nextjs/src/common/captureRequestError.ts b/packages/nextjs/src/common/captureRequestError.ts index 8350a0f2e593..1556076619a0 100644 --- a/packages/nextjs/src/common/captureRequestError.ts +++ b/packages/nextjs/src/common/captureRequestError.ts @@ -13,17 +13,9 @@ type ErrorContext = { }; /** - * Reports errors for the Next.js `onRequestError` instrumentation hook. - * - * Notice: This function is experimental and not intended for production use. Breaking changes may be done to this funtion in any release. - * - * @experimental + * Reports errors passed to the the Next.js `onRequestError` instrumentation hook. */ -export function experimental_captureRequestError( - error: unknown, - request: RequestInfo, - errorContext: ErrorContext, -): void { +export function captureRequestError(error: unknown, request: RequestInfo, errorContext: ErrorContext): void { withScope(scope => { scope.setSDKProcessingMetadata({ request: { @@ -48,3 +40,11 @@ export function experimental_captureRequestError( }); }); } + +/** + * Reports errors passed to the the Next.js `onRequestError` instrumentation hook. + * + * @deprecated Use `captureRequestError` instead. + */ +// TODO(v9): Remove this export +export const experimental_captureRequestError = captureRequestError; diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 23ddfa383772..354113637a30 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -11,4 +11,5 @@ export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry'; export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; export { withServerActionInstrumentation } from './withServerActionInstrumentation'; -export { experimental_captureRequestError } from './captureRequestError'; +// eslint-disable-next-line deprecation/deprecation +export { experimental_captureRequestError, captureRequestError } from './captureRequestError'; diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 883ba3c26d41..d2e78d87f4ae 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -307,6 +307,50 @@ export type SentryBuildOptions = { }; }; + /** + * Options to configure various bundle size optimizations related to the Sentry SDK. + */ + bundleSizeOptimizations?: { + /** + * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) any debugging code within itself during the build. + * Note that the success of this depends on tree shaking being enabled in your build tooling. + * + * Setting this option to `true` will disable features like the SDK's `debug` option. + */ + excludeDebugStatements?: boolean; + + /** + * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code within itself that is related to tracing and performance monitoring. + * Note that the success of this depends on tree shaking being enabled in your build tooling. + * **Notice:** Do not enable this when you're using any performance monitoring-related SDK features (e.g. `Sentry.startTransaction()`). + */ + excludeTracing?: boolean; + + /** + * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay Shadow DOM recording functionality. + * Note that the success of this depends on tree shaking being enabled in your build tooling. + * + * This option is safe to be used when you do not want to capture any Shadow DOM activity via Sentry Session Replay. + */ + excludeReplayShadowDom?: boolean; + + /** + * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay `iframe` recording functionality. + * Note that the success of this depends on tree shaking being enabled in your build tooling. + * + * You can safely do this when you do not want to capture any `iframe` activity via Sentry Session Replay. + */ + excludeReplayIframe?: boolean; + + /** + * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay's Compression Web Worker. + * Note that the success of this depends on tree shaking being enabled in your build tooling. + * + * **Notice:** You should only use this option if you manually host a compression worker and configure it in your Sentry Session Replay integration config via the `workerUrl` option. + */ + excludeReplayWorker?: boolean; + }; + /** * Options related to react component name annotations. * Disabled by default, unless a value is set for this option. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index ecc39f7372dd..8fbc94b42195 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -37,7 +37,7 @@ let showedMissingGlobalErrorWarningMsg = false; * - `plugins`, to add SentryWebpackPlugin * * @param userNextConfig The user's existing nextjs config, as passed to `withSentryConfig` - * @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` + * @param userSentryOptions The user's SentryWebpackPlugin config, as passed to `withSentryConfig` * @returns The function to set as the nextjs config's `webpack` value */ export function constructWebpackConfigFunction( diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index f70862bfe484..1bca9bff49b6 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -97,6 +97,9 @@ export function getWebpackPluginOptions( deploy: sentryBuildOptions.release?.deploy, ...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release, }, + bundleSizeOptimizations: { + ...sentryBuildOptions.bundleSizeOptimizations, + }, _metaOptions: { loggerPrefixOverride: `[@sentry/nextjs - ${prefixInsert}]`, telemetry: { diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index b093968bdebe..a272990162b3 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -141,4 +141,5 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C; -export { experimental_captureRequestError } from './common/captureRequestError'; +// eslint-disable-next-line deprecation/deprecation +export { experimental_captureRequestError, captureRequestError } from './common/captureRequestError'; diff --git a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts index 54d55a179e28..557859b2a7e1 100644 --- a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts +++ b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts @@ -108,6 +108,23 @@ describe('getWebpackPluginOptions()', () => { }); }); + it('forwards bundleSizeOptimization options', () => { + const buildContext = generateBuildContext({ isServer: false }); + const generatedPluginOptions = getWebpackPluginOptions(buildContext, { + bundleSizeOptimizations: { + excludeTracing: true, + excludeReplayShadowDom: false, + }, + }); + + expect(generatedPluginOptions).toMatchObject({ + bundleSizeOptimizations: { + excludeTracing: true, + excludeReplayShadowDom: false, + }, + }); + }); + it('returns the right `assets` and `ignore` values during the server build', () => { const buildContext = generateBuildContext({ isServer: true }); const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9ef89ab42fb7..5f4adf315fd5 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -127,6 +127,7 @@ export { spanToBaggageHeader, trpcMiddleware, zodErrorsIntegration, + profiler, } from '@sentry/core'; export type { diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 615506605c9b..0d5b2d4814d1 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -9,7 +9,6 @@ import { getCapturedScopesOnSpan, getCurrentScope, getIsolationScope, - isSentryRequestUrl, setCapturedScopesOnSpan, } from '@sentry/core'; import { getClient } from '@sentry/opentelemetry'; @@ -102,10 +101,6 @@ export const instrumentHttp = Object.assign( return false; } - if (isSentryRequestUrl(url, getClient())) { - return true; - } - const _ignoreOutgoingRequests = _httpOptions.ignoreOutgoingRequests; if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url, request)) { return true; diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts index babf80022c1f..cc83dda3855d 100644 --- a/packages/node/src/integrations/tracing/nest/helpers.ts +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -1,6 +1,7 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, withActiveSpan } from '@sentry/core'; +import type { Span } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; -import type { CatchTarget, InjectableTarget } from './types'; +import type { CatchTarget, InjectableTarget, NextFunction, Observable, Subscription } from './types'; const sentryPatched = 'sentryPatched'; @@ -23,12 +24,51 @@ export function isPatched(target: InjectableTarget | CatchTarget): boolean { * Returns span options for nest middleware spans. */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget) { +export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget, name: string | undefined = undefined) { + const span_name = name ?? target.name; // fallback to class name if no name is provided + return { - name: target.name, + name: span_name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', }, }; } + +/** + * Adds instrumentation to a js observable and attaches the span to an active parent span. + */ +export function instrumentObservable(observable: Observable, activeSpan: Span | undefined): void { + if (activeSpan) { + // eslint-disable-next-line @typescript-eslint/unbound-method + observable.subscribe = new Proxy(observable.subscribe, { + apply: (originalSubscribe, thisArgSubscribe, argsSubscribe) => { + return withActiveSpan(activeSpan, () => { + const subscription: Subscription = originalSubscribe.apply(thisArgSubscribe, argsSubscribe); + subscription.add(() => activeSpan.end()); + return subscription; + }); + }, + }); + } +} + +/** + * Proxies the next() call in a nestjs middleware to end the span when it is called. + */ +export function getNextProxy(next: NextFunction, span: Span, prevSpan: undefined | Span): NextFunction { + return new Proxy(next, { + apply: (originalNext, thisArgNext, argsNext) => { + span.end(); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { + return Reflect.apply(originalNext, thisArgNext, argsNext); + }); + } else { + return Reflect.apply(originalNext, thisArgNext, argsNext); + } + }, + }); +} diff --git a/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts index a8d02e5cbe69..2d59d97d87fd 100644 --- a/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts +++ b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts @@ -5,11 +5,11 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import { getActiveSpan, startSpan, startSpanManual, withActiveSpan } from '@sentry/core'; +import { getActiveSpan, startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from '@sentry/core'; import type { Span } from '@sentry/types'; -import { SDK_VERSION } from '@sentry/utils'; -import { getMiddlewareSpanOptions, isPatched } from './helpers'; -import type { CatchTarget, InjectableTarget } from './types'; +import { SDK_VERSION, addNonEnumerableProperty, isThenable } from '@sentry/utils'; +import { getMiddlewareSpanOptions, getNextProxy, instrumentObservable, isPatched } from './helpers'; +import type { CallHandler, CatchTarget, InjectableTarget, MinimalNestJsExecutionContext, Observable } from './types'; const supportedVersions = ['>=8.0.0 <11']; @@ -101,23 +101,19 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.use = new Proxy(target.prototype.use, { apply: (originalUse, thisArgUse, argsUse) => { const [req, res, next, ...args] = argsUse; - const prevSpan = getActiveSpan(); - return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { - const nextProxy = new Proxy(next, { - apply: (originalNext, thisArgNext, argsNext) => { - span.end(); + // Check that we can reasonably assume that the target is a middleware. + // Without these guards, instrumentation will fail if a function named 'use' on a service, which is + // decorated with @Injectable, is called. + if (!req || !res || !next || typeof next !== 'function') { + return originalUse.apply(thisArgUse, argsUse); + } - if (prevSpan) { - return withActiveSpan(prevSpan, () => { - return Reflect.apply(originalNext, thisArgNext, argsNext); - }); - } else { - return Reflect.apply(originalNext, thisArgNext, argsNext); - } - }, - }); + const prevSpan = getActiveSpan(); + return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { + // proxy next to end span on call + const nextProxy = getNextProxy(next, span, prevSpan); return originalUse.apply(thisArgUse, [req, res, nextProxy, args]); }); }, @@ -133,6 +129,12 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.canActivate = new Proxy(target.prototype.canActivate, { apply: (originalCanActivate, thisArgCanActivate, argsCanActivate) => { + const context: MinimalNestJsExecutionContext = argsCanActivate[0]; + + if (!context) { + return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); + } + return startSpan(getMiddlewareSpanOptions(target), () => { return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); }); @@ -148,6 +150,13 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.transform = new Proxy(target.prototype.transform, { apply: (originalTransform, thisArgTransform, argsTransform) => { + const value = argsTransform[0]; + const metadata = argsTransform[1]; + + if (!value || !metadata) { + return originalTransform.apply(thisArgTransform, argsTransform); + } + return startSpan(getMiddlewareSpanOptions(target), () => { return originalTransform.apply(thisArgTransform, argsTransform); }); @@ -163,33 +172,84 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.intercept = new Proxy(target.prototype.intercept, { apply: (originalIntercept, thisArgIntercept, argsIntercept) => { - const [executionContext, next, args] = argsIntercept; - const prevSpan = getActiveSpan(); + const context: MinimalNestJsExecutionContext = argsIntercept[0]; + const next: CallHandler = argsIntercept[1]; - return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { - const nextProxy = new Proxy(next, { - get: (thisArgNext, property, receiver) => { - if (property === 'handle') { - const originalHandle = Reflect.get(thisArgNext, property, receiver); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (...args: any[]) => { - span.end(); - - if (prevSpan) { - return withActiveSpan(prevSpan, () => { - return Reflect.apply(originalHandle, thisArgNext, args); - }); - } else { - return Reflect.apply(originalHandle, thisArgNext, args); + const parentSpan = getActiveSpan(); + let afterSpan: Span; + + // Check that we can reasonably assume that the target is an interceptor. + if (!context || !next || typeof next.handle !== 'function') { + return originalIntercept.apply(thisArgIntercept, argsIntercept); + } + + return startSpanManual(getMiddlewareSpanOptions(target), (beforeSpan: Span) => { + // eslint-disable-next-line @typescript-eslint/unbound-method + next.handle = new Proxy(next.handle, { + apply: (originalHandle, thisArgHandle, argsHandle) => { + beforeSpan.end(); + + if (parentSpan) { + return withActiveSpan(parentSpan, () => { + const handleReturnObservable = Reflect.apply(originalHandle, thisArgHandle, argsHandle); + + if (!context._sentryInterceptorInstrumented) { + addNonEnumerableProperty(context, '_sentryInterceptorInstrumented', true); + afterSpan = startInactiveSpan( + getMiddlewareSpanOptions(target, 'Interceptors - After Route'), + ); } - }; - } - return Reflect.get(target, property, receiver); + return handleReturnObservable; + }); + } else { + const handleReturnObservable = Reflect.apply(originalHandle, thisArgHandle, argsHandle); + + if (!context._sentryInterceptorInstrumented) { + addNonEnumerableProperty(context, '_sentryInterceptorInstrumented', true); + afterSpan = startInactiveSpan(getMiddlewareSpanOptions(target, 'Interceptors - After Route')); + } + + return handleReturnObservable; + } }, }); - return originalIntercept.apply(thisArgIntercept, [executionContext, nextProxy, args]); + let returnedObservableInterceptMaybePromise: Observable | Promise>; + + try { + returnedObservableInterceptMaybePromise = originalIntercept.apply(thisArgIntercept, argsIntercept); + } catch (e) { + beforeSpan?.end(); + afterSpan?.end(); + throw e; + } + + if (!afterSpan) { + return returnedObservableInterceptMaybePromise; + } + + // handle async interceptor + if (isThenable(returnedObservableInterceptMaybePromise)) { + return returnedObservableInterceptMaybePromise.then( + observable => { + instrumentObservable(observable, afterSpan ?? parentSpan); + return observable; + }, + e => { + beforeSpan?.end(); + afterSpan?.end(); + throw e; + }, + ); + } + + // handle sync interceptor + if (typeof returnedObservableInterceptMaybePromise.subscribe === 'function') { + instrumentObservable(returnedObservableInterceptMaybePromise, afterSpan ?? parentSpan); + } + + return returnedObservableInterceptMaybePromise; }); }, }); @@ -217,6 +277,13 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.catch = new Proxy(target.prototype.catch, { apply: (originalCatch, thisArgCatch, argsCatch) => { + const exception = argsCatch[0]; + const host = argsCatch[1]; + + if (!exception || !host) { + return originalCatch.apply(thisArgCatch, argsCatch); + } + return startSpan(getMiddlewareSpanOptions(target), () => { return originalCatch.apply(thisArgCatch, argsCatch); }); diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index 42aa0b003315..0590462c09d5 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -interface MinimalNestJsExecutionContext { +export interface MinimalNestJsExecutionContext { getType: () => string; switchToHttp: () => { @@ -14,6 +14,8 @@ interface MinimalNestJsExecutionContext { method?: string; }; }; + + _sentryInterceptorInstrumented?: boolean; } export interface NestJsErrorFilter { @@ -27,11 +29,15 @@ export interface MinimalNestJsApp { }) => void; } +export interface Subscription { + add(...args: any[]): void; +} + /** * A minimal interface for an Observable. */ export interface Observable { - subscribe(observer: (value: T) => void): void; + subscribe(next?: (value: T) => void, error?: (err: any) => void, complete?: () => void): Subscription; } /** @@ -67,3 +73,8 @@ export interface CatchTarget { catch?: (...args: any[]) => any; }; } + +/** + * Represents an express NextFunction. + */ +export type NextFunction = (err?: any) => void; diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 751e4f3b3f4d..c4f13c89ee1b 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -79,11 +79,8 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { ? (new HttpsProxyAgent(proxy) as http.Agent) : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); - // This ensures we do not generate any spans in OpenTelemetry for the transport - return suppressTracing(() => { - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport(options, requestExecutor); - }); + const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); + return createTransport(options, requestExecutor); } /** @@ -122,54 +119,59 @@ function createRequestExecutor( const { hostname, pathname, port, protocol, search } = new URL(options.url); return function makeRequest(request: TransportRequest): Promise { return new Promise((resolve, reject) => { - let body = streamFromBody(request.body); - - const headers: Record = { ...options.headers }; - - if (request.body.length > GZIP_THRESHOLD) { - headers['content-encoding'] = 'gzip'; - body = body.pipe(createGzip()); - } - - const req = httpModule.request( - { - method: 'POST', - agent, - headers, - hostname, - path: `${pathname}${search}`, - port, - protocol, - ca: options.caCerts, - }, - res => { - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - - res.setEncoding('utf8'); - - // "Key-value pairs of header names and values. Header names are lower-cased." - // https://nodejs.org/api/http.html#http_message_headers - const retryAfterHeader = res.headers['retry-after'] ?? null; - const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null; - - resolve({ - statusCode: res.statusCode, - headers: { - 'retry-after': retryAfterHeader, - 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] || null : rateLimitsHeader, - }, - }); - }, - ); - - req.on('error', reject); - body.pipe(req); + // This ensures we do not generate any spans in OpenTelemetry for the transport + suppressTracing(() => { + let body = streamFromBody(request.body); + + const headers: Record = { ...options.headers }; + + if (request.body.length > GZIP_THRESHOLD) { + headers['content-encoding'] = 'gzip'; + body = body.pipe(createGzip()); + } + + const req = httpModule.request( + { + method: 'POST', + agent, + headers, + hostname, + path: `${pathname}${search}`, + port, + protocol, + ca: options.caCerts, + }, + res => { + res.on('data', () => { + // Drain socket + }); + + res.on('end', () => { + // Drain socket + }); + + res.setEncoding('utf8'); + + // "Key-value pairs of header names and values. Header names are lower-cased." + // https://nodejs.org/api/http.html#http_message_headers + const retryAfterHeader = res.headers['retry-after'] ?? null; + const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null; + + resolve({ + statusCode: res.statusCode, + headers: { + 'retry-after': retryAfterHeader, + 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) + ? rateLimitsHeader[0] || null + : rateLimitsHeader, + }, + }); + }, + ); + + req.on('error', reject); + body.pipe(req); + }); }); }; } diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index 1227aa5e2c82..df41599e45b9 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -96,7 +96,7 @@ Add a `sentry.client.config.(js|ts)` file to the root of your project: import * as Sentry from '@sentry/nuxt'; Sentry.init({ - dsn: env.DSN, + dsn: process.env.SENTRY_DSN, }); ``` @@ -107,10 +107,10 @@ Add an `instrument.server.mjs` file to your `public` folder: ```javascript import * as Sentry from '@sentry/nuxt'; -// Only run `init` when DSN is available +// Only run `init` when process.env.SENTRY_DSN is available. if (process.env.SENTRY_DSN) { Sentry.init({ - dsn: process.env.DSN, + dsn: process.env.SENTRY_DSN, }); } ``` diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index bf6e48bbf1d7..16c9d46754d5 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -49,7 +49,7 @@ "@sentry/opentelemetry": "8.27.0", "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", - "@sentry/vite-plugin": "2.20.1", + "@sentry/vite-plugin": "2.22.3", "@sentry/vue": "8.27.0" }, "devDependencies": { diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index da7fcf778366..5d529c99330c 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -26,10 +26,18 @@ export default defineNuxtModule({ addPluginTemplate({ mode: 'client', filename: 'sentry-client-config.mjs', - getContents: () => - `import "${buildDirResolver.resolve(`/${clientConfigFile}`)}"\n` + - 'import { defineNuxtPlugin } from "#imports"\n' + - 'export default defineNuxtPlugin(() => {})', + + // Dynamic import of config file to wrap it within a Nuxt context (here: defineNuxtPlugin) + // Makes it possible to call useRuntimeConfig() in the user-defined sentry config file + getContents: () => ` + import { defineNuxtPlugin } from "#imports"; + + export default defineNuxtPlugin({ + name: 'sentry-client-config', + async setup() { + await import("${buildDirResolver.resolve(`/${clientConfigFile}`)}") + } + });`, }); addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index a6bcd0115528..95dc954c4b89 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -29,24 +29,28 @@ interface VueRouter { // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; -export default defineNuxtPlugin(nuxtApp => { - // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside - // will get tree-shaken away - if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { - const sentryClient = getClient(); +export default defineNuxtPlugin({ + name: 'sentry-client-integrations', + dependsOn: ['sentry-client-config'], + async setup(nuxtApp) { + // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside + // will get tree-shaken away + if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + const sentryClient = getClient(); - if (sentryClient && '$router' in nuxtApp) { - sentryClient.addIntegration( - browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), - ); + if (sentryClient && '$router' in nuxtApp) { + sentryClient.addIntegration( + browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), + ); + } } - } - nuxtApp.hook('app:created', vueApp => { - const sentryClient = getClient(); + nuxtApp.hook('app:created', vueApp => { + const sentryClient = getClient(); - if (sentryClient) { - sentryClient.addIntegration(vueIntegration({ app: vueApp })); - } - }); + if (sentryClient) { + sentryClient.addIntegration(vueIntegration({ app: vueApp })); + } + }); + }, }); diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 50f240bff732..c1a96015f0c4 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -9,7 +9,7 @@ import { spanToJSON, } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; -import type { Event, Integration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types'; +import type { Event, IntegrationFn, Profile, ProfileChunk, ProfilingIntegration, Span } from '@sentry/types'; import { LRUMap, logger, uuid4 } from '@sentry/utils'; @@ -159,6 +159,7 @@ interface ChunkData { timer: NodeJS.Timeout | undefined; startTraceID: string; } + class ContinuousProfiler { private _profilerId = uuid4(); private _client: NodeClient | undefined = undefined; @@ -384,12 +385,8 @@ class ContinuousProfiler { } } -export interface ProfilingIntegration extends Integration { - _profiler: ContinuousProfiler; -} - /** Exported only for tests. */ -export const _nodeProfilingIntegration = ((): ProfilingIntegration => { +export const _nodeProfilingIntegration = ((): ProfilingIntegration => { if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) { logger.warn( `[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`, @@ -407,7 +404,10 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration => { const options = client.getOptions(); const mode = - (options.profilesSampleRate === undefined || options.profilesSampleRate === 0) && !options.profilesSampler + (options.profilesSampleRate === undefined || + options.profilesSampleRate === null || + options.profilesSampleRate === 0) && + !options.profilesSampler ? 'continuous' : 'span'; switch (mode) { diff --git a/packages/profiling-node/test/cpu_profiler.test.ts b/packages/profiling-node/test/cpu_profiler.test.ts index c1086003c1af..1e3903be6fc5 100644 --- a/packages/profiling-node/test/cpu_profiler.test.ts +++ b/packages/profiling-node/test/cpu_profiler.test.ts @@ -316,7 +316,7 @@ describe('Profiler bindings', () => { expect(profile?.measurements?.['memory_footprint']?.values.length).toBeLessThanOrEqual(300); }); - // eslint-disable-next-line jest/no-disabled-tests + // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests it.skip('includes deopt reason', async () => { // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#52-the-object-being-iterated-is-not-a-simple-enumerable function iterateOverLargeHashTable() { diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 4a90caa0f353..f65556f57ab4 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -2,10 +2,11 @@ import * as Sentry from '@sentry/node'; import { getMainCarrier } from '@sentry/core'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; +import type { ProfilingIntegration } from '@sentry/types'; import type { ProfileChunk, Transport } from '@sentry/types'; import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils'; import { CpuProfilerBindings } from '../src/cpu_profiler'; -import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration'; +import { _nodeProfilingIntegration } from '../src/integration'; function makeClientWithHooks(): [Sentry.NodeClient, Transport] { const integration = _nodeProfilingIntegration(); @@ -299,7 +300,7 @@ describe('automated span instrumentation', () => { Sentry.setCurrentClient(client); client.init(); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -390,7 +391,7 @@ describe('continuous profiling', () => { }); afterEach(() => { const client = Sentry.getClient(); - const integration = client?.getIntegrationByName('ProfilingIntegration'); + const integration = client?.getIntegrationByName>('ProfilingIntegration'); if (integration) { integration._profiler.stop(); @@ -432,7 +433,7 @@ describe('continuous profiling', () => { const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -446,7 +447,7 @@ describe('continuous profiling', () => { expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); }); - it('initializes the continuous profiler and binds the sentry client', () => { + it('initializes the continuous profiler', () => { const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); const [client] = makeContinuousProfilingClient(); @@ -455,14 +456,13 @@ describe('continuous profiling', () => { expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } integration._profiler.start(); expect(integration._profiler).toBeDefined(); - expect(integration._profiler['_client']).toBe(client); }); it('starts a continuous profile', () => { @@ -473,7 +473,7 @@ describe('continuous profiling', () => { client.init(); expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -490,7 +490,7 @@ describe('continuous profiling', () => { client.init(); expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -509,7 +509,7 @@ describe('continuous profiling', () => { client.init(); expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -529,7 +529,7 @@ describe('continuous profiling', () => { client.init(); expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -548,7 +548,7 @@ describe('continuous profiling', () => { client.init(); expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -604,7 +604,7 @@ describe('continuous profiling', () => { const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -632,7 +632,7 @@ describe('continuous profiling', () => { }, }); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -692,7 +692,7 @@ describe('span profiling mode', () => { Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); expect(startProfilingSpy).toHaveBeenCalled(); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); @@ -703,6 +703,10 @@ describe('span profiling mode', () => { }); }); describe('continuous profiling mode', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it.each([ ['profilesSampleRate=0', makeClientOptions({ profilesSampleRate: 0 })], ['profilesSampleRate=undefined', makeClientOptions({ profilesSampleRate: undefined })], @@ -739,7 +743,7 @@ describe('continuous profiling mode', () => { jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } @@ -750,4 +754,31 @@ describe('continuous profiling mode', () => { Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); }); + + it('top level methods proxy to integration', () => { + const client = new Sentry.NodeClient({ + ...makeClientOptions({ profilesSampleRate: undefined }), + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/profiling-node/test/spanProfileUtils.worker.test.ts b/packages/profiling-node/test/spanProfileUtils.worker.test.ts index a119f80292d5..12727aebc954 100644 --- a/packages/profiling-node/test/spanProfileUtils.worker.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.worker.test.ts @@ -9,7 +9,8 @@ jest.setTimeout(10000); import * as Sentry from '@sentry/node'; import type { Transport } from '@sentry/types'; -import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration'; +import { type ProfilingIntegration } from '@sentry/types'; +import { _nodeProfilingIntegration } from '../src/integration'; function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { const integration = _nodeProfilingIntegration(); @@ -49,7 +50,7 @@ it('worker threads context', () => { }, }); - const integration = client.getIntegrationByName('ProfilingIntegration'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); if (!integration) { throw new Error('Profiling integration not found'); } diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 5dcb50c0ec06..1c3a8c202286 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.25.0" + "@sentry-internal/rrweb": "2.26.0" }, "dependencies": { "@sentry-internal/replay": "8.27.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 41913f084abb..d7b602af7dc1 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.27.0", - "@sentry-internal/rrweb": "2.25.0", - "@sentry-internal/rrweb-snapshot": "2.25.0", + "@sentry-internal/rrweb": "2.26.0", + "@sentry-internal/rrweb-snapshot": "2.26.0", "fflate": "^0.8.1", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.2.1" diff --git a/packages/replay-internal/src/coreHandlers/handleClick.ts b/packages/replay-internal/src/coreHandlers/handleClick.ts index da07474deebb..029588764610 100644 --- a/packages/replay-internal/src/coreHandlers/handleClick.ts +++ b/packages/replay-internal/src/coreHandlers/handleClick.ts @@ -40,6 +40,17 @@ type IncrementalMouseInteractionRecordingEvent = IncrementalRecordingEvent & { data: { type: MouseInteractions; id: number }; }; +/** Any IncrementalSource for rrweb that we interpret as a kind of mutation. */ +const IncrementalMutationSources = new Set([ + IncrementalSource.Mutation, + IncrementalSource.StyleSheetRule, + IncrementalSource.StyleDeclaration, + IncrementalSource.AdoptedStyleSheet, + IncrementalSource.CanvasMutation, + IncrementalSource.Selection, + IncrementalSource.MediaInteraction, +]); + /** Handle a click. */ export function handleClick(clickDetector: ReplayClickDetector, clickBreadcrumb: Breadcrumb, node: HTMLElement): void { clickDetector.handleClick(clickBreadcrumb, node); @@ -324,7 +335,7 @@ export function updateClickDetectorForRecordingEvent(clickDetector: ReplayClickD } const { source } = event.data; - if (source === IncrementalSource.Mutation) { + if (IncrementalMutationSources.has(source)) { clickDetector.registerMutation(event.timestamp); } diff --git a/packages/replay-internal/src/types/performance.ts b/packages/replay-internal/src/types/performance.ts index 6b264a44ee9c..7a60e51684f3 100644 --- a/packages/replay-internal/src/types/performance.ts +++ b/packages/replay-internal/src/types/performance.ts @@ -111,6 +111,10 @@ export interface WebVitalData { * The recording id of the web vital nodes. -1 if not found */ nodeIds?: number[]; + /** + * The layout shifts of a CLS metric + */ + attributions?: { value: number; sources?: number[] }[]; } /** diff --git a/packages/replay-internal/src/util/createPerformanceEntries.ts b/packages/replay-internal/src/util/createPerformanceEntries.ts index d55c2269d0f4..830f878dc8ea 100644 --- a/packages/replay-internal/src/util/createPerformanceEntries.ts +++ b/packages/replay-internal/src/util/createPerformanceEntries.ts @@ -43,7 +43,13 @@ export interface Metric { * The array may also be empty if the metric value was not based on any * entries (e.g. a CLS value of 0 given no layout shifts). */ - entries: PerformanceEntry[] | PerformanceEventTiming[]; + entries: PerformanceEntry[] | LayoutShift[]; +} + +interface LayoutShift extends PerformanceEntry { + value: number; + sources: LayoutShiftAttribution[]; + hadRecentInput: boolean; } interface LayoutShiftAttribution { @@ -52,6 +58,11 @@ interface LayoutShiftAttribution { currentRect: DOMRectReadOnly; } +interface Attribution { + value: number; + nodeIds?: number[]; +} + /** * Handler creater for web vitals */ @@ -187,22 +198,32 @@ export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntr return getWebVital(metric, 'largest-contentful-paint', node); } +function isLayoutShift(entry: PerformanceEntry | LayoutShift): entry is LayoutShift { + return (entry as LayoutShift).sources !== undefined; +} + /** * Add a CLS event to the replay based on a CLS metric. */ export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry { - const lastEntry = metric.entries[metric.entries.length - 1] as - | (PerformanceEntry & { sources?: LayoutShiftAttribution[] }) - | undefined; + const layoutShifts: Attribution[] = []; const nodes: Node[] = []; - if (lastEntry && lastEntry.sources) { - for (const source of lastEntry.sources) { - if (source.node) { - nodes.push(source.node); + for (const entry of metric.entries) { + if (isLayoutShift(entry)) { + const nodeIds = []; + for (const source of entry.sources) { + if (source.node) { + nodes.push(source.node); + const nodeId = record.mirror.getId(source.node); + if (nodeId) { + nodeIds.push(nodeId); + } + } } + layoutShifts.push({ value: entry.value, nodeIds }); } } - return getWebVital(metric, 'cumulative-layout-shift', nodes); + return getWebVital(metric, 'cumulative-layout-shift', nodes, layoutShifts); } /** @@ -226,7 +247,12 @@ export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntr /** * Add an web vital event to the replay based on the web vital metric. */ -function getWebVital(metric: Metric, name: string, nodes: Node[] | undefined): ReplayPerformanceEntry { +function getWebVital( + metric: Metric, + name: string, + nodes: Node[] | undefined, + attributions?: Attribution[], +): ReplayPerformanceEntry { const value = metric.value; const rating = metric.rating; @@ -242,6 +268,7 @@ function getWebVital(metric: Metric, name: string, nodes: Node[] | undefined): R size: value, rating, nodeIds: nodes ? nodes.map(node => record.mirror.getId(node)) : undefined, + attributions, }, }; diff --git a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts index d85698d1be1d..b82a8941269d 100644 --- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts +++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts @@ -83,7 +83,7 @@ describe('Unit | util | createPerformanceEntries', () => { name: 'largest-contentful-paint', start: 1672531205.108299, end: 1672531205.108299, - data: { value: 5108.299, rating: 'good', size: 5108.299, nodeIds: undefined }, + data: { value: 5108.299, rating: 'good', size: 5108.299, nodeIds: undefined, attributions: undefined }, }); }); }); @@ -103,7 +103,7 @@ describe('Unit | util | createPerformanceEntries', () => { name: 'cumulative-layout-shift', start: 1672531205.108299, end: 1672531205.108299, - data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [] }, + data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [], attributions: [] }, }); }); }); @@ -123,7 +123,7 @@ describe('Unit | util | createPerformanceEntries', () => { name: 'first-input-delay', start: 1672531205.108299, end: 1672531205.108299, - data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined }, + data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined }, }); }); }); @@ -143,7 +143,7 @@ describe('Unit | util | createPerformanceEntries', () => { name: 'interaction-to-next-paint', start: 1672531205.108299, end: 1672531205.108299, - data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined }, + data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined }, }); }); }); diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index b5d775781875..0b25a3a37e3e 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -73,10 +73,10 @@ Sentry.init({ ### 4. Server instrumentation -Complete the setup by adding the Sentry middlware to your `src/middleware.ts` file: +Complete the setup by adding the Sentry middleware to your `src/middleware.ts` file: ```typescript -import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware'; +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart'; import { createMiddleware } from '@solidjs/start/middleware'; export default createMiddleware({ diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index ee9fd2e4c956..efb530697ff6 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -39,17 +39,6 @@ "require": "./build/cjs/index.server.js" } }, - "./middleware": { - "types": "./middleware.d.ts", - "import": { - "types": "./middleware.d.ts", - "default": "./build/esm/middleware.js" - }, - "require": { - "types": "./middleware.d.ts", - "default": "./build/cjs/middleware.js" - } - }, "./solidrouter": { "types": "./solidrouter.d.ts", "browser": { @@ -84,7 +73,7 @@ "@sentry/solid": "8.27.0", "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", - "@sentry/vite-plugin": "2.19.0" + "@sentry/vite-plugin": "2.22.3" }, "devDependencies": { "@solidjs/router": "^0.13.4", @@ -106,7 +95,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts && madge --circular src/middleware.ts", + "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts", "clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", diff --git a/packages/solidstart/rollup.npm.config.mjs b/packages/solidstart/rollup.npm.config.mjs index 8e91d0371a27..b0087a93c6fe 100644 --- a/packages/solidstart/rollup.npm.config.mjs +++ b/packages/solidstart/rollup.npm.config.mjs @@ -12,7 +12,6 @@ export default makeNPMConfigVariants( 'src/solidrouter.server.ts', 'src/client/solidrouter.ts', 'src/server/solidrouter.ts', - 'src/middleware.ts', ], // prevent this internal code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 75a67d3bb847..c3499a82459a 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -128,3 +128,4 @@ export { withSentryErrorBoundary } from '@sentry/solid'; export { init } from './sdk'; export * from './withServerActionInstrumentation'; +export * from './middleware'; diff --git a/packages/solidstart/src/middleware.ts b/packages/solidstart/src/server/middleware.ts similarity index 100% rename from packages/solidstart/src/middleware.ts rename to packages/solidstart/src/server/middleware.ts diff --git a/packages/solidstart/test/middleware.test.ts b/packages/solidstart/test/server/middleware.test.ts similarity index 95% rename from packages/solidstart/test/middleware.test.ts rename to packages/solidstart/test/server/middleware.test.ts index c025dc38af97..c1d6ff644b9d 100644 --- a/packages/solidstart/test/middleware.test.ts +++ b/packages/solidstart/test/server/middleware.test.ts @@ -1,7 +1,7 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, it, vi } from 'vitest'; -import { sentryBeforeResponseMiddleware } from '../src/middleware'; -import type { ResponseMiddlewareResponse } from '../src/middleware'; +import { sentryBeforeResponseMiddleware } from '../../src/server'; +import type { ResponseMiddlewareResponse } from '../../src/server'; describe('middleware', () => { describe('sentryBeforeResponseMiddleware', () => { diff --git a/packages/solidstart/tsconfig.subexports-types.json b/packages/solidstart/tsconfig.subexports-types.json index 1c9daec11314..f800d830c511 100644 --- a/packages/solidstart/tsconfig.subexports-types.json +++ b/packages/solidstart/tsconfig.subexports-types.json @@ -15,7 +15,6 @@ "src/solidrouter.server.ts", "src/server/solidrouter.ts", "src/solidrouter.ts", - "src/middleware.ts", ], // Without this, we cannot output into the root dir "exclude": [] diff --git a/packages/solidstart/tsconfig.types.json b/packages/solidstart/tsconfig.types.json index bf2ca092abc1..51154c9c7878 100644 --- a/packages/solidstart/tsconfig.types.json +++ b/packages/solidstart/tsconfig.types.json @@ -15,6 +15,5 @@ "src/solidrouter.server.ts", "src/server/solidrouter.ts", "src/solidrouter.ts", - "src/middleware.ts", ] } diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index f332530c338c..f5af15b667d3 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -46,7 +46,7 @@ "@sentry/svelte": "8.27.0", "@sentry/types": "8.27.0", "@sentry/utils": "8.27.0", - "@sentry/vite-plugin": "2.22.0", + "@sentry/vite-plugin": "2.22.3", "magic-string": "0.30.7", "magicast": "0.2.8", "sorcery": "0.11.0" diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1022e69ad49e..b100c1e9c26a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -171,4 +171,5 @@ export type { Metrics, } from './metrics'; export type { ParameterizedString } from './parameterize'; +export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; diff --git a/packages/types/src/profiling.ts b/packages/types/src/profiling.ts index 8f5f4cc2e890..9ecba8ced48a 100644 --- a/packages/types/src/profiling.ts +++ b/packages/types/src/profiling.ts @@ -1,6 +1,23 @@ +import type { Client } from './client'; import type { DebugImage } from './debugMeta'; +import type { Integration } from './integration'; import type { MeasurementUnit } from './measurement'; +export interface ContinuousProfiler { + initialize(client: T): void; + start(): void; + stop(): void; +} + +export interface ProfilingIntegration extends Integration { + _profiler: ContinuousProfiler; +} + +export interface Profiler { + startProfiler(): void; + stopProfiler(): void; +} + export type ThreadId = string; export type FrameId = number; export type StackId = number; diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index ccce883d3366..aa51c69035f1 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -243,7 +243,7 @@ export interface Scope { /** * Capture a message for this scope. * - * @param exception The exception to capture. + * @param message The message to capture. * @param level An optional severity level to report the message with. * @param hint Optional additional data to attach to the Sentry event. * @returns the id of the captured message. @@ -253,7 +253,7 @@ export interface Scope { /** * Capture a Sentry event for this scope. * - * @param exception The event to capture. + * @param event The event to capture. * @param hint Optional additional data to attach to the Sentry event. * @returns the id of the captured event. */ diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index 0a3308a88561..b85c91c55a8d 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -30,6 +30,6 @@ export function isBrowserBundle(): boolean { * Get source of SDK. */ export function getSDKSource(): SdkSource { - // @ts-expect-error __SENTRY_SDK_SOURCE__ is injected by rollup during build process - return __SENTRY_SDK_SOURCE__; + // This comment is used to identify this line in the CDN bundle build step and replace this with "return 'cdn';" + /* __SENTRY_SDK_SOURCE__ */ return 'npm'; } diff --git a/packages/vercel-edge/src/transports/index.ts b/packages/vercel-edge/src/transports/index.ts index 4e8a35ac7c39..b938647b4415 100644 --- a/packages/vercel-edge/src/transports/index.ts +++ b/packages/vercel-edge/src/transports/index.ts @@ -1,4 +1,4 @@ -import { createTransport } from '@sentry/core'; +import { createTransport, suppressTracing } from '@sentry/core'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { SentryError } from '@sentry/utils'; @@ -89,14 +89,16 @@ export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transpor ...options.fetchOptions, }; - return fetch(options.url, requestOptions).then(response => { - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; + return suppressTracing(() => { + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); }); } diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 0ed2f773ca2d..13d9e8588350 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -22,6 +22,7 @@ export type ViewModel = { propsData?: { [key: string]: any }; _componentTag?: string; __file?: string; + __name?: string; }; }; diff --git a/packages/vue/src/vendor/components.ts b/packages/vue/src/vendor/components.ts index a19dab78d99f..f5d05b5cbc6c 100644 --- a/packages/vue/src/vendor/components.ts +++ b/packages/vue/src/vendor/components.ts @@ -51,7 +51,7 @@ export const formatComponentName = (vm?: ViewModel, includeFile?: boolean): stri const options = vm.$options; - let name = options.name || options._componentTag; + let name = options.name || options._componentTag || options.__name; const file = options.__file; if (!name && file) { const match = file.match(/([^/\\]+)\.vue$/); diff --git a/packages/vue/test/vendor/components.test.ts b/packages/vue/test/vendor/components.test.ts index 49d184325ee0..5a210a0a2fb5 100644 --- a/packages/vue/test/vendor/components.test.ts +++ b/packages/vue/test/vendor/components.test.ts @@ -80,6 +80,19 @@ describe('formatComponentName', () => { }); }); + describe('when the options have a __name', () => { + it('returns the __name', () => { + // arrange + vm.$options.__name = 'my-component-name'; + + // act + const formattedName = formatComponentName(vm); + + // assert + expect(formattedName).toEqual(''); + }); + }); + describe('when the options have a __file', () => { describe('and we do not wish to include the filename', () => { it.each([ diff --git a/yarn.lock b/yarn.lock index 481175f8db61..6f2432d7054a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8115,22 +8115,22 @@ dependencies: "@sentry-internal/rrweb-snapshot" "2.11.0" -"@sentry-internal/rrdom@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.25.0.tgz#4be842f7f4efae383bbd5a9dcbbecc212d378d70" - integrity sha512-YTxGHnCdv6D2JVJ6YFezMsGOHLy7CM8x8qMaY3Yh3QTubFOjdGpcGJGITF/9Lkx+rFVCTdjL32cQu9NUgEJO8g== +"@sentry-internal/rrdom@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.26.0.tgz#be3e4f14de56a6022aed3a00ac6ea2f2abe05d1c" + integrity sha512-QviUvwAPYDCmkeJsu3fx0pXlLBHwQLCKje9wuuhRVkmDL9dMbcCDa7+HhFa2V2UMXgZ7l6z/SMin2ymDReubSw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.25.0" + "@sentry-internal/rrweb-snapshot" "2.26.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.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.25.0.tgz#f20bd20436edac24ed1075b47fc4773894739d97" - integrity sha512-7j90eSGFRS1YWcuo0bXPtV9oDdCQxutilyYbim/I09GA7kx4/d8OG8ryxQl6WWXW+E50x6dEpDsZXWMPkSleEg== +"@sentry-internal/rrweb-snapshot@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.26.0.tgz#cb70bf2c006bf59824806f567ae6ca23ba2aac3a" + integrity sha512-wWa+OxAHhoozIlt3kjhmfrsn/+POnJgktOe5WT95fakfyv56mGKXqh4mXx7HRzGEwq4bbkhtcPhfh2gbueSPcA== "@sentry-internal/rrweb-types@2.11.0": version "2.11.0" @@ -8139,12 +8139,12 @@ dependencies: "@sentry-internal/rrweb-snapshot" "2.11.0" -"@sentry-internal/rrweb-types@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.25.0.tgz#61662befc57ed7054a491eb35ad3deda7d66157c" - integrity sha512-sM2YdevhIRxQ/Kr89cfbNBO7/EFhycTmQT0NKg4owdKkIvuuqz1AhbRpMMdpJ4NJnos+h06VPObeXm6rcrffsw== +"@sentry-internal/rrweb-types@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.26.0.tgz#0ba52a4e6f24238556134280b7cf77633bc68e21" + integrity sha512-og4X+OidRRc3bMuwfeio4UF8EcVFjtz/z0DDjpyV+0sH4LDoVoH1+Jlxbxl4WR83LALWMcsxV0UWYeXA5kfrOw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.25.0" + "@sentry-internal/rrweb-snapshot" "2.26.0" "@types/css-font-loading-module" "0.0.7" "@sentry-internal/rrweb@2.11.0": @@ -8161,89 +8161,32 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry-internal/rrweb@2.25.0": - version "2.25.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.25.0.tgz#0148f1904f1e9549f2c2cae209fe3d3fe891d3ec" - integrity sha512-0tgBI0CFpyO3Z3dw4IjS/D6AnQypro4dquRrcZZzqnMH65Vxw3yytGDtmvE/FzHzGC0vmKFTM+sTkzFY0bo+Bg== +"@sentry-internal/rrweb@2.26.0": + version "2.26.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.26.0.tgz#456a9ae1b48e87f086e4749346532879e9ac304c" + integrity sha512-J5db750QNlGdLrzbZwEVJgOtLwHtvh3a6VVxQ08G0yEZxqI7bdvcvxnvIXp8h+PwUk/S8yjoZwYgLFFDED3ePQ== dependencies: - "@sentry-internal/rrdom" "2.25.0" - "@sentry-internal/rrweb-snapshot" "2.25.0" - "@sentry-internal/rrweb-types" "2.25.0" + "@sentry-internal/rrdom" "2.26.0" + "@sentry-internal/rrweb-snapshot" "2.26.0" + "@sentry-internal/rrweb-types" "2.26.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/babel-plugin-component-annotate@2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.16.0.tgz#c831713b85516fb3f9da2985836ddf444dc634e6" - integrity sha512-+uy1qPkA5MSNgJ0L9ur/vNTydfdHwHnBX2RQ+0thsvkqf90fU788YjkkXwUiBBNuqNyI69JiOW6frixAWy7oUg== +"@sentry/babel-plugin-component-annotate@2.22.3": + version "2.22.3" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.3.tgz#de4970d51a54ef52b21f0d6ec49bd06bf37753c1" + integrity sha512-OlHA+i+vnQHRIdry4glpiS/xTOtgjmpXOt6IBOUqynx5Jd/iK1+fj+t8CckqOx9wRacO/hru2wfW/jFq0iViLg== -"@sentry/babel-plugin-component-annotate@2.19.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.19.0.tgz#70dcccb336bcec24148e1c9cd4e37724cebf5673" - integrity sha512-N2k8cMYu/7X6mzAH5j6bMeNcXQBJLL0lVAF63TDS57hUiT1v2uEqbeYFdH2CZBHb2LepLbMRXmvErIwy76FLTw== - -"@sentry/babel-plugin-component-annotate@2.20.1": - version "2.20.1" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.20.1.tgz#204c63ed006a048f48f633876e1b8bacf87a9722" - integrity sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg== - -"@sentry/babel-plugin-component-annotate@2.22.0": - version "2.22.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.0.tgz#a7e1cc99d1a738d1eb17757341dff4db3a93c2dc" - integrity sha512-UzH+NNhgnOo6UFku3C4TEz+pO/yDcIA5FKTJvLbJ7lQwAjsqLs3DZWm4cCA08skICb8mULArF6S/dn5/butVCA== - -"@sentry/bundler-plugin-core@2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.16.0.tgz#0c33e7a054fb56e43bd160ac141f71dfebf6dda5" - integrity sha512-dhgIZsIR3L9KnE2OO5JJm6hPtStAjEPYKQsZzxRr69uVhd9xAvfXeXr0afKVNVEcIDksas6yMgHqwQ2wOXFIAg== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.16.0" - "@sentry/cli" "^2.22.3" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.27.0" - unplugin "1.0.1" - -"@sentry/bundler-plugin-core@2.19.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.19.0.tgz#c21935ff5aea9daccfa4c9e0db405aecdec292f6" - integrity sha512-PGTwpue2k4HnLlCuvLeg+cILPWHJorzheNq8KVlXed8mpb8kxKeY9EWQFxBqPS+XyktOMAxZmCMZfKdnHNaJVQ== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.19.0" - "@sentry/cli" "^2.22.3" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - -"@sentry/bundler-plugin-core@2.20.1": - version "2.20.1" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.20.1.tgz#c9dd35e2177a4c22ecf675558eb84fbc2607e465" - integrity sha512-6ipbmGzHekxeRCbp7eoefr6bdd/lW4cNA9eNnrmd9+PicubweGaZZbH2NjhFHsaxzgOezwipDHjrTaap2kTHgw== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.20.1" - "@sentry/cli" "^2.22.3" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - -"@sentry/bundler-plugin-core@2.22.0": - version "2.22.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.0.tgz#6a67761ff5bc0dc897e56acba0b12547bc623e14" - integrity sha512-/xXN8o7565WMsewBnQFfjm0E5wqhYsegg++HJ5RjrY/cTM4qcd/ven44GEMxqGFJitZizvkk3NHszaHylzcRUw== +"@sentry/bundler-plugin-core@2.22.3": + version "2.22.3" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.3.tgz#f8c0a25321216ae9777749c1a4b9d982ae1ec2e1" + integrity sha512-DeoUl0WffcqZZRl5Wy9aHvX4WfZbbWt0QbJ7NJrcEViq+dRAI2FQTYECFLwdZi5Gtb3oyqZICO+P7k8wDnzsjQ== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.0" + "@sentry/babel-plugin-component-annotate" "2.22.3" "@sentry/cli" "^2.33.1" dotenv "^16.3.1" find-up "^5.0.0" @@ -8321,7 +8264,7 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.1.tgz#2d00b38a2dd9f3355df91825582ada3ea0034e86" integrity sha512-8VyRoJqtb2uQ8/bFRKNuACYZt7r+Xx0k2wXRGTyH05lCjAiVIXn7DiS2BxHFty7M1QEWUCMNsb/UC/x/Cu2wuA== -"@sentry/cli@^2.22.3", "@sentry/cli@^2.33.0": +"@sentry/cli@^2.33.0": version "2.33.0" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.33.0.tgz#5de59f829070ab20d360fae25924f39c55afd8ba" integrity sha512-9MOzQy1UunVBhPOfEuO0JH2ofWAMmZVavTTR/Bo2CkJwI1qjyVF0UKLTXE3l4ujiJnFufOoBsVyKmYWXFerbCw== @@ -8359,45 +8302,20 @@ "@sentry/cli-win32-i686" "2.33.1" "@sentry/cli-win32-x64" "2.33.1" -"@sentry/vite-plugin@2.19.0": - version "2.19.0" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.19.0.tgz#c7938fb13eee15036963b87d7b12c4fc851e488b" - integrity sha512-xmntz/bvRwhhU9q2thZas1vQQch9CLMyD8oCfYlNqN57t5XKhIs2dsCU/uS7HCnxIXuuUb/cZtIS7AXVg16fFA== - dependencies: - "@sentry/bundler-plugin-core" "2.19.0" - unplugin "1.0.1" - -"@sentry/vite-plugin@2.20.1", "@sentry/vite-plugin@^2.20.1": - version "2.20.1" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.20.1.tgz#80d5639fca3f68fbf81c316883272ffb34dbc544" - integrity sha512-IOYAJRcV+Uqn0EL8rxcoCvE77PbtGzKjP+B6iIgDZ229AWbpfbpWY8zHCcufwdDzb5PtgOhWWHT74iAsNyij/A== +"@sentry/vite-plugin@2.22.3", "@sentry/vite-plugin@^2.22.3": + version "2.22.3" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.3.tgz#b52802412b6f3d8e3e56742afc9624d9babae5b6" + integrity sha512-+5bsLFRKOZzBp68XigoNE1pJ3tJ4gt2jXluApu54ui0N/yjfqGQ7LQTD7nL4tmJvB5Agwi0e7M7+fcxe9gSgBA== dependencies: - "@sentry/bundler-plugin-core" "2.20.1" + "@sentry/bundler-plugin-core" "2.22.3" unplugin "1.0.1" -"@sentry/vite-plugin@2.22.0": - version "2.22.0" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.0.tgz#09743ac390cf8c1609f2fa6d5424548d0b6f7928" - integrity sha512-U1dWldo3gb1oDqERgiSM7zexMwAuqiXO/YUO3xVSpWmhoHz2AqxOcfIX1SygW02NF7Ss3ay4qMAta8PbvdsrnQ== +"@sentry/webpack-plugin@2.22.3": + version "2.22.3" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.3.tgz#a9eeb4689c062eb6dc50671c09f06ec6875b9b02" + integrity sha512-Sq1S6bL3nuoTP5typkj+HPjQ13dqftIE8kACAq4tKkXOpWO9bf6HtqcruEQCxMekbWDTdljsrknQ17ZBx2q66Q== dependencies: - "@sentry/bundler-plugin-core" "2.22.0" - unplugin "1.0.1" - -"@sentry/webpack-plugin@2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.16.0.tgz#4764577edb10c9575a8b4ce03135493f995f56b9" - integrity sha512-BeKLmtK4OD9V3j92fm/lm6yp+++s2U5Uf17HwNFGt39PEOq+wUDISsx0dhXA5Qls2Bg3WhguDK71blCaVefMeg== - dependencies: - "@sentry/bundler-plugin-core" "2.16.0" - unplugin "1.0.1" - uuid "^9.0.0" - -"@sentry/webpack-plugin@2.20.1": - version "2.20.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.20.1.tgz#285d325a0a1bd0a534126b97e0190da9486ff7f6" - integrity sha512-U6LzoE09Ndt0OCWROoRaZqqIHGxyMRdKpBhbqoBqyyfVwXN/zGW3I/cWZ1e8rreiKFj+2+c7+X0kOS+NGMTUrg== - dependencies: - "@sentry/bundler-plugin-core" "2.20.1" + "@sentry/bundler-plugin-core" "2.22.3" unplugin "1.0.1" uuid "^9.0.0" @@ -23508,13 +23426,6 @@ magic-string@0.26.2: dependencies: sourcemap-codec "^1.4.8" -magic-string@0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@0.30.7: version "0.30.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505"