|
1 | 1 | /* eslint-disable max-lines */ // TODO: We might want to split this file up
|
2 | 2 | import { addGlobalEventProcessor, captureException, getCurrentHub } from '@sentry/core';
|
3 |
| -import type { Breadcrumb, ReplayRecordingData, ReplayRecordingMode } from '@sentry/types'; |
| 3 | +import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types'; |
4 | 4 | import type { RateLimits } from '@sentry/utils';
|
5 | 5 | import { addInstrumentationHandler, disabledUntil, logger } from '@sentry/utils';
|
6 | 6 | import { EventType, record } from 'rrweb';
|
@@ -625,7 +625,7 @@ export class ReplayContainer implements ReplayContainerInterface {
|
625 | 625 | // Send replay when the page/tab becomes hidden. There is no reason to send
|
626 | 626 | // replay if it becomes visible, since no actions we care about were done
|
627 | 627 | // while it was hidden
|
628 |
| - this._conditionalFlush({ finishImmediate: true }); |
| 628 | + this._conditionalFlush({ sync: true }); |
629 | 629 | }
|
630 | 630 |
|
631 | 631 | /**
|
@@ -756,8 +756,8 @@ export class ReplayContainer implements ReplayContainerInterface {
|
756 | 756 | * Page is likely to unload so need to bypass debounce completely and
|
757 | 757 | * synchronously retrieve pending events from buffer and send request asap.
|
758 | 758 | */
|
759 |
| - if (options.finishImmediate) { |
760 |
| - void this._runFlush(options); |
| 759 | + if (options.sync) { |
| 760 | + this._flushSync(); |
761 | 761 | return;
|
762 | 762 | }
|
763 | 763 |
|
@@ -804,81 +804,143 @@ export class ReplayContainer implements ReplayContainerInterface {
|
804 | 804 | *
|
805 | 805 | * Should never be called directly, only by `flush`
|
806 | 806 | */
|
807 |
| - private async _runFlush(options: FlushOptions = {}): Promise<void> { |
808 |
| - if (!this.session || !this.eventBuffer) { |
809 |
| - __DEBUG_BUILD__ && logger.error('[Replay] No session or eventBuffer found to flush.'); |
810 |
| - return; |
811 |
| - } |
812 |
| - |
| 807 | + private async _runFlush(): Promise<void> { |
813 | 808 | try {
|
814 |
| - this._debouncedFlush.cancel(); |
| 809 | + const flushData = this._prepareFlush(); |
| 810 | + |
| 811 | + if (!flushData) { |
| 812 | + return; |
| 813 | + } |
815 | 814 |
|
816 |
| - const promises: Promise<any>[] = []; |
| 815 | + const { promises, replayId, segmentId, eventContext, eventBuffer, session } = flushData; |
817 | 816 |
|
818 |
| - promises.push(this._addPerformanceEntries()); |
| 817 | + // NOTE: Be mindful that nothing after this point (the first `await`) |
| 818 | + // will run after when the page is unloaded. |
| 819 | + await Promise.all(promises); |
819 | 820 |
|
820 |
| - // Do not continue if there are no pending events in buffer |
821 |
| - if (!this.eventBuffer || !this.eventBuffer.pendingLength) { |
| 821 | + // This can be empty due to blur events calling `runFlush` directly. In |
| 822 | + // the case where we have a snapshot checkout and a blur event |
| 823 | + // happening near the same time, the blur event can end up emptying the |
| 824 | + // buffer even if snapshot happens first. |
| 825 | + if (!eventBuffer.pendingLength) { |
822 | 826 | return;
|
823 | 827 | }
|
824 | 828 |
|
825 |
| - // Only attach memory entry if eventBuffer is not empty |
826 |
| - promises.push(addMemoryEntry(this)); |
| 829 | + // This empties the event buffer regardless of outcome of sending replay |
| 830 | + const recordingData = await eventBuffer.finish(); |
827 | 831 |
|
828 |
| - // NOTE: Copy values from instance members, as it's possible they could |
829 |
| - // change before the flush finishes. |
830 |
| - const replayId = this.session.id; |
831 |
| - const eventContext = this._popEventContext(); |
832 |
| - // Always increment segmentId regardless of outcome of sending replay |
833 |
| - const segmentId = this.session.segmentId++; |
834 |
| - |
835 |
| - // Save session (new segment id) after we save flush data assuming either |
836 |
| - // 1) request succeeds or 2) it fails or never happens, in which case we |
837 |
| - // need to retry this segment. |
838 |
| - this._maybeSaveSession(); |
| 832 | + await sendReplay({ |
| 833 | + replayId, |
| 834 | + recordingData, |
| 835 | + segmentId, |
| 836 | + includeReplayStartTimestamp: segmentId === 0, |
| 837 | + eventContext, |
| 838 | + session, |
| 839 | + options: this.getOptions(), |
| 840 | + timestamp: new Date().getTime(), |
| 841 | + }); |
| 842 | + } catch (err) { |
| 843 | + this._handleSendError(err); |
| 844 | + } |
| 845 | + } |
839 | 846 |
|
840 |
| - let recordingData: ReplayRecordingData; |
| 847 | + /** |
| 848 | + * Flush event buffer synchonously. |
| 849 | + * This is necessary e.g. when running flush on page unload or similar. |
| 850 | + */ |
| 851 | + private _flushSync(): void { |
| 852 | + try { |
| 853 | + const flushData = this._prepareFlush(); |
841 | 854 |
|
842 |
| - if (options.finishImmediate && this.eventBuffer.pendingLength) { |
843 |
| - recordingData = this.eventBuffer.finishImmediate(); |
844 |
| - } else { |
845 |
| - // NOTE: Be mindful that nothing after this point (the first `await`) |
846 |
| - // will run after when the page is unloaded. |
847 |
| - await Promise.all(promises); |
848 |
| - |
849 |
| - // This can be empty due to blur events calling `runFlush` directly. In |
850 |
| - // the case where we have a snapshot checkout and a blur event |
851 |
| - // happening near the same time, the blur event can end up emptying the |
852 |
| - // buffer even if snapshot happens first. |
853 |
| - if (!this.eventBuffer.pendingLength) { |
854 |
| - return; |
855 |
| - } |
856 |
| - // This empties the event buffer regardless of outcome of sending replay |
857 |
| - recordingData = await this.eventBuffer.finish(); |
| 855 | + if (!flushData) { |
| 856 | + return; |
858 | 857 | }
|
859 | 858 |
|
860 |
| - await sendReplay({ |
| 859 | + const { replayId, segmentId, eventContext, eventBuffer, session } = flushData; |
| 860 | + |
| 861 | + const recordingData = eventBuffer.finishSync(); |
| 862 | + |
| 863 | + sendReplay({ |
861 | 864 | replayId,
|
862 | 865 | recordingData,
|
863 | 866 | segmentId,
|
864 | 867 | includeReplayStartTimestamp: segmentId === 0,
|
865 | 868 | eventContext,
|
866 |
| - session: this.session, |
| 869 | + session, |
867 | 870 | options: this.getOptions(),
|
868 | 871 | timestamp: new Date().getTime(),
|
| 872 | + }).catch(err => { |
| 873 | + this._handleSendError(err); |
869 | 874 | });
|
870 | 875 | } catch (err) {
|
871 |
| - this._handleException(err); |
| 876 | + this._handleSendError(err); |
| 877 | + } |
| 878 | + } |
872 | 879 |
|
873 |
| - if (err instanceof RateLimitError) { |
874 |
| - this._handleRateLimit(err.rateLimits); |
875 |
| - return; |
| 880 | + /** Prepare flush data */ |
| 881 | + private _prepareFlush(): |
| 882 | + | { |
| 883 | + replayId: string; |
| 884 | + eventContext: PopEventContext; |
| 885 | + segmentId: number; |
| 886 | + promises: Promise<unknown>[]; |
| 887 | + eventBuffer: EventBuffer; |
| 888 | + session: Session; |
876 | 889 | }
|
| 890 | + | undefined { |
| 891 | + if (!this.session || !this.eventBuffer) { |
| 892 | + __DEBUG_BUILD__ && logger.error('[Replay] No session or eventBuffer found to flush.'); |
| 893 | + return; |
| 894 | + } |
877 | 895 |
|
878 |
| - // This means we retried 3 times, and all of them failed |
879 |
| - // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments |
880 |
| - this.stop(); |
| 896 | + this._debouncedFlush.cancel(); |
| 897 | + |
| 898 | + const promises: Promise<unknown>[] = []; |
| 899 | + |
| 900 | + promises.push(this._addPerformanceEntries()); |
| 901 | + |
| 902 | + // Do not continue if there are no pending events in buffer |
| 903 | + if (!this.eventBuffer || !this.eventBuffer.pendingLength) { |
| 904 | + return; |
881 | 905 | }
|
| 906 | + |
| 907 | + // Only attach memory entry if eventBuffer is not empty |
| 908 | + promises.push(addMemoryEntry(this)); |
| 909 | + |
| 910 | + // NOTE: Copy values from instance members, as it's possible they could |
| 911 | + // change before the flush finishes. |
| 912 | + const replayId = this.session.id; |
| 913 | + const eventContext = this._popEventContext(); |
| 914 | + // Always increment segmentId regardless of outcome of sending replay |
| 915 | + const segmentId = this.session.segmentId++; |
| 916 | + |
| 917 | + // Save session (new segment id) after we save flush data assuming either |
| 918 | + // 1) request succeeds or 2) it fails or never happens, in which case we |
| 919 | + // need to retry this segment. |
| 920 | + this._maybeSaveSession(); |
| 921 | + |
| 922 | + return { |
| 923 | + replayId, |
| 924 | + eventContext, |
| 925 | + segmentId, |
| 926 | + promises, |
| 927 | + eventBuffer: this.eventBuffer, |
| 928 | + session: this.session, |
| 929 | + }; |
| 930 | + } |
| 931 | + |
| 932 | + /** Handle an error when sending a replay. */ |
| 933 | + private _handleSendError(error: unknown): void { |
| 934 | + this._handleException(error); |
| 935 | + |
| 936 | + if (error instanceof RateLimitError) { |
| 937 | + this._handleRateLimit(error.rateLimits); |
| 938 | + return; |
| 939 | + } |
| 940 | + |
| 941 | + // This means we retried 3 times, and all of them failed |
| 942 | + // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments |
| 943 | + this.stop(); |
882 | 944 | }
|
883 | 945 |
|
884 | 946 | /**
|
|
0 commit comments