New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(replay): Throttle breadcrumbs to max 300/5s #8086
Changes from 5 commits
e98a356
7231d2a
433da18
f9eb6f5
13bc401
7b3a957
5c894e3
6e5fdc9
ee4440e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
window.loaded = []; | ||
const head = document.querySelector('head'); | ||
|
||
const COUNT = 250; | ||
|
||
window.__isLoaded = (run = 1) => { | ||
return window.loaded.length === COUNT * 2 * run; | ||
}; | ||
|
||
document.querySelector('[data-network]').addEventListener('click', () => { | ||
const offset = window.loaded.length; | ||
|
||
// Create many scripts | ||
for (let i = offset; i < offset + COUNT; i++) { | ||
const script = document.createElement('script'); | ||
script.src = `/virtual-assets/script-${i}.js`; | ||
script.setAttribute('crossorigin', 'anonymous'); | ||
head.appendChild(script); | ||
|
||
script.addEventListener('load', () => { | ||
window.loaded.push(`script-${i}`); | ||
}); | ||
} | ||
}); | ||
|
||
document.querySelector('[data-fetch]').addEventListener('click', () => { | ||
const offset = window.loaded.length; | ||
|
||
// Make many fetch requests | ||
for (let i = offset; i < offset + COUNT; i++) { | ||
fetch(`/virtual-assets/fetch-${i}.json`).then(() => { | ||
window.loaded.push(`fetch-${i}`); | ||
}); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
</head> | ||
<body> | ||
<button data-fetch>Trigger fetch requests</button> | ||
<button data-network>Trigger network requests</button> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,106 @@ | ||||||
import { expect } from '@playwright/test'; | ||||||
import type { Breadcrumb } from '@sentry/types'; | ||||||
|
||||||
import { sentryTest } from '../../../utils/fixtures'; | ||||||
import type { PerformanceSpan } from '../../../utils/replayHelpers'; | ||||||
import { | ||||||
getCustomRecordingEvents, | ||||||
getReplayEventFromRequest, | ||||||
shouldSkipReplayTest, | ||||||
waitForReplayRequest, | ||||||
} from '../../../utils/replayHelpers'; | ||||||
|
||||||
const COUNT = 250; | ||||||
const THROTTLE_LIMIT = 300; | ||||||
|
||||||
sentryTest( | ||||||
'throttles breadcrumbs when many requests are made at the same time', | ||||||
async ({ getLocalTestUrl, page, forceFlushReplay, browserName }) => { | ||||||
if (shouldSkipReplayTest() || browserName !== 'chromium') { | ||||||
sentryTest.skip(); | ||||||
} | ||||||
|
||||||
const reqPromise0 = waitForReplayRequest(page, 0); | ||||||
|
||||||
await page.route('https://dsn.ingest.sentry.io/**/*', route => { | ||||||
return route.fulfill({ | ||||||
status: 200, | ||||||
contentType: 'application/json', | ||||||
body: JSON.stringify({ id: 'test-id' }), | ||||||
}); | ||||||
}); | ||||||
|
||||||
let scriptsLoaded = 0; | ||||||
let fetchLoaded = 0; | ||||||
|
||||||
await page.route('**/virtual-assets/script-**', route => { | ||||||
scriptsLoaded++; | ||||||
return route.fulfill({ | ||||||
status: 200, | ||||||
contentType: 'text/javascript', | ||||||
body: `const aha = ${'xx'.repeat(20_000)};`, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this need to be valid js? i think it needs some extra quotes around the string
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ryan953 Francesco's out for the next two weeks. Do you want to take over this (only if you have availability)? |
||||||
}); | ||||||
}); | ||||||
|
||||||
await page.route('**/virtual-assets/fetch-**', route => { | ||||||
fetchLoaded++; | ||||||
return route.fulfill({ | ||||||
status: 200, | ||||||
contentType: 'application/json', | ||||||
body: JSON.stringify({ fetchResponse: 'aa'.repeat(20_000) }), | ||||||
}); | ||||||
}); | ||||||
|
||||||
const url = await getLocalTestUrl({ testDir: __dirname }); | ||||||
|
||||||
await page.goto(url); | ||||||
await reqPromise0; | ||||||
|
||||||
const collectedSpans: PerformanceSpan[] = []; | ||||||
const collectedBreadcrumbs: Breadcrumb[] = []; | ||||||
|
||||||
page.on('response', response => { | ||||||
// We only capture sentry stuff | ||||||
if (!response.url().includes('https://dsn.ingest.sentry')) { | ||||||
|
||||||
return; | ||||||
} | ||||||
|
||||||
// If this is undefined, this is not a replay request | ||||||
if (!getReplayEventFromRequest(response.request())) { | ||||||
return; | ||||||
} | ||||||
|
||||||
const { performanceSpans, breadcrumbs } = getCustomRecordingEvents(response); | ||||||
|
||||||
collectedSpans.push( | ||||||
...performanceSpans.filter(span => span.op === 'resource.script' || span.op === 'resource.fetch'), | ||||||
); | ||||||
collectedBreadcrumbs.push(...breadcrumbs.filter(breadcrumb => breadcrumb.category === 'replay.throttled')); | ||||||
}); | ||||||
|
||||||
await page.click('[data-network]'); | ||||||
await page.click('[data-fetch]'); | ||||||
|
||||||
await page.waitForFunction('window.__isLoaded()'); | ||||||
await forceFlushReplay(); | ||||||
|
||||||
await waitForFunction(() => collectedBreadcrumbs.length === 1, 6_000, 100); | ||||||
|
||||||
// All assets have been _loaded_ | ||||||
expect(scriptsLoaded).toBe(COUNT); | ||||||
expect(fetchLoaded).toBe(COUNT); | ||||||
|
||||||
// But only some have been captured by replay | ||||||
// We give it some wiggle room to account for flakyness | ||||||
expect(collectedSpans.length).toBeLessThanOrEqual(THROTTLE_LIMIT); | ||||||
expect(collectedSpans.length).toBeGreaterThanOrEqual(THROTTLE_LIMIT - 50); | ||||||
expect(collectedBreadcrumbs.length).toBe(1); | ||||||
}, | ||||||
); | ||||||
|
||||||
async function waitForFunction(cb: () => boolean, timeout = 2000, increment = 100) { | ||||||
while (timeout > 0 && !cb()) { | ||||||
await new Promise(resolve => setTimeout(resolve, increment)); | ||||||
await waitForFunction(cb, timeout - increment, increment); | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RE the flakiness we discussed in the call yesterday: iiuc, we're throttling all kinds of breadcrumbs, right? So to decrease/get rid of the flakiness, should we perhaps only create and check for click breadcrumbs? Maybe these are more reliably created than the network breadcrumbs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, would clicks end up getting throttled by the core sdk first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh I thought we didn't but it turns out we do, good catch!
sentry-javascript/packages/utils/src/instrument.ts
Lines 458 to 507 in de3e675
Well, then I guess we could do console breadcrumbs? 😅