diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml new file mode 100644 index 000000000000..29ecbcfbd2d1 --- /dev/null +++ b/.github/actions/install-playwright/action.yml @@ -0,0 +1,28 @@ +name: "Install Playwright dependencies" +description: "Installs Playwright dependencies and caches them." + +runs: + using: "composite" + steps: + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache playwright binaries + uses: actions/cache@v4 + id: playwright-cache + with: + path: | + ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright dependencies (uncached) + run: npx playwright install chromium webkit firefox --with-deps + if: steps.playwright-cache.outputs.cache-hit != 'true' + shell: bash + + - name: Install Playwright system dependencies only (cached) + run: npx playwright install-deps chromium webkit firefox + if: steps.playwright-cache.outputs.cache-hit == 'true' + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c160b8752a26..6eea92c884ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,6 @@ env: ${{ github.workspace }}/node_modules ${{ github.workspace }}/packages/*/node_modules ${{ github.workspace }}/dev-packages/*/node_modules - ~/.cache/ms-playwright/ ~/.cache/mongodb-binaries/ # DEPENDENCY_CACHE_KEY: can't be set here because we don't have access to yarn.lock @@ -74,6 +73,7 @@ jobs: # We need to check out not only the fake merge commit between the PR and the base branch which GH creates, but # also its parents, so that we can pull the commit message from the head commit of the PR fetch-depth: 2 + - name: Get metadata id: get_metadata # We need to try a number of different options for finding the head commit, because each kind of trigger event @@ -83,79 +83,20 @@ jobs: echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV echo "COMMIT_MESSAGE=$(git log -n 1 --pretty=format:%s $COMMIT_SHA)" >> $GITHUB_ENV + # Most changed packages are determined in job_build via Nx + # However, for profiling-node we only want to run certain things when in this specific package + # something changed, not in any of the dependencies (which include core, utils, ...) - name: Determine changed packages uses: dorny/paths-filter@v3.0.1 id: changed with: filters: | - workflow: &workflow + workflow: - '.github/**' - shared: &shared - - *workflow - - '*.{js,ts,json,yml,lock}' - - 'CHANGELOG.md' - - 'jest/**' - - 'scripts/**' - - 'packages/core/**' - - 'packages/rollup-utils/**' - - 'packages/utils/**' - - 'packages/types/**' - - 'dev-packages/test-utils/**' - browser: &browser - - *shared - - 'packages/browser/**' - - 'packages/browser-utils/**' - - 'packages/replay-internal/**' - - 'packages/replay-worker/**' - - 'packages/replay-canvas/**' - - 'packages/feedback/**' - - 'packages/wasm/**' - node: &node - - *shared - - 'packages/node/**' - - 'packages/opentelemetry/**' - browser_integration: - - *shared - - *browser - - 'dev-packages/browser-integration-tests/**' - ember: - - *shared - - *browser - - 'packages/ember/**' - node_integration: - - *shared - - *node - - 'dev-packages/node-integration-tests/**' - - 'packages/nestjs/**' - nextjs: - - *shared - - *browser - - *node - - 'packages/nextjs/**' - - 'packages/react/**' - - 'packages/vercel-edge/**' - remix: - - *shared - - *browser - - *node - - 'packages/remix/**' - - 'packages/react/**' profiling_node: - - *shared - - 'packages/node/**' - - 'packages/profiling-node/**' - - 'dev-packages/e2e-tests/test-applications/node-profiling/**' - profiling_node_bindings: - 'packages/profiling-node/**' - 'dev-packages/e2e-tests/test-applications/node-profiling/**' - deno: - - *shared - - 'packages/deno/**' - bun: - - *shared - - 'packages/bun/**' - any_code: - - '!**/*.md' + - name: Get PR labels id: pr-labels @@ -163,21 +104,11 @@ jobs: outputs: commit_label: '${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}' - changed_nextjs: ${{ steps.changed.outputs.nextjs }} - changed_ember: ${{ steps.changed.outputs.ember }} - changed_remix: ${{ steps.changed.outputs.remix }} - changed_node: ${{ steps.changed.outputs.node }} - changed_node_integration: ${{ steps.changed.outputs.node_integration }} - changed_profiling_node: ${{ steps.changed.outputs.profiling_node }} - changed_profiling_node_bindings: ${{ steps.changed.outputs.profiling_node_bindings }} - changed_deno: ${{ steps.changed.outputs.deno }} - changed_bun: ${{ steps.changed.outputs.bun }} - changed_browser: ${{ steps.changed.outputs.browser }} - changed_browser_integration: ${{ steps.changed.outputs.browser_integration }} - changed_any_code: ${{ steps.changed.outputs.any_code }} # Note: These next three have to be checked as strings ('true'/'false')! is_develop: ${{ github.ref == 'refs/heads/develop' }} is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }} + changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }} + changed_ci: ${{ steps.changed.outputs.workflow == 'true' }} # When merging into master, or from master is_gitflow_sync: ${{ github.head_ref == 'master' || github.ref == 'refs/heads/master' }} has_gitflow_label: @@ -186,22 +117,30 @@ jobs: ${{ github.event_name == 'schedule' || (github.event_name == 'pull_request' && contains(steps.pr-labels.outputs.labels, ' ci-skip-cache ')) }} - job_install_deps: - name: Install Dependencies + job_build: + name: Build needs: job_get_metadata runs-on: ubuntu-20.04 timeout-minutes: 15 if: | (needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false') steps: + - name: Check out base commit (${{ github.event.pull_request.base.sha }}) + uses: actions/checkout@v4 + if: github.event_name == 'pull_request' + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: 'Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})' uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node uses: actions/setup-node@v4 with: node-version-file: 'package.json' + # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, # so no need to reinstall them - name: Compute dependency cache key @@ -218,46 +157,14 @@ jobs: - name: Install dependencies if: steps.cache_dependencies.outputs.cache-hit != 'true' run: yarn install --ignore-engines --frozen-lockfile - outputs: - dependency_cache_key: ${{ steps.compute_lockfile_hash.outputs.hash }} - - job_check_branches: - name: Check PR branches - needs: job_get_metadata - runs-on: ubuntu-20.04 - if: github.event_name == 'pull_request' - permissions: - pull-requests: write - steps: - - name: PR is opened against master - uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 - if: ${{ github.base_ref == 'master' && !startsWith(github.head_ref, 'prepare-release/') }} - with: - message: | - ⚠️ This PR is opened against **master**. You probably want to open it against **develop**. - job_build: - name: Build - needs: [job_get_metadata, job_install_deps] - runs-on: ubuntu-20.04-large-js - timeout-minutes: 30 - if: | - (needs.job_get_metadata.outputs.changed_any_code == 'true' || github.event_name != 'pull_request') - steps: - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version-file: 'package.json' - - name: Check dependency cache - uses: actions/cache/restore@v4 + - name: Check for Affected Nx Projects + uses: dkhunt27/action-nx-affected-list@v5.3 + id: checkForAffected + if: github.event_name == 'pull_request' with: - path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} - fail-on-cache-miss: true + base: ${{ github.event.pull_request.base.sha }} + head: ${{ env.HEAD_COMMIT }} - name: Check build cache uses: actions/cache@v4 @@ -286,10 +193,31 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: yarn build + outputs: - # this needs to be passed on, because the `needs` context only looks at direct ancestors (so steps which depend on - # `job_build` can't see `job_install_deps` and what it returned) - dependency_cache_key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + dependency_cache_key: ${{ steps.compute_lockfile_hash.outputs.hash }} + changed_node_integration: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry-internal/node-integration-tests') }} + changed_remix: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/remix') }} + changed_node: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/node') }} + changed_deno: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/deno') }} + changed_bun: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/bun') }} + changed_browser_integration: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry-internal/browser-integration-tests') }} + # If you are looking for changed_profiling_node, this is defined in job_get_metadata + + job_check_branches: + name: Check PR branches + needs: job_get_metadata + runs-on: ubuntu-20.04 + if: github.event_name == 'pull_request' + permissions: + pull-requests: write + steps: + - name: PR is opened against master + uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 + if: ${{ github.base_ref == 'master' && !startsWith(github.head_ref, 'prepare-release/') }} + with: + message: | + ⚠️ This PR is opened against **master**. You probably want to open it against **develop**. job_size_check: name: Size Check @@ -346,7 +274,7 @@ jobs: job_check_format: name: Check file formatting - needs: [job_get_metadata, job_install_deps] + needs: [job_get_metadata, job_build] timeout-minutes: 10 runs-on: ubuntu-20.04 steps: @@ -362,7 +290,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} fail-on-cache-miss: true - name: Check file formatting run: yarn lint:prettier && yarn lint:biome @@ -438,6 +366,7 @@ jobs: steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 + if: github.event_name == 'pull_request' with: ref: ${{ github.event.pull_request.base.sha }} @@ -470,7 +399,7 @@ jobs: job_bun_unit_tests: name: Bun Unit Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_bun == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_bun == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-20.04 strategy: @@ -497,7 +426,7 @@ jobs: job_deno_unit_tests: name: Deno Unit Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_deno == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_deno == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-20.04 strategy: @@ -537,6 +466,7 @@ jobs: steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 + if: github.event_name == 'pull_request' with: ref: ${{ github.event.pull_request.base.sha }} - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -572,7 +502,7 @@ jobs: job_profiling_node_unit_tests: name: Node Profiling Unit Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.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: @@ -600,7 +530,7 @@ jobs: job_browser_playwright_tests: name: Playwright (${{ matrix.bundle }}${{ matrix.shard && format(' {0}/{1}', matrix.shard, matrix.shards) || ''}}) Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04-large-js timeout-minutes: 25 strategy: @@ -659,26 +589,10 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Get npm cache directory - id: npm-cache-dir - run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - - name: Get Playwright version - id: playwright-version - run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 - name: Check if Playwright browser is cached - id: playwright-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}} - - name: Install Playwright browser if not cached - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps - env: - PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}} - - name: Install OS dependencies of Playwright if cache hit - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: npx playwright install-deps + + - name: Install Playwright + uses: ./.github/actions/install-playwright + - name: Run Playwright tests env: PW_BUNDLE: ${{ matrix.bundle }} @@ -691,11 +605,10 @@ jobs: name: playwright-traces path: dev-packages/browser-integration-tests/test-results - job_browser_loader_tests: name: Playwright Loader (${{ matrix.bundle }}) Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 timeout-minutes: 15 strategy: @@ -723,26 +636,10 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Get npm cache directory - id: npm-cache-dir - run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - - name: Get Playwright version - id: playwright-version - run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 - name: Check if Playwright browser is cached - id: playwright-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}} - - name: Install Playwright browser if not cached - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps - env: - PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}} - - name: Install OS dependencies of Playwright if cache hit - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: npx playwright install-deps + + - name: Install Playwright + uses: ./.github/actions/install-playwright + - name: Run Playwright Loader tests env: PW_BUNDLE: ${{ matrix.bundle }} @@ -781,13 +678,12 @@ jobs: exit 1 fi - job_node_integration_tests: name: Node (${{ matrix.node }})${{ (matrix.typescript && format(' (TS {0})', matrix.typescript)) || '' }} Integration Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 timeout-minutes: 15 strategy: @@ -829,7 +725,7 @@ jobs: job_remix_integration_tests: name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_remix == 'true' || github.event_name != 'pull_request' + if: needs.job_build.outputs.changed_remix == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 timeout-minutes: 10 strategy: @@ -854,6 +750,8 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Install Playwright + uses: ./.github/actions/install-playwright - name: Run integration tests env: NODE_VERSION: ${{ matrix.node }} @@ -897,14 +795,14 @@ jobs: # Rebuild profiling by compiling TS and pull the precompiled binary artifacts - name: Build Profiling Node if: | - (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries if: | - (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') uses: actions/download-artifact@v4 @@ -1052,6 +950,9 @@ jobs: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + - name: Install Playwright + uses: ./.github/actions/install-playwright + - name: Get node version id: versions run: | @@ -1146,6 +1047,9 @@ jobs: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + - name: Install Playwright + uses: ./.github/actions/install-playwright + - name: Get node version id: versions run: | @@ -1192,7 +1096,7 @@ jobs: always() && needs.job_e2e_prepare.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && ( - (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') ) @@ -1243,6 +1147,9 @@ jobs: key: ${{ env.BUILD_CACHE_TARBALL_KEY }} fail-on-cache-miss : true + - name: Install Playwright + uses: ./.github/actions/install-playwright + - name: Get node version id: versions run: | @@ -1342,11 +1249,11 @@ jobs: 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' }} - needs: [job_get_metadata, job_install_deps, job_build] + needs: [job_get_metadata, job_build] # Compiling bindings can be very slow (especially on windows), so only run precompile # Skip precompile unless we are on a release branch as precompile slows down CI times. if: | - (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') runs-on: ${{ matrix.os }} @@ -1503,7 +1410,7 @@ jobs: id: restore-dependencies with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} enableCrossOsArchive: true - name: Restore build cache diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 44edf51fd45d..d2238ce58e71 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -49,26 +49,8 @@ jobs: - name: Build packages run: yarn build - - name: Get npm cache directory - id: npm-cache-dir - run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - - name: Get Playwright version - id: playwright-version - run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 - name: Check if Playwright browser is cached - id: playwright-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}} - - name: Install Playwright browser if not cached - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps - env: - PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}} - - name: Install OS dependencies of Playwright if cache hit - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: npx playwright install-deps + - name: Install Playwright + uses: ./.github/actions/install-playwright - name: Determine changed tests uses: dorny/paths-filter@v3.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eeb60765430..0ad5a1920aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.24.0 + +- feat(nestjs): Filter RPC exceptions (#13227) +- fix: Guard getReader function for other fetch implementations (#13246) +- fix(feedback): Ensure feedback can be lazy loaded in CDN bundles (#13241) + ## 8.23.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js new file mode 100644 index 000000000000..d0ff6bbb92c9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], +}); + +window.Sentry = { + ...Sentry, +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/subject.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/subject.js new file mode 100644 index 000000000000..4ef2f8f41622 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/subject.js @@ -0,0 +1,7 @@ +window._testLazyLoadIntegration = async function run() { + const integration = await window.Sentry.lazyLoadIntegration('feedbackIntegration'); + + window.Sentry.getClient()?.addIntegration(integration()); + + window._integrationLoaded = true; +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/test.ts b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/test.ts new file mode 100644 index 000000000000..d054c2da2e99 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import { SDK_VERSION } from '@sentry/browser'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest('it allows to lazy load the feedback integration', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE || ''; + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route(`https://browser.sentry-cdn.com/${SDK_VERSION}/feedback.min.js`, route => { + return route.fulfill({ + status: 200, + contentType: 'application/javascript;', + body: "window.Sentry.feedbackIntegration = () => ({ name: 'Feedback', attachTo: () => {} })", + }); + }); + + await page.goto(url); + + await page.waitForFunction('window.Sentry?.getClient()'); + + const integrationOutput1 = await page.evaluate('window.Sentry.feedbackIntegration?._isShim'); + + // Multiple cases are possible here: + // 1. Bundle without feedback, should have _isShim property + if (bundle.startsWith('bundle') && !bundle.includes('feedback')) { + expect(integrationOutput1).toBe(true); + } else { + // 2. Either bundle with feedback, or ESM, should not have _isShim property + expect(integrationOutput1).toBe(undefined); + } + + await page.evaluate('window._testLazyLoadIntegration()'); + await page.waitForFunction('window._integrationLoaded'); + + const integrationOutput2 = await page.evaluate('window.Sentry.feedbackIntegration?._isShim'); + expect(integrationOutput2).toBe(undefined); +}); diff --git a/dev-packages/e2e-tests/Dockerfile.publish-packages b/dev-packages/e2e-tests/Dockerfile.publish-packages index 4d5b2ba3abf3..88fd7f116728 100644 --- a/dev-packages/e2e-tests/Dockerfile.publish-packages +++ b/dev-packages/e2e-tests/Dockerfile.publish-packages @@ -3,4 +3,4 @@ ARG NODE_VERSION=18.18.0 FROM node:${NODE_VERSION} WORKDIR /sentry-javascript/dev-packages/e2e-tests -CMD [ "yarn", "ts-node", "publish-packages.ts" ] +CMD [ "yarn", "ts-node", "publish-packages.ts", "--transpile-only" ] diff --git a/dev-packages/e2e-tests/publish-packages.ts b/dev-packages/e2e-tests/publish-packages.ts index 2be19b173bd3..408d046977a2 100644 --- a/dev-packages/e2e-tests/publish-packages.ts +++ b/dev-packages/e2e-tests/publish-packages.ts @@ -13,9 +13,22 @@ const packageTarballPaths = glob.sync('packages/*/sentry-*.tgz', { // Publish built packages to the fake registry packageTarballPaths.forEach(tarballPath => { // `--userconfig` flag needs to be before `publish` - childProcess.execSync(`npm --userconfig ${__dirname}/test-registry.npmrc publish ${tarballPath}`, { - cwd: repositoryRoot, // Can't use __dirname here because npm would try to publish `@sentry-internal/e2e-tests` - encoding: 'utf8', - stdio: 'inherit', - }); + childProcess.exec( + `npm --userconfig ${__dirname}/test-registry.npmrc publish ${tarballPath}`, + { + cwd: repositoryRoot, // Can't use __dirname here because npm would try to publish `@sentry-internal/e2e-tests` + encoding: 'utf8', + }, + (err, stdout, stderr) => { + // eslint-disable-next-line no-console + console.log(stdout); + // eslint-disable-next-line no-console + console.log(stderr); + if (err) { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); + } + }, + ); }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs index e74f395b6237..86605fcb7b9a 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/start-event-proxy.mjs @@ -3,5 +3,4 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, proxyServerName: 'aws-serverless-esm', - forwardToSentry: false, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json index f4c44ff7cef3..f7a0da2fc403 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json @@ -17,6 +17,7 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", "@nestjs/schedule": "^4.1.0", "@nestjs/platform-express": "^10.0.0", "@sentry/nestjs": "latest || *", 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 c04fd5613e95..ec0a921da2c4 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,4 +1,5 @@ import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; import { ExampleInterceptor } from './example.interceptor'; @@ -49,6 +50,11 @@ export class AppController { return this.appService.testExpected500Exception(id); } + @Get('test-expected-rpc-exception/:id') + async testExpectedRpcException(@Param('id') id: string) { + return this.appService.testExpectedRpcException(id); + } + @Get('test-span-decorator-async') async testSpanDecoratorAsync() { return { result: await this.appService.testSpanDecoratorAsync() }; @@ -63,4 +69,9 @@ export class AppController { async killTestCron() { this.appService.killTestCron(); } + + @Get('flush') + async flush() { + await flush(); + } } 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 b2dadbb0a269..f1c935257013 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 @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; import { Cron, SchedulerRegistry } from '@nestjs/schedule'; import * as Sentry from '@sentry/nestjs'; import { SentryCron, SentryTraced } from '@sentry/nestjs'; @@ -38,6 +39,10 @@ export class AppService { throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR); } + testExpectedRpcException(id: string) { + throw new RpcException(`This is an expected RPC exception with id ${id}`); + } + @SentryTraced('wait and return a string') async wait() { await new Promise(resolve => setTimeout(resolve, 500)); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts index cffc5f4946a3..34e626cb8c52 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts @@ -65,7 +65,32 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { await transactionEventPromise400; await transactionEventPromise500; - await new Promise(resolve => setTimeout(resolve, 10000)); + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-basic', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); + expect(response.status).toBe(500); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); expect(errorEventOccurred).toBe(false); }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/app.controller.ts index 71a410e8d0a8..0d2c46e90da2 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/app.controller.ts @@ -1,4 +1,5 @@ import { Controller, Get, Param } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; import { AppService } from './app.service'; @Controller() @@ -14,4 +15,9 @@ export class AppController { async testExpectedException(@Param('id') id: string) { return this.appService.testExpectedException(id); } + + @Get('flush') + async flush() { + await flush(); + } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts index 87b828dc8501..6fbc9f2c1f32 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts @@ -81,7 +81,7 @@ test('Does not send exception to Sentry if user-defined global exception filter await transactionEventPromise; - await new Promise(resolve => setTimeout(resolve, 10000)); + (await fetch(`${baseURL}/flush`)).text(); expect(errorEventOccurred).toBe(false); }); @@ -111,7 +111,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a await transactionEventPromise; - await new Promise(resolve => setTimeout(resolve, 10000)); + (await fetch(`${baseURL}/flush`)).text(); expect(errorEventOccurred).toBe(false); }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json index ec6510ac03ff..b7334026d18f 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json @@ -17,6 +17,7 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", "@nestjs/schedule": "^4.1.0", "@nestjs/platform-express": "^10.0.0", "@sentry/nestjs": "latest || *", 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 c04fd5613e95..ec0a921da2c4 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,4 +1,5 @@ import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; import { ExampleInterceptor } from './example.interceptor'; @@ -49,6 +50,11 @@ export class AppController { return this.appService.testExpected500Exception(id); } + @Get('test-expected-rpc-exception/:id') + async testExpectedRpcException(@Param('id') id: string) { + return this.appService.testExpectedRpcException(id); + } + @Get('test-span-decorator-async') async testSpanDecoratorAsync() { return { result: await this.appService.testSpanDecoratorAsync() }; @@ -63,4 +69,9 @@ export class AppController { async killTestCron() { this.appService.killTestCron(); } + + @Get('flush') + async flush() { + await flush(); + } } 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 b2dadbb0a269..f1c935257013 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 @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { RpcException } from '@nestjs/microservices'; import { Cron, SchedulerRegistry } from '@nestjs/schedule'; import * as Sentry from '@sentry/nestjs'; import { SentryCron, SentryTraced } from '@sentry/nestjs'; @@ -38,6 +39,10 @@ export class AppService { throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR); } + testExpectedRpcException(id: string) { + throw new RpcException(`This is an expected RPC exception with id ${id}`); + } + @SentryTraced('wait and return a string') async wait() { await new Promise(resolve => setTimeout(resolve, 500)); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts index 11eafc38f430..0155c3887805 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts @@ -65,7 +65,32 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { await transactionEventPromise400; await transactionEventPromise500; - await new Promise(resolve => setTimeout(resolve, 10000)); + (await fetch(`${baseURL}/flush`)).text(); + + expect(errorEventOccurred).toBe(false); +}); + +test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('node-nestjs-basic', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { + errorEventOccurred = true; + } + + return event?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; + }); + + const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); + expect(response.status).toBe(500); + + await transactionEventPromise; + + (await fetch(`${baseURL}/flush`)).text(); expect(errorEventOccurred).toBe(false); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index a487d61a144b..93471fff7aab 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.11.2" + "nuxt": "3.12.4" }, "devDependencies": { "@nuxt/test-utils": "^3.13.1", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 19a8898fe90c..5a94953e054e 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -31,7 +31,9 @@ "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", "@prisma/client": "5.9.1", + "@sentry/aws-serverless": "8.23.0", "@sentry/node": "8.23.0", + "@sentry/utils": "8.23.0", "@sentry/types": "8.23.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 760fdc05daa6..ffc24f58174a 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -133,6 +133,8 @@ export function makeTerserPlugin() { '_sentryIsolationScope', // require-in-the-middle calls `Module._resolveFilename`. We cannot mangle this (AWS lambda layer bundle). '_resolveFilename', + // Set on e.g. the shim feedbackIntegration to be able to detect it + '_isShim', ], }, }, diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 58c39de95c8c..17922a4f90aa 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -1,5 +1,4 @@ /* eslint-disable max-lines */ - import * as fs from 'fs'; import * as http from 'http'; import type { AddressInfo } from 'net'; @@ -18,11 +17,6 @@ interface EventProxyServerOptions { port: number; /** The name for the proxy server used for referencing it with listener functions */ proxyServerName: string; - /** - * Whether or not to forward the event to sentry. @default `false` - * This is helpful when you can't register a tunnel in the SDK setup (e.g. lambda layer without Sentry.init call) - */ - forwardToSentry?: boolean; } interface SentryRequestCallbackData { @@ -36,12 +30,16 @@ interface EventCallbackListener { (data: string): void; } +type SentryResponseStatusCode = number; +type SentryResponseBody = string; +type SentryResponseHeaders = Record | undefined; + type OnRequest = ( eventCallbackListeners: Set, proxyRequest: http.IncomingMessage, proxyRequestBody: string, eventBuffer: BufferedEvent[], -) => Promise<[number, string, Record | undefined]>; +) => Promise<[SentryResponseStatusCode, SentryResponseBody, SentryResponseHeaders]>; interface BufferedEvent { timestamp: number; @@ -170,83 +168,28 @@ export async function startProxyServer( */ export async function startEventProxyServer(options: EventProxyServerOptions): Promise { await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { - const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string); + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody: '', + sentryResponseStatusCode: 200, + }; - const shouldForwardEventToSentry = options.forwardToSentry || false; + const dataString = Buffer.from(JSON.stringify(data)).toString('base64'); - if (!envelopeHeader.dsn && shouldForwardEventToSentry) { - // eslint-disable-next-line no-console - console.log( - '[event-proxy-server] Warn: No dsn on envelope header. Maybe a client-report was received. Proxy request body:', - proxyRequestBody, - ); - - return [200, '{}', {}]; - } - - if (!shouldForwardEventToSentry) { - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody: '', - sentryResponseStatusCode: 200, - }; - eventCallbackListeners.forEach(listener => { - listener(Buffer.from(JSON.stringify(data)).toString('base64')); - }); - - return [ - 200, - '{}', - { - 'Access-Control-Allow-Origin': '*', - }, - ]; - } - - const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); - - const projectId = pathname.substring(1); - const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; - - proxyRequest.headers.host = host; - - const reqHeaders: Record = {}; - for (const [key, value] of Object.entries(proxyRequest.headers)) { - reqHeaders[key] = value as string; - } - - // Fetch does not like this - delete reqHeaders['transfer-encoding']; - - return fetch(sentryIngestUrl, { - body: proxyRequestBody, - headers: reqHeaders, - method: proxyRequest.method, - }).then(async res => { - const rawSentryResponseBody = await res.text(); - const data: SentryRequestCallbackData = { - envelope: parseEnvelope(proxyRequestBody), - rawProxyRequestBody: proxyRequestBody, - rawSentryResponseBody, - sentryResponseStatusCode: res.status, - }; - - const dataString = Buffer.from(JSON.stringify(data)).toString('base64'); - - eventBuffer.push({ data: dataString, timestamp: getNanosecondTimestamp() }); - - eventCallbackListeners.forEach(listener => { - listener(dataString); - }); - - const resHeaders: Record = {}; - for (const [key, value] of res.headers.entries()) { - resHeaders[key] = value; - } + eventBuffer.push({ data: dataString, timestamp: getNanosecondTimestamp() }); - return [res.status, rawSentryResponseBody, resHeaders]; + eventCallbackListeners.forEach(listener => { + listener(dataString); }); + + return [ + 200, + '{}', + { + 'Access-Control-Allow-Origin': '*', + }, + ]; }); } diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index 4479c2e69590..b023bc18aa95 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -43,7 +43,10 @@ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegra // Bail if the integration already exists const existing = sentryOnWindow[name]; - if (typeof existing === 'function') { + // The `feedbackIntegration` is loaded by default in the CDN bundles, + // so we need to differentiate between the real integration and the shim. + // if only the shim exists, we still want to lazy load the real integration. + if (typeof existing === 'function' && !('_isShim' in existing)) { return existing; } diff --git a/packages/core/src/utils/hasTracingEnabled.ts b/packages/core/src/utils/hasTracingEnabled.ts index 5e673bc08caa..9441a0fc7bed 100644 --- a/packages/core/src/utils/hasTracingEnabled.ts +++ b/packages/core/src/utils/hasTracingEnabled.ts @@ -16,12 +16,8 @@ export function hasTracingEnabled( return false; } - const options = maybeOptions || getClientOptions(); + const client = getClient(); + const options = maybeOptions || (client && client.getOptions()); // eslint-disable-next-line deprecation/deprecation return !!options && (options.enableTracing || 'tracesSampleRate' in options || 'tracesSampler' in options); } - -function getClientOptions(): Options | undefined { - const client = getClient(); - return client && client.getOptions(); -} diff --git a/packages/integration-shims/src/Feedback.ts b/packages/integration-shims/src/Feedback.ts index 189dc074cd1e..ef4010bb0494 100644 --- a/packages/integration-shims/src/Feedback.ts +++ b/packages/integration-shims/src/Feedback.ts @@ -2,7 +2,7 @@ import type { Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { FAKE_FUNCTION } from './common'; -const FEEDBACK_INTEGRATION_METHODS = ['attachTo', 'createWidget', 'remove'] as const; +const FEEDBACK_INTEGRATION_METHODS = ['attachTo', 'createForm', 'createWidget', 'remove'] as const; type FeedbackSpecificMethods = Record<(typeof FEEDBACK_INTEGRATION_METHODS)[number], () => void>; @@ -13,17 +13,22 @@ interface FeedbackIntegration extends Integration, FeedbackSpecificMethods {} * It is needed in order for the CDN bundles to continue working when users add/remove feedback * from it, without changing their config. This is necessary for the loader mechanism. */ -export function feedbackIntegrationShim(_options: unknown): FeedbackIntegration { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('You are using feedbackIntegration() even though this bundle does not include feedback.'); - }); +export const feedbackIntegrationShim = Object.assign( + (_options: unknown): FeedbackIntegration => { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('You are using feedbackIntegration() even though this bundle does not include feedback.'); + }); - return { - name: 'Feedback', - ...(FEEDBACK_INTEGRATION_METHODS.reduce((acc, method) => { - acc[method] = FAKE_FUNCTION; - return acc; - }, {} as FeedbackSpecificMethods) as FeedbackSpecificMethods), - }; -} + return { + name: 'Feedback', + ...(FEEDBACK_INTEGRATION_METHODS.reduce((acc, method) => { + acc[method] = FAKE_FUNCTION; + return acc; + }, {} as FeedbackSpecificMethods) as FeedbackSpecificMethods), + }; + }, + { + _isShim: true, + }, +); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index cc35e0c33a5c..d218c0be07c2 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -51,11 +51,13 @@ }, "devDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/microservices": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/microservices": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index f788ccb9b67c..b068ed052a91 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -11,6 +11,7 @@ import { Catch } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { Global, Module } from '@nestjs/common'; import { APP_FILTER, APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; +import { RpcException } from '@nestjs/microservices'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -68,7 +69,7 @@ class SentryGlobalFilter extends BaseExceptionFilter { */ public catch(exception: unknown, host: ArgumentsHost): void { // don't report expected errors - if (exception instanceof HttpException) { + if (exception instanceof HttpException || exception instanceof RpcException) { return super.catch(exception, host); } diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 4f7f7a1f59d3..4f8d88fa8f86 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -87,10 +87,16 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE const originalCatch = Reflect.get(target, prop, receiver); return (exception: unknown, host: unknown) => { - const status_code = (exception as { status?: number }).status; - - // don't report expected errors - if (status_code !== undefined) { + const exceptionIsObject = typeof exception === 'object' && exception !== null; + const exceptionStatusCode = exceptionIsObject && 'status' in exception ? exception.status : null; + const exceptionErrorProperty = exceptionIsObject && 'error' in exception ? exception.error : null; + + /* + Don't report expected NestJS control flow errors + - `HttpException` errors will have a `status` property + - `RpcException` errors will have an `error` property + */ + if (exceptionStatusCode !== null || exceptionErrorProperty !== null) { return originalCatch.apply(target, [exception, host]); } diff --git a/packages/nuxt/test/server/runtime/plugin.test.ts b/packages/nuxt/test/runtime/plugins/server.test.ts similarity index 100% rename from packages/nuxt/test/server/runtime/plugin.test.ts rename to packages/nuxt/test/runtime/plugins/server.test.ts diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/runtime/utils.test.ts similarity index 97% rename from packages/nuxt/test/client/runtime/utils.test.ts rename to packages/nuxt/test/runtime/utils.test.ts index b0b039d52e54..08c66193caa3 100644 --- a/packages/nuxt/test/client/runtime/utils.test.ts +++ b/packages/nuxt/test/runtime/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { extractErrorContext } from '../../../src/runtime/utils'; +import { extractErrorContext } from '../../src/runtime/utils'; describe('extractErrorContext', () => { it('returns empty object for undefined or empty context', () => { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 8d84dc8b15c8..20ec11c33512 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,4 +1,5 @@ import * as SentryNode from '@sentry/node'; +import type { NodeClient } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/server'; @@ -38,5 +39,47 @@ describe('Nuxt Server SDK', () => { it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); + + it('filters out low quality transactions', async () => { + const beforeSendEvent = vi.fn(event => event); + const client = init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }) as NodeClient; + client.on('beforeSendEvent', beforeSendEvent); + + client.captureEvent({ type: 'transaction', transaction: 'GET /' }); + client.captureEvent({ type: 'transaction', transaction: 'GET /_nuxt/some_asset.js' }); + // Although this has the name of the build asset directory (_nuxt), it should not be filtered out as it would not match the regex + client.captureEvent({ type: 'transaction', transaction: 'GET _nuxt/some_asset.js' }); + client.captureEvent({ type: 'transaction', transaction: 'POST /_server' }); + + await client!.flush(); + + expect(beforeSendEvent).toHaveBeenCalledTimes(3); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /', + }), + expect.any(Object), + ); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET _nuxt/some_asset.js', + }), + expect.any(Object), + ); + expect(beforeSendEvent).not.toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /_nuxt/some_asset.js', + }), + expect.any(Object), + ); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'POST /_server', + }), + expect.any(Object), + ); + }); }); }); diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index afa209c01929..a161b8db79bb 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -116,7 +116,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat } async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise { - if (res && res.body) { + if (res && res.body && res.body.getReader) { const responseReader = res.body.getReader(); // eslint-disable-next-line no-inner-declarations diff --git a/yarn.lock b/yarn.lock index 953f0eee5f3a..660c53cce594 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6135,6 +6135,14 @@ path-to-regexp "3.2.0" tslib "2.6.3" +"@nestjs/microservices@^8.0.0 || ^9.0.0 || ^10.0.0": + version "10.3.10" + resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-10.3.10.tgz#e00957e0c22b0cc8b041242a40538e2d862255fb" + integrity sha512-zZrilhZmXU2Ik5Usrcy4qEX262Uhvz0/9XlIdX6SRn8I39ns1EE9tAhEBmmkMwh7lsEikRFa4aaa05loi8Gsow== + dependencies: + iterare "1.2.1" + tslib "2.6.3" + "@nestjs/platform-express@^10.3.3": version "10.3.3" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.3.3.tgz#c1484d30d1e7666c4c8d0d7cde31cfc0b9d166d7"