Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(replay): Throttle breadcrumbs to max 300/5s (#8086)
This updates custom breadcrumb handling to be throttled to max. 300 breadcrumbs/5s. If we exceed this amount of breadcrumbs, we drop any further breadcrumbs and instead add a single breadcrumb with `category: 'replay.throttled'`, which the UI can use to indicate that _something_ was dropped. Closes #8072 --------- Co-authored-by: Billy Vong <billyvg@users.noreply.github.com>
- Loading branch information
Showing
12 changed files
with
344 additions
and
18 deletions.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.Sentry = Sentry; | ||
window.Replay = new Sentry.Replay({ | ||
flushMinDelay: 5000, | ||
flushMaxDelay: 5000, | ||
useCompression: false, | ||
}); | ||
|
||
Sentry.init({ | ||
dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
sampleRate: 0, | ||
replaysSessionSampleRate: 1.0, | ||
replaysOnErrorSampleRate: 0.0, | ||
|
||
integrations: [window.Replay], | ||
}); |
8 changes: 8 additions & 0 deletions
8
packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/subject.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
const COUNT = 400; | ||
|
||
document.querySelector('[data-console]').addEventListener('click', () => { | ||
// Call console.log() many times | ||
for (let i = 0; i < COUNT; i++) { | ||
console.log(`testing ${i}`); | ||
} | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/template.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
</head> | ||
<body> | ||
<button data-console>Trigger console.log</button> | ||
</body> | ||
</html> |
41 changes: 41 additions & 0 deletions
41
packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { expect } from '@playwright/test'; | ||
|
||
import { sentryTest } from '../../../utils/fixtures'; | ||
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; | ||
|
||
const THROTTLE_LIMIT = 300; | ||
|
||
sentryTest( | ||
'throttles breadcrumbs when many `console.log` are made at the same time', | ||
async ({ getLocalTestUrl, page, forceFlushReplay, browserName }) => { | ||
if (shouldSkipReplayTest() || browserName !== 'chromium') { | ||
sentryTest.skip(); | ||
} | ||
|
||
const reqPromise0 = waitForReplayRequest(page, 0); | ||
const reqPromise1 = waitForReplayRequest(page, 1); | ||
|
||
await page.route('https://dsn.ingest.sentry.io/**/*', route => { | ||
return route.fulfill({ | ||
status: 200, | ||
contentType: 'application/json', | ||
body: JSON.stringify({ id: 'test-id' }), | ||
}); | ||
}); | ||
|
||
const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
||
await page.goto(url); | ||
await reqPromise0; | ||
|
||
await page.click('[data-console]'); | ||
await forceFlushReplay(); | ||
|
||
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); | ||
|
||
// 1 click breadcrumb + 1 throttled breadcrumb is why console logs are less | ||
// than throttle limit | ||
expect(breadcrumbs.length).toBe(THROTTLE_LIMIT); | ||
expect(breadcrumbs.filter(breadcrumb => breadcrumb.category === 'replay.throttled').length).toBe(1); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
export const THROTTLED = '__THROTTLED'; | ||
export const SKIPPED = '__SKIPPED'; | ||
|
||
/** | ||
* Create a throttled function off a given function. | ||
* When calling the throttled function, it will call the original function only | ||
* if it hasn't been called more than `maxCount` times in the last `durationSeconds`. | ||
* | ||
* Returns `THROTTLED` if throttled for the first time, after that `SKIPPED`, | ||
* or else the return value of the original function. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function throttle<T extends (...rest: any[]) => any>( | ||
fn: T, | ||
maxCount: number, | ||
durationSeconds: number, | ||
): (...rest: Parameters<T>) => ReturnType<T> | typeof THROTTLED | typeof SKIPPED { | ||
const counter = new Map<number, number>(); | ||
|
||
const _cleanup = (now: number): void => { | ||
const threshold = now - durationSeconds; | ||
counter.forEach((_value, key) => { | ||
if (key < threshold) { | ||
counter.delete(key); | ||
} | ||
}); | ||
}; | ||
|
||
const _getTotalCount = (): number => { | ||
return [...counter.values()].reduce((a, b) => a + b, 0); | ||
}; | ||
|
||
let isThrottled = false; | ||
|
||
return (...rest: Parameters<T>): ReturnType<T> | typeof THROTTLED | typeof SKIPPED => { | ||
// Date in second-precision, which we use as basis for the throttling | ||
const now = Math.floor(Date.now() / 1000); | ||
|
||
// First, make sure to delete any old entries | ||
_cleanup(now); | ||
|
||
// If already over limit, do nothing | ||
if (_getTotalCount() >= maxCount) { | ||
const wasThrottled = isThrottled; | ||
isThrottled = true; | ||
return wasThrottled ? SKIPPED : THROTTLED; | ||
} | ||
|
||
isThrottled = false; | ||
const count = counter.get(now) || 0; | ||
counter.set(now, count + 1); | ||
|
||
return fn(...rest); | ||
}; | ||
} |
Oops, something went wrong.