diff --git a/CHANGELOG.md b/CHANGELOG.md index 371111ddbadb..cc9f10fddf1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.60.0 + +### Important Changes + +- **feat(replay): Ensure min/max duration when flushing (#8596)** + +We will not send replays that are <5s long anymore. Additionally, we also added further safeguards to avoid overly long (>1h) replays. +You can optionally configure the min. replay duration (defaults to 5s): + +```js +new Replay({ + minReplayDuration: 10000 // in ms - note that this is capped at 15s max! +}) +``` + +### Other Changes + +- fix(profiling): Align to SDK selected time origin (#8599) +- fix(replay): Ensure multi click has correct timestamps (#8591) +- fix(utils): Truncate aggregate exception values (LinkedErrors) (#8593) + ## 7.59.3 - fix(browser): 0 is a valid index (#8581) diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts index 896ba2d53c0a..09a10464c22e 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/captureException/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('captureException works', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.message).toBe('Test exception'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts index 96c49a49ab4c..64b1db8104ec 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('error handler works with a recursive custom error handler', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values?.length).toBe(1); expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandler/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandler/test.ts index 7c41e6993c20..b553fe295add 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandler/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandler/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('error handler works', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values?.length).toBe(1); expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandlerLater/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandlerLater/test.ts index 760fba879b5c..55a447e2f7f4 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandlerLater/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/errorHandlerLater/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('error handler works for later errors', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values?.length).toBe(1); expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/pageloadTransaction/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/pageloadTransaction/test.ts index d224c77bc9c1..410cf5e72382 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/pageloadTransaction/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/pageloadTransaction/test.ts @@ -1,19 +1,21 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should create a pageload transaction', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const req = waitForTransactionRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForTransactionRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); const timeOrigin = await page.evaluate('window._testBaseTimestamp'); const { start_timestamp: startTimestamp } = eventData; diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts index 722d2923fcff..84f8d7869f60 100644 --- a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts @@ -4,7 +4,7 @@ import path from 'path'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; import { LOADER_CONFIGS } from '../../../../utils/generatePlugin'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; const bundle = process.env.PW_BUNDLE || ''; const isLazy = LOADER_CONFIGS[bundle]?.lazy; @@ -40,13 +40,10 @@ sentryTest('it does not download the SDK if the SDK was loaded in the meanwhile' return fs.existsSync(filePath) ? route.fulfill({ path: filePath }) : route.continue(); }); - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname, skipRouteHandler: true }); + const req = await waitForErrorRequestOnUrl(page, url); - await page.goto(url); - - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); await waitForFunction(() => cdnLoadedCount === 2); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts index 896ba2d53c0a..09a10464c22e 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('captureException works', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.message).toBe('Test exception'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts index e860ec8c1906..b63a8d6db1e4 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('captureException works inside of onLoad', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.message).toBe('Test exception'); }); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/test.ts index 19ae8be8a366..ab278f2a9736 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/test.ts @@ -1,19 +1,21 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should handle custom added BrowserTracing integration', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const req = waitForTransactionRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForTransactionRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); const timeOrigin = await page.evaluate('window._testBaseTimestamp'); const { start_timestamp: startTimestamp } = eventData; diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandler/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandler/test.ts index 116e65e94a3a..e1692a2e08d3 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandler/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandler/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('error handler works', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values?.length).toBe(1); expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandlerLater/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandlerLater/test.ts index 760fba879b5c..55a447e2f7f4 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandlerLater/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/errorHandlerLater/test.ts @@ -1,15 +1,13 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('error handler works for later errors', async ({ getLocalTestUrl, page }) => { - const req = waitForErrorRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForErrorRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); expect(eventData.exception?.values?.length).toBe(1); expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/test.ts index d224c77bc9c1..410cf5e72382 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/pageloadTransaction/test.ts @@ -1,19 +1,21 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should create a pageload transaction', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const req = waitForTransactionRequest(page); - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); + const req = await waitForTransactionRequestOnUrl(page, url); - const eventData = envelopeRequestParser(await req); + const eventData = envelopeRequestParser(req); const timeOrigin = await page.evaluate('window._testBaseTimestamp'); const { start_timestamp: startTimestamp } = eventData; diff --git a/packages/browser-integration-tests/scripts/detectFlakyTests.ts b/packages/browser-integration-tests/scripts/detectFlakyTests.ts index 88435731137e..11eb04e651e4 100644 --- a/packages/browser-integration-tests/scripts/detectFlakyTests.ts +++ b/packages/browser-integration-tests/scripts/detectFlakyTests.ts @@ -3,8 +3,6 @@ import * as path from 'path'; import * as childProcess from 'child_process'; import { promisify } from 'util'; -const exec = promisify(childProcess.exec); - async function run(): Promise { let testPaths: string[] = []; diff --git a/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js b/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js new file mode 100644 index 000000000000..f7eb81412b13 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js @@ -0,0 +1,13 @@ +const wat = new Error(`This is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be`); + +wat.cause = new Error(`This is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be,`); + +Sentry.captureException(wat); diff --git a/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts b/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts new file mode 100644 index 000000000000..b78ae5e12525 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture a linked error with messages', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(2); + expect(eventData.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: `This is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be, +this is a very long message that should be truncated and hopefully will be, +this is a very long me...`, + mechanism: { + type: 'chained', + handled: true, + }, + stacktrace: { + frames: expect.any(Array), + }, + }); + expect(eventData.exception?.values?.[1]).toMatchObject({ + type: 'Error', + value: `This is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be, +this is a very long message that should be truncated and will be, +this is a very long message that should be truncated...`, + mechanism: { + type: 'generic', + handled: true, + }, + stacktrace: { + frames: expect.any(Array), + }, + }); +}); diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/init.js b/packages/browser-integration-tests/suites/replay/bufferMode/init.js index c75a803ae33e..2453efcfbe1d 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/init.js +++ b/packages/browser-integration-tests/suites/replay/bufferMode/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index 16b46e3adc54..be0f9cab95d5 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -5,6 +5,7 @@ window.Sentry = Sentry; window.Replay = new Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/compression/init.js b/packages/browser-integration-tests/suites/replay/compression/init.js index 639cf05628e4..c2dd47ab0c25 100644 --- a/packages/browser-integration-tests/suites/replay/compression/init.js +++ b/packages/browser-integration-tests/suites/replay/compression/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: true, }); diff --git a/packages/browser-integration-tests/suites/replay/customEvents/init.js b/packages/browser-integration-tests/suites/replay/customEvents/init.js index a850366eaebf..f76a1207243b 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, blockAllMedia: false, }); diff --git a/packages/browser-integration-tests/suites/replay/dsc/init.js b/packages/browser-integration-tests/suites/replay/dsc/init.js index 90919aeaeb70..c3c2f62f2c14 100644 --- a/packages/browser-integration-tests/suites/replay/dsc/init.js +++ b/packages/browser-integration-tests/suites/replay/dsc/init.js @@ -5,6 +5,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js b/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js index bd8259208409..2c9cd8b23147 100644 --- a/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js index dca771f16c87..acf3b91c0dd3 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 4f8dcac1ea28..49d938b15060 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index 019777c27156..29486082ff8a 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/init.js b/packages/browser-integration-tests/suites/replay/errors/init.js index cd21267f1cc7..89c185dacc7f 100644 --- a/packages/browser-integration-tests/suites/replay/errors/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js index 2323cd2dda7f..21c548a5e349 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo', 'http://sentry-test.io/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js index a60fcdcfc530..4c3f0a7969c6 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkRequestHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js index 15be2bb2764d..3aa81d299ae2 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js index 241dcc7adc29..ff1e66e53411 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkResponseHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js index 6f80c5e4cb8f..52c219e99dc9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js index 2323cd2dda7f..21c548a5e349 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo', 'http://sentry-test.io/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js index a60fcdcfc530..4c3f0a7969c6 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkRequestHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js index 15be2bb2764d..3aa81d299ae2 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js index 241dcc7adc29..ff1e66e53411 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkResponseHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js index 6f80c5e4cb8f..52c219e99dc9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/fileInput/init.js b/packages/browser-integration-tests/suites/replay/fileInput/init.js index 4081a8b9182d..0e08fdfaa6d0 100644 --- a/packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: false, }); diff --git a/packages/browser-integration-tests/suites/replay/flushing/init.js b/packages/browser-integration-tests/suites/replay/flushing/init.js index db6a0aa21821..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/packages/browser-integration-tests/suites/replay/flushing/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/init.js b/packages/browser-integration-tests/suites/replay/init.js index 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/init.js +++ b/packages/browser-integration-tests/suites/replay/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index 1b5f4f447543..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index f64b8fff4e50..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index 3aa2548f1522..35c6feed4df7 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, mutationLimit: 250, }); diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js b/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js new file mode 100644 index 000000000000..429559c5781a --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 2000, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + debug: true, + + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html b/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html new file mode 100644 index 000000000000..7223a20f82ba --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts b/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts new file mode 100644 index 000000000000..1bdb0567de77 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts @@ -0,0 +1,63 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +const MIN_DURATION = 2000; + +sentryTest('doest not send replay before min. duration', async ({ getLocalTestPath, page }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + let counter = 0; + const reqPromise0 = waitForReplayRequest(page, () => { + counter++; + return true; + }); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + // This triggers a page blur, which should trigger a flush + // However, as we are only here too short, this should not actually _send_ anything + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, +}); +document.dispatchEvent(new Event('visibilitychange'));`); + expect(counter).toBe(0); + + // Now wait for 2s until min duration is reached, and try again + await new Promise(resolve => setTimeout(resolve, MIN_DURATION + 100)); + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange'));`); + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange'));`); + + const replayEvent0 = getReplayEvent(await reqPromise0); + expect(replayEvent0).toEqual(getExpectedReplayEvent({})); + expect(counter).toBe(1); +}); diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/init.js b/packages/browser-integration-tests/suites/replay/multiple-pages/init.js index 0242b48100b8..a856a0d13c3e 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js index 6c37e3d85e44..f5360c53561b 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, blockAllMedia: false, block: ['link[rel="icon"]', 'video', '.nested-hide'], diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js index 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/init.js b/packages/browser-integration-tests/suites/replay/privacyInput/init.js index 4081a8b9182d..0e08fdfaa6d0 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: false, }); diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index cff4c6dab7bd..1657e879ef87 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: true, }); diff --git a/packages/browser-integration-tests/suites/replay/replayShim/init.js b/packages/browser-integration-tests/suites/replay/replayShim/init.js index b53ada8b5e7c..a89c744e7480 100644 --- a/packages/browser-integration-tests/suites/replay/replayShim/init.js +++ b/packages/browser-integration-tests/suites/replay/replayShim/init.js @@ -6,6 +6,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/requests/init.js b/packages/browser-integration-tests/suites/replay/requests/init.js index db6a0aa21821..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/requests/init.js +++ b/packages/browser-integration-tests/suites/replay/requests/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/sampling/init.js b/packages/browser-integration-tests/suites/replay/sampling/init.js index 9e99c3536d05..8a98bb9c45de 100644 --- a/packages/browser-integration-tests/suites/replay/sampling/init.js +++ b/packages/browser-integration-tests/suites/replay/sampling/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index 46af904118a6..a3b9726f3103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js index 4c641d160d79..781e7b583109 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 0c16dc6ca3a1..de8b260647ad 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index 28bb6ed8778e..aa5be4406824 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, slowClickTimeout: 0, }); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/init.js b/packages/browser-integration-tests/suites/replay/slowClick/init.js index 1699d299530e..030b2722d236 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, slowClickTimeout: 3100, slowClickIgnoreSelectors: ['.ignore-class', '[ignore-attribute]'], }); 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 86582bf98153..370db54777f3 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts @@ -62,6 +62,43 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc timestamp: expect.any(Number), }, ]); + + // 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'); + }); + + await page.click('#mutationButtonImmediately', { clickCount: 3 }); + + const { breadcrumbs: breadcrumbb2 } = getCustomRecordingEvents(await reqPromise2); + + const slowClickBreadcrumbs2 = breadcrumbb2.filter(breadcrumb => breadcrumb.category === 'ui.multiClick'); + + expect(slowClickBreadcrumbs2).toEqual([ + { + category: 'ui.multiClick', + type: 'default', + data: { + clickCount: 3, + metric: true, + node: { + attributes: { + id: 'mutationButtonImmediately', + }, + id: expect.any(Number), + tagName: 'button', + textContent: '******* ******** ***********', + }, + nodeId: expect.any(Number), + url: 'http://sentry-test.io/index.html', + }, + message: 'body > button#mutationButtonImmediately', + timestamp: expect.any(Number), + }, + ]); }); sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, forceFlushReplay, browserName }) => { diff --git a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js index 11207b23752d..3146e64131fd 100644 --- a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js +++ b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 5000, flushMaxDelay: 5000, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js b/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js index 86f8f5932ddb..2fe6781ee15e 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: true, maskAllText: false, }); diff --git a/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js b/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js index 98581679c8b6..956586937a19 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -2,8 +2,9 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllText: false, }); diff --git a/packages/browser-integration-tests/utils/helpers.ts b/packages/browser-integration-tests/utils/helpers.ts index 525877e9763f..25ea6cf07e80 100644 --- a/packages/browser-integration-tests/utils/helpers.ts +++ b/packages/browser-integration-tests/utils/helpers.ts @@ -108,6 +108,16 @@ async function getSentryEvents(page: Page, url?: string): Promise> return eventsHandle.jsonValue(); } +export async function waitForErrorRequestOnUrl(page: Page, url: string): Promise { + const [req] = await Promise.all([waitForErrorRequest(page), page.goto(url)]); + return req; +} + +export async function waitForTransactionRequestOnUrl(page: Page, url: string): Promise { + const [req] = await Promise.all([waitForTransactionRequest(page), page.goto(url)]); + return req; +} + export function waitForErrorRequest(page: Page): Promise { return page.waitForRequest(req => { const postData = req.postData(); diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 08581fecb9b3..709ba8228107 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -54,9 +54,11 @@ export class LinkedErrors implements Integration { return event; } + const options = client.getOptions(); applyAggregateErrorsToEvent( exceptionFromError, - client.getOptions().stackParser, + options.stackParser, + options.maxValueLength, self._key, self._limit, event, diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 4a9a9af1d658..e720a2152f9f 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -3,7 +3,7 @@ import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core'; import type { DebugImage, Envelope, Event, StackFrame, StackParser } from '@sentry/types'; import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling'; -import { forEachEnvelopeItem, GLOBAL_OBJ, logger, uuid4 } from '@sentry/utils'; +import { browserPerformanceTimeOrigin, forEachEnvelopeItem, GLOBAL_OBJ, logger, uuid4 } from '@sentry/utils'; import { WINDOW } from '../helpers'; import type { JSSelfProfile, JSSelfProfileStack } from './jsSelfProfiling'; @@ -213,6 +213,13 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi // We assert samples.length > 0 above and timestamp should always be present const start = input.samples[0].timestamp; + // The JS SDK might change it's time origin based on some heuristic (see See packages/utils/src/time.ts) + // when that happens, we need to ensure we are correcting the profile timings so the two timelines stay in sync. + // Since JS self profiling time origin is always initialized to performance.timeOrigin, we need to adjust for + // the drift between the SDK selected value and our profile time origin. + const origin = + typeof performance.timeOrigin === 'number' ? performance.timeOrigin : browserPerformanceTimeOrigin || 0; + const adjustForOriginChange = origin - (browserPerformanceTimeOrigin || origin); for (let i = 0; i < input.samples.length; i++) { const jsSample = input.samples[i]; @@ -227,7 +234,7 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi profile['samples'][i] = { // convert ms timestamp to ns - elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), + elapsed_since_start_ns: ((jsSample.timestamp + adjustForOriginChange - start) * MS_TO_NS).toFixed(0), stack_id: EMPTY_STACK_ID, thread_id: THREAD_ID_STRING, }; @@ -260,7 +267,7 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi const sample: Profile['profile']['samples'][0] = { // convert ms timestamp to ns - elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0), + elapsed_since_start_ns: ((jsSample.timestamp + adjustForOriginChange - start) * MS_TO_NS).toFixed(0), stack_id: STACK_ID, thread_id: THREAD_ID_STRING, }; diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index 316d03997483..69e4dc7a062a 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -79,7 +79,8 @@ fields: **An important thing to note:** In the context of the `buildCommand` the fake test registry is available at `http://localhost:4873`. It hosts all of our packages as if they were to be published with the state of the current branch. This means we can install the packages from this registry via the `.npmrc` configuration as seen above. If you -add Sentry dependencies to your test application, you should set the dependency versions set to `*`: +add Sentry dependencies to your test application, you should set the dependency versions set to `latest || *` in order +for it to work with both regular and prerelease versions: ```jsonc // package.json @@ -91,7 +92,7 @@ add Sentry dependencies to your test application, you should set the dependency "test": "echo \"Hello world!\"" }, "dependencies": { - "@sentry/node": "*" + "@sentry/node": "latest || *" } } ``` diff --git a/packages/e2e-tests/test-applications/create-next-app/package.json b/packages/e2e-tests/test-applications/create-next-app/package.json index 3232c1eca7fe..2935a28b9dd3 100644 --- a/packages/e2e-tests/test-applications/create-next-app/package.json +++ b/packages/e2e-tests/test-applications/create-next-app/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@next/font": "13.0.7", - "@sentry/nextjs": "*", + "@sentry/nextjs": "latest || *", "@types/node": "18.11.17", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", diff --git a/packages/e2e-tests/test-applications/create-react-app/package.json b/packages/e2e-tests/test-applications/create-react-app/package.json index 062eef12aa45..2ba893bc70ed 100644 --- a/packages/e2e-tests/test-applications/create-react-app/package.json +++ b/packages/e2e-tests/test-applications/create-react-app/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@sentry/react": "*", - "@sentry/tracing": "*", + "@sentry/react": "latest || *", + "@sentry/tracing": "latest || *", "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "13.0.0", "@testing-library/user-event": "13.2.1", diff --git a/packages/e2e-tests/test-applications/create-remix-app/package.json b/packages/e2e-tests/test-applications/create-remix-app/package.json index 080bc7de0446..ce7b8cd8456c 100644 --- a/packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/packages/e2e-tests/test-applications/create-remix-app/package.json @@ -8,7 +8,7 @@ "typecheck": "tsc" }, "dependencies": { - "@sentry/remix": "*", + "@sentry/remix": "latest || *", "@remix-run/css-bundle": "^1.16.1", "@remix-run/node": "^1.16.1", "@remix-run/react": "^1.16.1", diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 45f79250d05f..58e4ac82b793 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@next/font": "13.0.7", - "@sentry/nextjs": "*", + "@sentry/nextjs": "latest || *", "@types/node": "18.11.17", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json b/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json index 7dbbde86ce78..134eecd517e1 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json @@ -16,7 +16,7 @@ "canaryVersions": [ { "dependencyOverrides": { - "next": "latest" + "next": "latest || *" } }, { diff --git a/packages/e2e-tests/test-applications/node-express-app/package.json b/packages/e2e-tests/test-applications/node-express-app/package.json index 2bdb01bb3daf..ffd74762f286 100644 --- a/packages/e2e-tests/test-applications/node-express-app/package.json +++ b/packages/e2e-tests/test-applications/node-express-app/package.json @@ -8,10 +8,10 @@ "test": "playwright test" }, "dependencies": { - "@sentry/integrations": "*", - "@sentry/node": "*", - "@sentry/tracing": "*", - "@sentry/types": "*", + "@sentry/integrations": "latest || *", + "@sentry/node": "latest || *", + "@sentry/tracing": "latest || *", + "@sentry/types": "latest || *", "express": "4.18.2", "@types/express": "4.17.17", "@types/node": "18.15.1", diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/package.json b/packages/e2e-tests/test-applications/react-create-hash-router/package.json index bac46c9562d0..fbd4f9017c50 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/package.json +++ b/packages/e2e-tests/test-applications/react-create-hash-router/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@sentry/react": "*", + "@sentry/react": "latest || *", "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "13.0.0", "@testing-library/user-event": "13.2.1", diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/test-recipe.json b/packages/e2e-tests/test-applications/react-create-hash-router/test-recipe.json index 7955a96ea1d0..2fa5a9b823aa 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/test-recipe.json +++ b/packages/e2e-tests/test-applications/react-create-hash-router/test-recipe.json @@ -11,8 +11,8 @@ "canaryVersions": [ { "dependencyOverrides": { - "react": "latest", - "react-dom": "latest" + "react": "latest || *", + "react-dom": "latest || *" } } ] diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json index abfb3bfea914..f024d9426fb3 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@sentry/react": "*", - "@sentry/tracing": "*", + "@sentry/react": "latest || *", + "@sentry/tracing": "latest || *", "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "13.0.0", "@testing-library/user-event": "13.2.1", diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/package.json b/packages/e2e-tests/test-applications/standard-frontend-react/package.json index 8ea59b80e55a..875fa3157b5a 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@sentry/react": "*", + "@sentry/react": "latest || *", "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "13.0.0", "@testing-library/user-event": "13.2.1", diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json b/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json index 2f2e72c8f501..c0ac9c697a1d 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json @@ -18,8 +18,8 @@ "canaryVersions": [ { "dependencyOverrides": { - "react": "latest", - "react-dom": "latest" + "react": "latest || *", + "react-dom": "latest || *" } } ] diff --git a/packages/e2e-tests/test-applications/sveltekit/package.json b/packages/e2e-tests/test-applications/sveltekit/package.json index 5c4139365599..37fa0663edb7 100644 --- a/packages/e2e-tests/test-applications/sveltekit/package.json +++ b/packages/e2e-tests/test-applications/sveltekit/package.json @@ -12,7 +12,7 @@ "test:dev": "TEST_ENV=development playwright test" }, "dependencies": { - "@sentry/sveltekit": "*" + "@sentry/sveltekit": "latest || *" }, "devDependencies": { "@playwright/test": "^1.27.1", @@ -29,8 +29,8 @@ }, "pnpm": { "overrides": { - "@sentry/node": "*", - "@sentry/tracing": "*" + "@sentry/node": "latest || *", + "@sentry/tracing": "latest || *" } }, "type": "module" diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index d6a130ce5d54..610a376f640a 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -50,9 +50,12 @@ export class LinkedErrors implements Integration { return event; } + const options = client.getOptions(); + applyAggregateErrorsToEvent( exceptionFromError, - client.getOptions().stackParser, + options.stackParser, + options.maxValueLength, self._key, self._limit, event, diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index 86dc228c50bf..d8d5e792a619 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -45,3 +45,8 @@ export const SLOW_CLICK_SCROLL_TIMEOUT = 300; /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */ export const REPLAY_MAX_EVENT_BUFFER_SIZE = 20_000_000; // ~20MB + +/** Replays must be min. 5s long before we send them. */ +export const MIN_REPLAY_DURATION = 4_999; +/* The max. allowed value that the minReplayDuration can be set to. */ +export const MIN_REPLAY_DURATION_LIMIT = 15_000; diff --git a/packages/replay/src/coreHandlers/handleClick.ts b/packages/replay/src/coreHandlers/handleClick.ts index 5f7e46dbcbce..ddb7de8d237c 100644 --- a/packages/replay/src/coreHandlers/handleClick.ts +++ b/packages/replay/src/coreHandlers/handleClick.ts @@ -2,6 +2,7 @@ import type { Breadcrumb } from '@sentry/types'; import { WINDOW } from '../constants'; import type { MultiClickFrame, ReplayClickDetector, ReplayContainer, SlowClickConfig, SlowClickFrame } from '../types'; +import { timestampToS } from '../util/timestamp'; import { addBreadcrumbEvent } from './util/addBreadcrumbEvent'; import { getClickTargetNode } from './util/domUtils'; import { onWindowOpen } from './util/onWindowOpen'; @@ -125,7 +126,7 @@ export class ClickDetector implements ReplayClickDetector { } const newClick: Click = { - timestamp: breadcrumb.timestamp, + timestamp: timestampToS(breadcrumb.timestamp), clickBreadcrumb: breadcrumb, // Set this to 0 so we know it originates from the click breadcrumb clickCount: 0, @@ -165,6 +166,7 @@ export class ClickDetector implements ReplayClickDetector { click.scrollAfter = click.timestamp <= this._lastScroll ? this._lastScroll - click.timestamp : undefined; } + // All of these are in seconds! if (click.timestamp + this._timeout <= now) { timedOutClicks.push(click); } @@ -172,10 +174,10 @@ export class ClickDetector implements ReplayClickDetector { // Remove "old" clicks for (const click of timedOutClicks) { - this._generateBreadcrumbs(click); - const pos = this._clicks.indexOf(click); - if (pos !== -1) { + + if (pos > -1) { + this._generateBreadcrumbs(click); this._clicks.splice(pos, 1); } } diff --git a/packages/replay/src/eventBuffer/EventBufferArray.ts b/packages/replay/src/eventBuffer/EventBufferArray.ts index a4a823269ece..c2fb7aa4c7f5 100644 --- a/packages/replay/src/eventBuffer/EventBufferArray.ts +++ b/packages/replay/src/eventBuffer/EventBufferArray.ts @@ -1,6 +1,6 @@ import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants'; import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; -import { timestampToMs } from '../util/timestampToMs'; +import { timestampToMs } from '../util/timestamp'; import { EventBufferSizeExceededError } from './error'; /** diff --git a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts index 695114ebec77..42b26f58a927 100644 --- a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts +++ b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts @@ -2,7 +2,7 @@ import type { ReplayRecordingData } from '@sentry/types'; import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants'; import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; -import { timestampToMs } from '../util/timestampToMs'; +import { timestampToMs } from '../util/timestamp'; import { EventBufferSizeExceededError } from './error'; import { WorkerHandler } from './WorkerHandler'; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 2e795829894b..9afe7c9716a8 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -2,7 +2,12 @@ import { getCurrentHub } from '@sentry/core'; import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; -import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY } from './constants'; +import { + DEFAULT_FLUSH_MAX_DELAY, + DEFAULT_FLUSH_MIN_DELAY, + MIN_REPLAY_DURATION, + MIN_REPLAY_DURATION_LIMIT, +} from './constants'; import { ReplayContainer } from './replay'; import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions, SendBufferedReplayOptions } from './types'; import { getPrivacyOptions } from './util/getPrivacyOptions'; @@ -51,6 +56,7 @@ export class Replay implements Integration { public constructor({ flushMinDelay = DEFAULT_FLUSH_MIN_DELAY, flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, + minReplayDuration = MIN_REPLAY_DURATION, stickySession = true, useCompression = true, _experiments = {}, @@ -127,6 +133,7 @@ export class Replay implements Integration { this._initialOptions = { flushMinDelay, flushMaxDelay, + minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_LIMIT), stickySession, sessionSampleRate, errorSampleRate, diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index c72306239533..9ca3077e914f 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -981,6 +981,9 @@ export class ReplayContainer implements ReplayContainerInterface { const earliestEvent = eventBuffer.getEarliestTimestamp(); if (earliestEvent && earliestEvent < this._context.initialTimestamp) { + // eslint-disable-next-line no-console + const log = this.getOptions()._experiments.traceInternals ? console.info : logger.info; + __DEBUG_BUILD__ && log(`[Replay] Updating initial timestamp to ${earliestEvent}`); this._context.initialTimestamp = earliestEvent; } } @@ -1100,6 +1103,23 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + const start = this._context.initialTimestamp; + const now = Date.now(); + const duration = now - start; + + // If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it + // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar + if (duration < this._options.minReplayDuration || duration > this.timeouts.maxSessionLife + 5_000) { + // eslint-disable-next-line no-console + const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.warn; + __DEBUG_BUILD__ && + log( + `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too short or too long, not sending replay.`, + ); + + return; + } + // A flush is about to happen, cancel any queued flushes this._debouncedFlush.cancel(); diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 00cda5e3c6e7..46f1e8f4ef93 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -180,6 +180,13 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { */ slowClickIgnoreSelectors: string[]; + /** + * The min. duration (in ms) a replay has to have before it is sent to Sentry. + * Whenever attempting to flush a session that is shorter than this, it will not actually send it to Sentry. + * Note that this is capped at max. 15s. + */ + minReplayDuration: number; + /** * Callback before adding a custom recording event * diff --git a/packages/replay/src/util/addEvent.ts b/packages/replay/src/util/addEvent.ts index fdc755ada91c..982ac3b5374d 100644 --- a/packages/replay/src/util/addEvent.ts +++ b/packages/replay/src/util/addEvent.ts @@ -4,7 +4,7 @@ import { logger } from '@sentry/utils'; import { EventBufferSizeExceededError } from '../eventBuffer/error'; import type { AddEventResult, RecordingEvent, ReplayContainer, ReplayFrameEvent, ReplayPluginOptions } from '../types'; -import { timestampToMs } from './timestampToMs'; +import { timestampToMs } from './timestamp'; function isCustomEvent(event: RecordingEvent): event is ReplayFrameEvent { return event.type === EventType.Custom; diff --git a/packages/replay/src/util/timestampToMs.ts b/packages/replay/src/util/timestamp.ts similarity index 50% rename from packages/replay/src/util/timestampToMs.ts rename to packages/replay/src/util/timestamp.ts index 7b7469b84c5f..2251d240ed29 100644 --- a/packages/replay/src/util/timestampToMs.ts +++ b/packages/replay/src/util/timestamp.ts @@ -5,3 +5,11 @@ export function timestampToMs(timestamp: number): number { const isMs = timestamp > 9999999999; return isMs ? timestamp : timestamp * 1000; } + +/** + * Converts a timestamp to s, if it was in ms, or keeps it as s. + */ +export function timestampToS(timestamp: number): number { + const isMs = timestamp > 9999999999; + return isMs ? timestamp / 1000 : timestamp; +} diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index fe3049f9704f..f92f66078c97 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -450,6 +450,9 @@ describe('Integration | errorSampleRate', () => { jest.runAllTimers(); await new Promise(process.nextTick); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); + // still no new replay sent expect(replay).not.toHaveLastSentReplay(); @@ -694,6 +697,9 @@ describe('Integration | errorSampleRate', () => { jest.advanceTimersByTime(2 * MAX_SESSION_LIFE); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); + captureException(new Error('testing')); // Flush due to exception diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index cf142ae8c45c..ee737fd53b54 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -82,6 +82,10 @@ describe('Integration | flush', () => { mockRunFlush.mockClear(); mockAddMemoryEntry.mockClear(); + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + if (replay.eventBuffer) { jest.spyOn(replay.eventBuffer, 'finish'); } @@ -93,9 +97,6 @@ describe('Integration | flush', () => { jest.runAllTimers(); await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); - sessionStorage.clear(); - clearSession(replay); - replay['_loadAndCheckSession'](); mockRecord.takeFullSnapshot.mockClear(); Object.defineProperty(WINDOW, 'location', { value: prevLocation, @@ -258,4 +259,50 @@ describe('Integration | flush', () => { await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); expect(mockFlush).toHaveBeenCalledTimes(1); }); + + it('does not flush if session is too short', async () => { + replay.getOptions().minReplayDuration = 100_000; + + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + + // click happens first + domHandler({ + name: 'click', + }); + + // checkout + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockSendReplay).toHaveBeenCalledTimes(0); + }); + + it('does not flush if session is too long', async () => { + replay.timeouts.maxSessionLife = 100_000; + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + + await advanceTimers(120_000); + + // click happens first + domHandler({ + name: 'click', + }); + + // checkout + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockSendReplay).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/replay/test/mocks/mockSdk.ts b/packages/replay/test/mocks/mockSdk.ts index 4a27b7286d22..f7e268bb49ba 100644 --- a/packages/replay/test/mocks/mockSdk.ts +++ b/packages/replay/test/mocks/mockSdk.ts @@ -76,6 +76,7 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } const replayIntegration = new TestReplayIntegration({ stickySession: false, + minReplayDuration: 0, ...replayOptions, }); diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index e2a49052a799..02a965b7d9c2 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -6,6 +6,7 @@ import type { RecordingOptions, ReplayPluginOptions } from '../../src/types'; const DEFAULT_OPTIONS = { flushMinDelay: 100, flushMaxDelay: 100, + minReplayDuration: 0, stickySession: false, sessionSampleRate: 0, errorSampleRate: 1, diff --git a/packages/utils/src/aggregate-errors.ts b/packages/utils/src/aggregate-errors.ts index 1dcf9b1628ef..4547203b4fd2 100644 --- a/packages/utils/src/aggregate-errors.ts +++ b/packages/utils/src/aggregate-errors.ts @@ -1,6 +1,7 @@ import type { Event, EventHint, Exception, ExtendedError, StackParser } from '@sentry/types'; import { isInstanceOf } from './is'; +import { truncate } from './string'; /** * Creates exceptions inside `event.exception.values` for errors that are nested on properties based on the `key` parameter. @@ -8,6 +9,7 @@ import { isInstanceOf } from './is'; export function applyAggregateErrorsToEvent( exceptionFromErrorImplementation: (stackParser: StackParser, ex: Error) => Exception, parser: StackParser, + maxValueLimit: number = 250, key: string, limit: number, event: Event, @@ -23,15 +25,18 @@ export function applyAggregateErrorsToEvent( // We only create exception grouping if there is an exception in the event. if (originalException) { - event.exception.values = aggregateExceptionsFromError( - exceptionFromErrorImplementation, - parser, - limit, - hint.originalException as ExtendedError, - key, - event.exception.values, - originalException, - 0, + event.exception.values = truncateAggregateExceptions( + aggregateExceptionsFromError( + exceptionFromErrorImplementation, + parser, + limit, + hint.originalException as ExtendedError, + key, + event.exception.values, + originalException, + 0, + ), + maxValueLimit, ); } } @@ -123,3 +128,17 @@ function applyExceptionGroupFieldsForChildException( parent_id: parentId, }; } + +/** + * Truncate the message (exception.value) of all exceptions in the event. + * Because this event processor is ran after `applyClientOptions`, + * we need to truncate the message of the added exceptions here. + */ +function truncateAggregateExceptions(exceptions: Exception[], maxValueLength: number): Exception[] { + return exceptions.map(exception => { + if (exception.value) { + exception.value = truncate(exception.value, maxValueLength); + } + return exception; + }); +} diff --git a/packages/utils/test/aggregate-errors.test.ts b/packages/utils/test/aggregate-errors.test.ts index 9a42bba12858..66b0f3fcfdb1 100644 --- a/packages/utils/test/aggregate-errors.test.ts +++ b/packages/utils/test/aggregate-errors.test.ts @@ -11,7 +11,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain an exception', () => { const event: Event = { exception: undefined }; const eventHint: EventHint = { originalException: new Error() }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: undefined }); @@ -20,7 +20,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain exception values', () => { const event: Event = { exception: { values: undefined } }; const eventHint: EventHint = { originalException: new Error() }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: undefined } }); @@ -28,7 +28,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if event does not contain an event hint', () => { const event: Event = { exception: { values: [] } }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, undefined); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, undefined); // no changes expect(event).toStrictEqual({ exception: { values: [] } }); @@ -37,7 +37,7 @@ describe('applyAggregateErrorsToEvent()', () => { test('should not do anything if the event hint does not contain an original exception', () => { const event: Event = { exception: { values: [] } }; const eventHint: EventHint = { originalException: undefined }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: [] } }); @@ -52,7 +52,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -97,7 +97,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); // no changes expect(event).toStrictEqual({ exception: { values: [exceptionFromError(stackParser, originalException)] } }); @@ -116,7 +116,7 @@ describe('applyAggregateErrorsToEvent()', () => { } const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 5, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 5, event, eventHint); // 6 -> one for original exception + 5 linked expect(event.exception?.values).toHaveLength(5 + 1); @@ -140,7 +140,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError)] } }; const eventHint: EventHint = { originalException: fakeAggregateError }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); expect(event.exception?.values?.[event.exception.values.length - 1].mechanism?.type).toBe('instrument'); }); @@ -155,7 +155,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, fakeAggregateError1)] } }; const eventHint: EventHint = { originalException: fakeAggregateError1 }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, 'cause', 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -234,7 +234,7 @@ describe('applyAggregateErrorsToEvent()', () => { const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; const eventHint: EventHint = { originalException }; - applyAggregateErrorsToEvent(exceptionFromError, stackParser, key, 100, event, eventHint); + applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, key, 100, event, eventHint); expect(event).toStrictEqual({ exception: { values: [ @@ -272,4 +272,52 @@ describe('applyAggregateErrorsToEvent()', () => { }, }); }); + + test('should truncate the exception values if they exceed the `maxValueLength` option', () => { + const originalException: ExtendedError = new Error('Root Error with long message'); + originalException.cause = new Error('Nested Error 1 with longer message'); + originalException.cause.cause = new Error('Nested Error 2 with longer message with longer message'); + + const event: Event = { exception: { values: [exceptionFromError(stackParser, originalException)] } }; + const eventHint: EventHint = { originalException }; + + const maxValueLength = 15; + applyAggregateErrorsToEvent(exceptionFromError, stackParser, maxValueLength, 'cause', 10, event, eventHint); + expect(event).toStrictEqual({ + exception: { + values: [ + { + value: 'Nested Error 2 ...', + mechanism: { + exception_id: 2, + handled: true, + parent_id: 1, + source: 'cause', + type: 'chained', + }, + }, + { + value: 'Nested Error 1 ...', + mechanism: { + exception_id: 1, + handled: true, + parent_id: 0, + is_exception_group: true, + source: 'cause', + type: 'chained', + }, + }, + { + value: 'Root Error with...', + mechanism: { + exception_id: 0, + handled: true, + is_exception_group: true, + type: 'instrument', + }, + }, + ], + }, + }); + }); });