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 39b024bd77dd..77ddae4705b6 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 @@ -456,7 +458,8 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18, 20, 22] + # TODO(lforst): Unpin Node.js version 22 when https://github.com/protobufjs/protobuf.js/issues/2025 is resolved which broke the nodejs tests + node: [14, 16, 18, 20, '22.6.0'] steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 @@ -473,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 }} @@ -496,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: @@ -512,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 @@ -580,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 @@ -595,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 @@ -630,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 @@ -645,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 @@ -667,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 @@ -705,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 @@ -746,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 @@ -784,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: @@ -896,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', @@ -946,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 @@ -985,6 +1003,15 @@ jobs: working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 5 run: pnpm test:assert + + - name: Upload Playwright Traces + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-traces-job_e2e_playwright_tests-${{ matrix.test-application}} + path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application}}/test-results + overwrite: true + retention-days: 7 job_optional_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test @@ -1046,15 +1073,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 @@ -1108,8 +1139,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 @@ -1141,8 +1171,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 @@ -1151,12 +1181,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 @@ -1237,8 +1272,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 @@ -1259,6 +1294,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' }} @@ -1267,8 +1303,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..5da293511976 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -10,6 +10,31 @@ module.exports = [ gzip: true, limit: '24 KB', }, + { + name: '@sentry/browser - with treeshaking flags', + path: 'packages/browser/build/npm/esm/index.js', + import: createImport('init'), + gzip: true, + limit: '24 KB', + modifyWebpackConfig: function (config) { + const webpack = require('webpack'); + const TerserPlugin = require('terser-webpack-plugin'); + + config.plugins.push( + new webpack.DefinePlugin({ + __SENTRY_DEBUG__: false, + __RRWEB_EXCLUDE_SHADOW_DOM__: true, + __RRWEB_EXCLUDE_IFRAME__: true, + __SENTRY_EXCLUDE_REPLAY_WORKER__: true, + }), + ); + + config.optimization.minimize = true; + config.optimization.minimizer = [new TerserPlugin()]; + + return config; + }, + }, { name: '@sentry/browser (incl. Tracing)', path: 'packages/browser/build/npm/esm/index.js', @@ -22,16 +47,18 @@ 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'); + const TerserPlugin = require('terser-webpack-plugin'); + config.plugins.push( new webpack.DefinePlugin({ __SENTRY_DEBUG__: false, @@ -40,6 +67,10 @@ module.exports = [ __SENTRY_EXCLUDE_REPLAY_WORKER__: true, }), ); + + config.optimization.minimize = true; + config.optimization.minimizer = [new TerserPlugin()]; + return config; }, }, @@ -170,7 +201,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', @@ -222,11 +253,17 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { const webpack = require('webpack'); + const TerserPlugin = require('terser-webpack-plugin'); + config.plugins.push( new webpack.DefinePlugin({ __SENTRY_TRACING__: false, }), ); + + config.optimization.minimize = true; + config.optimization.minimizer = [new TerserPlugin()]; + return config; }, }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 7864efac6871..234c9e5704a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,69 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @Zen-cronic. Thank you for your contribution! + +## 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 + +- **fix(nestjs): Exception filters in main app module are not being executed (#13278)** + + With this release nestjs error monitoring is no longer automatically set up after adding the `SentryModule` to your + application, which led to issues in certain scenarios. You will now have to either add the `SentryGlobalFilter` to + your main module providers or decorate the `catch()` method in your existing global exception filters with the newly + released `@WithSentry()` decorator. See the [docs](https://docs.sentry.io/platforms/javascript/guides/nestjs/) for + more details. + +### Other Changes + +- feat: Add options for passing nonces to feedback integration (#13347) +- feat: Add support for SENTRY_SPOTLIGHT env var in Node (#13325) +- feat(deps): bump @prisma/instrumentation from 5.17.0 to 5.18.0 (#13327) +- feat(feedback): Improve error message for 403 errors (#13441) +- fix(deno): Don't rely on `Deno.permissions.querySync` (#13378) +- fix(replay): Ensure we publish replay CDN bundles (#13437) + Work in this release was contributed by @charpeni. Thank you for your contribution! ## 8.26.0 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 dfd3b78c2c14..d012484a4e0b 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "8.26.0", + "version": "8.28.0", "main": "index.js", "license": "MIT", "engines": { @@ -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", @@ -43,12 +43,12 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.44.1", "@sentry-internal/rrweb": "2.11.0", - "@sentry/browser": "8.26.0", + "@sentry/browser": "8.28.0", "axios": "1.6.7", "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", "pako": "^2.1.0", - "webpack": "^5.90.3" + "webpack": "^5.94.0" }, "devDependencies": { "@types/glob": "8.0.0", diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js new file mode 100644 index 000000000000..5eb27143fdc7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +const feedback = feedbackIntegration({ + autoInject: false, +}); + +window.Sentry = Sentry; +window.feedback = feedback; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [feedback], +}); + +feedback.attachTo('#custom-feedback-buttom'); diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/template.html b/dev-packages/browser-integration-tests/suites/feedback/attachTo/template.html new file mode 100644 index 000000000000..ae36b0c69c7b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts new file mode 100644 index 000000000000..507b08685092 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts @@ -0,0 +1,82 @@ +import { expect } from '@playwright/test'; + +import { TEST_HOST, sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, getEnvelopeType, shouldSkipFeedbackTest } from '../../../utils/helpers'; + +sentryTest('should capture feedback with custom button', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeedbackTest()) { + sentryTest.skip(); + } + + const feedbackRequestPromise = page.waitForResponse(res => { + const req = res.request(); + + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + return getEnvelopeType(req) === 'feedback'; + } catch (err) { + return false; + } + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + await page.locator('#custom-feedback-buttom').click(); + await page.waitForSelector(':visible:text-is("Report a Bug")'); + + expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1); + await page.locator('[name="name"]').fill('Jane Doe'); + await page.locator('[name="email"]').fill('janedoe@example.org'); + await page.locator('[name="message"]').fill('my example feedback'); + await page.locator('[data-sentry-feedback] .btn--primary').click(); + + const feedbackEvent = envelopeRequestParser((await feedbackRequestPromise).request()); + expect(feedbackEvent).toEqual({ + type: 'feedback', + breadcrumbs: expect.any(Array), + contexts: { + feedback: { + contact_email: 'janedoe@example.org', + message: 'my example feedback', + name: 'Jane Doe', + source: 'widget', + url: `${TEST_HOST}/index.html`, + }, + trace: { + trace_id: expect.stringMatching(/\w{32}/), + span_id: expect.stringMatching(/\w{16}/), + }, + }, + level: 'info', + timestamp: expect.any(Number), + event_id: expect.stringMatching(/\w{32}/), + environment: 'production', + tags: {}, + sdk: { + integrations: expect.arrayContaining(['Feedback']), + version: expect.any(String), + name: 'sentry.javascript.browser', + packages: expect.anything(), + }, + request: { + url: `${TEST_HOST}/index.html`, + headers: { + 'User-Agent': expect.stringContaining(''), + }, + }, + platform: 'javascript', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/feedback/logger/init.js b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js new file mode 100644 index 000000000000..3251bd6c7a4c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +// Import this separately so that generatePlugin can handle it for CDN scenarios +import { feedbackIntegration } from '@sentry/browser'; + +const feedback = feedbackIntegration({ + autoInject: false, +}); + +window.Sentry = Sentry; +window.feedback = feedback; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + debug: true, + integrations: [feedback], +}); + +// This should log an error! +feedback.attachTo('#does-not-exist'); diff --git a/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts b/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts new file mode 100644 index 000000000000..34fadfc2503b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipFeedbackTest } from '../../../utils/helpers'; + +/** + * This test is mostly relevant for ensuring that the logger works in all combinations of CDN bundles. + * Even if feedback is included via the CDN, this test ensures that the logger is working correctly. + */ +sentryTest('should log error correctly', async ({ getLocalTestUrl, page }) => { + // In minified bundles we do not have logger messages, so we skip the test + if (shouldSkipFeedbackTest() || (process.env.PW_BUNDLE || '').includes('_min')) { + sentryTest.skip(); + } + + const messages: string[] = []; + + page.on('console', message => { + messages.push(message.text()); + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + expect(messages).toContain('Sentry Logger [log]: Integration installed: Feedback'); + expect(messages).toContain('Sentry Logger [error]: [Feedback] Unable to attach to target element'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js new file mode 100644 index 000000000000..984534454ac7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +const integrations = Sentry.getDefaultIntegrations({}).filter( + defaultIntegration => defaultIntegration.name === 'HttpContext', +); + +const client = new Sentry.BrowserClient({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: Sentry.makeFetchTransport, + stackParser: Sentry.defaultStackParser, + integrations: integrations, +}); + +const scope = new Sentry.Scope(); +scope.setClient(client); +client.init(); + +window._sentryScope = scope; diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js new file mode 100644 index 000000000000..62ce205e2ffc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js @@ -0,0 +1 @@ +window._sentryScope.captureException(new Error('client init')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts new file mode 100644 index 000000000000..02a62142e02b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts @@ -0,0 +1,26 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('httpContextIntegration captures user-agent and referrer', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const errorEventPromise = getFirstSentryEnvelopeRequest(page); + + // Simulate document.referrer being set to test full functionality of the integration + await page.goto(url, { referer: 'https://sentry.io/' }); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + + expect(errorEvent.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + Referer: 'https://sentry.io/', + }, + url: expect.any(String), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js deleted file mode 100644 index 8540ab176c38..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/browser'; -import { httpClientIntegration } from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [httpClientIntegration()], - tracesSampleRate: 1, - sendDefaultPii: true, -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js deleted file mode 100644 index 563b069e66cc..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js +++ /dev/null @@ -1,8 +0,0 @@ -const xhr = new XMLHttpRequest(); - -xhr.open('GET', 'http://sentry-test.io/foo', true); -xhr.withCredentials = true; -xhr.setRequestHeader('Accept', 'application/json'); -xhr.setRequestHeader('Content-Type', 'application/json'); -xhr.setRequestHeader('Cache', 'no-cache'); -xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts deleted file mode 100644 index f064a8652b48..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; - -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; - -sentryTest('works with httpClientIntegration', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - - await page.route('**/foo', route => { - return route.fulfill({ - status: 500, - body: JSON.stringify({ - error: { - message: 'Internal Server Error', - }, - }), - headers: { - 'Content-Type': 'text/html', - }, - }); - }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.exception?.values).toHaveLength(1); - - // Not able to get the cookies from the request/response because of Playwright bug - // https://github.com/microsoft/playwright/issues/11035 - expect(eventData).toMatchObject({ - message: 'HTTP Client Error with status code: 500', - exception: { - values: [ - { - type: 'Error', - value: 'HTTP Client Error with status code: 500', - mechanism: { - type: 'http.client', - handled: false, - }, - }, - ], - }, - request: { - url: 'http://sentry-test.io/foo', - method: 'GET', - headers: { - accept: 'application/json', - cache: 'no-cache', - 'content-type': 'application/json', - }, - }, - contexts: { - response: { - status_code: 500, - body_size: 45, - headers: { - 'content-type': 'text/html', - 'content-length': '45', - }, - }, - }, - }); -}); 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/replay/logger/init.js b/dev-packages/browser-integration-tests/suites/replay/logger/init.js new file mode 100644 index 000000000000..195be16ddad3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/logger/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = Sentry.replayIntegration({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + debug: true, + + integrations: [window.Replay], +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/logger/test.ts b/dev-packages/browser-integration-tests/suites/replay/logger/test.ts new file mode 100644 index 000000000000..fa034a12b003 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/logger/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +sentryTest('should output logger messages', async ({ getLocalTestPath, page }) => { + // In minified bundles we do not have logger messages, so we skip the test + if (shouldSkipReplayTest() || (process.env.PW_BUNDLE || '').includes('_min')) { + sentryTest.skip(); + } + + const messages: string[] = []; + + page.on('console', message => { + messages.push(message.text()); + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const reqPromise0 = waitForReplayRequest(page, 0); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await Promise.all([page.goto(url), reqPromise0]); + + expect(messages).toContain('Sentry Logger [log]: Integration installed: Replay'); + expect(messages).toContain('Sentry Logger [info]: [Replay] Creating new session'); + expect(messages).toContain('Sentry Logger [info]: [Replay] Starting replay in session mode'); + expect(messages).toContain('Sentry Logger [info]: [Replay] Using compression worker'); +}); 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..acc583506df4 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'); @@ -30,7 +30,6 @@ const useLoader = bundleKey.startsWith('loader'); const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { httpClientIntegration: 'httpclient', captureConsoleIntegration: 'captureconsole', - CaptureConsole: 'captureconsole', debugIntegration: 'debug', rewriteFramesIntegration: 'rewriteframes', contextLinesIntegration: 'contextlines', @@ -214,7 +213,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 +247,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 +263,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 +293,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/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index a9f2c9265e72..b1375418e16f 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "8.26.0", + "version": "8.28.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", @@ -9,7 +9,7 @@ "private": true, "dependencies": { "html-webpack-plugin": "^5.6.0", - "webpack": "^5.92.1", + "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2" }, "scripts": { 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..60aa626f0dde --- /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.28.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/package.json b/dev-packages/e2e-tests/package.json index 69211cd141a8..61f859eab5c2 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "8.26.0", + "version": "8.28.0", "license": "MIT", "private": true, "scripts": { 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/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 3d81148be51a..6204fcab3e84 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "8.26.0", + "version": "8.28.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index e381dc99e5d6..0ac7fdbf0aa2 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "8.26.0", + "version": "8.28.0", "license": "MIT", "engines": { "node": ">=14.18" @@ -31,10 +31,10 @@ "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", "@prisma/client": "5.9.1", - "@sentry/aws-serverless": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/aws-serverless": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", @@ -44,6 +44,7 @@ "cors": "^2.8.5", "cron": "^3.1.6", "express": "^4.17.3", + "generic-pool": "^3.9.0", "graphql": "^16.3.0", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool/scenario.js b/dev-packages/node-integration-tests/suites/tracing/genericPool/scenario.js new file mode 100644 index 000000000000..74d5f73693f5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/genericPool/scenario.js @@ -0,0 +1,71 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const mysql = require('mysql'); +const genericPool = require('generic-pool'); + +const factory = { + create: function () { + return mysql.createConnection({ + user: 'root', + password: 'docker', + }); + }, + destroy: function (client) { + client.end(err => { + if (err) { + // eslint-disable-next-line no-console + console.error('Error while disconnecting MySQL:', err); + } + }); + }, +}; + +const opts = { + max: 10, + min: 2, +}; + +const myPool = genericPool.createPool(factory, opts); + +async function run() { + await Sentry.startSpan( + { + op: 'transaction', + name: 'Test Transaction', + }, + async () => { + try { + const client1 = await myPool.acquire(); + const client2 = await myPool.acquire(); + + client1.query('SELECT NOW()', function () { + myPool.release(client1); + }); + + client2.query('SELECT 1 + 1 AS solution', function () { + myPool.release(client2); + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error while pooling MySQL:', err); + } finally { + await myPool.drain(); + await myPool.clear(); + } + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts b/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts new file mode 100644 index 000000000000..a61782ae0fe5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts @@ -0,0 +1,34 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('genericPool auto instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should auto-instrument `genericPool` package when calling pool.require()', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: expect.stringMatching(/^generic-pool\.ac?quire/), + origin: 'auto.db.otel.generic-pool', + data: { + 'sentry.origin': 'auto.db.otel.generic-pool', + }, + status: 'ok', + }), + + expect.objectContaining({ + description: expect.stringMatching(/^generic-pool\.ac?quire/), + origin: 'auto.db.otel.generic-pool', + data: { + 'sentry.origin': 'auto.db.otel.generic-pool', + }, + status: 'ok', + }), + ]), + }; + + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + }); +}); 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/overhead-metrics/package.json b/dev-packages/overhead-metrics/package.json index 4b4825b185df..af30273fd41b 100644 --- a/dev-packages/overhead-metrics/package.json +++ b/dev-packages/overhead-metrics/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "8.26.0", + "version": "8.28.0", "name": "@sentry-internal/overhead-metrics", "main": "index.js", "author": "Sentry", 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/package.json b/dev-packages/rollup-utils/package.json index 6ac6f0a1a729..5606d15799e6 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "8.26.0", + "version": "8.28.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", 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/dev-packages/size-limit-gh-action/.eslintrc.cjs b/dev-packages/size-limit-gh-action/.eslintrc.cjs index 8c67e0037908..4a92730d3b0b 100644 --- a/dev-packages/size-limit-gh-action/.eslintrc.cjs +++ b/dev-packages/size-limit-gh-action/.eslintrc.cjs @@ -7,7 +7,7 @@ module.exports = { overrides: [ { - files: ['*.mjs'], + files: ['**/*.mjs'], extends: ['@sentry-internal/sdk/src/base'], }, ], diff --git a/dev-packages/size-limit-gh-action/index.mjs b/dev-packages/size-limit-gh-action/index.mjs index 3dbb8aa22127..680d12237bf5 100644 --- a/dev-packages/size-limit-gh-action/index.mjs +++ b/dev-packages/size-limit-gh-action/index.mjs @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,13 +8,22 @@ import { exec } from '@actions/exec'; import { context, getOctokit } from '@actions/github'; import * as glob from '@actions/glob'; import * as io from '@actions/io'; -import bytes from 'bytes'; import { markdownTable } from 'markdown-table'; +import { SizeLimitFormatter } from './utils/SizeLimitFormatter.mjs'; +import { getArtifactsForBranchAndWorkflow } from './utils/getArtifactsForBranchAndWorkflow.mjs'; + const SIZE_LIMIT_HEADING = '## size-limit report 📦 '; const ARTIFACT_NAME = 'size-limit-action'; const RESULTS_FILE = 'size-limit-results.json'; +function getResultsFilePath() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + return path.resolve(__dirname, RESULTS_FILE); +} + +const { getInput, setFailed } = core; + async function fetchPreviousComment(octokit, repo, pr) { const { data: commentList } = await octokit.rest.issues.listComments({ ...repo, @@ -26,114 +34,6 @@ async function fetchPreviousComment(octokit, repo, pr) { return !sizeLimitComment ? null : sizeLimitComment; } -class SizeLimit { - formatBytes(size) { - return bytes.format(size, { unitSeparator: ' ' }); - } - - formatPercentageChange(base = 0, current = 0) { - if (base === 0) { - return 'added'; - } - - if (current === 0) { - return 'removed'; - } - - const value = ((current - base) / base) * 100; - const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100; - - if (value > 0) { - return `+${formatted}%`; - } - - if (value === 0) { - return '-'; - } - - return `${formatted}%`; - } - - formatChange(base = 0, current = 0) { - if (base === 0) { - return 'added'; - } - - if (current === 0) { - return 'removed'; - } - - const value = current - base; - const formatted = this.formatBytes(value); - - if (value > 0) { - return `+${formatted} 🔺`; - } - - if (value === 0) { - return '-'; - } - - return `${formatted} 🔽`; - } - - formatLine(value, change) { - return `${value} (${change})`; - } - - formatSizeResult(name, base, current) { - return [ - name, - this.formatBytes(current.size), - this.formatPercentageChange(base.size, current.size), - this.formatChange(base.size, current.size), - ]; - } - - parseResults(output) { - const results = JSON.parse(output); - - return results.reduce((current, result) => { - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...current, - [result.name]: { - name: result.name, - size: +result.size, - }, - }; - }, {}); - } - - hasSizeChanges(base, current, threshold = 0) { - const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])]; - - return !!names.find(name => { - const baseResult = base?.[name] || EmptyResult; - const currentResult = current[name] || EmptyResult; - - if (baseResult.size === 0 && currentResult.size === 0) { - return true; - } - - return Math.abs((currentResult.size - baseResult.size) / baseResult.size) * 100 > threshold; - }); - } - - formatResults(base, current) { - const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])]; - const header = SIZE_RESULTS_HEADER; - const fields = names.map(name => { - const baseResult = base?.[name] || EmptyResult; - const currentResult = current[name] || EmptyResult; - - return this.formatSizeResult(name, baseResult, currentResult); - }); - - return [header, ...fields]; - } -} - async function execSizeLimit() { let output = ''; @@ -151,15 +51,8 @@ async function execSizeLimit() { return { status, output }; } -const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change']; - -const EmptyResult = { - name: '-', - size: 0, -}; - async function run() { - const { getInput, setFailed } = core; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); try { const { payload, repo } = context; @@ -174,36 +67,12 @@ async function run() { } const octokit = getOctokit(githubToken); - const limit = new SizeLimit(); - const artifactClient = artifact.create(); - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const resultsFilePath = path.resolve(__dirname, RESULTS_FILE); + const limit = new SizeLimitFormatter(); + const resultsFilePath = getResultsFilePath(); // If we have no comparison branch, we just run size limit & store the result as artifact if (!comparisonBranch) { - let base; - const { output: baseOutput } = await execSizeLimit(); - - try { - base = limit.parseResults(baseOutput); - } catch (error) { - core.error('Error parsing size-limit output. The output should be a json.'); - throw error; - } - - try { - await fs.writeFile(resultsFilePath, JSON.stringify(base), 'utf8'); - } catch (err) { - core.error(err); - } - const globber = await glob.create(resultsFilePath, { - followSymbolicLinks: false, - }); - const files = await globber.glob(); - - await artifactClient.uploadArtifact(ARTIFACT_NAME, files, __dirname); - - return; + return runSizeLimitOnComparisonBranch(); } // Else, we run size limit for the current branch, AND fetch it for the comparison branch @@ -213,12 +82,15 @@ async function run() { let baseWorkflowRun; try { + const workflowName = `${process.env.GITHUB_WORKFLOW || ''}`; + core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${comparisonBranch}"`); const artifacts = await getArtifactsForBranchAndWorkflow(octokit, { ...repo, artifactName: ARTIFACT_NAME, branch: comparisonBranch, - workflowName: `${process.env.GITHUB_WORKFLOW || ''}`, + workflowName, }); + core.endGroup(); if (!artifacts) { throw new Error('No artifacts found'); @@ -255,9 +127,12 @@ async function run() { const thresholdNumber = Number(threshold); - // @ts-ignore const sizeLimitComment = await fetchPreviousComment(octokit, repo, pr); + if (sizeLimitComment) { + core.debug('Found existing size limit comment, udpating it instead of creating a new one...'); + } + const shouldComment = isNaN(thresholdNumber) || limit.hasSizeChanges(base, current, thresholdNumber) || sizeLimitComment; @@ -269,8 +144,12 @@ async function run() { '⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.', ); } - - bodyParts.push(markdownTable(limit.formatResults(base, current))); + try { + bodyParts.push(markdownTable(limit.formatResults(base, current))); + } catch (error) { + core.error('Error generating markdown table'); + core.error(error); + } if (baseWorkflowRun) { bodyParts.push(''); @@ -298,6 +177,8 @@ async function run() { "Error updating comment. This can happen for PR's originating from a fork without write permissions.", ); } + } else { + core.debug('Skipping comment because there are no changes.'); } if (status > 0) { @@ -309,136 +190,28 @@ async function run() { } } -// max pages of workflows to pagination through -const DEFAULT_MAX_PAGES = 50; -// max results per page -const DEFAULT_PAGE_LIMIT = 10; - -/** - * Fetch artifacts from a workflow run from a branch - * - * This is a bit hacky since GitHub Actions currently does not directly - * support downloading artifacts from other workflows - */ -/** - * Fetch artifacts from a workflow run from a branch - * - * This is a bit hacky since GitHub Actions currently does not directly - * support downloading artifacts from other workflows - */ -async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) { - core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${branch}"`); - - let repositoryWorkflow = null; +async function runSizeLimitOnComparisonBranch() { + const resultsFilePath = getResultsFilePath(); - // For debugging - const allWorkflows = []; - - // - // Find workflow id from `workflowName` - // - for await (const response of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, { - owner, - repo, - })) { - const targetWorkflow = response.data.find(({ name }) => name === workflowName); + const limit = new SizeLimitFormatter(); + const artifactClient = artifact.create(); - allWorkflows.push(...response.data.map(({ name }) => name)); + const { output: baseOutput } = await execSizeLimit(); - // If not found in responses, continue to search on next page - if (!targetWorkflow) { - continue; - } - - repositoryWorkflow = targetWorkflow; - break; - } - - if (!repositoryWorkflow) { - core.info( - `Unable to find workflow with name "${workflowName}" in the repository. Found workflows: ${allWorkflows.join( - ', ', - )}`, - ); - core.endGroup(); - return null; + try { + const base = limit.parseResults(baseOutput); + await fs.writeFile(resultsFilePath, JSON.stringify(base), 'utf8'); + } catch (error) { + core.error('Error parsing size-limit output. The output should be a json.'); + throw error; } - const workflow_id = repositoryWorkflow.id; - - let currentPage = 0; - let latestWorkflowRun = null; - - for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, { - owner, - repo, - workflow_id, - branch, - per_page: DEFAULT_PAGE_LIMIT, - event: 'push', - })) { - if (!response.data.length) { - core.warning(`Workflow ${workflow_id} not found in branch ${branch}`); - core.endGroup(); - return null; - } - - // Do not allow downloading artifacts from a fork. - const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`); - - // Sort to ensure the latest workflow run is the first - filtered.sort((a, b) => { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); - }); - - // Store the first workflow run, to determine if this is the latest one... - if (!latestWorkflowRun) { - latestWorkflowRun = filtered[0]; - } - - // Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for - for (const workflowRun of filtered) { - core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`); - - const { - data: { artifacts }, - } = await octokit.rest.actions.listWorkflowRunArtifacts({ - owner, - repo, - run_id: workflowRun.id, - }); - - if (!artifacts) { - core.warning( - `Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`, - ); - } else { - const foundArtifact = artifacts.find(({ name }) => name === artifactName); - if (foundArtifact) { - core.info(`Found suitable artifact: ${foundArtifact.url}`); - return { - artifact: foundArtifact, - workflowRun, - isLatest: latestWorkflowRun.id === workflowRun.id, - }; - } else { - core.info(`No artifact found for ${artifactName}, trying next workflow run...`); - } - } - } - - if (currentPage > DEFAULT_MAX_PAGES) { - core.warning(`Workflow ${workflow_id} not found in branch: ${branch}`); - core.endGroup(); - return null; - } - - currentPage++; - } + const globber = await glob.create(resultsFilePath, { + followSymbolicLinks: false, + }); + const files = await globber.glob(); - core.warning(`Artifact not found: ${artifactName}`); - core.endGroup(); - return null; + await artifactClient.uploadArtifact(ARTIFACT_NAME, files, __dirname); } run(); diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index 7d76088b54b6..a2d942ff30e6 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "8.26.0", + "version": "8.28.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/size-limit-gh-action/utils/SizeLimitFormatter.mjs b/dev-packages/size-limit-gh-action/utils/SizeLimitFormatter.mjs new file mode 100644 index 000000000000..034281b38224 --- /dev/null +++ b/dev-packages/size-limit-gh-action/utils/SizeLimitFormatter.mjs @@ -0,0 +1,137 @@ +import * as core from '@actions/core'; +import bytes from 'bytes'; + +const SIZE_RESULTS_HEADER = ['Path', 'Size', '% Change', 'Change']; + +const EmptyResult = { + name: '-', + size: 0, +}; + +export class SizeLimitFormatter { + formatBytes(size) { + return bytes.format(size, { unitSeparator: ' ' }); + } + + formatName(name, sizeLimit, passed) { + if (passed) { + return name; + } + + return `⛔️ ${name} (max: ${this.formatBytes(sizeLimit)})`; + } + + formatPercentageChange(base = 0, current = 0) { + if (base === 0) { + return 'added'; + } + + if (current === 0) { + return 'removed'; + } + + const value = ((current - base) / base) * 100; + const formatted = (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100; + + if (value > 0) { + return `+${formatted}%`; + } + + if (value === 0) { + return '-'; + } + + return `${formatted}%`; + } + + formatChange(base = 0, current = 0) { + if (base === 0) { + return 'added'; + } + + if (current === 0) { + return 'removed'; + } + + const value = current - base; + const formatted = this.formatBytes(value); + + if (value > 0) { + return `+${formatted} 🔺`; + } + + if (value === 0) { + return '-'; + } + + return `${formatted} 🔽`; + } + + formatLine(value, change) { + return `${value} (${change})`; + } + + formatSizeResult(name, base, current) { + if (!current.passed) { + core.debug( + `Size limit exceeded for ${name} - ${this.formatBytes(current.size)} > ${this.formatBytes(current.sizeLimit)}`, + ); + } + + return [ + this.formatName(name, current.sizeLimit, current.passed), + this.formatBytes(current.size), + this.formatPercentageChange(base.size, current.size), + this.formatChange(base.size, current.size), + ]; + } + + parseResults(output) { + const results = JSON.parse(output); + + return results.reduce((current, result) => { + return { + // biome-ignore lint/performance/noAccumulatingSpread: + ...current, + [result.name]: { + name: result.name, + size: +result.size, + sizeLimit: +result.sizeLimit, + passed: result.passed || false, + }, + }; + }, {}); + } + + hasSizeChanges(base, current, threshold = 0) { + if (!base || !current) { + return true; + } + + const names = [...new Set([...Object.keys(base), ...Object.keys(current)])]; + + return names.some(name => { + const baseResult = base[name] || EmptyResult; + const currentResult = current[name] || EmptyResult; + + if (!baseResult.size || !currentResult.size) { + return true; + } + + return Math.abs((currentResult.size - baseResult.size) / baseResult.size) * 100 > threshold; + }); + } + + formatResults(base, current) { + const names = [...new Set([...(base ? Object.keys(base) : []), ...Object.keys(current)])]; + const header = SIZE_RESULTS_HEADER; + const fields = names.map(name => { + const baseResult = base?.[name] || EmptyResult; + const currentResult = current[name] || EmptyResult; + + return this.formatSizeResult(name, baseResult, currentResult); + }); + + return [header, ...fields]; + } +} diff --git a/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs b/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs new file mode 100644 index 000000000000..6d512b46afe1 --- /dev/null +++ b/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs @@ -0,0 +1,128 @@ +import * as core from '@actions/core'; + +// max pages of workflows to pagination through +const DEFAULT_MAX_PAGES = 50; +// max results per page +const DEFAULT_PAGE_LIMIT = 10; + +/** + * Fetch artifacts from a workflow run from a branch + * + * This is a bit hacky since GitHub Actions currently does not directly + * support downloading artifacts from other workflows + */ +/** + * Fetch artifacts from a workflow run from a branch + * + * This is a bit hacky since GitHub Actions currently does not directly + * support downloading artifacts from other workflows + */ +export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) { + let repositoryWorkflow = null; + + // For debugging + const allWorkflows = []; + + // + // Find workflow id from `workflowName` + // + for await (const response of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, { + owner, + repo, + })) { + const targetWorkflow = response.data.find(({ name }) => name === workflowName); + + allWorkflows.push(...response.data.map(({ name }) => name)); + + // If not found in responses, continue to search on next page + if (!targetWorkflow) { + continue; + } + + repositoryWorkflow = targetWorkflow; + break; + } + + if (!repositoryWorkflow) { + core.info( + `Unable to find workflow with name "${workflowName}" in the repository. Found workflows: ${allWorkflows.join( + ', ', + )}`, + ); + return null; + } + + const workflow_id = repositoryWorkflow.id; + + let currentPage = 0; + let latestWorkflowRun = null; + + for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id, + branch, + per_page: DEFAULT_PAGE_LIMIT, + event: 'push', + })) { + if (!response.data.length) { + core.warning(`Workflow ${workflow_id} not found in branch ${branch}`); + return null; + } + + // Do not allow downloading artifacts from a fork. + const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`); + + // Sort to ensure the latest workflow run is the first + filtered.sort((a, b) => { + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + }); + + // Store the first workflow run, to determine if this is the latest one... + if (!latestWorkflowRun) { + latestWorkflowRun = filtered[0]; + } + + // Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for + for (const workflowRun of filtered) { + core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`); + + const { + data: { artifacts }, + } = await octokit.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: workflowRun.id, + }); + + if (!artifacts) { + core.warning( + `Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`, + ); + } else { + const foundArtifact = artifacts.find(({ name }) => name === artifactName); + if (foundArtifact) { + core.info(`Found suitable artifact: ${foundArtifact.url}`); + return { + artifact: foundArtifact, + workflowRun, + isLatest: latestWorkflowRun.id === workflowRun.id, + }; + } else { + core.info(`No artifact found for ${artifactName}, trying next workflow run...`); + } + } + } + + if (currentPage > DEFAULT_MAX_PAGES) { + core.warning(`Workflow ${workflow_id} not found in branch: ${branch}`); + return null; + } + + currentPage++; + } + + core.warning(`Artifact not found: ${artifactName}`); + core.endGroup(); + return null; +} diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index a0b1688f4142..10df04020be0 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "8.26.0", + "version": "8.28.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,8 +45,8 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/test-utils/src/playwright-config.ts b/dev-packages/test-utils/src/playwright-config.ts index da2a10d0b477..d30c8cad4475 100644 --- a/dev-packages/test-utils/src/playwright-config.ts +++ b/dev-packages/test-utils/src/playwright-config.ts @@ -46,7 +46,7 @@ export function getPlaywrightConfig( baseURL: `http://localhost:${appPort}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: 'retain-on-failure', }, /* Configure projects for major browsers */ diff --git a/lerna.json b/lerna.json index e3c3f83f3096..dba33b299b7f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "8.26.0", + "version": "8.28.0", "npmClient": "yarn" } 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/angular/package.json b/packages/angular/package.json index a6c6bb06ef94..076c5d08bbf4 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,10 +21,10 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index b5fd94e25d3a..77f9c17cb8db 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,12 +56,12 @@ "astro": ">=3.x || >=4.0.0-beta" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/vite-plugin": "^2.20.1" + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", + "@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..747870da3014 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -43,6 +43,7 @@ export { fsIntegration, functionToStringIntegration, generateInstrumentOnce, + genericPoolIntegration, getActiveSpan, getAutoPerformanceIntegrations, getClient, @@ -123,6 +124,7 @@ export { withMonitor, withScope, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 881976a8db34..fa20bebe24ff 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -66,10 +66,10 @@ "dependencies": { "@opentelemetry/instrumentation-aws-lambda": "0.43.0", "@opentelemetry/instrumentation-aws-sdk": "0.43.1", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 7b05f8df3a86..f648dba045ec 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -87,6 +87,7 @@ export { setupConnectErrorHandler, fastifyIntegration, fsIntegration, + genericPoolIntegration, graphqlIntegration, mongoIntegration, mongooseIntegration, @@ -107,6 +108,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 235032606e5d..f2aba350da39 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "8.26.0", + "version": "8.28.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "scripts": { "build": "run-p build:transpile build:types", 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/package.json b/packages/browser/package.json index 07322e621598..e85d3da99aa6 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,16 +39,16 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "8.26.0", - "@sentry-internal/feedback": "8.26.0", - "@sentry-internal/replay": "8.26.0", - "@sentry-internal/replay-canvas": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry-internal/feedback": "8.28.0", + "@sentry-internal/replay": "8.28.0", + "@sentry-internal/replay-canvas": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "8.26.0", + "@sentry-internal/integration-shims": "8.28.0", "fake-indexeddb": "^4.0.1" }, "scripts": { 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/package.json b/packages/bun/package.json index fe0c181bf70a..af78c308caa2 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,11 +39,11 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e49e4163af31..1d8b02c33568 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -108,6 +108,7 @@ export { setupKoaErrorHandler, connectIntegration, setupConnectErrorHandler, + genericPoolIntegration, graphqlIntegration, mongoIntegration, mongooseIntegration, @@ -128,6 +129,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index fd9d9bcffe36..0e4f10ca706e 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "8.26.0", + "version": "8.28.0", "description": "Offical Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "optionalDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 560c17afb9e7..7a474c3b27cb 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -31,7 +31,13 @@ export function wrapRequestHandler( handler: (...args: unknown[]) => Response | Promise, ): Promise { return withIsolationScope(async isolationScope => { - const { options, request, context } = wrapperOptions; + const { options, request } = wrapperOptions; + + // In certain situations, the passed context can become undefined. + // For example, for Astro while prerendering pages at build time. + // see: https://github.com/getsentry/sentry-javascript/issues/13217 + const context = wrapperOptions.context as ExecutionContext | undefined; + const client = init(options); isolationScope.setClient(client); @@ -89,7 +95,7 @@ export function wrapRequestHandler( captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); throw e; } finally { - context.waitUntil(flush(2000)); + context?.waitUntil(flush(2000)); } }, ); 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/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 93764a292ab4..5218e8afe20b 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -45,6 +45,15 @@ describe('withSentry', () => { expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise)); }); + test("doesn't error if context is undefined", () => { + expect(() => + wrapRequestHandler( + { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: undefined as any }, + () => new Response('test'), + ), + ).not.toThrow(); + }); + test('creates a cloudflare client and sets it on the handler', async () => { const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); await wrapRequestHandler( diff --git a/packages/core/package.json b/packages/core/package.json index 8f140bfbb09c..da87626676a8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "8.26.0", + "version": "8.28.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "scripts": { "build": "run-p build:transpile build:types", 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/package.json b/packages/deno/package.json index 4178c41cff65..0462939ae06f 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,9 +24,9 @@ "/build" ], "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.5", 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/ember/package.json b/packages/ember/package.json index 046d187d8da3..1bec2d30f9bf 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -33,10 +33,10 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", @@ -80,7 +80,7 @@ "qunit": "~2.19.2", "qunit-dom": "~2.0.0", "sinon": "15.2.0", - "webpack": "~5.90.3" + "webpack": "~5.94.0" }, "engines": { "node": ">=14.18" diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index f7d26ab13625..3da6854a5c3c 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "8.26.0", - "@sentry-internal/typescript": "8.26.0", + "@sentry-internal/eslint-plugin-sdk": "8.28.0", + "@sentry-internal/typescript": "8.28.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", 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/package.json b/packages/eslint-plugin-sdk/package.json index 6f01f96eee3e..577bce3380a9 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", 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/feedback/package.json b/packages/feedback/package.json index ff4c10fcad8a..b635b2dd46c8 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "8.26.0", + "version": "8.28.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 4bfd08a2dc80..013d02cd8d84 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,11 +45,11 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/react": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/webpack-plugin": "2.16.0" + "@sentry/core": "8.28.0", + "@sentry/react": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.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/package.json b/packages/google-cloud-serverless/package.json index d8bcc758752b..72c5a0b94d9a 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud", @@ -48,10 +48,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 9a501307e79f..463a0c5c1246 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -87,6 +87,7 @@ export { connectIntegration, setupConnectErrorHandler, fastifyIntegration, + genericPoolIntegration, graphqlIntegration, mongoIntegration, mongooseIntegration, @@ -107,6 +108,7 @@ export { trpcMiddleware, addOpenTelemetryInstrumentation, zodErrorsIntegration, + profiler, } from '@sentry/node'; export { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 88eee4b1175a..70c7737fcdca 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "8.26.0", + "version": "8.28.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -55,9 +55,9 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" 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/package.json b/packages/nestjs/package.json index 902b89d11591..65fa623595e7 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -44,10 +44,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", 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 466a98f8512f..354870f6e834 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -71,14 +71,14 @@ "@opentelemetry/instrumentation-http": "0.52.1", "@opentelemetry/semantic-conventions": "^1.25.1", "@rollup/plugin-commonjs": "26.0.1", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/react": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/vercel-edge": "8.26.0", - "@sentry/webpack-plugin": "2.20.1", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/react": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", + "@sentry/vercel-edge": "8.28.0", + "@sentry/webpack-plugin": "2.22.3", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "3.29.4", @@ -91,7 +91,7 @@ }, "peerDependencies": { "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0", - "webpack": ">= 5.0.0" + "webpack": "5.94.0" }, "peerDependenciesMeta": { "webpack": { 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/package.json b/packages/node/package.json index fa8fa1a797c7..6a64f646f64c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "8.26.0", + "version": "8.28.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -73,6 +73,7 @@ "@opentelemetry/instrumentation-express": "0.41.1", "@opentelemetry/instrumentation-fastify": "0.38.0", "@opentelemetry/instrumentation-fs": "0.14.0", + "@opentelemetry/instrumentation-generic-pool": "0.38.0", "@opentelemetry/instrumentation-graphql": "0.42.0", "@opentelemetry/instrumentation-hapi": "0.40.0", "@opentelemetry/instrumentation-http": "0.52.1", @@ -89,10 +90,10 @@ "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.25.1", "@prisma/instrumentation": "5.18.0", - "@sentry/core": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/core": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "import-in-the-middle": "^1.11.0" }, "devDependencies": { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9ef89ab42fb7..6ce3c325e3ff 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -26,6 +26,7 @@ export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/h export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa'; export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect'; export { spotlightIntegration } from './integrations/spotlight'; +export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; @@ -127,6 +128,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/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts index 093b314a6138..fa7a9974135a 100644 --- a/packages/node/src/integrations/node-fetch.ts +++ b/packages/node/src/integrations/node-fetch.ts @@ -192,11 +192,11 @@ function getBreadcrumbData(request: FetchRequest): Partial function getAbsoluteUrl(origin: string, path: string = '/'): string { const url = `${origin}`; - if (origin.endsWith('/') && path.startsWith('/')) { + if (url.endsWith('/') && path.startsWith('/')) { return `${url}${path.slice(1)}`; } - if (!origin.endsWith('/') && !path.startsWith('/')) { + if (!url.endsWith('/') && !path.startsWith('/')) { return `${url}/${path.slice(1)}`; } diff --git a/packages/node/src/integrations/tracing/genericPool.ts b/packages/node/src/integrations/tracing/genericPool.ts new file mode 100644 index 000000000000..55b8b6095f64 --- /dev/null +++ b/packages/node/src/integrations/tracing/genericPool.ts @@ -0,0 +1,40 @@ +import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; + +const INTEGRATION_NAME = 'GenericPool'; + +export const instrumentGenericPool = generateInstrumentOnce(INTEGRATION_NAME, () => new GenericPoolInstrumentation({})); + +const _genericPoolIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentGenericPool(); + }, + + setup(client) { + client.on('spanStart', span => { + const spanJSON = spanToJSON(span); + + const spanDescription = spanJSON.description; + + // typo in emitted span for version <= 0.38.0 of @opentelemetry/instrumentation-generic-pool + const isGenericPoolSpan = + spanDescription === 'generic-pool.aquire' || spanDescription === 'generic-pool.acquire'; + + if (isGenericPoolSpan) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.generic-pool'); + } + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * GenericPool integration + * + * Capture tracing data for GenericPool. + */ +export const genericPoolIntegration = defineIntegration(_genericPoolIntegration); diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 886c11683674..46a9f79e4caa 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -4,6 +4,7 @@ import { instrumentHttp } from '../http'; import { connectIntegration, instrumentConnect } from './connect'; import { expressIntegration, instrumentExpress } from './express'; import { fastifyIntegration, instrumentFastify } from './fastify'; +import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; import { instrumentKoa, koaIntegration } from './koa'; @@ -37,6 +38,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { hapiIntegration(), koaIntegration(), connectIntegration(), + genericPoolIntegration(), ]; } @@ -61,5 +63,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentHapi, instrumentGraphql, instrumentRedis, + instrumentGenericPool, ]; } 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 2216991b5e6e..2aaa9f06dd78 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -43,14 +43,14 @@ }, "dependencies": { "@nuxt/kit": "^3.12.2", - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/vite-plugin": "2.20.1", - "@sentry/vue": "8.26.0" + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", + "@sentry/vite-plugin": "2.22.3", + "@sentry/vue": "8.28.0" }, "devDependencies": { "@nuxt/module-builder": "0.8.1", 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/opentelemetry/package.json b/packages/opentelemetry/package.json index 1ffc241172f9..fadbcadc6f5e 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index a779edc4e732..504706ab6418 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -75,10 +75,10 @@ "test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "detect-libc": "^2.0.2", "node-abi": "^3.61.0" }, 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/react/package.json b/packages/react/package.json index 9f2c8d5c3c12..0afed2e367ad 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,10 +39,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 120f4592d94a..286926c79f1d 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -55,12 +55,12 @@ "@opentelemetry/instrumentation-http": "0.52.1", "@remix-run/router": "1.x", "@sentry/cli": "^2.33.0", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/react": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/react": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "glob": "^10.3.4", "opentelemetry-instrumentation-remix": "0.7.1", "yargs": "^17.6.0" diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 7ab6efb15827..457dcb9f8685 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -47,6 +47,7 @@ export { flush, functionToStringIntegration, generateInstrumentOnce, + genericPoolIntegration, getActiveSpan, getAutoPerformanceIntegrations, getClient, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 4dd27ce4a18c..e28e297b2b72 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "8.26.0", + "version": "8.28.0", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -65,13 +65,13 @@ }, "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.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry-internal/replay": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index cf655cbf1a38..fe9221915e62 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "8.26.0", + "version": "8.28.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -68,18 +68,18 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "8.26.0", - "@sentry-internal/rrweb": "2.25.0", - "@sentry-internal/rrweb-snapshot": "2.25.0", + "@sentry-internal/replay-worker": "8.28.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" }, "dependencies": { - "@sentry-internal/browser-utils": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" 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..b3dcab0e7dd7 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; nodeIds?: number[] }[]; } /** diff --git a/packages/replay-internal/src/util/createPerformanceEntries.ts b/packages/replay-internal/src/util/createPerformanceEntries.ts index d55c2269d0f4..0c22ba73163a 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): 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/replay-worker/package.json b/packages/replay-worker/package.json index 337490ecd4f2..126c3b48cb20 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "8.26.0", + "version": "8.28.0", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index e8c7547014c0..7ae9f47239cd 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -44,10 +44,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "peerDependencies": { "@solidjs/router": "^0.13.4", 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 3251549a62b5..80de3c6b5b36 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -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": { @@ -78,13 +67,13 @@ }, "dependencies": { "@opentelemetry/instrumentation": "^0.52.1", - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/solid": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/vite-plugin": "2.19.0" + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/solid": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.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/client/sdk.ts b/packages/solidstart/src/client/sdk.ts index f44a2134ce50..8b1b6f7f39d5 100644 --- a/packages/solidstart/src/client/sdk.ts +++ b/packages/solidstart/src/client/sdk.ts @@ -1,13 +1,21 @@ import { applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/solid'; -import { init as initSolidSDK } from '@sentry/solid'; -import type { Client } from '@sentry/types'; +import { + browserTracingIntegration, + getDefaultIntegrations as getDefaultSolidIntegrations, + init as initSolidSDK, +} from '@sentry/solid'; +import type { Client, Integration } from '@sentry/types'; + +// Treeshakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; /** * Initializes the client side of the Solid Start SDK. */ export function init(options: BrowserOptions): Client | undefined { const opts = { + defaultIntegrations: getDefaultIntegrations(options), ...options, }; @@ -15,3 +23,20 @@ export function init(options: BrowserOptions): Client | undefined { return initSolidSDK(opts); } + +function getDefaultIntegrations(options: BrowserOptions): Integration[] { + const integrations = getDefaultSolidIntegrations(options); + + // 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__) { + // We add the default BrowserTracingIntegration here always. + // We can do this, even if `solidRouterBrowserTracingIntegration` is + // supplied as integration in `init` by users because it will win + // over the default integration by virtue of having the same + // `BrowserTracing` integration name and being added later. + integrations.push(browserTracingIntegration()); + } + + return integrations; +} diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 75a67d3bb847..995f58d057e3 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -38,6 +38,7 @@ export { flush, functionToStringIntegration, generateInstrumentOnce, + genericPoolIntegration, getActiveSpan, getAutoPerformanceIntegrations, getClient, @@ -128,3 +129,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/client/sdk.test.ts b/packages/solidstart/test/client/sdk.test.ts index 886bb29b515d..b242e03ceb70 100644 --- a/packages/solidstart/test/client/sdk.test.ts +++ b/packages/solidstart/test/client/sdk.test.ts @@ -3,6 +3,7 @@ import * as SentrySolid from '@sentry/solid'; import { vi } from 'vitest'; import { init as solidStartInit } from '../../src/client'; +import { solidRouterBrowserTracingIntegration } from '../../src/client/solidrouter'; const browserInit = vi.spyOn(SentrySolid, 'init'); @@ -34,3 +35,47 @@ describe('Initialize Solid Start SDK', () => { expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); }); }); + +describe('browserTracingIntegration', () => { + it('adds the `browserTracingIntegration` when `__SENTRY_TRACING__` is not set', () => { + const client = solidStartInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const browserTracingIntegration = client + ?.getOptions() + .integrations.find(integration => integration.name === 'BrowserTracing'); + expect(browserTracingIntegration).toBeDefined(); + expect(browserTracingIntegration!.isDefaultInstance).toEqual(true); + }); + + it("doesn't add the `browserTracingIntegration` if `__SENTRY_TRACING__` is false", () => { + // @ts-expect-error Test setup for build-time flag + globalThis.__SENTRY_TRACING__ = false; + + const client = solidStartInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const browserTracingIntegration = client + ?.getOptions() + .integrations.find(integration => integration.name === 'BrowserTracing'); + expect(browserTracingIntegration).toBeUndefined(); + + // @ts-expect-error Test setup for build-time flag + delete globalThis.__SENTRY_TRACING__; + }); + + it("doesn't add the default `browserTracingIntegration` if `solidBrowserTracingIntegration` was already passed in", () => { + const client = solidStartInit({ + integrations: [solidRouterBrowserTracingIntegration()], + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const browserTracingIntegration = client + ?.getOptions() + .integrations.find(integration => integration.name === 'BrowserTracing'); + expect(browserTracingIntegration).toBeDefined(); + expect(browserTracingIntegration!.isDefaultInstance).toBeUndefined(); + }); +}); 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/svelte/package.json b/packages/svelte/package.json index cd01ba37e5bc..ea9ee95b49d7 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,10 +39,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index a3df07e38eff..0a47d189fd8c 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -40,13 +40,13 @@ } }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/node": "8.26.0", - "@sentry/opentelemetry": "8.26.0", - "@sentry/svelte": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0", - "@sentry/vite-plugin": "2.22.0", + "@sentry/core": "8.28.0", + "@sentry/node": "8.28.0", + "@sentry/opentelemetry": "8.28.0", + "@sentry/svelte": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", + "@sentry/vite-plugin": "2.22.3", "magic-string": "0.30.7", "magicast": "0.2.8", "sorcery": "0.11.0" diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 32dd6627d7a6..d57ec35bd7cc 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -37,6 +37,7 @@ export { fastifyIntegration, flush, functionToStringIntegration, + genericPoolIntegration, generateInstrumentOnce, getActiveSpan, getAutoPerformanceIntegrations, diff --git a/packages/types/package.json b/packages/types/package.json index 3d649c4f3c0f..5d9a011c0677 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "8.26.0", + "version": "8.28.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", 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/typescript/package.json b/packages/typescript/package.json index 890db27ce2f2..1aeea0611add 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "8.26.0", + "version": "8.28.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/utils/package.json b/packages/utils/package.json index f9aaffaeaf56..0f11c016dde3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/utils", - "version": "8.26.0", + "version": "8.28.0", "description": "Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/types": "8.26.0" + "@sentry/types": "8.28.0" }, "devDependencies": { "@types/array.prototype.flat": "^1.2.1", 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/utils/src/logger.ts b/packages/utils/src/logger.ts index e996d87202b2..533b59fd5882 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -1,7 +1,7 @@ import type { ConsoleLevel } from '@sentry/types'; import { DEBUG_BUILD } from './debug-build'; -import { GLOBAL_OBJ } from './worldwide'; +import { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; /** Prefix for logging strings */ const PREFIX = 'Sentry Logger '; @@ -97,4 +97,8 @@ function makeLogger(): Logger { return logger as Logger; } -export const logger = makeLogger(); +/** + * This is a logger singleton which either logs things or no-ops if logging is not enabled. + * The logger is a singleton on the carrier, to ensure that a consistent logger is used throughout the SDK. + */ +export const logger = getGlobalSingleton('logger', makeLogger); diff --git a/packages/utils/src/version.ts b/packages/utils/src/version.ts index 1b32cd60a8c4..a42658a94943 100644 --- a/packages/utils/src/version.ts +++ b/packages/utils/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '8.26.0'; +export const SDK_VERSION = '8.28.0'; diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index e323f12034a2..2a1ca7b958d8 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -15,6 +15,7 @@ import type { Client, MetricsAggregator, Scope } from '@sentry/types'; import type { SdkSource } from './env'; +import type { logger } from './logger'; import { SDK_VERSION } from './version'; interface SentryCarrier { @@ -25,6 +26,7 @@ interface SentryCarrier { defaultIsolationScope?: Scope; defaultCurrentScope?: Scope; globalMetricsAggregators?: WeakMap | undefined; + logger?: typeof logger; /** Overwrites TextEncoder used in `@sentry/utils`, need for `react-native@0.73` and older */ encodePolyfill?: (input: string) => Uint8Array; diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 93c6b9e46201..3ff7c934eba1 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "8.26.0", + "version": "8.28.0", "description": "Offical Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1" 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/package.json b/packages/vue/package.json index dbb639eccc40..6e6022c58236 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "8.26.0", + "version": "8.28.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,10 +39,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "peerDependencies": { "vue": "2.x || 3.x" diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 70f662559adf..135b7fa8c9cc 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -114,6 +114,8 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', }, + // UI spans should only be created if there is an active root span (transaction) + onlyIfParent: true, }); } } else { 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/packages/wasm/package.json b/packages/wasm/package.json index 36d7116050f4..961c78f9e9b2 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "8.26.0", + "version": "8.28.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,10 +39,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.26.0", - "@sentry/core": "8.26.0", - "@sentry/types": "8.26.0", - "@sentry/utils": "8.26.0" + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", diff --git a/yarn.lock b/yarn.lock index 481175f8db61..b0a8c712e374 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7113,6 +7113,13 @@ "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.52.0" +"@opentelemetry/instrumentation-generic-pool@0.38.0": + version "0.38.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.0.tgz#9ea4d82da23541cda613d553bd405b2cbc0da184" + integrity sha512-0/ULi6pIco1fEnDPmmAul8ZoudFL7St0hjgBbWZlZPBCSyslDll1J7DFeEbjiRSSyUd+0tu73ae0DOKVKNd7VA== + dependencies: + "@opentelemetry/instrumentation" "^0.52.0" + "@opentelemetry/instrumentation-graphql@0.42.0": version "0.42.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz#588a18c39e3b3f655bc09243566172ab0b638d35" @@ -8115,22 +8122,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 +8146,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 +8168,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.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/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/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== +"@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.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== - 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 +8271,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 +8309,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== - dependencies: - "@sentry/bundler-plugin-core" "2.20.1" - 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/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.22.0" + "@sentry/bundler-plugin-core" "2.22.3" 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== +"@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.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" @@ -9756,17 +9681,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -10087,15 +10002,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -11006,14 +10913,6 @@ "@webassemblyjs/helper-numbers" "1.11.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.1" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -11047,11 +10946,6 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== - "@webassemblyjs/helper-buffer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" @@ -11095,16 +10989,6 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.1" "@webassemblyjs/wasm-gen" "1.11.1" -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/helper-wasm-section@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" @@ -11167,20 +11051,6 @@ "@webassemblyjs/wasm-parser" "1.11.1" "@webassemblyjs/wast-printer" "1.11.1" -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" - "@webassemblyjs/wasm-edit@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" @@ -11206,17 +11076,6 @@ "@webassemblyjs/leb128" "1.11.1" "@webassemblyjs/utf8" "1.11.1" -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" - "@webassemblyjs/wasm-gen@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" @@ -11238,16 +11097,6 @@ "@webassemblyjs/wasm-gen" "1.11.1" "@webassemblyjs/wasm-parser" "1.11.1" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wasm-opt@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" @@ -11270,18 +11119,6 @@ "@webassemblyjs/leb128" "1.11.1" "@webassemblyjs/utf8" "1.11.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/ieee754" "1.11.6" - "@webassemblyjs/leb128" "1.11.6" - "@webassemblyjs/utf8" "1.11.6" - "@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" @@ -11302,14 +11139,6 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== - dependencies: - "@webassemblyjs/ast" "1.11.6" - "@xtuc/long" "4.2.2" - "@webassemblyjs/wast-printer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" @@ -16983,18 +16812,10 @@ enhanced-resolve@^5.14.1: graceful-fs "^4.2.4" tapable "^2.2.0" -enhanced-resolve@^5.15.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enhanced-resolve@^5.17.0: - version "5.17.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" - integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -19166,7 +18987,7 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" -generic-pool@3.9.0: +generic-pool@3.9.0, generic-pool@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== @@ -23508,13 +23329,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" @@ -28604,7 +28418,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -28619,13 +28433,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -31104,16 +30911,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -31225,14 +31023,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -33892,42 +33683,11 @@ webpack@5.76.1: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@^5.90.3, webpack@~5.90.3: - version "5.90.3" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.90.3.tgz#37b8f74d3ded061ba789bb22b31e82eed75bd9ac" - integrity sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.5" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" - acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.21.10" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.15.0" - es-module-lexer "^1.2.1" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" - json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.2.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.3.10" - watchpack "^2.4.0" - webpack-sources "^3.2.3" - -webpack@^5.92.1: - version "5.92.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.1.tgz#eca5c1725b9e189cffbd86e8b6c3c7400efc5788" - integrity sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA== +webpack@^5.90.3, webpack@^5.94.0, webpack@~5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" "@webassemblyjs/wasm-edit" "^1.12.1" @@ -33936,7 +33696,7 @@ webpack@^5.92.1: acorn-import-attributes "^1.9.5" browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" @@ -34232,16 +33992,7 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==