From 768b02548be7cccbe529aa5e4678cebdc7000f9c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Jul 2023 14:52:46 +0200 Subject: [PATCH 1/8] test(replay): Reduce flush delay for shorter tests (#8595) --- .../suites/replay/bufferMode/init.js | 4 ++-- .../suites/replay/compression/init.js | 4 ++-- .../suites/replay/customEvents/init.js | 4 ++-- .../suites/replay/errors/droppedError/init.js | 4 ++-- .../suites/replay/errors/errorModeCustomTransport/init.js | 4 ++-- .../suites/replay/errors/errorNotSent/init.js | 4 ++-- .../suites/replay/errors/errorsInSession/init.js | 4 ++-- .../browser-integration-tests/suites/replay/errors/init.js | 4 ++-- .../browser-integration-tests/suites/replay/flushing/init.js | 4 ++-- .../suites/replay/keyboardEvents/init.js | 4 ++-- .../suites/replay/largeMutations/defaultOptions/init.js | 4 ++-- .../suites/replay/largeMutations/mutationLimit/init.js | 4 ++-- .../suites/replay/multiple-pages/init.js | 4 ++-- .../browser-integration-tests/suites/replay/requests/init.js | 4 ++-- .../suites/replay/sessionExpiry/init.js | 4 ++-- .../suites/replay/sessionInactive/init.js | 4 ++-- .../suites/replay/sessionMaxAge/init.js | 4 ++-- .../suites/replay/slowClick/disable/init.js | 4 ++-- .../browser-integration-tests/suites/replay/slowClick/init.js | 4 ++-- .../suites/replay/unicode/compressed/init.js | 4 ++-- .../suites/replay/unicode/uncompressed/init.js | 4 ++-- 21 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/init.js b/packages/browser-integration-tests/suites/replay/bufferMode/init.js index c75a803ae33e..5691b52d96a3 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/init.js +++ b/packages/browser-integration-tests/suites/replay/bufferMode/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..0c068170f01e 100644 --- a/packages/browser-integration-tests/suites/replay/compression/init.js +++ b/packages/browser-integration-tests/suites/replay/compression/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, 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..5ddfef307e9c 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, useCompression: false, blockAllMedia: 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..1a441494482c 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..c925f082c665 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..528ca8b3285e 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..dc77197ef93f 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..863d143939cf 100644 --- a/packages/browser-integration-tests/suites/replay/errors/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/flushing/init.js b/packages/browser-integration-tests/suites/replay/flushing/init.js index db6a0aa21821..10f4385a1369 100644 --- a/packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/packages/browser-integration-tests/suites/replay/flushing/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index 1b5f4f447543..7a0337445768 100644 --- a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 1000, - flushMaxDelay: 1000, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..7a0337445768 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..ba43582d7816 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, mutationLimit: 250, }); 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..105c0114f2b0 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..10f4385a1369 100644 --- a/packages/browser-integration-tests/suites/replay/requests/init.js +++ b/packages/browser-integration-tests/suites/replay/requests/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index 46af904118a6..d9008eb4f9d6 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..5de219254c61 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..4d7fc285d7b7 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, }); 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..b7008ced8f6b 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, 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..367357937e9a 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, slowClickTimeout: 3100, slowClickIgnoreSelectors: ['.ignore-class', '[ignore-attribute]'], }); 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..0420898a5b88 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, 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..a4a5a0cbd5c6 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,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 200, + flushMaxDelay: 200, useCompression: false, maskAllText: false, }); From 96fdbf4f9c7c33b4d82ebf4985fc933109ef32c0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Jul 2023 17:38:43 +0200 Subject: [PATCH 2/8] fix(replay): Ensure multi click has correct timestamps (#8591) --- .../replay/slowClick/multiClick/test.ts | 37 +++++++++++++++++++ .../replay/src/coreHandlers/handleClick.ts | 10 +++-- .../src/eventBuffer/EventBufferArray.ts | 2 +- .../EventBufferCompressionWorker.ts | 2 +- packages/replay/src/util/addEvent.ts | 2 +- .../util/{timestampToMs.ts => timestamp.ts} | 8 ++++ 6 files changed, 54 insertions(+), 7 deletions(-) rename packages/replay/src/util/{timestampToMs.ts => timestamp.ts} (50%) 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/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/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; +} From 07e2e43f7412c7cdc1e85ddcb7d0893c6c37b888 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Jul 2023 17:39:14 +0200 Subject: [PATCH 3/8] feat(replay): Ensure min/max duration when flushing (#8596) This PR adds a safeguard to ensure we do not flush (=send) a replay that is either too short or too long. We allow to configure a `minReplayDuration`, which defaults to 5s and maxes out at 15s. Whenever we try to flush and the duration is shorter than this, we'll just skip flushing. Additionally, we also skip flushing if the replay is longer than MAX_SESSION_LIFE + 5s (=60min + 5s). This _should not_ happen, technically, but apparently it still does. So while we figure out the root cause of this, we can at least avoid sending stuff in that case. --- .../scripts/detectFlakyTests.ts | 2 - .../suites/replay/bufferMode/init.js | 1 + .../captureReplayFromReplayPackage/init.js | 1 + .../suites/replay/compression/init.js | 1 + .../suites/replay/customEvents/init.js | 1 + .../suites/replay/dsc/init.js | 1 + .../suites/replay/errors/droppedError/init.js | 1 + .../errors/errorModeCustomTransport/init.js | 1 + .../suites/replay/errors/errorNotSent/init.js | 1 + .../replay/errors/errorsInSession/init.js | 1 + .../suites/replay/errors/init.js | 1 + .../fetch/captureRequestBody/init.js | 1 + .../fetch/captureRequestHeaders/init.js | 1 + .../fetch/captureResponseBody/init.js | 1 + .../fetch/captureResponseHeaders/init.js | 1 + .../extendNetworkBreadcrumbs/fetch/init.js | 1 + .../xhr/captureRequestBody/init.js | 1 + .../xhr/captureRequestHeaders/init.js | 1 + .../xhr/captureResponseBody/init.js | 1 + .../xhr/captureResponseHeaders/init.js | 1 + .../extendNetworkBreadcrumbs/xhr/init.js | 1 + .../suites/replay/fileInput/init.js | 1 + .../suites/replay/flushing/init.js | 1 + .../suites/replay/init.js | 1 + .../suites/replay/keyboardEvents/init.js | 1 + .../largeMutations/defaultOptions/init.js | 1 + .../largeMutations/mutationLimit/init.js | 1 + .../suites/replay/minReplayDuration/init.js | 18 ++++++ .../replay/minReplayDuration/template.html | 10 +++ .../suites/replay/minReplayDuration/test.ts | 63 +++++++++++++++++++ .../suites/replay/multiple-pages/init.js | 1 + .../suites/replay/privacyBlock/init.js | 1 + .../suites/replay/privacyDefault/init.js | 1 + .../suites/replay/privacyInput/init.js | 1 + .../suites/replay/privacyInputMaskAll/init.js | 1 + .../suites/replay/replayShim/init.js | 1 + .../suites/replay/requests/init.js | 1 + .../suites/replay/sampling/init.js | 1 + .../suites/replay/sessionExpiry/init.js | 1 + .../suites/replay/sessionInactive/init.js | 1 + .../suites/replay/sessionMaxAge/init.js | 1 + .../suites/replay/slowClick/disable/init.js | 1 + .../suites/replay/slowClick/init.js | 1 + .../suites/replay/throttleBreadcrumbs/init.js | 1 + .../suites/replay/unicode/compressed/init.js | 1 + .../replay/unicode/uncompressed/init.js | 1 + packages/replay/src/constants.ts | 5 ++ packages/replay/src/integration.ts | 9 ++- packages/replay/src/replay.ts | 20 ++++++ packages/replay/src/types/replay.ts | 7 +++ .../test/integration/errorSampleRate.test.ts | 6 ++ .../replay/test/integration/flush.test.ts | 53 +++++++++++++++- packages/replay/test/mocks/mockSdk.ts | 1 + .../replay/test/utils/setupReplayContainer.ts | 1 + 54 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/init.js create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/template.html create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts 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/replay/bufferMode/init.js b/packages/browser-integration-tests/suites/replay/bufferMode/init.js index 5691b52d96a3..2453efcfbe1d 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/init.js +++ b/packages/browser-integration-tests/suites/replay/bufferMode/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/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 0c068170f01e..c2dd47ab0c25 100644 --- a/packages/browser-integration-tests/suites/replay/compression/init.js +++ b/packages/browser-integration-tests/suites/replay/compression/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ 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 5ddfef307e9c..f76a1207243b 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ 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 1a441494482c..2c9cd8b23147 100644 --- a/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/droppedError/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/errors/errorModeCustomTransport/init.js b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js index c925f082c665..acf3b91c0dd3 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/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/errors/errorNotSent/init.js b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 528ca8b3285e..49d938b15060 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/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/errors/errorsInSession/init.js b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index dc77197ef93f..29486082ff8a 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/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/errors/init.js b/packages/browser-integration-tests/suites/replay/errors/init.js index 863d143939cf..89c185dacc7f 100644 --- a/packages/browser-integration-tests/suites/replay/errors/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/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/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 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/packages/browser-integration-tests/suites/replay/flushing/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/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 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/keyboardEvents/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/largeMutations/defaultOptions/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/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/largeMutations/mutationLimit/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index ba43582d7816..35c6feed4df7 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ 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 105c0114f2b0..a856a0d13c3e 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/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/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 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/requests/init.js +++ b/packages/browser-integration-tests/suites/replay/requests/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/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 d9008eb4f9d6..a3b9726f3103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/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/sessionInactive/init.js b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js index 5de219254c61..781e7b583109 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/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/sessionMaxAge/init.js b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 4d7fc285d7b7..de8b260647ad 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/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/slowClick/disable/init.js b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index b7008ced8f6b..aa5be4406824 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ 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 367357937e9a..030b2722d236 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, slowClickTimeout: 3100, slowClickIgnoreSelectors: ['.ignore-class', '[ignore-attribute]'], }); 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 0420898a5b88..2fe6781ee15e 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ 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 a4a5a0cbd5c6..956586937a19 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllText: false, }); 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/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/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, From 67c3b6a5799b48fc37f4f8b78d11508b7c9300c1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Jul 2023 08:16:19 +0200 Subject: [PATCH 4/8] test(e2e): Use `latest || *` instead of `*` as version (#8585) Due to the [way pnpm handles versions](https://github.com/pnpm/pnpm/issues/6463), the version `*` we use in E2E tests is actually incorrect, because it installs the _lowest_ version it can find, e.g. 0.1.0. This is usually not a problem as when we use verdaccio, there is only a single version in the repository. however, when running things locally/debugging stuff, and you run `pnpm install` without verdaccio, stuff fails. This updates the versions used in E2E tests to `latest || *`, which results in a correct resolution. --- packages/e2e-tests/README.md | 5 +++-- .../test-applications/create-next-app/package.json | 2 +- .../test-applications/create-react-app/package.json | 4 ++-- .../test-applications/create-remix-app/package.json | 2 +- .../test-applications/nextjs-app-dir/package.json | 2 +- .../test-applications/nextjs-app-dir/test-recipe.json | 2 +- .../test-applications/node-express-app/package.json | 8 ++++---- .../react-create-hash-router/package.json | 2 +- .../react-create-hash-router/test-recipe.json | 4 ++-- .../standard-frontend-react-tracing-import/package.json | 4 ++-- .../standard-frontend-react/package.json | 2 +- .../standard-frontend-react/test-recipe.json | 4 ++-- .../e2e-tests/test-applications/sveltekit/package.json | 6 +++--- 13 files changed, 24 insertions(+), 23 deletions(-) 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" From da075420536302921616cdef821c95b30491c857 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 21 Jul 2023 07:56:49 +0100 Subject: [PATCH 5/8] fix(tests): Update `loader` integration tests to avoid flakes. (#8601) Playwright's event listeners and `page.goto` functions can occasionally end up in race condition, even when they are invoked in the correct order. The workaround is to invoke them in `Promise.all`, unless there's a specific need to separate them. --- .../loader/noOnLoad/captureException/test.ts | 8 +++----- .../loader/noOnLoad/customOnErrorHandler/test.ts | 8 +++----- .../loader/noOnLoad/errorHandler/test.ts | 8 +++----- .../loader/noOnLoad/errorHandlerLater/test.ts | 8 +++----- .../loader/noOnLoad/pageloadTransaction/test.ts | 12 +++++++----- .../loader/noOnLoad/sdkLoadedInMeanwhile/test.ts | 9 +++------ .../loader/onLoad/captureException/test.ts | 8 +++----- .../loader/onLoad/captureExceptionInOnLoad/test.ts | 8 +++----- .../loader/onLoad/customBrowserTracing/test.ts | 12 +++++++----- .../loader-suites/loader/onLoad/errorHandler/test.ts | 8 +++----- .../loader/onLoad/errorHandlerLater/test.ts | 8 +++----- .../loader/onLoad/pageloadTransaction/test.ts | 12 +++++++----- packages/browser-integration-tests/utils/helpers.ts | 10 ++++++++++ 13 files changed, 58 insertions(+), 61 deletions(-) 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/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(); From 7d7b8adc496e83edcac2089f1c1ec98235f2ed8c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 21 Jul 2023 08:57:58 +0200 Subject: [PATCH 6/8] fix(utils): Truncate aggregate exception values (LinkedErrors) (#8593) Aggregate exceptions' `exception.value` strings weren't truncated correctly in our event preparation pipeline. The tricky part here is that the `LinkedErrors` integration adds the linked/child exceptions to the event in an event processor which is applied only after we truncate the original event. Hence, we need to also truncate the values in the event processor itself. --- .../captureException/linkedErrrors/subject.js | 13 ++++ .../captureException/linkedErrrors/test.ts | 41 +++++++++++ .../browser/src/integrations/linkederrors.ts | 4 +- .../node/src/integrations/linkederrors.ts | 5 +- packages/utils/src/aggregate-errors.ts | 37 +++++++--- packages/utils/test/aggregate-errors.test.ts | 68 ++++++++++++++++--- 6 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/subject.js create mode 100644 packages/browser-integration-tests/suites/public-api/captureException/linkedErrrors/test.ts 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/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/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/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', + }, + }, + ], + }, + }); + }); }); From f669f66a205a346aca4d1f140bd3f8a014b73f2c Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 21 Jul 2023 03:32:20 -0400 Subject: [PATCH 7/8] fix(profiling): align to SDK selected time origin (#8599) This was causing drift between profiling and transaction timelines --- packages/browser/src/profiling/utils.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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, }; From 6aa95c78aa771f3f7a60bfb0112f224292793600 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Jul 2023 10:49:36 +0200 Subject: [PATCH 8/8] meta(changelog): Update changelog for 7.60.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) 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)