Skip to content
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
7 changes: 7 additions & 0 deletions .changeset/swallow-focus-session-touch-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
---

Ignore errors from the background session touch that runs when the page regains focus.

These errors were previously both unhandled and uncaught, now they are just intentionally unhandled. The page is usually in a good enough state to recover gracefully, but the uncaught errors led to noise in the browser console and error tracking tools which we now avoid.
115 changes: 115 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { waitFor } from '@testing-library/react';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';

import { mockJwt } from '@/test/core-fixtures';
import { restoreDocument, setDocumentVisibilityState } from '@/test/document-helpers';

import { mockNativeRuntime } from '../../test/utils';
import { Clerk } from '../clerk';
Expand Down Expand Up @@ -124,6 +125,7 @@ describe('Clerk singleton', () => {
};

Object.defineProperty(global.window, 'addEventListener', {
configurable: true,
value: mockAddEventListener,
});

Expand Down Expand Up @@ -4066,4 +4068,117 @@ describe('Clerk singleton', () => {
expect(mockClerkUICtor).toHaveBeenCalled();
});
});

describe('page focus session touch', () => {
const windowListeners = new Map<string, Array<(event: any) => void>>();
Comment thread
Ephem marked this conversation as resolved.

const firePageFocus = () => {
for (const listener of windowListeners.get('focus') ?? []) {
listener(new Event('focus'));
}
};

const mockSession = {
id: 'sess_1',
status: 'active',
user: {},
touch: vi.fn(() => Promise.resolve()),
getToken: vi.fn(),
lastActiveToken: { getRawString: () => 'mocked-token' },
};

beforeEach(() => {
windowListeners.clear();
const recordWindowListener = (type: string, callback: (e: any) => void) => {
const listeners = windowListeners.get(type) ?? [];
listeners.push(callback);
windowListeners.set(type, listeners);

if (type === 'message') {
callback({
origin: 'https://' + productionPublishableKey,
data: {
browserToken: 'hey',
},
});
}
};
Object.defineProperty(global.window, 'addEventListener', {
configurable: true,
value: recordWindowListener,
});

mockSession.touch.mockReset();
mockSession.touch.mockResolvedValue(undefined);
setDocumentVisibilityState('visible');
mockEnvironmentFetch.mockReturnValue(
Promise.resolve({
authConfig: { singleSessionMode: true },
userSettings: mockUserSettings,
displayConfig: mockDisplayConfig,
isSingleSession: () => true,
isProduction: () => false,
isDevelopmentOrStaging: () => true,
}),
);
mockClientFetch.mockReturnValue(
Promise.resolve({
signedInSessions: [mockSession],
lastActiveSessionId: mockSession.id,
}),
);
});

afterEach(() => {
restoreDocument();
});

it('does not surface a failed focus touch as an unhandled rejection', async () => {
const unhandled: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandled.push(reason);
};
process.on('unhandledRejection', onUnhandledRejection);

try {
mockSession.touch.mockRejectedValue(
new Error(
'ClerkJS: Network error at "https://clerk.example.com/v1/client/sessions/sess_1/touch" - TypeError: NetworkError when attempting to fetch resource. Please try again.',
),
);

const sut = new Clerk(productionPublishableKey);
await sut.load();
firePageFocus();

expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'focus' });
await new Promise(resolve => setTimeout(resolve, 0));
expect(unhandled).toEqual([]);
} finally {
process.off('unhandledRejection', onUnhandledRejection);
}
});

it('still handles an unauthenticated focus touch', async () => {
const sut = new Clerk(productionPublishableKey);
await sut.load();
const handleUnauthenticated = vi.spyOn(sut, 'handleUnauthenticated').mockResolvedValue(undefined);
mockSession.touch.mockRejectedValue({ status: 401 });

firePageFocus();

await waitFor(() => {
expect(handleUnauthenticated).toHaveBeenCalled();
});
});

it('does not touch the session on focus when touchSession is false', async () => {
const sut = new Clerk(productionPublishableKey);
await sut.load({ touchSession: false });

firePageFocus();

expect(mockSession.touch).not.toHaveBeenCalled();
});
});
Comment thread
Ephem marked this conversation as resolved.
});
12 changes: 11 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3654,7 +3654,17 @@ export class Clerk implements ClerkInterface {
this.#touchThrottledUntil = Date.now() + 5_000;

if (this.#options.touchSession) {
void this.#touchCurrentSession(this.session, 'focus');
// Even if touch fails, we're still in a generally good state that can recover.
// There are some caveats and edge cases, like if you reload the tab after a failed
// touch in a multi-tab scenario, you might get the last active user/org that was
// recorded by the other tab, but that's not catastrophic.
// We were previously not swallowing errors here, which led to unnecessary uncaught
// error logs in the browser console and noise in error tracking tools.
// This is a POST and does not currently retry, we could reconsider that if we wanted
// to, but probably only leads to unnecessary complexity for little gain.
this.#touchCurrentSession(this.session, 'focus').catch(error => {
debugLogger.warn('Session touch on page focus failed', { error }, 'clerk');
});
}
});

Expand Down
Loading