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): Fixes potential out-of-order segments #13609

Merged
merged 1 commit into from
Sep 10, 2024
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
28 changes: 15 additions & 13 deletions packages/replay-internal/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,27 +1226,29 @@ export class ReplayContainer implements ReplayContainerInterface {
// TODO FN: Evaluate if we want to stop here, or remove this again?
}

// this._flushLock acts as a lock so that future calls to `_flush()`
// will be blocked until this promise resolves
const _flushInProgress = !!this._flushLock;
Copy link
Member

@c298lee c298lee Sep 6, 2024

Choose a reason for hiding this comment

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

what does !! do?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah it's a shortcut to coerce to a boolean instead of e.g. Boolean()


// this._flushLock acts as a lock so that future calls to `_flush()` will
// be blocked until current flush is finished (i.e. this promise resolves)
if (!this._flushLock) {
this._flushLock = this._runFlush();
await this._flushLock;
this._flushLock = undefined;
return;
}

// Wait for previous flush to finish, then call the debounced `_flush()`.
// It's possible there are other flush requests queued and waiting for it
// to resolve. We want to reduce all outstanding requests (as well as any
// new flush requests that occur within a second of the locked flush
// completing) into a single flush.

try {
await this._flushLock;
} catch (err) {
DEBUG_BUILD && logger.error(err);
this.handleException(err);
} finally {
this._debouncedFlush();
this._flushLock = undefined;

if (_flushInProgress) {
// Wait for previous flush to finish, then call the debounced
// `_flush()`. It's possible there are other flush requests queued and
// waiting for it to resolve. We want to reduce all outstanding
// requests (as well as any new flush requests that occur within a
// second of the locked flush completing) into a single flush.
this._debouncedFlush();
}
}
};

Expand Down
58 changes: 58 additions & 0 deletions packages/replay-internal/test/integration/flush.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,64 @@ describe('Integration | flush', () => {
await replay.start();
});

it('resets flush lock if runFlush rejects/throws', async () => {
mockRunFlush.mockImplementation(
() =>
new Promise((resolve, reject) => {
reject(new Error('runFlush'));
}),
);
try {
await replay['_flush']();
} catch {
// do nothing
}
expect(replay['_flushLock']).toBeUndefined();
});

it('resets flush lock when flush is called multiple times before it resolves', async () => {
let _resolve;
mockRunFlush.mockImplementation(
() =>
new Promise(resolve => {
_resolve = resolve;
}),
);
const mockDebouncedFlush: MockedFunction<ReplayContainer['_debouncedFlush']> = vi.spyOn(replay, '_debouncedFlush');
mockDebouncedFlush.mockImplementation(vi.fn);
mockDebouncedFlush.cancel = vi.fn();

const results = [replay['_flush'](), replay['_flush']()];
expect(replay['_flushLock']).not.toBeUndefined();

_resolve && _resolve();
await Promise.all(results);
expect(replay['_flushLock']).toBeUndefined();
mockDebouncedFlush.mockRestore();
});

it('resets flush lock when flush is called multiple times before it rejects', async () => {
let _reject;
mockRunFlush.mockImplementation(
() =>
new Promise((_, reject) => {
_reject = reject;
}),
);
const mockDebouncedFlush: MockedFunction<ReplayContainer['_debouncedFlush']> = vi.spyOn(replay, '_debouncedFlush');
mockDebouncedFlush.mockImplementation(vi.fn);
mockDebouncedFlush.cancel = vi.fn();
expect(replay['_flushLock']).toBeUndefined();
replay['_flush']();
const result = replay['_flush']();
expect(replay['_flushLock']).not.toBeUndefined();

_reject && _reject(new Error('Throw runFlush'));
await result;
expect(replay['_flushLock']).toBeUndefined();
mockDebouncedFlush.mockRestore();
});

/**
* Assuming the user wants to record a session
* when calling flush() without replay being enabled
Expand Down
Loading