diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 765e1c53615f..26b6f695069f 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -13,7 +13,7 @@ jobs: name: 'Prepare a new version' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GH_RELEASE_PAT }} fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56f61126a45a..84c26af7e9b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: pull-requests: read steps: - name: Check out current commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} # We need to check out not only the fake merge commit between the PR and the base branch which GH creates, but @@ -159,7 +159,7 @@ jobs: (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@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -207,7 +207,7 @@ jobs: timeout-minutes: 30 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -265,7 +265,7 @@ jobs: needs.job_get_metadata.outputs.is_release == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -297,7 +297,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -320,7 +320,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -342,7 +342,7 @@ jobs: if: needs.job_get_metadata.outputs.is_release == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -373,7 +373,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -400,7 +400,7 @@ jobs: fail-fast: false steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -430,7 +430,7 @@ jobs: node: [8, 10, 12, 14, 16, 18, 20] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -462,7 +462,7 @@ jobs: node: [10, 12, 14, 16, 18, 20] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -527,7 +527,7 @@ jobs: steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -584,7 +584,7 @@ jobs: steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -637,7 +637,7 @@ jobs: - WebkitHeadless steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -663,7 +663,7 @@ jobs: timeout-minutes: 5 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -703,7 +703,7 @@ jobs: typescript: '3.8' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -739,7 +739,7 @@ jobs: remix: [1, 2] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -768,7 +768,7 @@ jobs: timeout-minutes: 15 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -840,7 +840,7 @@ jobs: steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v2 @@ -925,7 +925,7 @@ jobs: needs.job_get_metadata.outputs.is_develop == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index d70c95f033e0..04adce087fd4 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 30 steps: - name: Check out current commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node @@ -82,7 +82,7 @@ jobs: steps: - name: Check out current commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v2 @@ -148,7 +148,7 @@ jobs: scenario: [ember-release, embroider-optimized, ember-4.0] steps: - name: 'Check out current commit' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index db6ba7c55ac5..a6ca6542ec28 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 4eaa0f5d64ab..4f3c92da09ba 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.base_ref != 'master' && github.ref != 'refs/heads/master' }} steps: - name: Check out current branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/gitflow-sync-develop.yml b/.github/workflows/gitflow-sync-develop.yml index bf9ba8feaf9a..ca7f27d1f5f9 100644 --- a/.github/workflows/gitflow-sync-develop.yml +++ b/.github/workflows/gitflow-sync-develop.yml @@ -23,7 +23,7 @@ jobs: contents: write steps: - name: git checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # https://github.com/marketplace/actions/github-pull-request-action - name: Create Pull Request diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38e875baf9fd..cf4561746bb4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-20.04 name: 'Release a new version' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.GH_RELEASE_PAT }} fetch-depth: 0 diff --git a/.size-limit.js b/.size-limit.js index 71346dc3d48d..be8fa57b4934 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -5,7 +5,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: '{ init, Replay, BrowserTracing }', gzip: true, - limit: '80 KB', + limit: '90 KB', }, { name: '@sentry/browser (incl. Tracing) - Webpack (gzipped)', @@ -47,7 +47,7 @@ module.exports = [ name: '@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.tracing.replay.min.js', gzip: false, - limit: '250 KB', + limit: '260 KB', }, { name: '@sentry/browser (incl. Tracing) - ES6 CDN Bundle (minified & uncompressed)', @@ -77,7 +77,7 @@ module.exports = [ path: 'packages/react/build/esm/index.js', import: '{ init, BrowserTracing, Replay }', gzip: true, - limit: '80 KB', + limit: '90 KB', }, { name: '@sentry/react - Webpack (gzipped)', @@ -93,7 +93,7 @@ module.exports = [ path: 'packages/nextjs/build/esm/client/index.js', import: '{ init, BrowserTracing, Replay }', gzip: true, - limit: '100 KB', + limit: '110 KB', }, { name: '@sentry/nextjs Client - Webpack (gzipped)', diff --git a/CHANGELOG.md b/CHANGELOG.md index d2612400a12f..70c9c3702fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.73.0 + +### Important Changes + +- **feat(replay): Upgrade to rrweb2** + +This is fully backwards compatible with prior versions of the Replay SDK. The only breaking change that we will making is to not be masking `aria-label` by default. The reason for this change is to align with our core SDK which also does not mask `aria-label`. This change also enables better support of searching by clicks. + +Another change that needs to be highlighted is the 13% bundle size increase. This bundle size increase is necessary to bring improved recording performance and improved replay fidelity, especially in regards to web components and iframes. We will be investigating the reduction of the bundle size in [this PR](https://github.com/getsentry/sentry-javascript/issues/8815). + +Here are benchmarks comparing the version 1 of rrweb to version 2 + +| metric | v1 | v2 | +| --------- | ---------- | ---------- | +| lcp | 1486.06 ms | 1529.11 ms | +| cls | 0.40 ms | 0.40 ms | +| fid | 1.53 ms | 1.50 ms | +| tbt | 3207.22 ms | 3036.80 ms | +| memoryAvg | 131.83 MB | 124.84 MB | +| memoryMax | 324.8 MB | 339.03 MB | +| netTx | 282.67 KB | 272.51 KB | +| netRx | 8.02 MB | 8.07 MB | + +### Other Changes + +- feat: Always assemble Envelopes (#9101) +- feat(node): Rate limit local variables for caught exceptions and enable `captureAllExceptions` by default (#9102) +- fix(core): Ensure `tunnel` is considered for `isSentryUrl` checks (#9130) +- fix(nextjs): Fix `RequestAsyncStorage` fallback path (#9126) +- fix(node-otel): Suppress tracing for generated sentry spans (#9142) +- fix(node): fill in span data from http request options object (#9112) +- fix(node): Fixes and improvements to ANR detection (#9128) +- fix(sveltekit): Avoid data invalidation in wrapped client-side `load` functions (#9071) +- ref(core): Refactor `InboundFilters` integration to use `processEvent` (#9020) +- ref(wasm): Refactor Wasm integration to use `processEvent` (#9019) + +Work in this release contributed by @vlad-zhukov. Thank you for your contribution! + ## 7.72.0 ### Important Changes diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts index f049267a51c1..c4da79faa1d9 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts @@ -84,12 +84,13 @@ sentryTest( await reqErrorPromise; expect(callsToSentry).toEqual(2); - await page.evaluate(async () => { - const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; - await replayIntegration.flush(); - }); - - const req0 = await reqPromise0; + const [req0] = await Promise.all([ + reqPromise0, + page.evaluate(async () => { + const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; + await replayIntegration.flush(); + }), + ]); // 2 errors, 1 flush await reqErrorPromise; @@ -226,12 +227,13 @@ sentryTest( await reqErrorPromise; expect(callsToSentry).toEqual(2); - await page.evaluate(async () => { - const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; - await replayIntegration.flush({ continueRecording: false }); - }); - - const req0 = await reqPromise0; + const [req0] = await Promise.all([ + reqPromise0, + page.evaluate(async () => { + const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay; + await replayIntegration.flush({ continueRecording: false }); + }), + ]); // 2 errors, 1 flush await reqErrorPromise; @@ -346,9 +348,12 @@ sentryTest( // Error sample rate is now at 1.0, this error should create a replay const reqErrorPromise1 = waitForErrorRequest(page); - await page.click('#error2'); - // 1 unsampled error, 1 sampled error -> 1 flush - const req0 = await reqPromise0; + const [req0] = await Promise.all([ + // 1 unsampled error, 1 sampled error -> 1 flush + reqPromise0, + page.click('#error2'), + ]); + const reqError1 = await reqErrorPromise1; const errorEvent1 = envelopeRequestParser(reqError1); expect(callsToSentry).toEqual(3); diff --git a/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts b/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts index c0ceed092995..13353bf1fb6c 100644 --- a/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts @@ -10,8 +10,6 @@ sentryTest('should capture console messages in replay', async ({ getLocalTestPat sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,10 +18,11 @@ sentryTest('should capture console messages in replay', async ({ getLocalTestPat }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([page.goto(url), reqPromise0]); const reqPromise1 = waitForReplayRequest( page, @@ -38,11 +37,10 @@ sentryTest('should capture console messages in replay', async ({ getLocalTestPat await page.click('[data-log]'); // Sometimes this doesn't seem to trigger, so we trigger it twice to be sure... - await page.click('[data-log]'); - + const [req1] = await Promise.all([reqPromise1, page.click('[data-log]')]); await forceFlushReplay(); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs.filter(breadcrumb => breadcrumb.category === 'console')).toEqual( expect.arrayContaining([ @@ -65,8 +63,6 @@ sentryTest('should capture very large console logs', async ({ getLocalTestPath, sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -75,10 +71,11 @@ sentryTest('should capture very large console logs', async ({ getLocalTestPath, }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([page.goto(url), reqPromise0]); const reqPromise1 = waitForReplayRequest( page, @@ -90,14 +87,10 @@ sentryTest('should capture very large console logs', async ({ getLocalTestPath, 5_000, ); - await page.click('[data-log-large]'); - - // Sometimes this doesn't seem to trigger, so we trigger it twice to be sure... - await page.click('[data-log-large]'); - + const [req1] = await Promise.all([reqPromise1, page.click('[data-log-large]')]); await forceFlushReplay(); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs.filter(breadcrumb => breadcrumb.category === 'console')).toEqual( expect.arrayContaining([ diff --git a/packages/browser-integration-tests/suites/replay/customEvents/test.ts b/packages/browser-integration-tests/suites/replay/customEvents/test.ts index 1966ba2d4e4c..c4bcbcc2c792 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/test.ts +++ b/packages/browser-integration-tests/suites/replay/customEvents/test.ts @@ -117,7 +117,7 @@ sentryTest( nodeId: expect.any(Number), node: { attributes: { - 'aria-label': '** ***** ** **********', + 'aria-label': 'An Error in aria-label', class: 'btn btn-error', id: 'error', role: 'button', diff --git a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts index 15e9ab60dc3c..4bb4c45ae978 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts @@ -50,26 +50,25 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - await page.click('#go-background'); - await new Promise(resolve => setTimeout(resolve, 1000)); + await Promise.all([ + page.goto(url), + page.click('#go-background'), + new Promise(resolve => setTimeout(resolve, 1000)), + ]); expect(callsToSentry).toEqual(0); - await page.click('#error'); - const req0 = await reqPromise0; + const [req0] = await Promise.all([reqPromise0, page.click('#error')]); expect(callsToSentry).toEqual(2); // 1 error, 1 replay event - await page.click('#go-background'); - const req1 = await reqPromise1; - await reqErrorPromise; + const [req1] = await Promise.all([reqPromise1, page.click('#go-background'), reqErrorPromise]); expect(callsToSentry).toEqual(3); // 1 error, 2 replay events await page.click('#log'); - await page.click('#go-background'); - const req2 = await reqPromise2; + + const [req2] = await Promise.all([reqPromise2, page.click('#go-background')]); const event0 = getReplayEvent(req0); const content0 = getReplayRecordingContent(req0); diff --git a/packages/browser-integration-tests/suites/replay/fileInput/test.ts b/packages/browser-integration-tests/suites/replay/fileInput/test.ts index e0827538ba56..be8db2893a4f 100644 --- a/packages/browser-integration-tests/suites/replay/fileInput/test.ts +++ b/packages/browser-integration-tests/suites/replay/fileInput/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; +import type { inputData } from '@sentry-internal/rrweb'; import { IncrementalSource } from '@sentry-internal/rrweb'; -import type { inputData } from '@sentry-internal/rrweb/typings/types'; import { sentryTest } from '../../../utils/fixtures'; import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts index b7d0e558b844..efbd3384ec20 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts @@ -10,8 +10,6 @@ sentryTest( sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -22,26 +20,17 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - const res0 = await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page); - - void page.click('#button-add'); + const [res0] = await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); await forceFlushReplay(); - const res1 = await reqPromise1; - const reqPromise2 = waitForReplayRequest(page); - - void page.click('#button-modify'); + const [res1] = await Promise.all([waitForReplayRequest(page), page.click('#button-add')]); await forceFlushReplay(); - const res2 = await reqPromise2; - const reqPromise3 = waitForReplayRequest(page); + const [res2] = await Promise.all([waitForReplayRequest(page), page.click('#button-modify')]); + await forceFlushReplay(); - void page.click('#button-remove'); + const [res3] = await Promise.all([waitForReplayRequest(page), page.click('#button-remove')]); await forceFlushReplay(); - const res3 = await reqPromise3; const replayData0 = getReplayRecordingContent(res0); const replayData1 = getReplayRecordingContent(res1); diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts index eb144fa012ef..4c10e9db0436 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts @@ -15,8 +15,6 @@ sentryTest( sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -25,23 +23,24 @@ sentryTest( }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - const res0 = await reqPromise0; + const [res0] = await Promise.all([reqPromise0, page.goto(url)]); + await forceFlushReplay(); const reqPromise1 = waitForReplayRequest(page); - void page.click('#button-add'); + const [res1] = await Promise.all([reqPromise1, page.click('#button-add')]); await forceFlushReplay(); - const res1 = await reqPromise1; // replay should be stopped due to mutation limit let replay = await getReplaySnapshot(page); expect(replay.session).toBe(undefined); expect(replay._isEnabled).toBe(false); - void page.click('#button-modify'); + await page.click('#button-modify'); await forceFlushReplay(); await page.click('#button-remove'); diff --git a/packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts b/packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts index a40387159bfc..2890f58691b5 100644 --- a/packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts +++ b/packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts @@ -11,9 +11,6 @@ sentryTest('keeps track of max duration across reloads', async ({ getLocalTestPa sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -22,33 +19,42 @@ sentryTest('keeps track of max duration across reloads', async ({ getLocalTestPa }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); await new Promise(resolve => setTimeout(resolve, MAX_REPLAY_DURATION / 2)); - await page.reload(); - await page.click('#button1'); + await Promise.all([page.reload(), page.click('#button1')]); // After the second reload, we should have a new session (because we exceeded max age) const reqPromise3 = waitForReplayRequest(page, 0); await new Promise(resolve => setTimeout(resolve, MAX_REPLAY_DURATION / 2 + 100)); - void page.click('#button1'); - await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + const [req0, req1] = await Promise.all([ + reqPromise0, + reqPromise1, + page.click('#button1'), + page.evaluate( + `Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { return 'hidden'; }, }); - document.dispatchEvent(new Event('visibilitychange'));`); - const replayEvent0 = getReplayEvent(await reqPromise0); + document.dispatchEvent(new Event('visibilitychange'));`, + ), + ]); + + const replayEvent0 = getReplayEvent(req0); expect(replayEvent0).toEqual(getExpectedReplayEvent({})); - const replayEvent1 = getReplayEvent(await reqPromise1); + const replayEvent1 = getReplayEvent(req1); expect(replayEvent1).toEqual( getExpectedReplayEvent({ segment_id: 1, diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index 956397c84a49..4b04a61995a4 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -56,8 +56,7 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - const req0 = await reqPromise0; + const [req0] = await Promise.all([reqPromise0, page.goto(url)]); const replayEvent0 = getReplayEvent(req0); const recording0 = getReplayRecordingContent(req0); @@ -65,9 +64,8 @@ sentryTest( expect(normalize(recording0.fullSnapshots)).toMatchSnapshot('seg-0-snap-full'); expect(recording0.incrementalSnapshots.length).toEqual(0); - await page.click('#go-background'); + const [req1] = await Promise.all([reqPromise1, page.click('#go-background')]); - const req1 = await reqPromise1; const replayEvent1 = getReplayEvent(req1); const recording1 = getReplayRecordingContent(req1); @@ -97,9 +95,8 @@ sentryTest( // ----------------------------------------------------------------------------------------- // Test page reload - await page.reload(); + const [req2] = await Promise.all([reqPromise2, page.reload()]); - const req2 = await reqPromise2; const replayEvent2 = getReplayEvent(req2); const recording2 = getReplayRecordingContent(req2); @@ -107,9 +104,8 @@ sentryTest( expect(normalize(recording2.fullSnapshots)).toMatchSnapshot('seg-2-snap-full'); expect(recording2.incrementalSnapshots.length).toEqual(0); - await page.click('#go-background'); + const [req3] = await Promise.all([reqPromise3, page.click('#go-background')]); - const req3 = await reqPromise3; const replayEvent3 = getReplayEvent(req3); const recording3 = getReplayRecordingContent(req3); @@ -137,9 +133,8 @@ sentryTest( // ----------------------------------------------------------------------------------------- // Test subsequent link navigation to another page - await page.click('a'); + const [req4] = await Promise.all([reqPromise4, page.click('a')]); - const req4 = await reqPromise4; const replayEvent4 = getReplayEvent(req4); const recording4 = getReplayRecordingContent(req4); @@ -161,9 +156,8 @@ sentryTest( expect(normalize(recording4.fullSnapshots)).toMatchSnapshot('seg-4-snap-full'); expect(recording4.incrementalSnapshots.length).toEqual(0); - await page.click('#go-background'); + const [req5] = await Promise.all([reqPromise5, page.click('#go-background')]); - const req5 = await reqPromise5; const replayEvent5 = getReplayEvent(req5); const recording5 = getReplayRecordingContent(req5); @@ -207,9 +201,8 @@ sentryTest( // ----------------------------------------------------------------------------------------- // Test subsequent navigation without a page reload (i.e. SPA navigation) - await page.click('#spa-navigation'); + const [req6] = await Promise.all([reqPromise6, page.click('#spa-navigation')]); - const req6 = await reqPromise6; const replayEvent6 = getReplayEvent(req6); const recording6 = getReplayRecordingContent(req6); @@ -231,9 +224,8 @@ sentryTest( expect(recording6.fullSnapshots.length).toEqual(0); expect(normalize(recording6.incrementalSnapshots)).toMatchSnapshot('seg-6-snap-incremental'); - await page.click('#go-background'); + const [req7] = await Promise.all([reqPromise7, page.click('#go-background')]); - const req7 = await reqPromise7; const replayEvent7 = getReplayEvent(req7); const recording7 = getReplayRecordingContent(req7); @@ -279,9 +271,8 @@ sentryTest( // // ----------------------------------------------------------------------------------------- // // And just to finish this off, let's go back to the index page - await page.click('a'); + const [req8] = await Promise.all([reqPromise8, page.click('a')]); - const req8 = await reqPromise8; const replayEvent8 = getReplayEvent(req8); const recording8 = getReplayRecordingContent(req8); @@ -293,9 +284,8 @@ sentryTest( expect(normalize(recording8.fullSnapshots)).toMatchSnapshot('seg-8-snap-full'); expect(recording8.incrementalSnapshots.length).toEqual(0); - await page.click('#go-background'); + const [req9] = await Promise.all([reqPromise9, page.click('#go-background')]); - const req9 = await reqPromise9; const replayEvent9 = getReplayEvent(req9); const recording9 = getReplayRecordingContent(req9); diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental index 13c75c43bf61..6dd84be3e2dc 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 12, "x": 41, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental-chromium index 13c75c43bf61..6dd84be3e2dc 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 12, "x": 41, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental index c7be8ab3861a..575f1210087b 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 15, - "x": 157, - "y": 90 + "x": 157.13999938964844, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -34,8 +34,8 @@ "source": 2, "type": 0, "id": 15, - "x": 157, - "y": 90 + "x": 157.13999938964844, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -46,7 +46,8 @@ "type": 2, "id": 15, "x": 157, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental-chromium index c7be8ab3861a..575f1210087b 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 15, - "x": 157, - "y": 90 + "x": 157.13999938964844, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -34,8 +34,8 @@ "source": 2, "type": 0, "id": 15, - "x": 157, - "y": 90 + "x": 157.13999938964844, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -46,7 +46,8 @@ "type": 2, "id": 15, "x": 157, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental index 5b461c8cb66c..f952a6e3bfaa 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -34,8 +34,8 @@ "source": 2, "type": 0, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -46,7 +46,8 @@ "type": 2, "id": 12, "x": 41, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental-chromium index 5b461c8cb66c..f952a6e3bfaa 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -34,8 +34,8 @@ "source": 2, "type": 0, "id": 12, - "x": 41, - "y": 90 + "x": 41.810001373291016, + "y": 90.37000274658203 }, "timestamp": [timestamp] }, @@ -46,7 +46,8 @@ "type": 2, "id": 12, "x": 41, - "y": 90 + "y": 90, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium index f612eadc8f80..02a3e3f893d6 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium @@ -5,8 +5,8 @@ "source": 2, "type": 1, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -25,8 +25,8 @@ "source": 2, "type": 0, "id": 9, - "x": 41, - "y": 18 + "x": 41.810001373291016, + "y": 18.479999542236328 }, "timestamp": [timestamp] }, @@ -37,7 +37,8 @@ "type": 2, "id": 9, "x": 41, - "y": 18 + "y": 18, + "pointerType": 0 }, "timestamp": [timestamp] } diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-chromium.json b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-chromium.json index 4ac06ffeb444..a3c9c494b0b5 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-chromium.json +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-chromium.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-firefox.json b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-firefox.json index 9ca91c2dc5da..a3c9c494b0b5 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-firefox.json +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-firefox.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ @@ -194,6 +194,21 @@ "textContent": "\n ", "id": 28 }, + { + "type": 2, + "tagName": "div", + "attributes": { + "rr_width": "[1250-1300]px", + "rr_height": "[0-50]px" + }, + "childNodes": [], + "id": 29 + }, + { + "type": 3, + "textContent": "\n ", + "id": 30 + }, { "type": 2, "tagName": "svg", @@ -211,7 +226,7 @@ }, "childNodes": [], "isSVG": true, - "id": 30 + "id": 32 }, { "type": 2, @@ -219,7 +234,7 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 31 + "id": 33 }, { "type": 2, @@ -227,16 +242,16 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 32 + "id": 34 } ], "isSVG": true, - "id": 29 + "id": 31 }, { "type": 3, "textContent": "\n ", - "id": 33 + "id": 35 }, { "type": 2, @@ -246,12 +261,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 34 + "id": 36 }, { "type": 3, "textContent": "\n ", - "id": 35 + "id": 37 }, { "type": 2, @@ -262,12 +277,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 36 + "id": 38 }, { "type": 3, "textContent": "\n ", - "id": 37 + "id": 39 }, { "type": 2, @@ -277,12 +292,12 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 38 + "id": 40 }, { "type": 3, "textContent": "\n ", - "id": 39 + "id": 41 }, { "type": 2, @@ -293,17 +308,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 40 + "id": 42 }, { "type": 3, "textContent": "\n ", - "id": 41 + "id": 43 }, { "type": 3, "textContent": "\n\n", - "id": 42 + "id": 44 } ], "id": 8 diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-webkit.json b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-webkit.json index 4ac06ffeb444..a3c9c494b0b5 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-webkit.json +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy-webkit.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy.json b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy.json index 9ca91c2dc5da..a3c9c494b0b5 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy.json +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/test.ts-snapshots/privacy.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ @@ -194,6 +194,21 @@ "textContent": "\n ", "id": 28 }, + { + "type": 2, + "tagName": "div", + "attributes": { + "rr_width": "[1250-1300]px", + "rr_height": "[0-50]px" + }, + "childNodes": [], + "id": 29 + }, + { + "type": 3, + "textContent": "\n ", + "id": 30 + }, { "type": 2, "tagName": "svg", @@ -211,7 +226,7 @@ }, "childNodes": [], "isSVG": true, - "id": 30 + "id": 32 }, { "type": 2, @@ -219,7 +234,7 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 31 + "id": 33 }, { "type": 2, @@ -227,16 +242,16 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 32 + "id": 34 } ], "isSVG": true, - "id": 29 + "id": 31 }, { "type": 3, "textContent": "\n ", - "id": 33 + "id": 35 }, { "type": 2, @@ -246,12 +261,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 34 + "id": 36 }, { "type": 3, "textContent": "\n ", - "id": 35 + "id": 37 }, { "type": 2, @@ -262,12 +277,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 36 + "id": 38 }, { "type": 3, "textContent": "\n ", - "id": 37 + "id": 39 }, { "type": 2, @@ -277,12 +292,12 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 38 + "id": 40 }, { "type": 3, "textContent": "\n ", - "id": 39 + "id": 41 }, { "type": 2, @@ -293,17 +308,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 40 + "id": 42 }, { "type": 3, "textContent": "\n ", - "id": 41 + "id": 43 }, { "type": 3, "textContent": "\n\n", - "id": 42 + "id": 44 } ], "id": 8 diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json index 6bf1ea7659d3..69f74ba00da8 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-chromium.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json index f1b55c0884ef..4f20b93e13ab 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-firefox.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ @@ -171,45 +171,17 @@ "type": 2, "tagName": "svg", "attributes": { - "style": "width:200px;height:200px", - "viewBox": "0 0 80 80", - "data-sentry-unblock": "" + "rr_width": "[200-250]px", + "rr_height": "[200-250]px" }, - "childNodes": [ - { - "type": 2, - "tagName": "path", - "attributes": { - "d": "" - }, - "childNodes": [], - "isSVG": true, - "id": 27 - }, - { - "type": 2, - "tagName": "area", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 28 - }, - { - "type": 2, - "tagName": "rect", - "attributes": {}, - "childNodes": [], - "isSVG": true, - "id": 29 - } - ], + "childNodes": [], "isSVG": true, "id": 26 }, { "type": 3, "textContent": "\n ", - "id": 30 + "id": 27 }, { "type": 2, @@ -219,28 +191,27 @@ "rr_height": "[100-150]px" }, "childNodes": [], - "id": 31 + "id": 28 }, { "type": 3, "textContent": "\n ", - "id": 32 + "id": 29 }, { "type": 2, "tagName": "img", "attributes": { - "data-sentry-unblock": "", - "style": "width:100px;height:100px", - "src": "file:///none.png" + "rr_width": "[100-150]px", + "rr_height": "[100-150]px" }, "childNodes": [], - "id": 33 + "id": 30 }, { "type": 3, "textContent": "\n ", - "id": 34 + "id": 31 }, { "type": 2, @@ -250,17 +221,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 35 + "id": 32 }, { "type": 3, "textContent": "\n ", - "id": 36 + "id": 33 }, { "type": 3, "textContent": "\n\n", - "id": 37 + "id": 34 } ], "id": 8 diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json index 6bf1ea7659d3..69f74ba00da8 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy-webkit.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json index f1b55c0884ef..69f74ba00da8 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/test.ts-snapshots/privacy.json @@ -62,7 +62,7 @@ "type": 2, "tagName": "button", "attributes": { - "aria-label": "***** **", + "aria-label": "Click me", "onclick": "console.log('Test log')" }, "childNodes": [ @@ -131,6 +131,21 @@ "textContent": "\n ", "id": 20 }, + { + "type": 2, + "tagName": "input", + "attributes": { + "data-sentry-unmask": "", + "placeholder": "Placeholder can be unmasked" + }, + "childNodes": [], + "id": 21 + }, + { + "type": 3, + "textContent": "\n ", + "id": 22 + }, { "type": 2, "tagName": "div", @@ -141,15 +156,15 @@ { "type": 3, "textContent": "***** ****** ** ******", - "id": 22 + "id": 24 } ], - "id": 21 + "id": 23 }, { "type": 3, "textContent": "\n ", - "id": 23 + "id": 25 }, { "type": 2, @@ -160,12 +175,12 @@ }, "childNodes": [], "isSVG": true, - "id": 24 + "id": 26 }, { "type": 3, "textContent": "\n ", - "id": 25 + "id": 27 }, { "type": 2, @@ -184,7 +199,7 @@ }, "childNodes": [], "isSVG": true, - "id": 27 + "id": 29 }, { "type": 2, @@ -192,7 +207,7 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 28 + "id": 30 }, { "type": 2, @@ -200,16 +215,16 @@ "attributes": {}, "childNodes": [], "isSVG": true, - "id": 29 + "id": 31 } ], "isSVG": true, - "id": 26 + "id": 28 }, { "type": 3, "textContent": "\n ", - "id": 30 + "id": 32 }, { "type": 2, @@ -219,12 +234,12 @@ "rr_height": "[100-150]px" }, "childNodes": [], - "id": 31 + "id": 33 }, { "type": 3, "textContent": "\n ", - "id": 32 + "id": 34 }, { "type": 2, @@ -235,12 +250,12 @@ "src": "file:///none.png" }, "childNodes": [], - "id": 33 + "id": 35 }, { "type": 3, "textContent": "\n ", - "id": 34 + "id": 36 }, { "type": 2, @@ -250,17 +265,17 @@ "rr_height": "[0-50]px" }, "childNodes": [], - "id": 35 + "id": 37 }, { "type": 3, "textContent": "\n ", - "id": 36 + "id": 38 }, { "type": 3, "textContent": "\n\n", - "id": 37 + "id": 39 } ], "id": 8 diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts index 3ffc628c133d..3b76e5622225 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts +++ b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; +import type { inputData } from '@sentry-internal/rrweb'; import { IncrementalSource } from '@sentry-internal/rrweb'; -import type { inputData } from '@sentry-internal/rrweb/typings/types'; import { sentryTest } from '../../../utils/fixtures'; import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts index 4ec89c97a2cb..e98839f7fa37 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; +import type { inputData } from '@sentry-internal/rrweb'; import { IncrementalSource } from '@sentry-internal/rrweb'; -import type { inputData } from '@sentry-internal/rrweb/typings/types'; import { sentryTest } from '../../../utils/fixtures'; import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; diff --git a/packages/browser-integration-tests/suites/replay/requests/test.ts b/packages/browser-integration-tests/suites/replay/requests/test.ts index ba1d6ec5d0a2..52d78a144787 100644 --- a/packages/browser-integration-tests/suites/replay/requests/test.ts +++ b/packages/browser-integration-tests/suites/replay/requests/test.ts @@ -10,9 +10,6 @@ sentryTest('replay recording should contain fetch request span', async ({ getLoc sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -29,15 +26,16 @@ sentryTest('replay recording should contain fetch request span', async ({ getLoc }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - await page.click('#go-background'); - const { performanceSpans: spans0 } = getReplayRecordingContent(await reqPromise0); + const [req0] = await Promise.all([reqPromise0, page.goto(url), page.click('#go-background')]); + + const { performanceSpans: spans0 } = getReplayRecordingContent(req0); - const receivedResponse = page.waitForResponse('https://example.com'); - await page.click('#fetch'); - await receivedResponse; + await Promise.all([page.waitForResponse('https://example.com'), page.click('#fetch')]); const { performanceSpans: spans1 } = getReplayRecordingContent(await reqPromise1); @@ -50,9 +48,6 @@ sentryTest('replay recording should contain XHR request span', async ({ getLocal sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -69,15 +64,16 @@ sentryTest('replay recording should contain XHR request span', async ({ getLocal }); }); + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const url = await getLocalTestPath({ testDir: __dirname }); - await page.goto(url); - await page.click('#go-background'); - const { performanceSpans: spans0 } = getReplayRecordingContent(await reqPromise0); + const [req0] = await Promise.all([reqPromise0, page.goto(url), page.click('#go-background')]); + + const { performanceSpans: spans0 } = getReplayRecordingContent(req0); - const receivedResponse = page.waitForResponse('https://example.com'); - await page.click('#xhr'); - await receivedResponse; + await Promise.all([page.waitForResponse('https://example.com'), page.click('#xhr')]); const { performanceSpans: spans1 } = getReplayRecordingContent(await reqPromise1); diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts index f1765b2a3c22..3b78c859b8ae 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts @@ -14,8 +14,8 @@ import { // Session should expire after 2s - keep in sync with init.js const SESSION_TIMEOUT = 2000; -sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => { - if (shouldSkipReplayTest()) { +sentryTest('handles an expired session', async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => { + if (shouldSkipReplayTest() || browserName === 'webkit') { sentryTest.skip(); } @@ -47,6 +47,7 @@ sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => { const reqPromise2 = waitForReplayRequest(page, 0); await page.click('#button1'); + await forceFlushReplay(); const req1 = await reqPromise1; const replayEvent1 = getReplayEvent(req1); @@ -58,6 +59,7 @@ sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => { await new Promise(resolve => setTimeout(resolve, SESSION_TIMEOUT)); await page.click('#button2'); + await forceFlushReplay(); const req2 = await reqPromise2; const replay2 = await getReplaySnapshot(page); diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-chromium.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts-snapshots/snapshot-2-webkit.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts index ef2b841cbea3..60b78d121bbb 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts @@ -14,8 +14,10 @@ import { // Session should be paused after 2s - keep in sync with init.js const SESSION_PAUSED = 2000; -sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) => { - if (shouldSkipReplayTest()) { +sentryTest('handles an inactive session', async ({ getLocalTestPath, page, browserName }) => { + // webkit is a bit flakey here, the ids are sometimes off by , so seems like there is a race condition with checkout? + if (shouldSkipReplayTest() || browserName === 'webkit') { sentryTest.skip(); } diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-chromium.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/test.ts-snapshots/snapshot-1-webkit.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts index 3d5d17507d4f..7cc02cb83a05 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts @@ -18,7 +18,7 @@ const MAX_REPLAY_DURATION = 4000; The main difference between this and sessionExpiry test, is that here we wait for the overall time (4s) in multiple steps (2s, 2s) instead of waiting for the whole time at once (4s). */ -sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, page }) => { +sentryTest('handles session that exceeds max age', async ({ forceFlushReplay, getLocalTestPath, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -60,6 +60,7 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa const timePassed1 = Date.now() - startTimestamp; await new Promise(resolve => setTimeout(resolve, Math.max(MAX_REPLAY_DURATION / 2 - timePassed1, 0))); await page.click('#button1'); + await forceFlushReplay(); const req1 = await reqPromise1; const replayEvent1 = getReplayEvent(req1); @@ -73,6 +74,7 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa const timePassed2 = Date.now() - startTimestamp; await new Promise(resolve => setTimeout(resolve, Math.max(MAX_REPLAY_DURATION - timePassed2, 0))); await page.click('#button2'); + await forceFlushReplay(); const req2 = await reqPromise2; const replay2 = await getReplaySnapshot(page); diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-chromium.json b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-chromium.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-chromium.json +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-chromium.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-webkit.json b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-webkit.json index d510b410a343..13e5b1b70103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-webkit.json +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts-snapshots/snapshot-2-webkit.json @@ -9,7 +9,8 @@ "name": "html", "publicId": "", "systemId": "", - "id": 2 + "rootId": 16, + "id": 17 }, { "type": 2, @@ -28,15 +29,18 @@ "charset": "utf-8" }, "childNodes": [], - "id": 5 + "rootId": 16, + "id": 20 } ], - "id": 4 + "rootId": 16, + "id": 19 }, { "type": 3, "textContent": "\n ", - "id": 6 + "rootId": 16, + "id": 21 }, { "type": 2, @@ -46,7 +50,8 @@ { "type": 3, "textContent": "\n ", - "id": 8 + "rootId": 16, + "id": 23 }, { "type": 2, @@ -59,15 +64,18 @@ { "type": 3, "textContent": "***** **", - "id": 10 + "rootId": 16, + "id": 25 } ], - "id": 9 + "rootId": 16, + "id": 24 }, { "type": 3, "textContent": "\n ", - "id": 11 + "rootId": 16, + "id": 26 }, { "type": 2, @@ -80,29 +88,35 @@ { "type": 3, "textContent": "***** **", - "id": 13 + "rootId": 16, + "id": 28 } ], - "id": 12 + "rootId": 16, + "id": 27 }, { "type": 3, "textContent": "\n ", - "id": 14 + "rootId": 16, + "id": 29 }, { "type": 3, "textContent": "\n\n", - "id": 15 + "rootId": 16, + "id": 30 } ], - "id": 7 + "rootId": 16, + "id": 22 } ], - "id": 3 + "rootId": 16, + "id": 18 } ], - "id": 1 + "id": 16 }, "initialOffset": { "left": 0, diff --git a/packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts index 59bfb2ea26e8..8074f34a40fb 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts @@ -35,8 +35,6 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -47,18 +45,19 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), - await page.click(`#${id}`); + page.click(`#${id}`), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); @@ -92,8 +91,6 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -104,18 +101,18 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); - - await page.click(`#${id}`); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click(`#${id}`), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { diff --git a/packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts index 1a88d992714e..aef218a63cae 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts @@ -8,8 +8,6 @@ sentryTest('does not capture slow click when slowClickTimeout === 0', async ({ g sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,18 +18,18 @@ sentryTest('does not capture slow click when slowClickTimeout === 0', async ({ g const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationButton'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#mutationButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { diff --git a/packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts index 15e891b22e52..f80789d46a78 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts @@ -8,8 +8,6 @@ sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,18 +18,18 @@ sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationIgnoreButton'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#mutationIgnoreButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { @@ -60,8 +58,6 @@ sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => { sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -72,18 +68,19 @@ sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), - await page.click('#mutationDiv'); + await page.click('#mutationDiv'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { diff --git a/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts index cdb099d82a18..a21327058a81 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts @@ -13,8 +13,6 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -25,18 +23,18 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.multiClick'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationButtonImmediately', { clickCount: 4 }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.multiClick'); + }), + page.click('#mutationButtonImmediately', { clickCount: 4 }), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.multiClick'); @@ -65,15 +63,16 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc // When this has been flushed, the timeout has exceeded - so add a new click now, which should trigger another multi click - const reqPromise2 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); - - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.multiClick'); - }); + const [req2] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationButtonImmediately', { clickCount: 3 }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.multiClick'); + }), + page.click('#mutationButtonImmediately', { clickCount: 3 }), + ]); - const { breadcrumbs: breadcrumbb2 } = getCustomRecordingEvents(await reqPromise2); + const { breadcrumbs: breadcrumbb2 } = getCustomRecordingEvents(req2); const slowClickBreadcrumbs2 = breadcrumbb2.filter(breadcrumb => breadcrumb.category === 'ui.multiClick'); @@ -106,8 +105,6 @@ sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, for sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -118,8 +115,7 @@ sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, for const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); let multiClickBreadcrumbCount = 0; diff --git a/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts index cb5b9160d13b..196719783229 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts @@ -8,8 +8,6 @@ sentryTest('mutation after threshold results in slow click', async ({ forceFlush sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,19 +18,20 @@ sentryTest('mutation after threshold results in slow click', async ({ forceFlush const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); await forceFlushReplay(); - await reqPromise0; - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), - await page.click('#mutationButton'); + page.click('#mutationButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); @@ -69,8 +68,6 @@ sentryTest('multiple clicks are counted', async ({ getLocalTestUrl, page }) => { sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -81,18 +78,18 @@ sentryTest('multiple clicks are counted', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - void page.click('#mutationButton', { clickCount: 4 }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), + page.click('#mutationButton', { clickCount: 4 }), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); const multiClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.multiClick'); @@ -131,8 +128,6 @@ sentryTest('immediate mutation does not trigger slow click', async ({ forceFlush sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -143,19 +138,19 @@ sentryTest('immediate mutation does not trigger slow click', async ({ forceFlush const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); await forceFlushReplay(); - await reqPromise0; - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); - - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationButtonImmediately'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#mutationButtonImmediately'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { @@ -183,8 +178,6 @@ sentryTest('inline click handler does not trigger slow click', async ({ forceFlu sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -195,19 +188,19 @@ sentryTest('inline click handler does not trigger slow click', async ({ forceFlu const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); await forceFlushReplay(); - await reqPromise0; - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); - - await page.click('#mutationButtonInline'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#mutationButtonInline'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { @@ -235,8 +228,6 @@ sentryTest('mouseDown events are considered', async ({ getLocalTestUrl, page }) sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -247,18 +238,18 @@ sentryTest('mouseDown events are considered', async ({ getLocalTestUrl, page }) const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mouseDownButton'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#mouseDownButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { diff --git a/packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts index a8e59752fc4a..1f5b672ff659 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts @@ -8,8 +8,6 @@ sentryTest('immediate scroll does not trigger slow click', async ({ getLocalTest sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,18 +18,18 @@ sentryTest('immediate scroll does not trigger slow click', async ({ getLocalTest const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#scrollButton'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }), + page.click('#scrollButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); expect(breadcrumbs).toEqual([ { @@ -59,8 +57,6 @@ sentryTest('late scroll triggers slow click', async ({ getLocalTestUrl, page }) sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -71,18 +67,18 @@ sentryTest('late scroll triggers slow click', async ({ getLocalTestUrl, page }) const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#scrollLateButton'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), + page.click('#scrollLateButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts index ed53bc2d5fb4..6e6a1f13e3b6 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts @@ -8,8 +8,6 @@ sentryTest('mutation after timeout results in slow click', async ({ getLocalTest sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,18 +18,18 @@ sentryTest('mutation after timeout results in slow click', async ({ getLocalTest const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; - - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - await page.click('#mutationButtonLate'); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), + page.click('#mutationButtonLate'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); @@ -65,8 +63,6 @@ sentryTest('console.log results in slow click', async ({ getLocalTestUrl, page } sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -77,18 +73,19 @@ sentryTest('console.log results in slow click', async ({ getLocalTestUrl, page } const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); - const reqPromise1 = waitForReplayRequest(page, (event, res) => { - const { breadcrumbs } = getCustomRecordingEvents(res); + const [req1] = await Promise.all([ + waitForReplayRequest(page, (event, res) => { + const { breadcrumbs } = getCustomRecordingEvents(res); - return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); - }); + return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); + }), - await page.click('#consoleLogButton'); + page.click('#consoleLogButton'), + ]); - const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs } = getCustomRecordingEvents(req1); const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected'); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts index 4c9401234ea2..98ca0cc4c2a0 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts @@ -8,8 +8,6 @@ sentryTest('window.open() is considered for slow click', async ({ getLocalTestUr sentryTest.skip(); } - const reqPromise0 = waitForReplayRequest(page, 0); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ status: 200, @@ -20,8 +18,7 @@ sentryTest('window.open() is considered for slow click', async ({ getLocalTestUr const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await reqPromise0; + await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); const reqPromise1 = waitForReplayRequest(page, (event, res) => { const { breadcrumbs } = getCustomRecordingEvents(res); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 5027c4f0f1a4..4c0ace57547b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -127,6 +127,7 @@ export class BrowserClient extends BaseClient { return; } + // This is really the only place where we want to check for a DSN and only send outcomes then if (!this._dsn) { __DEBUG_BUILD__ && logger.log('No dsn provided, will not send outcomes'); return; diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 1a15c4bc37bd..c811c4f827a3 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -127,7 +127,7 @@ export abstract class BaseClient implements Client { if (options.dsn) { this._dsn = makeDsn(options.dsn); } else { - __DEBUG_BUILD__ && logger.warn('No DSN provided, client will not do anything.'); + __DEBUG_BUILD__ && logger.warn('No DSN provided, client will not send events.'); } if (this._dsn) { @@ -216,11 +216,6 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public captureSession(session: Session): void { - if (!this._isEnabled()) { - __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture session.'); - return; - } - if (!(typeof session.release === 'string')) { __DEBUG_BUILD__ && logger.warn('Discarded session because of missing or non-string release'); } else { @@ -297,8 +292,8 @@ export abstract class BaseClient implements Client { /** * Sets up the integrations */ - public setupIntegrations(): void { - if (this._isEnabled() && !this._integrationsInitialized) { + public setupIntegrations(forceInitialize?: boolean): void { + if ((forceInitialize && !this._integrationsInitialized) || (this._isEnabled() && !this._integrationsInitialized)) { this._integrations = setupIntegrations(this, this._options.integrations); this._integrationsInitialized = true; } @@ -338,23 +333,21 @@ export abstract class BaseClient implements Client { public sendEvent(event: Event, hint: EventHint = {}): void { this.emit('beforeSendEvent', event, hint); - if (this._dsn) { - let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel); - - for (const attachment of hint.attachments || []) { - env = addItemToEnvelope( - env, - createAttachmentEnvelopeItem( - attachment, - this._options.transportOptions && this._options.transportOptions.textEncoder, - ), - ); - } + let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel); - const promise = this._sendEnvelope(env); - if (promise) { - promise.then(sendResponse => this.emit('afterSendEvent', event, sendResponse), null); - } + for (const attachment of hint.attachments || []) { + env = addItemToEnvelope( + env, + createAttachmentEnvelopeItem( + attachment, + this._options.transportOptions && this._options.transportOptions.textEncoder, + ), + ); + } + + const promise = this._sendEnvelope(env); + if (promise) { + promise.then(sendResponse => this.emit('afterSendEvent', event, sendResponse), null); } } @@ -362,10 +355,8 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public sendSession(session: Session | SessionAggregates): void { - if (this._dsn) { - const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); - void this._sendEnvelope(env); - } + const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); + void this._sendEnvelope(env); } /** @@ -531,9 +522,9 @@ export abstract class BaseClient implements Client { }); } - /** Determines whether this SDK is enabled and a valid Dsn is present. */ + /** Determines whether this SDK is enabled and a transport is present. */ protected _isEnabled(): boolean { - return this.getOptions().enabled !== false && this._dsn !== undefined; + return this.getOptions().enabled !== false && this._transport !== undefined; } /** @@ -635,10 +626,6 @@ export abstract class BaseClient implements Client { const options = this.getOptions(); const { sampleRate } = options; - if (!this._isEnabled()) { - return rejectedSyncPromise(new SentryError('SDK not enabled, will not capture event.', 'log')); - } - const isTransaction = isTransactionEvent(event); const isError = isErrorEvent(event); const eventType = event.type || 'error'; @@ -738,9 +725,9 @@ export abstract class BaseClient implements Client { * @inheritdoc */ protected _sendEnvelope(envelope: Envelope): PromiseLike | void { - if (this._transport && this._dsn) { - this.emit('beforeEnvelope', envelope); + this.emit('beforeEnvelope', envelope); + if (this._isEnabled() && this._transport) { return this._transport.send(envelope).then(null, reason => { __DEBUG_BUILD__ && logger.error('Error while sending event:', reason); }); diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 5e79d1707d67..9ec29c9d2a7e 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -36,7 +36,7 @@ function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event { /** Creates an envelope from a Session */ export function createSessionEnvelope( session: Session | SessionAggregates, - dsn: DsnComponents, + dsn?: DsnComponents, metadata?: SdkMetadata, tunnel?: string, ): SessionEnvelope { @@ -44,7 +44,7 @@ export function createSessionEnvelope( const envelopeHeaders = { sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(!!tunnel && { dsn: dsnToString(dsn) }), + ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; const envelopeItem: SessionItem = @@ -58,7 +58,7 @@ export function createSessionEnvelope( */ export function createEventEnvelope( event: Event, - dsn: DsnComponents, + dsn?: DsnComponents, metadata?: SdkMetadata, tunnel?: string, ): EventEnvelope { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 21f7cab37505..81fcd7c3af8d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -51,6 +51,7 @@ export { FunctionToString, InboundFilters } from './integrations'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; +export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; import * as Integrations from './integrations'; diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 37ea8bac06e9..9c6e8c9a546a 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Client, Event, EventHint, Integration, StackFrame } from '@sentry/types'; import { getEventDescription, logger, stringMatchesSomePattern } from '@sentry/utils'; // "Script error." is hard coded into browsers for errors that it can't read. @@ -48,23 +48,15 @@ export class InboundFilters implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void { - const eventProcess: EventProcessor = (event: Event) => { - const hub = getCurrentHub(); - if (hub) { - const self = hub.getIntegration(InboundFilters); - if (self) { - const client = hub.getClient(); - const clientOptions = client ? client.getOptions() : {}; - const options = _mergeOptions(self._options, clientOptions); - return _shouldDropEvent(event, options) ? null : event; - } - } - return event; - }; + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } - eventProcess.id = this.name; - addGlobalEventProcessor(eventProcess); + /** @inheritDoc */ + public processEvent(event: Event, _eventHint: EventHint, client: Client): Event | null { + const clientOptions = client.getOptions(); + const options = _mergeOptions(this._options, clientOptions); + return _shouldDropEvent(event, options) ? null : event; } } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index ee0a5724b9bf..73bfb16996bb 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -2,11 +2,11 @@ import type { Context, Contexts, DynamicSamplingContext, - Event, Measurements, MeasurementUnit, Transaction as TransactionInterface, TransactionContext, + TransactionEvent, TransactionMetadata, } from '@sentry/types'; import { dropUndefinedKeys, logger } from '@sentry/utils'; @@ -134,84 +134,10 @@ export class Transaction extends SpanClass implements TransactionInterface { * @inheritDoc */ public finish(endTimestamp?: number): string | undefined { - // This transaction is already finished, so we should not flush it again. - if (this.endTimestamp !== undefined) { + const transaction = this._finishTransaction(endTimestamp); + if (!transaction) { return undefined; } - - if (!this.name) { - __DEBUG_BUILD__ && logger.warn('Transaction has no name, falling back to ``.'); - this.name = ''; - } - - // just sets the end timestamp - super.finish(endTimestamp); - - const client = this._hub.getClient(); - if (client && client.emit) { - client.emit('finishTransaction', this); - } - - if (this.sampled !== true) { - // At this point if `sampled !== true` we want to discard the transaction. - __DEBUG_BUILD__ && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); - - if (client) { - client.recordDroppedEvent('sample_rate', 'transaction'); - } - - return undefined; - } - - const finishedSpans = this.spanRecorder ? this.spanRecorder.spans.filter(s => s !== this && s.endTimestamp) : []; - - if (this._trimEnd && finishedSpans.length > 0) { - this.endTimestamp = finishedSpans.reduce((prev: SpanClass, current: SpanClass) => { - if (prev.endTimestamp && current.endTimestamp) { - return prev.endTimestamp > current.endTimestamp ? prev : current; - } - return prev; - }).endTimestamp; - } - - const metadata = this.metadata; - - const transaction: Event = { - contexts: { - ...this._contexts, - // We don't want to override trace context - trace: this.getTraceContext(), - }, - spans: finishedSpans, - start_timestamp: this.startTimestamp, - tags: this.tags, - timestamp: this.endTimestamp, - transaction: this.name, - type: 'transaction', - sdkProcessingMetadata: { - ...metadata, - dynamicSamplingContext: this.getDynamicSamplingContext(), - }, - ...(metadata.source && { - transaction_info: { - source: metadata.source, - }, - }), - }; - - const hasMeasurements = Object.keys(this._measurements).length > 0; - - if (hasMeasurements) { - __DEBUG_BUILD__ && - logger.log( - '[Measurements] Adding measurements to transaction', - JSON.stringify(this._measurements, undefined, 2), - ); - transaction.measurements = this._measurements; - } - - __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); - return this._hub.captureEvent(transaction); } @@ -289,4 +215,89 @@ export class Transaction extends SpanClass implements TransactionInterface { public setHub(hub: Hub): void { this._hub = hub; } + + /** + * Finish the transaction & prepare the event to send to Sentry. + */ + protected _finishTransaction(endTimestamp?: number): TransactionEvent | undefined { + // This transaction is already finished, so we should not flush it again. + if (this.endTimestamp !== undefined) { + return undefined; + } + + if (!this.name) { + __DEBUG_BUILD__ && logger.warn('Transaction has no name, falling back to ``.'); + this.name = ''; + } + + // just sets the end timestamp + super.finish(endTimestamp); + + const client = this._hub.getClient(); + if (client && client.emit) { + client.emit('finishTransaction', this); + } + + if (this.sampled !== true) { + // At this point if `sampled !== true` we want to discard the transaction. + __DEBUG_BUILD__ && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); + + if (client) { + client.recordDroppedEvent('sample_rate', 'transaction'); + } + + return undefined; + } + + const finishedSpans = this.spanRecorder ? this.spanRecorder.spans.filter(s => s !== this && s.endTimestamp) : []; + + if (this._trimEnd && finishedSpans.length > 0) { + this.endTimestamp = finishedSpans.reduce((prev: SpanClass, current: SpanClass) => { + if (prev.endTimestamp && current.endTimestamp) { + return prev.endTimestamp > current.endTimestamp ? prev : current; + } + return prev; + }).endTimestamp; + } + + const metadata = this.metadata; + + const transaction: TransactionEvent = { + contexts: { + ...this._contexts, + // We don't want to override trace context + trace: this.getTraceContext(), + }, + spans: finishedSpans, + start_timestamp: this.startTimestamp, + tags: this.tags, + timestamp: this.endTimestamp, + transaction: this.name, + type: 'transaction', + sdkProcessingMetadata: { + ...metadata, + dynamicSamplingContext: this.getDynamicSamplingContext(), + }, + ...(metadata.source && { + transaction_info: { + source: metadata.source, + }, + }), + }; + + const hasMeasurements = Object.keys(this._measurements).length > 0; + + if (hasMeasurements) { + __DEBUG_BUILD__ && + logger.log( + '[Measurements] Adding measurements to transaction', + JSON.stringify(this._measurements, undefined, 2), + ); + transaction.measurements = this._measurements; + } + + __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); + + return transaction; + } } diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts new file mode 100644 index 000000000000..0256e3cf7835 --- /dev/null +++ b/packages/core/src/utils/isSentryRequestUrl.ts @@ -0,0 +1,29 @@ +import type { DsnComponents, Hub } from '@sentry/types'; + +/** + * Checks whether given url points to Sentry server + * @param url url to verify + */ +export function isSentryRequestUrl(url: string, hub: Hub): boolean { + const client = hub.getClient(); + const dsn = client && client.getDsn(); + const tunnel = client && client.getOptions().tunnel; + + return checkDsn(url, dsn) || checkTunnel(url, tunnel); +} + +function checkTunnel(url: string, tunnel: string | undefined): boolean { + if (!tunnel) { + return false; + } + + return removeTrailingSlash(url) === removeTrailingSlash(tunnel); +} + +function checkDsn(url: string, dsn: DsnComponents | undefined): boolean { + return dsn ? url.includes(dsn.host) : false; +} + +function removeTrailingSlash(str: string): string { + return str[str.length - 1] === '/' ? str.slice(0, -1) : str; +} diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index dd0906921fa3..27e34a1402d4 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -398,30 +398,6 @@ describe('BaseClient', () => { }); describe('captureEvent() / prepareEvent()', () => { - test('skips when disabled', () => { - expect.assertions(1); - - const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); - const client = new TestClient(options); - const scope = new Scope(); - - client.captureEvent({}, undefined, scope); - - expect(TestClient.instance!.event).toBeUndefined(); - }); - - test('skips without a Dsn', () => { - expect.assertions(1); - - const options = getDefaultTestClientOptions({}); - const client = new TestClient(options); - const scope = new Scope(); - - client.captureEvent({}, undefined, scope); - - expect(TestClient.instance!.event).toBeUndefined(); - }); - test.each([ ['`Error` instance', new Error('Will I get caught twice?')], ['plain object', { 'Will I': 'get caught twice?' }], @@ -1616,9 +1592,9 @@ describe('BaseClient', () => { test('close', async () => { jest.useRealTimers(); - expect.assertions(2); + expect.assertions(4); - const { makeTransport, delay } = makeFakeTransport(300); + const { makeTransport, delay, getSentCount } = makeFakeTransport(300); const client = new TestClient( getDefaultTestClientOptions({ @@ -1630,9 +1606,12 @@ describe('BaseClient', () => { expect(client.captureMessage('test')).toBeTruthy(); await client.close(delay); + expect(getSentCount()).toBe(1); + expect(client.captureMessage('test')).toBeTruthy(); + await client.close(delay); // Sends after close shouldn't work anymore - expect(client.captureMessage('test')).toBeFalsy(); + expect(getSentCount()).toBe(1); }); test('multiple concurrent flush calls should just work', async () => { @@ -1798,18 +1777,6 @@ describe('BaseClient', () => { expect(TestClient.instance!.session).toEqual(session); }); - - test('skips when disabled', () => { - expect.assertions(1); - - const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); - const client = new TestClient(options); - const session = makeSession({ release: 'test' }); - - client.captureSession(session); - - expect(TestClient.instance!.session).toBeUndefined(); - }); }); describe('recordDroppedEvent()/_clearOutcomes()', () => { diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 9888a59eedff..8e42c5bc29ee 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -2,6 +2,9 @@ import type { Event, EventProcessor } from '@sentry/types'; import type { InboundFiltersOptions } from '../../../src/integrations/inboundfilters'; import { InboundFilters } from '../../../src/integrations/inboundfilters'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +const PUBLIC_DSN = 'https://username@domain/123'; /** * Creates an instance of the InboundFilters integration and returns @@ -25,30 +28,22 @@ function createInboundFiltersEventProcessor( options: Partial = {}, clientOptions: Partial = {}, ): EventProcessor { - const eventProcessors: EventProcessor[] = []; - const inboundFiltersInstance = new InboundFilters(options); - - function addGlobalEventProcessor(processor: EventProcessor): void { - eventProcessors.push(processor); - expect(eventProcessors).toHaveLength(1); - } - - function getCurrentHub(): any { - return { - getIntegration(_integration: any): any { - // pretend integration is enabled - return inboundFiltersInstance; - }, - getClient(): any { - return { - getOptions: () => clientOptions, - }; - }, - }; - } - - inboundFiltersInstance.setupOnce(addGlobalEventProcessor, getCurrentHub); - return eventProcessors[0]; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + ...clientOptions, + defaultIntegrations: false, + integrations: [new InboundFilters(options)], + }), + ); + + client.setupIntegrations(); + + const eventProcessors = client['_eventProcessors']; + const eventProcessor = eventProcessors.find(processor => processor.id === 'InboundFilters'); + + expect(eventProcessor).toBeDefined(); + return eventProcessor!; } // Fixtures diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts new file mode 100644 index 000000000000..b1671b9410e8 --- /dev/null +++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts @@ -0,0 +1,26 @@ +import type { Hub } from '@sentry/types'; + +import { isSentryRequestUrl } from '../../../src'; + +describe('isSentryRequestUrl', () => { + it.each([ + ['', 'sentry-dsn.com', '', false], + ['http://sentry-dsn.com/my-url', 'sentry-dsn.com', '', true], + ['http://sentry-dsn.com', 'sentry-dsn.com', '', true], + ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200', true], + ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200/', true], + ['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200', true], + ['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200', false], + ])('works with url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string, expected: boolean) => { + const hub = { + getClient: () => { + return { + getOptions: () => ({ tunnel }), + getDsn: () => ({ host: dsn }), + }; + }, + } as unknown as Hub; + + expect(isSentryRequestUrl(url, hub)).toBe(expected); + }); +}); diff --git a/packages/integrations/package.json b/packages/integrations/package.json index e0ff1c29d7ec..01d1d566840f 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -23,6 +23,7 @@ } }, "dependencies": { + "@sentry/core": "7.72.0", "@sentry/types": "7.72.0", "@sentry/utils": "7.72.0", "localforage": "^1.8.1", diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 5492116d7722..6ceb832fc1ff 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,3 +1,4 @@ +import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; import type { Event as SentryEvent, EventProcessor, @@ -345,22 +346,6 @@ export class HttpClient implements Integration { ); } - /** - * Checks whether given url points to Sentry server - * - * @param url url to verify - */ - private _isSentryRequest(url: string): boolean { - const client = this._getCurrentHub && this._getCurrentHub().getClient(); - - if (!client) { - return false; - } - - const dsn = client.getDsn(); - return dsn ? url.includes(dsn.host) : false; - } - /** * Checks whether to capture given response as an event * @@ -368,7 +353,11 @@ export class HttpClient implements Integration { * @param url response url */ private _shouldCaptureResponse(status: number, url: string): boolean { - return this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !this._isSentryRequest(url); + return ( + this._isInGivenStatusRanges(status) && + this._isInGivenRequestTargets(url) && + !isSentryRequestUrl(url, getCurrentHub()) + ); } /** diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index 7fc882794e5c..346f56a91cbb 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -189,7 +189,7 @@ export default function wrappingLoader( } templateCode = templateCode.replace( /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, - '@sentry/nextjs/build/esm/config/templates/requestAsyncStorageShim.js', + '@sentry/nextjs/esm/config/templates/requestAsyncStorageShim.js', ); } diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index d2a3e35397be..6a4b8766a242 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,7 +3,7 @@ import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { hasTracingEnabled, Transaction } from '@sentry/core'; +import { hasTracingEnabled, isSentryRequestUrl, Transaction } from '@sentry/core'; import { getCurrentHub } from '@sentry/node'; import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; @@ -11,6 +11,7 @@ import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; import type { NodeExperimentalClient, OtelSpan } from '../types'; import { getRequestSpanData } from '../utils/getRequestSpanData'; +import { getRequestUrl } from '../utils/getRequestUrl'; interface TracingOptions { /** @@ -93,8 +94,8 @@ export class Http implements Integration { instrumentations: [ new HttpInstrumentation({ ignoreOutgoingRequestHook: request => { - const host = request.host || request.hostname; - return isSentryHost(host); + const url = getRequestUrl(request); + return url ? isSentryRequestUrl(url, getCurrentHub()) : false; }, ignoreIncomingRequestHook: request => { @@ -224,11 +225,3 @@ function getHttpUrl(attributes: Attributes): string | undefined { const url = attributes[SemanticAttributes.HTTP_URL]; return typeof url === 'string' ? url : undefined; } - -/** - * Checks whether given host points to Sentry server - */ -function isSentryHost(host: string | undefined): boolean { - const dsn = getCurrentHub().getClient()?.getDsn(); - return dsn && host ? host.includes(dsn.host) : false; -} diff --git a/packages/node-experimental/src/utils/getRequestUrl.ts b/packages/node-experimental/src/utils/getRequestUrl.ts new file mode 100644 index 000000000000..1e4dcfb71232 --- /dev/null +++ b/packages/node-experimental/src/utils/getRequestUrl.ts @@ -0,0 +1,15 @@ +import type { RequestOptions } from 'http'; + +/** Build a full URL from request options. */ +export function getRequestUrl(requestOptions: RequestOptions): string { + const protocol = requestOptions.protocol || ''; + const hostname = requestOptions.hostname || requestOptions.host || ''; + // Don't log standard :80 (http) and :443 (https) ports to reduce the noise + // Also don't add port if the hostname already includes a port + const port = + !requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 || /^(.*):(\d+)$/.test(hostname) + ? '' + : `:${requestOptions.port}`; + const path = requestOptions.path ? requestOptions.path : '/'; + return `${protocol}//${hostname}${port}${path}`; +} diff --git a/packages/node-experimental/test/utils/getRequestUrl.test.ts b/packages/node-experimental/test/utils/getRequestUrl.test.ts new file mode 100644 index 000000000000..caa92aa10a59 --- /dev/null +++ b/packages/node-experimental/test/utils/getRequestUrl.test.ts @@ -0,0 +1,20 @@ +import type { RequestOptions } from 'http'; + +import { getRequestUrl } from '../../src/utils/getRequestUrl'; + +describe('getRequestUrl', () => { + it.each([ + [{ protocol: 'http:', hostname: 'localhost', port: 80 }, 'http://localhost/'], + [{ protocol: 'http:', hostname: 'localhost', host: 'localhost:80', port: 80 }, 'http://localhost/'], + [{ protocol: 'http:', hostname: 'localhost', port: 3000 }, 'http://localhost:3000/'], + [{ protocol: 'http:', host: 'localhost:3000', port: 3000 }, 'http://localhost:3000/'], + [{ protocol: 'https:', hostname: 'localhost', port: 443 }, 'https://localhost/'], + [{ protocol: 'https:', hostname: 'localhost', port: 443, path: '/my-path' }, 'https://localhost/my-path'], + [ + { protocol: 'https:', hostname: 'www.example.com', port: 443, path: '/my-path' }, + 'https://www.example.com/my-path', + ], + ])('works with %s', (input: RequestOptions, expected: string | undefined) => { + expect(getRequestUrl(input)).toBe(expected); + }); +}); diff --git a/packages/node-integration-tests/suites/anr/scenario.js b/packages/node-integration-tests/suites/anr/basic.js similarity index 100% rename from packages/node-integration-tests/suites/anr/scenario.js rename to packages/node-integration-tests/suites/anr/basic.js diff --git a/packages/node-integration-tests/suites/anr/scenario.mjs b/packages/node-integration-tests/suites/anr/basic.mjs similarity index 100% rename from packages/node-integration-tests/suites/anr/scenario.mjs rename to packages/node-integration-tests/suites/anr/basic.mjs diff --git a/packages/node-integration-tests/suites/anr/forked.js b/packages/node-integration-tests/suites/anr/forked.js new file mode 100644 index 000000000000..3abadc09b9c3 --- /dev/null +++ b/packages/node-integration-tests/suites/anr/forked.js @@ -0,0 +1,31 @@ +const crypto = require('crypto'); + +const Sentry = require('@sentry/node'); + +// close both processes after 5 seconds +setTimeout(() => { + process.exit(); +}, 5000); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + beforeSend: event => { + // eslint-disable-next-line no-console + console.log(JSON.stringify(event)); + }, +}); + +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200, debug: true }).then(() => { + function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + } + } + + setTimeout(() => { + longWork(); + }, 1000); +}); diff --git a/packages/node-integration-tests/suites/anr/forker.js b/packages/node-integration-tests/suites/anr/forker.js new file mode 100644 index 000000000000..c1ac5e1ccd1c --- /dev/null +++ b/packages/node-integration-tests/suites/anr/forker.js @@ -0,0 +1,7 @@ +const { fork } = require('child_process'); +const { join } = require('path'); + +const child = fork(join(__dirname, 'forked.js'), { stdio: 'inherit' }); +child.on('exit', () => { + process.exit(); +}); diff --git a/packages/node-integration-tests/suites/anr/test.ts b/packages/node-integration-tests/suites/anr/test.ts index ec820dca9c62..4fd83c0b3205 100644 --- a/packages/node-integration-tests/suites/anr/test.ts +++ b/packages/node-integration-tests/suites/anr/test.ts @@ -12,7 +12,7 @@ describe('should report ANR when event loop blocked', () => { expect.assertions(testFramesDetails ? 6 : 4); - const testScriptPath = path.resolve(__dirname, 'scenario.js'); + const testScriptPath = path.resolve(__dirname, 'basic.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { const event = JSON.parse(stdout) as Event; @@ -39,7 +39,7 @@ describe('should report ANR when event loop blocked', () => { expect.assertions(6); - const testScriptPath = path.resolve(__dirname, 'scenario.mjs'); + const testScriptPath = path.resolve(__dirname, 'basic.mjs'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { const event = JSON.parse(stdout) as Event; @@ -54,4 +54,29 @@ describe('should report ANR when event loop blocked', () => { done(); }); }); + + test('from forked process', done => { + // The stack trace is different when node < 12 + const testFramesDetails = NODE_VERSION >= 12; + + expect.assertions(testFramesDetails ? 6 : 4); + + const testScriptPath = path.resolve(__dirname, 'forker.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const event = JSON.parse(stdout) as Event; + + expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); + expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); + expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); + expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); + + if (testFramesDetails) { + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); + } + + done(); + }); + }); }); diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 2d546447ddd7..5c4edd808aa3 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,6 +1,6 @@ import type { Event, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { fork } from 'child_process'; +import { spawn } from 'child_process'; import * as inspector from 'inspector'; import { addGlobalEventProcessor, captureEvent, flush } from '..'; @@ -98,28 +98,44 @@ function sendEvent(blockedMs: number, frames?: StackFrame[]): void { }); } +/** + * Starts the node debugger and returns the inspector url. + * + * When inspector.url() returns undefined, it means the port is already in use so we try the next port. + */ +function startInspector(startPort: number = 9229): string | undefined { + let inspectorUrl: string | undefined = undefined; + let port = startPort; + + while (inspectorUrl === undefined && port < startPort + 100) { + inspector.open(port); + inspectorUrl = inspector.url(); + port++; + } + + return inspectorUrl; +} + function startChildProcess(options: Options): void { - function log(message: string, err?: unknown): void { + function log(message: string, ...args: unknown[]): void { if (options.debug) { - if (err) { - logger.log(`[ANR] ${message}`, err); - } else { - logger.log(`[ANR] ${message}`); - } + logger.log(`[ANR] ${message}`, ...args); } } try { const env = { ...process.env }; + env.SENTRY_ANR_CHILD_PROCESS = 'true'; if (options.captureStackTrace) { - inspector.open(); - env.SENTRY_INSPECT_URL = inspector.url(); + env.SENTRY_INSPECT_URL = startInspector(); } - const child = fork(options.entryScript, { + log(`Spawning child process with execPath:'${process.execPath}' and entryScript'${options.entryScript}'`); + + const child = spawn(process.execPath, [options.entryScript], { env, - stdio: options.debug ? 'inherit' : 'ignore', + stdio: options.debug ? ['inherit', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', 'ignore', 'ipc'], }); // The child process should not keep the main process alive child.unref(); @@ -133,14 +149,16 @@ function startChildProcess(options: Options): void { } }, options.pollInterval); - const end = (err: unknown): void => { - clearInterval(timer); - log('Child process ended', err); + const end = (type: string): ((...args: unknown[]) => void) => { + return (...args): void => { + clearInterval(timer); + log(`Child process ${type}`, ...args); + }; }; - child.on('error', end); - child.on('disconnect', end); - child.on('exit', end); + child.on('error', end('error')); + child.on('disconnect', end('disconnect')); + child.on('exit', end('exit')); } catch (e) { log('Failed to start child process', e); } @@ -153,6 +171,8 @@ function handleChildProcess(options: Options): void { } } + process.title = 'sentry-anr'; + log('Started'); addGlobalEventProcessor(event => { @@ -197,6 +217,13 @@ function handleChildProcess(options: Options): void { }); } +/** + * Returns true if the current process is an ANR child process. + */ +export function isAnrChildProcess(): boolean { + return !!process.send && !!process.env.SENTRY_ANR_CHILD_PROCESS; +} + /** * **Note** This feature is still in beta so there may be breaking changes in future releases. * @@ -221,17 +248,19 @@ function handleChildProcess(options: Options): void { * ``` */ export function enableAnrDetection(options: Partial): Promise { - const isChildProcess = !!process.send; + // When pm2 runs the script in cluster mode, process.argv[1] is the pm2 script and process.env.pm_exec_path is the + // path to the entry script + const entryScript = options.entryScript || process.env.pm_exec_path || process.argv[1]; const anrOptions: Options = { - entryScript: options.entryScript || process.argv[1], + entryScript, pollInterval: options.pollInterval || DEFAULT_INTERVAL, anrThreshold: options.anrThreshold || DEFAULT_HANG_THRESHOLD, captureStackTrace: !!options.captureStackTrace, debug: !!options.debug, }; - if (isChildProcess) { + if (isAnrChildProcess()) { handleChildProcess(anrOptions); // In the child process, the promise never resolves which stops the app code from running return new Promise(() => { diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 95b68e2c8eb3..1f4b5c9c9224 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,5 +1,5 @@ import type { Hub } from '@sentry/core'; -import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core'; +import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; import type { DynamicSamplingContext, EventProcessor, @@ -21,7 +21,7 @@ import { LRUMap } from 'lru_map'; import type { NodeClient } from '../client'; import { NODE_VERSION } from '../nodeVersion'; import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http'; -import { cleanSpanDescription, extractRawUrl, extractUrl, isSentryRequest, normalizeRequestArgs } from './utils/http'; +import { cleanSpanDescription, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http'; interface TracingOptions { /** @@ -238,7 +238,7 @@ function _createWrappedRequestMethodFactory( const requestUrl = extractUrl(requestOptions); // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method - if (isSentryRequest(requestUrl)) { + if (isSentryRequestUrl(requestUrl, getCurrentHub())) { return originalRequestMethod.apply(httpModule, requestArgs); } diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index f98011d94832..39f250c23778 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame, StackParser } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; @@ -11,6 +12,8 @@ type OnPauseEvent = InspectorNotification; export interface DebugSession { /** Configures and connects to the debug session */ configureAndConnect(onPause: (message: OnPauseEvent, complete: () => void) => void, captureAll: boolean): void; + /** Updates which kind of exceptions to capture */ + setPauseOnExceptions(captureAll: boolean): void; /** Gets local variables for an objectId */ getLocalVariables(objectId: string, callback: (vars: Variables) => void): void; } @@ -19,6 +22,52 @@ type Next = (result: T) => void; type Add = (fn: Next) => void; type CallbackWrapper = { add: Add; next: Next }; +type RateLimitIncrement = () => void; + +/** + * Creates a rate limiter + * @param maxPerSecond Maximum number of calls per second + * @param enable Callback to enable capture + * @param disable Callback to disable capture + * @returns A function to call to increment the rate limiter count + */ +export function createRateLimiter( + maxPerSecond: number, + enable: () => void, + disable: (seconds: number) => void, +): RateLimitIncrement { + let count = 0; + let retrySeconds = 5; + let disabledTimeout = 0; + + setInterval(() => { + if (disabledTimeout === 0) { + if (count > maxPerSecond) { + retrySeconds *= 2; + disable(retrySeconds); + + // Cap at one day + if (retrySeconds > 86400) { + retrySeconds = 86400; + } + disabledTimeout = retrySeconds; + } + } else { + disabledTimeout -= 1; + + if (disabledTimeout === 0) { + enable(); + } + } + + count = 0; + }, 1_000).unref(); + + return () => { + count += 1; + }; +} + /** Creates a container for callbacks to be called sequentially */ export function createCallbackList(complete: Next): CallbackWrapper { // A collection of callbacks to be executed last to first @@ -103,6 +152,10 @@ class AsyncSession implements DebugSession { this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); } + public setPauseOnExceptions(captureAll: boolean): void { + this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); + } + /** @inheritdoc */ public getLocalVariables(objectId: string, complete: (vars: Variables) => void): void { this._getProperties(objectId, props => { @@ -245,19 +298,33 @@ export interface FrameVariables { vars?: Variables; } -/** There are no options yet. This allows them to be added later without breaking changes */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface interface Options { /** - * Capture local variables for both handled and unhandled exceptions + * Capture local variables for both caught and uncaught exceptions + * + * - When false, only uncaught exceptions will have local variables + * - When true, both caught and uncaught exceptions will have local variables. + * + * Defaults to `true`. * - * Default: false - Only captures local variables for uncaught exceptions + * Capturing local variables for all exceptions can be expensive since the debugger pauses for every throw to collect + * local variables. + * + * To reduce the likelihood of this feature impacting app performance or throughput, this feature is rate-limited. + * Once the rate limit is reached, local variables will only be captured for uncaught exceptions until a timeout has + * been reached. */ captureAllExceptions?: boolean; + /** + * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered. + */ + maxExceptionsPerSecond?: number; } /** * Adds local variables to exception frames + * + * Default: 50 */ export class LocalVariables implements Integration { public static id: string = 'LocalVariables'; @@ -265,6 +332,7 @@ export class LocalVariables implements Integration { public readonly name: string = LocalVariables.id; private readonly _cachedFrames: LRUMap = new LRUMap(20); + private _rateLimiter: RateLimitIncrement | undefined; public constructor( private readonly _options: Options = {}, @@ -293,12 +361,32 @@ export class LocalVariables implements Integration { return; } + const captureAll = this._options.captureAllExceptions !== false; + this._session.configureAndConnect( (ev, complete) => this._handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), - !!this._options.captureAllExceptions, + captureAll, ); + if (captureAll) { + const max = this._options.maxExceptionsPerSecond || 50; + + this._rateLimiter = createRateLimiter( + max, + () => { + logger.log('Local variables rate-limit lifted.'); + this._session?.setPauseOnExceptions(true); + }, + seconds => { + logger.log( + `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, + ); + this._session?.setPauseOnExceptions(false); + }, + ); + } + addGlobalEventProcessor(async event => this._addLocalVariables(event)); } } @@ -316,6 +404,8 @@ export class LocalVariables implements Integration { return; } + this._rateLimiter?.(); + // data.description contains the original error.stack const exceptionHash = hashFromStack(stackParser, data?.description); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 1cc51ab1fb70..25888780a30c 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core'; +import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; import type { EventProcessor, Integration, Span } from '@sentry/types'; import { dynamicRequire, @@ -12,7 +12,6 @@ import { LRUMap } from 'lru_map'; import type { NodeClient } from '../../client'; import { NODE_VERSION } from '../../nodeVersion'; -import { isSentryRequest } from '../utils/http'; import type { DiagnosticsChannel, RequestCreateMessage, @@ -138,7 +137,7 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequest(stringUrl) || request.__sentry_span__ !== undefined) { + if (isSentryRequestUrl(stringUrl, hub) || request.__sentry_span__ !== undefined) { return; } @@ -198,7 +197,7 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequest(stringUrl)) { + if (isSentryRequestUrl(stringUrl, hub)) { return; } @@ -238,7 +237,7 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequest(stringUrl)) { + if (isSentryRequestUrl(stringUrl, hub)) { return; } diff --git a/packages/node/src/integrations/utils/http.ts b/packages/node/src/integrations/utils/http.ts index d49b01ac8275..fc2e6a1e88bd 100644 --- a/packages/node/src/integrations/utils/http.ts +++ b/packages/node/src/integrations/utils/http.ts @@ -1,19 +1,9 @@ -import { getCurrentHub } from '@sentry/core'; import type * as http from 'http'; import type * as https from 'https'; import { URL } from 'url'; import { NODE_VERSION } from '../../nodeVersion'; -/** - * Checks whether given url points to Sentry server - * @param url url to verify - */ -export function isSentryRequest(url: string): boolean { - const dsn = getCurrentHub().getClient()?.getDsn(); - return dsn ? url.includes(dsn.host) : false; -} - /** * Assembles a URL that's passed to the users to filter on. * It can include raw (potentially PII containing) data, which we'll allow users to access to filter @@ -24,11 +14,7 @@ export function isSentryRequest(url: string): boolean { */ // TODO (v8): This function should include auth, query and fragment (it's breaking, so we need to wait for v8) export function extractRawUrl(requestOptions: RequestOptions): string { - const protocol = requestOptions.protocol || ''; - const hostname = requestOptions.hostname || requestOptions.host || ''; - // Don't log standard :80 (http) and :443 (https) ports to reduce the noise - const port = - !requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 ? '' : `:${requestOptions.port}`; + const { protocol, hostname, port } = parseRequestOptions(requestOptions); const path = requestOptions.path ? requestOptions.path : '/'; return `${protocol}//${hostname}${port}${path}`; } @@ -40,13 +26,10 @@ export function extractRawUrl(requestOptions: RequestOptions): string { * @returns Fully-formed URL */ export function extractUrl(requestOptions: RequestOptions): string { - const protocol = requestOptions.protocol || ''; - const hostname = requestOptions.hostname || requestOptions.host || ''; - // Don't log standard :80 (http) and :443 (https) ports to reduce the noise - const port = - !requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 ? '' : `:${requestOptions.port}`; - // do not include search or hash in span descriptions, per https://develop.sentry.dev/sdk/data-handling/#structuring-data + const { protocol, hostname, port } = parseRequestOptions(requestOptions); + const path = requestOptions.pathname || '/'; + // always filter authority, see https://develop.sentry.dev/sdk/data-handling/#structuring-data const authority = requestOptions.auth ? redactAuthority(requestOptions.auth) : ''; @@ -168,6 +151,21 @@ export function normalizeRequestArgs( requestOptions = urlToOptions(requestArgs[0]); } else { requestOptions = requestArgs[0]; + + try { + const parsed = new URL( + requestOptions.path || '', + `${requestOptions.protocol || 'http:'}//${requestOptions.hostname}`, + ); + requestOptions = { + pathname: parsed.pathname, + search: parsed.search, + hash: parsed.hash, + ...requestOptions, + }; + } catch (e) { + // ignore + } } // if the options were given separately from the URL, fold them in @@ -206,3 +204,20 @@ export function normalizeRequestArgs( return [requestOptions]; } } + +function parseRequestOptions(requestOptions: RequestOptions): { + protocol: string; + hostname: string; + port: string; +} { + const protocol = requestOptions.protocol || ''; + const hostname = requestOptions.hostname || requestOptions.host || ''; + // Don't log standard :80 (http) and :443 (https) ports to reduce the noise + // Also don't add port if the hostname already includes a port + const port = + !requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 || /^(.*):(\d+)$/.test(hostname) + ? '' + : `:${requestOptions.port}`; + + return { protocol, hostname, port }; +} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 20e8160b7985..7bb0257fc463 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -15,6 +15,7 @@ import { tracingContextFromHeaders, } from '@sentry/utils'; +import { isAnrChildProcess } from './anr'; import { setNodeAsyncContextStrategy } from './async'; import { NodeClient } from './client'; import { @@ -110,7 +111,13 @@ export const defaultIntegrations = [ * * @see {@link NodeOptions} for documentation on configuration options. */ +// eslint-disable-next-line complexity export function init(options: NodeOptions = {}): void { + if (isAnrChildProcess()) { + options.autoSessionTracking = false; + options.tracesSampleRate = 0; + } + const carrier = getMainCarrier(); setNodeAsyncContextStrategy(); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index bb162789f0be..02c13b544b01 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -298,6 +298,25 @@ describe('tracing', () => { expect(spans[1].data['http.fragment']).toEqual('learn-more'); }); + it('fills in span data from http.RequestOptions object', () => { + nock('http://dogs.are.great').get('/spaniel?tail=wag&cute=true#learn-more').reply(200); + + const transaction = createTransactionOnScope(); + const spans = (transaction as unknown as Span).spanRecorder?.spans as Span[]; + + http.request({ method: 'GET', host: 'dogs.are.great', path: '/spaniel?tail=wag&cute=true#learn-more' }); + + expect(spans.length).toEqual(2); + + // our span is at index 1 because the transaction itself is at index 0 + expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(spans[1].op).toEqual('http.client'); + expect(spans[1].data['http.method']).toEqual('GET'); + expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); + expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); + expect(spans[1].data['http.fragment']).toEqual('learn-more'); + }); + it.each([ ['user:pwd', '[Filtered]:[Filtered]@'], ['user:', '[Filtered]:@'], diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 15f62f305231..55e179c879e6 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -4,10 +4,12 @@ import type { LRUMap } from 'lru_map'; import { defaultStackParser } from '../../src'; import type { DebugSession, FrameVariables } from '../../src/integrations/localvariables'; -import { createCallbackList, LocalVariables } from '../../src/integrations/localvariables'; +import { createCallbackList, createRateLimiter, LocalVariables } from '../../src/integrations/localvariables'; import { NODE_VERSION } from '../../src/nodeVersion'; import { getDefaultNodeClientOptions } from '../../test/helper/node-client-options'; +jest.setTimeout(20_000); + const describeIf = (condition: boolean) => (condition ? describe : describe.skip); interface ThrowOn { @@ -31,6 +33,8 @@ class MockDebugSession implements DebugSession { this._onPause = onPause; } + public setPauseOnExceptions(_: boolean): void {} + public getLocalVariables(objectId: string, callback: (vars: Record) => void): void { if (this._throwOn?.getLocalVariables) { throw new Error('getLocalVariables should not be called'); @@ -431,4 +435,67 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { next(10); }); }); + + describe('rateLimiter', () => { + it('calls disable if exceeded', done => { + const increment = createRateLimiter( + 5, + () => {}, + () => { + done(); + }, + ); + + for (let i = 0; i < 7; i++) { + increment(); + } + }); + + it('does not call disable if not exceeded', done => { + const increment = createRateLimiter( + 5, + () => { + throw new Error('Should not be called'); + }, + () => { + throw new Error('Should not be called'); + }, + ); + + let count = 0; + + const timer = setInterval(() => { + for (let i = 0; i < 4; i++) { + increment(); + } + + count += 1; + + if (count >= 5) { + clearInterval(timer); + done(); + } + }, 1_000); + }); + + it('re-enables after timeout', done => { + let called = false; + + const increment = createRateLimiter( + 5, + () => { + expect(called).toEqual(true); + done(); + }, + () => { + expect(called).toEqual(false); + called = true; + }, + ); + + for (let i = 0; i < 10; i++) { + increment(); + } + }); + }); }); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 5a0b47358ed2..012ead8b9d3d 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -1,5 +1,6 @@ import type { Context } from '@opentelemetry/api'; -import { SpanKind, trace } from '@opentelemetry/api'; +import { context, SpanKind, trace } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { addGlobalEventProcessor, addTracingExtensions, getCurrentHub, Transaction } from '@sentry/core'; import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; @@ -121,7 +122,10 @@ export class SentrySpanProcessor implements OtelSpanProcessor { updateSpanWithOtelData(sentrySpan, otelSpan); } - sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime)); + // Ensure we do not capture any OTEL spans for finishing (and sending) this + context.with(suppressTracing(context.active()), () => { + sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime)); + }); clearSpan(otelSpanId); } diff --git a/packages/opentelemetry-node/src/utils/isSentryRequest.ts b/packages/opentelemetry-node/src/utils/isSentryRequest.ts index b02e3b4cb588..5b285bb0ec68 100644 --- a/packages/opentelemetry-node/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry-node/src/utils/isSentryRequest.ts @@ -1,6 +1,6 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { getCurrentHub } from '@sentry/core'; +import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; /** * @@ -16,14 +16,5 @@ export function isSentryRequestSpan(otelSpan: OtelSpan): boolean { return false; } - return isSentryRequestUrl(httpUrl.toString()); -} - -/** - * Checks whether given url points to Sentry server - * @param url url to verify - */ -function isSentryRequestUrl(url: string): boolean { - const dsn = getCurrentHub().getClient()?.getDsn(); - return dsn ? url.includes(dsn.host) : false; + return isSentryRequestUrl(httpUrl.toString(), getCurrentHub()); } diff --git a/packages/replay/.eslintignore b/packages/replay/.eslintignore index c76c6c2d64d1..3a347e0e1879 100644 --- a/packages/replay/.eslintignore +++ b/packages/replay/.eslintignore @@ -1,6 +1,3 @@ node_modules/ build/ -demo/build/ -# TODO: Check if we can re-introduce linting in demo -demo -metrics + diff --git a/packages/replay/package.json b/packages/replay/package.json index 43a511742a0d..7c9e6984d9ce 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -37,8 +37,6 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", "test": "jest", "test:watch": "jest --watch", - "bootstrap:demo": "cd demo && yarn", - "start:demo": "yarn build:dev && cd demo && yarn start", "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push" }, "repository": { @@ -54,8 +52,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.72.0", - "@sentry-internal/rrweb": "1.108.0", - "@sentry-internal/rrweb-snapshot": "1.108.0", + "@sentry-internal/rrweb": "2.0.0", + "@sentry-internal/rrweb-snapshot": "2.0.0", "jsdom-worker": "^0.2.1", "tslib": "^2.4.1 || ^1.9.3" }, diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index f12127970fed..13ccea43df22 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -50,3 +50,6 @@ export const MIN_REPLAY_DURATION_LIMIT = 15_000; /** The max. length of a replay. */ export const MAX_REPLAY_DURATION = 3_600_000; // 60 minutes in ms; + +/** Default attributes to be ignored when `maskAllText` is enabled */ +export const DEFAULT_IGNORED_ATTRIBUTES = ['title', 'placeholder']; diff --git a/packages/replay/src/coreHandlers/handleDom.ts b/packages/replay/src/coreHandlers/handleDom.ts index e97e3fe5b8fe..60220f0a5a66 100644 --- a/packages/replay/src/coreHandlers/handleDom.ts +++ b/packages/replay/src/coreHandlers/handleDom.ts @@ -1,4 +1,5 @@ -import type { INode } from '@sentry-internal/rrweb-snapshot'; +import { record } from '@sentry-internal/rrweb'; +import type { serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot'; import { NodeType } from '@sentry-internal/rrweb-snapshot'; import type { Breadcrumb } from '@sentry/types'; import { htmlTreeAsString } from '@sentry/utils'; @@ -49,28 +50,26 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHa }; /** Get the base DOM breadcrumb. */ -export function getBaseDomBreadcrumb(target: Node | INode | null, message: string): Breadcrumb { - // `__sn` property is the serialized node created by rrweb - const serializedNode = target && isRrwebNode(target) && target.__sn.type === NodeType.Element ? target.__sn : null; +export function getBaseDomBreadcrumb(target: Node | null, message: string): Breadcrumb { + const nodeId = record.mirror.getId(target); + const node = nodeId && record.mirror.getNode(nodeId); + const meta = node && record.mirror.getMeta(node); + const element = meta && isElement(meta) ? meta : null; return { message, - data: serializedNode + data: element ? { - nodeId: serializedNode.id, + nodeId, node: { - id: serializedNode.id, - tagName: serializedNode.tagName, - textContent: target - ? Array.from(target.childNodes) - .map( - (node: Node | INode) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent, - ) - .filter(Boolean) // filter out empty values - .map(text => (text as string).trim()) - .join('') - : '', - attributes: getAttributesToRecord(serializedNode.attributes), + id: nodeId, + tagName: element.tagName, + textContent: Array.from(element.childNodes) + .map((node: serializedNodeWithId) => node.type === NodeType.Text && node.textContent) + .filter(Boolean) // filter out empty values + .map(text => (text as string).trim()) + .join(''), + attributes: getAttributesToRecord(element.attributes), }, } : {}, @@ -90,11 +89,11 @@ export function handleDom(handlerData: DomHandlerData): Breadcrumb | null { }); } -function getDomTarget(handlerData: DomHandlerData): { target: Node | INode | null; message: string } { +function getDomTarget(handlerData: DomHandlerData): { target: Node | null; message: string } { const isClick = handlerData.name === 'click'; let message: string | undefined; - let target: Node | INode | null = null; + let target: Node | null = null; // Accessing event.target can throw (see getsentry/raven-js#838, #768) try { @@ -107,6 +106,6 @@ function getDomTarget(handlerData: DomHandlerData): { target: Node | INode | nul return { target, message }; } -function isRrwebNode(node: EventTarget): node is INode { - return '__sn' in node; +function isElement(node: serializedNodeWithId): node is serializedElementNodeWithId { + return node.type === NodeType.Element; } diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 57fb36f66b35..753fd62d5660 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -13,6 +13,7 @@ import { ReplayContainer } from './replay'; import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions, SendBufferedReplayOptions } from './types'; import { getPrivacyOptions } from './util/getPrivacyOptions'; import { isBrowser } from './util/isBrowser'; +import { maskAttribute } from './util/maskAttribute'; const MEDIA_SELECTORS = 'img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]'; @@ -81,6 +82,7 @@ export class Replay implements Integration { networkResponseHeaders = [], mask = [], + maskAttributes = ['title', 'placeholder'], unmask = [], block = [], unblock = [], @@ -104,25 +106,36 @@ export class Replay implements Integration { }: ReplayConfiguration = {}) { this.name = Replay.id; + const privacyOptions = getPrivacyOptions({ + mask, + unmask, + block, + unblock, + ignore, + blockClass, + blockSelector, + maskTextClass, + maskTextSelector, + ignoreClass, + }); + this._recordingOptions = { maskAllInputs, maskAllText, maskInputOptions: { ...(maskInputOptions || {}), password: true }, maskTextFn: maskFn, maskInputFn: maskFn, - - ...getPrivacyOptions({ - mask, - unmask, - block, - unblock, - ignore, - blockClass, - blockSelector, - maskTextClass, - maskTextSelector, - ignoreClass, - }), + maskAttributeFn: (key: string, value: string, el: HTMLElement): string => + maskAttribute({ + maskAttributes, + maskAllText, + privacyOptions, + key, + value, + el, + }), + + ...privacyOptions, // Our defaults slimDOMOptions: 'all', @@ -132,6 +145,16 @@ export class Replay implements Integration { // collect fonts, but be aware that `sentry.io` needs to be an allowed // origin for playback collectFonts: true, + errorHandler: (err: Error) => { + try { + // @ts-expect-error Set this so that replay SDK can ignore errors originating from rrweb + err.__rrweb__ = true; + } catch { + // avoid any potential hazards here + } + // return true to suppress throwing the error inside of rrweb + return true; + }, }; this._initialOptions = { diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 8ad157606c53..4c7156262ea0 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -264,7 +264,12 @@ export interface ReplayIntegrationPrivacyOptions { } // These are optional for ReplayPluginOptions because the plugin sets default values -type OptionalReplayPluginOptions = Partial; +type OptionalReplayPluginOptions = Partial & { + /** + * Mask element attributes that are contained in list + */ + maskAttributes?: string[]; +}; export interface DeprecatedPrivacyOptions { /** diff --git a/packages/replay/src/util/createPerformanceEntries.ts b/packages/replay/src/util/createPerformanceEntries.ts index 13526677c323..9a259200a94e 100644 --- a/packages/replay/src/util/createPerformanceEntries.ts +++ b/packages/replay/src/util/createPerformanceEntries.ts @@ -172,9 +172,7 @@ function createLargestContentfulPaint( data: { value, // LCP "duration" in ms size, - // Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - nodeId: record.mirror.getId(entry.element as any), + nodeId: record.mirror.getId(entry.element), }, }; } diff --git a/packages/replay/src/util/getPrivacyOptions.ts b/packages/replay/src/util/getPrivacyOptions.ts index a2aec1dcdb9d..c37075923d90 100644 --- a/packages/replay/src/util/getPrivacyOptions.ts +++ b/packages/replay/src/util/getPrivacyOptions.ts @@ -5,8 +5,6 @@ type GetPrivacyOptions = Required; + key: string; + value: string; + el: HTMLElement; +} + +/** + * Masks an attribute if necessary, otherwise return attribute value as-is. + */ +export function maskAttribute({ + el, + key, + maskAttributes, + maskAllText, + privacyOptions, + value, +}: MaskAttributeParams): string { + // We only mask attributes if `maskAllText` is true + if (!maskAllText) { + return value; + } + + // unmaskTextSelector takes precendence + if (privacyOptions.unmaskTextSelector && el.matches(privacyOptions.unmaskTextSelector)) { + return value; + } + + if ( + maskAttributes.includes(key) || + // Need to mask `value` attribute for `` if it's a button-like + // type + (key === 'value' && el.tagName === 'INPUT' && ['submit', 'button'].includes(el.getAttribute('type') || '')) + ) { + return value.replace(/[\S]/g, '*'); + } + + return value; +} diff --git a/packages/replay/src/util/shouldFilterRequest.ts b/packages/replay/src/util/shouldFilterRequest.ts index 7d66cf31d780..fcfd75b1b048 100644 --- a/packages/replay/src/util/shouldFilterRequest.ts +++ b/packages/replay/src/util/shouldFilterRequest.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; import type { ReplayContainer } from '../types'; @@ -12,14 +12,5 @@ export function shouldFilterRequest(replay: ReplayContainer, url: string): boole return false; } - return _isSentryRequest(url); -} - -/** - * Checks wether a given URL belongs to the configured Sentry DSN. - */ -function _isSentryRequest(url: string): boolean { - const client = getCurrentHub().getClient(); - const dsn = client && client.getDsn(); - return dsn ? url.includes(dsn.host) : false; + return isSentryRequestUrl(url, getCurrentHub()); } diff --git a/packages/replay/test/integration/coreHandlers/handleScope.test.ts b/packages/replay/test/integration/coreHandlers/handleScope.test.ts index 2ccaafdefff7..f77f165981ef 100644 --- a/packages/replay/test/integration/coreHandlers/handleScope.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleScope.test.ts @@ -18,8 +18,7 @@ describe('Integration | coreHandlers | handleScope', () => { }; }); - await replay.start(); - jest.runAllTimers(); + replay.start(); expect(mockHandleScopeListener).toHaveBeenCalledTimes(1); diff --git a/packages/replay/test/integration/rrweb.test.ts b/packages/replay/test/integration/rrweb.test.ts index cc40061b4a3d..82dd18f2d6ec 100644 --- a/packages/replay/test/integration/rrweb.test.ts +++ b/packages/replay/test/integration/rrweb.test.ts @@ -20,22 +20,22 @@ describe('Integration | rrweb', () => { "blockSelector": ".sentry-block,[data-sentry-block],base[href=\\"/\\"],img,image,svg,video,object,picture,embed,map,audio,link[rel=\\"icon\\"],link[rel=\\"apple-touch-icon\\"]", "collectFonts": true, "emit": [Function], + "errorHandler": [Function], "ignoreSelector": ".sentry-test-ignore,.sentry-ignore,[data-sentry-ignore],input[type=\\"file\\"]", "inlineImages": false, "inlineStylesheet": true, "maskAllInputs": true, "maskAllText": true, + "maskAttributeFn": [Function], "maskInputFn": undefined, "maskInputOptions": Object { "password": true, }, - "maskInputSelector": ".sentry-mask,[data-sentry-mask]", "maskTextFn": undefined, "maskTextSelector": ".sentry-mask,[data-sentry-mask]", "onMutation": [Function], "slimDOMOptions": "all", "unblockSelector": ".sentry-unblock,[data-sentry-unblock]", - "unmaskInputSelector": ".sentry-unmask,[data-sentry-unmask]", "unmaskTextSelector": ".sentry-unmask,[data-sentry-unmask]", } `); diff --git a/packages/replay/test/mocks/mockRrweb.ts b/packages/replay/test/mocks/mockRrweb.ts index c6fbd2be6e55..30c5557298fc 100644 --- a/packages/replay/test/mocks/mockRrweb.ts +++ b/packages/replay/test/mocks/mockRrweb.ts @@ -45,6 +45,8 @@ export function mockRrweb(): { record: RecordMock } { jest.mock('@sentry-internal/rrweb', () => { const ActualRrweb = jest.requireActual('@sentry-internal/rrweb'); + mockRecordFn.mirror = ActualRrweb.record.mirror; + return { ...ActualRrweb, record: mockRecordFn, diff --git a/packages/replay/test/unit/util/getPrivacyOptions.test.ts b/packages/replay/test/unit/util/getPrivacyOptions.test.ts index 27cf839fdd3b..c1e7695487e3 100644 --- a/packages/replay/test/unit/util/getPrivacyOptions.test.ts +++ b/packages/replay/test/unit/util/getPrivacyOptions.test.ts @@ -21,10 +21,8 @@ describe('Unit | util | getPrivacyOptions', () => { Object { "blockSelector": ".custom-block,.sentry-block,[data-sentry-block],base[href=\\"/\\"]", "ignoreSelector": ".custom-ignore,.sentry-ignore,[data-sentry-ignore],input[type=\\"file\\"]", - "maskInputSelector": ".custom-mask,.sentry-mask,[data-sentry-mask]", "maskTextSelector": ".custom-mask,.sentry-mask,[data-sentry-mask]", "unblockSelector": ".custom-unblock,.sentry-unblock,[data-sentry-unblock]", - "unmaskInputSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", "unmaskTextSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", } `); @@ -49,10 +47,8 @@ describe('Unit | util | getPrivacyOptions', () => { Object { "blockSelector": ".custom-block,.deprecated-block-selector,.sentry-block,[data-sentry-block],base[href=\\"/\\"],.deprecated-block-class", "ignoreSelector": ".custom-ignore,.sentry-ignore,[data-sentry-ignore],input[type=\\"file\\"],.deprecated-ignore-class", - "maskInputSelector": ".custom-mask,.deprecated-mask-selector,.sentry-mask,[data-sentry-mask],.deprecated-mask-class", "maskTextSelector": ".custom-mask,.deprecated-mask-selector,.sentry-mask,[data-sentry-mask],.deprecated-mask-class", "unblockSelector": ".custom-unblock,.sentry-unblock,[data-sentry-unblock]", - "unmaskInputSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", "unmaskTextSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", } `); @@ -75,11 +71,9 @@ describe('Unit | util | getPrivacyOptions', () => { "blockClass": /deprecated-block-\\*/, "blockSelector": ".custom-block,.sentry-block,[data-sentry-block],base[href=\\"/\\"]", "ignoreSelector": ".custom-ignore,.sentry-ignore,[data-sentry-ignore],input[type=\\"file\\"]", - "maskInputSelector": ".custom-mask,.sentry-mask,[data-sentry-mask]", "maskTextClass": /deprecated-mask-\\*/, "maskTextSelector": ".custom-mask,.sentry-mask,[data-sentry-mask]", "unblockSelector": ".custom-unblock,.sentry-unblock,[data-sentry-unblock]", - "unmaskInputSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", "unmaskTextSelector": ".custom-unmask,.sentry-unmask,[data-sentry-unmask]", } `); diff --git a/packages/replay/test/unit/util/maskAttribute.test.ts b/packages/replay/test/unit/util/maskAttribute.test.ts new file mode 100644 index 000000000000..73bcbaa91395 --- /dev/null +++ b/packages/replay/test/unit/util/maskAttribute.test.ts @@ -0,0 +1,54 @@ +import { maskAttribute } from '../../../src/util/maskAttribute'; + +describe('maskAttribute', () => { + const defaultEl = document.createElement('div'); + defaultEl.className = 'classy'; + const privacyOptions = { + maskTextSelector: '', + unmaskTextSelector: '.unmask', + blockSelector: '', + unblockSelector: '', + ignoreSelector: '', + }; + const defaultArgs = { + el: defaultEl, + key: 'title', + maskAttributes: ['title'], + maskAllText: true, + privacyOptions, + value: 'foo', + }; + + const inputSubmit = document.createElement('input'); + const inputButton = document.createElement('input'); + [inputSubmit, inputButton].forEach(el => { + el.type = 'submit'; + }); + + test.each([ + ['masks if `maskAllText` is true', defaultArgs, '***'], + [ + 'does not mask if `maskAllText` is false, despite `maskTextSelector` ', + { ...defaultArgs, maskAllText: false, maskTextSelector: 'classy' }, + 'foo', + ], + ['does not mask if `maskAllText` is false', { ...defaultArgs, maskAllText: false }, 'foo'], + [ + 'does not mask if `unmaskTextSelector` matches', + { ...defaultArgs, privacyOptions: { ...privacyOptions, unmaskTextSelector: '.classy' } }, + 'foo', + ], + [ + 'masks `value` attribute on `` with type "submit"', + { ...defaultArgs, el: inputSubmit, value: 'input value' }, + '***** *****', + ], + [ + 'masks `value` attribute on `` with type "button"', + { ...defaultArgs, el: inputButton, value: 'input value' }, + '***** *****', + ], + ])('%s', (_: string, input, output) => { + expect(maskAttribute(input)).toEqual(output); + }); +}); diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 673941d76488..93d835dd72d1 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -71,7 +71,17 @@ export function wrapLoadWithSentry any>(origLoad: T) addNonEnumerableProperty(patchedEvent as unknown as Record, '__sentry_wrapped__', true); - const routeId = event.route.id; + // Accessing any member of `event.route` causes SvelteKit to invalidate the + // client-side universal `load` function's data prefetched data, causing another reload on the actual navigation. + // To work around this, we use `Object.getOwnPropertyDescriptor` which doesn't invoke the proxy. + const routeIdDescriptor = event.route && Object.getOwnPropertyDescriptor(event.route, 'id'); + // First, we try to access the route id from the property descriptor. + // This will only work for @sveltejs/kit >= 1.24.0 + const routeIdFromDescriptor = routeIdDescriptor && (routeIdDescriptor.value as string | undefined); + // If routeIdFromDescriptor is undefined, we fall back to the old behavior of accessing + // `event.route.id` directly. This will still cause invalidations but we get a route name. + const routeId = routeIdFromDescriptor || event.route.id; + return trace( { op: 'function.sveltekit.load', diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index eb0276b7f95d..23528dcf6870 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -296,7 +296,7 @@ describe('handleSentry', () => { } catch (e) { expect(mockCaptureException).toBeCalledTimes(1); expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(1); + expect(mockAddExceptionMechanism).toBeCalledTimes(2); expect(mockAddExceptionMechanism).toBeCalledWith( {}, { handled: false, type: 'sveltekit', data: { function: 'handle' } }, diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 1b7b78066f0c..8aeabaa6cc8d 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -149,7 +149,7 @@ export interface Client { addIntegration?(integration: Integration): void; /** This is an internal function to setup all integrations that should run on the client */ - setupIntegrations(): void; + setupIntegrations(forceInitialize?: boolean): void; /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index e91aefdbab5b..e249564eca51 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -234,14 +234,14 @@ export function createEventEnvelopeHeaders( event: Event, sdkInfo: SdkInfo | undefined, tunnel: string | undefined, - dsn: DsnComponents, + dsn?: DsnComponents, ): EventEnvelopeHeaders { const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; return { event_id: event.event_id as string, sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), - ...(!!tunnel && { dsn: dsnToString(dsn) }), + ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), ...(dynamicSamplingContext && { trace: dropUndefinedKeys({ ...dynamicSamplingContext }), }), diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index c07f9cd7ca16..a45b18ac4e96 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { patchWebAssembly } from './patchWebAssembly'; import { getImage, getImages } from './registry'; @@ -49,26 +49,27 @@ export class Wasm implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { patchWebAssembly(); + } - addGlobalEventProcessor((event: Event) => { - let haveWasm = false; + /** @inheritDoc */ + public processEvent(event: Event): Event { + let haveWasm = false; - if (event.exception && event.exception.values) { - event.exception.values.forEach(exception => { - if (exception?.stacktrace?.frames) { - haveWasm = haveWasm || patchFrames(exception.stacktrace.frames); - } - }); - } + if (event.exception && event.exception.values) { + event.exception.values.forEach(exception => { + if (exception?.stacktrace?.frames) { + haveWasm = haveWasm || patchFrames(exception.stacktrace.frames); + } + }); + } - if (haveWasm) { - event.debug_meta = event.debug_meta || {}; - event.debug_meta.images = [...(event.debug_meta.images || []), ...getImages()]; - } + if (haveWasm) { + event.debug_meta = event.debug_meta || {}; + event.debug_meta.images = [...(event.debug_meta.images || []), ...getImages()]; + } - return event; - }); + return event; } } diff --git a/yarn.lock b/yarn.lock index 25082dd4c5c7..bc849269e256 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4446,22 +4446,38 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrweb-snapshot@1.108.0": - version "1.108.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-1.108.0.tgz#9b09b7e5d6b13d4d7493017ee190b097f9916284" - integrity sha512-ypR/4oBB8s7d5+7JTkdk+VvlMPRRhbuz3xSFMXShCH2LJ6kINGfYBAYr6rr6o2Bko9j5rVHjYDDrVWkTw4CXSg== +"@sentry-internal/rrdom@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.0.0.tgz#326b5f26c76d2077874db7edffd5be3aa72848fb" + integrity sha512-PLSw54GWCmxOmJWJ2NGDfz9b+/76IBpGsWnIjBiW7L3NDVuTo705/7+DmKTrDADO7xXAZZRpbuQjqBjV8Mu+yQ== + dependencies: + "@sentry-internal/rrweb-snapshot" "2.0.0" + +"@sentry-internal/rrweb-snapshot@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.0.0.tgz#6d034f4f65736e990e279842cf1c2868fc9f47dd" + integrity sha512-MFpUw2Kuq4OVQn1dv6l/oSPgbHdy8N0oWBeVeHQlBzxugje4i2KU9tf6K7KH2RAce7Bi9r5UgHvCsNG3PNi/XQ== -"@sentry-internal/rrweb@1.108.0": - version "1.108.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-1.108.0.tgz#4b724c1fff44fb4705723c121ca424c00fabc398" - integrity sha512-IuRuA1k2N23e6oTRnV9866mauoOvesYFZFlQHgOvt7p3pJDfXhDUZj1DKaQZJrbooTUUIh7YrpZ2Vukoq0wCFw== +"@sentry-internal/rrweb-types@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.0.0.tgz#8606e47d98e14580f46f98d5dc5d95bc9ebc8b59" + integrity sha512-3dgoh4sbqgY8XwsKh6ofA8WRtUE+qWLHPDMzipp1XefKfEhr6qTtw0riurnJBrO5lD6dJuewK5BWwjcrFb3Gag== dependencies: - "@sentry-internal/rrweb-snapshot" "1.108.0" + "@sentry-internal/rrweb-snapshot" "2.0.0" + +"@sentry-internal/rrweb@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.0.0.tgz#180e2763b77f83aa24bae964dd2f8c8065ddfc49" + integrity sha512-SOyIGjCi1q9ocMOHAAU6DhO2vecRkLk9/zQ6YbIJsUz1vB1ZoF0L1xDlwuL+fGw3HjZ6Wn8RoZWSSiQRokL7lg== + dependencies: + "@sentry-internal/rrdom" "2.0.0" + "@sentry-internal/rrweb-snapshot" "2.0.0" + "@sentry-internal/rrweb-types" "2.0.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" fflate "^0.4.4" - mitt "^1.1.3" + mitt "^3.0.0" "@sentry/bundler-plugin-core@0.6.1": version "0.6.1" @@ -14519,9 +14535,9 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" @@ -19560,6 +19576,11 @@ mitt@^1.1.3: resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== +mitt@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"