Skip to content
Draft
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
Expand Up @@ -17,6 +17,7 @@ import { replay } from '@/app/sentry';
import { sessionRecordingSelector } from '@/selectors/sessionRecordingSelector';
import { ExplainerModal } from '../ExplainerModal';
import { Tabs } from '@/constants/Tabs';
import { getReplaySessionIdWithRetry } from './getReplaySessionIdWithRetry';

// TODO: expose types from @tokens-studio/ui/checkbox
type CheckedState = boolean | 'indeterminate';
Expand All @@ -42,15 +43,9 @@ function Settings() {

dispatch.settings.setSessionRecording(!!checked);
if (checked) {
// Display the info to the user
try {
let id = await replay.getReplayId();
if (!id) {
// Force start the replay functionality
replay.start();
id = await replay.getReplayId();
}
setDebugSession(id || '');
const replaySessionId = await getReplaySessionIdWithRetry(replay);
setDebugSession(replaySessionId);
} catch (err) {
console.warn('Replay is likely in progress already', err);
}
Expand All @@ -65,16 +60,16 @@ function Settings() {

// Load once on mount.
useEffect(() => {
async function getSessionId() {
async function loadSessionId() {
try {
const id = replay.getReplayId();
setDebugSession(id || '');
const replaySessionId = await getReplaySessionIdWithRetry(replay, { startIfMissing: false });
setDebugSession(replaySessionId);
} catch (err) {
// Silently fail
}
}
getSessionId();
});
loadSessionId();
}, []);

const closeOnboarding = React.useCallback(() => {
dispatch.uiState.setOnboardingExplainerSyncProviders(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getReplaySessionIdWithRetry } from './getReplaySessionIdWithRetry';

describe('getReplaySessionIdWithRetry', () => {
it('returns the current replay id when already available', async () => {
const replayController = {
getReplayId: jest.fn().mockReturnValue('replay-id-1'),
start: jest.fn(),
};

const replayId = await getReplaySessionIdWithRetry(replayController);

expect(replayId).toBe('replay-id-1');
expect(replayController.start).not.toHaveBeenCalled();
expect(replayController.getReplayId).toHaveBeenCalledTimes(1);
});

it('starts replay and retries until an id becomes available', async () => {
const replayController = {
getReplayId: jest
.fn()
.mockReturnValueOnce('')
.mockReturnValueOnce('')
.mockReturnValueOnce('replay-id-2'),
start: jest.fn(),
};

const replayId = await getReplaySessionIdWithRetry(replayController, {
attempts: 3,
retryDelayMs: 0,
});

expect(replayId).toBe('replay-id-2');
expect(replayController.start).toHaveBeenCalledTimes(1);
expect(replayController.getReplayId).toHaveBeenCalledTimes(3);
});

it('returns an empty string when no replay id is available', async () => {
const replayController = {
getReplayId: jest.fn().mockReturnValue(''),
start: jest.fn(),
};

const replayId = await getReplaySessionIdWithRetry(replayController, {
attempts: 2,
retryDelayMs: 0,
});

expect(replayId).toBe('');
expect(replayController.start).toHaveBeenCalledTimes(1);
expect(replayController.getReplayId).toHaveBeenCalledTimes(3);
});

it('continues retrying when start throws', async () => {
const replayController = {
getReplayId: jest
.fn()
.mockReturnValueOnce('')
.mockReturnValueOnce('')
.mockReturnValueOnce('replay-id-3'),
start: jest.fn().mockImplementation(() => {
throw new Error('start failed');
}),
};

const replayId = await getReplaySessionIdWithRetry(replayController, {
attempts: 3,
retryDelayMs: 0,
});

expect(replayId).toBe('replay-id-3');
expect(replayController.start).toHaveBeenCalledTimes(1);
expect(replayController.getReplayId).toHaveBeenCalledTimes(3);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
type ReplayController = {
getReplayId: () => string | undefined | Promise<string | undefined>;
start: () => void | Promise<void>;
};

type GetReplaySessionIdWithRetryOptions = {
attempts?: number;
retryDelayMs?: number;
startIfMissing?: boolean;
};

const DEFAULT_ATTEMPTS = 6;
const DEFAULT_RETRY_DELAY_MS = 300;

function wait(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});
}

async function getReplayId(replayController: ReplayController) {
return (await Promise.resolve(replayController.getReplayId())) || '';
}

export async function getReplaySessionIdWithRetry(
replayController: ReplayController,
options: GetReplaySessionIdWithRetryOptions = {},
) {
const {
attempts = DEFAULT_ATTEMPTS,
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
startIfMissing = true,
} = options;

let replayId = await getReplayId(replayController);
if (replayId) return replayId;

if (startIfMissing) {
try {
await Promise.resolve(replayController.start());
} catch (error) {
// If replay start fails, keep trying to read any existing replay id.
}
}

for (let attempt = 0; attempt < attempts; attempt += 1) {
await wait(retryDelayMs);
replayId = await getReplayId(replayController);
if (replayId) return replayId;
}

return '';
}
Loading