Skip to content
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

fix(replay): Ensure we do not try to flush when we force stop replay #8783

Merged
merged 7 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 onclick="console.log('Test log')" id="button1">Click me</button>
<button onclick="console.log('Test log 2')" id="button2">Click me</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser } from '../../../utils/helpers';
import {
getDecompressedRecordingEvents,
getReplaySnapshot,
isReplayEvent,
REPLAY_DEFAULT_FLUSH_MAX_DELAY,
shouldSkipReplayTest,
waitForReplayRequest,
} from '../../../utils/replayHelpers';

sentryTest(
'should stop recording when running into eventBuffer error',
async ({ getLocalTestPath, page, forceFlushReplay }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
});
});

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);

await waitForReplayRequest(page);
const replay = await getReplaySnapshot(page);
expect(replay._isEnabled).toBe(true);

await forceFlushReplay();

let called = 0;

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
const event = envelopeRequestParser(route.request());

// We only want to count replays here
if (event && isReplayEvent(event)) {
const events = getDecompressedRecordingEvents(route.request());
// this makes sure we ignore e.g. mouse move events which can otherwise lead to flakes
if (events.length > 0) {
called++;
}
}

return route.fulfill({
status: 200,
});
});

called = 0;

/**
* We test the following here:
* 1. First click should add an event (so the eventbuffer is not empty)
* 2. Second click should throw an error in eventBuffer (which should lead to stopping the replay)
* 3. Nothing should be sent to API, as we stop the replay due to the eventBuffer error.
*/
await page.evaluate(`
window._count = 0;
window._addEvent = window.Replay._replay.eventBuffer.addEvent.bind(window.Replay._replay.eventBuffer);
window.Replay._replay.eventBuffer.addEvent = (...args) => {
window._count++;
if (window._count === 2) {
throw new Error('provoked error');
}
window._addEvent(...args);
};
`);

void page.click('#button1');
void page.click('#button2');

// Should immediately skip retrying and just cancel, no backoff
// This waitForTimeout call should be okay, as we're not checking for any
// further network requests afterwards.
await page.waitForTimeout(REPLAY_DEFAULT_FLUSH_MAX_DELAY + 100);

expect(called).toBe(0);

const replay2 = await getReplaySnapshot(page);

expect(replay2._isEnabled).toBe(false);
},
);
4 changes: 2 additions & 2 deletions packages/browser-integration-tests/utils/replayHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ function getOptionsEvents(replayRequest: Request): CustomRecordingEvent[] {
return getAllCustomRrwebRecordingEvents(events).filter(data => data.tag === 'options');
}

function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] {
export function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] {
const replayRequest = getRequest(resOrReq);
return (
(replayEnvelopeRequestParser(replayRequest, 5) as eventWithTime[])
Expand Down Expand Up @@ -302,7 +302,7 @@ const replayEnvelopeRequestParser = (request: Request | null, envelopeIndex = 2)
return envelope[envelopeIndex] as Event;
};

const replayEnvelopeParser = (request: Request | null): unknown[] => {
export const replayEnvelopeParser = (request: Request | null): unknown[] => {
// https://develop.sentry.dev/sdk/envelopes/
const envelopeBytes = request?.postDataBuffer() || '';

Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
return Promise.resolve();
}

return this._replay.stop();
return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
}

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/replay/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export class ReplayContainer implements ReplayContainerInterface {
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
* does not support a teardown
*/
public async stop(reason?: string): Promise<void> {
public async stop({ forceFlush = false, reason }: { forceFlush?: boolean; reason?: string } = {}): Promise<void> {
if (!this._isEnabled) {
return;
}
Expand All @@ -388,7 +388,7 @@ export class ReplayContainer implements ReplayContainerInterface {
this._debouncedFlush.cancel();
// See comment above re: `_isEnabled`, we "force" a flush, ignoring the
// `_isEnabled` state of the plugin since it was disabled above.
if (this.recordingMode === 'session') {
if (forceFlush) {
await this._flush({ force: true });
}

Expand Down Expand Up @@ -777,7 +777,7 @@ export class ReplayContainer implements ReplayContainerInterface {
this.session = session;

if (!this.session.sampled) {
void this.stop('session not refreshed');
void this.stop({ reason: 'session not refreshed' });
return false;
}

Expand Down Expand Up @@ -1099,7 +1099,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// This means we retried 3 times and all of them failed,
// or we ran into a problem we don't want to retry, like rate limiting.
// In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
void this.stop('sendReplay');
void this.stop({ reason: 'sendReplay' });

const client = getCurrentHub().getClient();

Expand Down Expand Up @@ -1223,7 +1223,7 @@ export class ReplayContainer implements ReplayContainerInterface {

// Stop replay if over the mutation limit
if (overMutationLimit) {
void this.stop('mutationLimit');
void this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this case we still want to flush I guess? The integration test failed for this, and I guess it is OK to do a final flush here to have details on why this was stopped - we do not add the large mutations anyhow yet, so should be fine?

return false;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/types/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export interface ReplayContainer {
getContext(): InternalEventContext;
initializeSampling(): void;
start(): void;
stop(reason?: string): Promise<void>;
stop(options?: { reason?: string; forceflush?: boolean }): Promise<void>;
pause(): void;
resume(): void;
startRecording(): void;
Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/util/addEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function addEvent(
const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';

__DEBUG_BUILD__ && logger.error(error);
await replay.stop(reason);
await replay.stop({ reason });

const client = getCurrentHub().getClient();

Expand Down
3 changes: 1 addition & 2 deletions packages/replay/test/integration/flush.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,9 @@ describe('Integration | flush', () => {
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP + 100, type: 2 };
mockRecord._emitter(TEST_EVENT);

await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);
await advanceTimers(160_000);

expect(mockFlush).toHaveBeenCalledTimes(2);
expect(mockFlush).toHaveBeenCalledTimes(1);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also shows the incorrect behavior, it was flushing twice before even though it shouldn't have.

expect(mockSendReplay).toHaveBeenCalledTimes(0);
expect(replay.isEnabled()).toBe(false);

Expand Down