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
36 changes: 36 additions & 0 deletions apps/postshow/src/lib/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,42 @@
expect(mocks.startSessionRecording).toHaveBeenCalled();
});

it('drops benign supabase stale-refresh-token exceptions before capture', async () => {
const { setAnalyticsConsent } = await analytics();

setAnalyticsConsent('accepted');
await vi.waitFor(() => expect(mocks.init).toHaveBeenCalledTimes(1));

Check warning on line 118 in apps/postshow/src/lib/analytics.test.ts

View workflow job for this annotation

GitHub Actions / Unslop Checks

unslop-ci code/verbose-naming (low)

Over-verbose, robotically self-documenting identifier. Fix: A name that is a whole sentence (getUserDataFromApiResponseHandler) reads as machine-generated. Trim it.
const config = mocks.init.mock.calls[0]?.[1] as { before_send: (result: unknown) => unknown };
const beforeSend = config.before_send;

const authNoise = {
event: '$exception',
properties: {
$exception_list: [
{ type: 'AuthApiError', value: 'Invalid Refresh Token: Refresh Token Not Found' },
],
},
};
expect(beforeSend(authNoise)).toBeNull();

const authNoiseFlat = {
event: '$exception',
properties: {
$exception_types: ['AuthApiError'],
$exception_values: ['Invalid Refresh Token: Refresh Token Not Found'],
},
};
expect(beforeSend(authNoiseFlat)).toBeNull();

const realError = {
event: '$exception',
properties: {
$exception_list: [{ type: 'TypeError', value: 'x is not a function' }],
},
};
expect(beforeSend(realError)).toBe(realError);
});

it('captures and identifies only after explicit consent, without email properties', async () => {
const { identify, initAnalytics, setAnalyticsConsent, track } = await analytics();
initAnalytics();
Expand Down
28 changes: 28 additions & 0 deletions apps/postshow/src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,36 @@
}
}

/** supabase-js runs a background timer (`autoRefreshToken: true`) that tries to
* rotate the session token. When the stored refresh token is stale (signed out
* in another tab, cleared storage, or an already-expired session) that timer
* throws an uncaught `AuthApiError`, which exception autocapture would file as
* an error. It is an expected auth condition, not a bug — our own `getSession`
* path already handles it — so drop it before it becomes error-tracking noise. */
function isStaleRefreshToken(type: unknown, value: unknown): boolean {
return (
type === 'AuthApiError' && typeof value === 'string' && /invalid refresh token/i.test(value)
);
}

function isBenignAuthRefreshException(result: CaptureResult): boolean {

Check warning on line 72 in apps/postshow/src/lib/analytics.ts

View workflow job for this annotation

GitHub Actions / Unslop Checks

unslop-ci code/verbose-naming (low)

Over-verbose, robotically self-documenting identifier. Fix: A name that is a whole sentence (getUserDataFromApiResponseHandler) reads as machine-generated. Trim it.
if (result.event !== '$exception') return false;
const properties = result.properties;
const list = properties?.$exception_list;
if (Array.isArray(list) && list.some((e) => isStaleRefreshToken(e?.type, e?.value))) return true;
// Fall back to the flat convenience arrays some capture paths populate instead.
const types = properties?.$exception_types;
const values = properties?.$exception_values;
return (
Array.isArray(types) &&
Array.isArray(values) &&
types.some((t, i) => isStaleRefreshToken(t, values[i]))
);
}

function sanitizeCapture(result: CaptureResult | null): CaptureResult | null {
if (!result) return null;
if (isBenignAuthRefreshException(result)) return null;

Check warning on line 89 in apps/postshow/src/lib/analytics.ts

View workflow job for this annotation

GitHub Actions / Unslop Checks

unslop-ci code/verbose-naming (low)

Over-verbose, robotically self-documenting identifier. Fix: A name that is a whole sentence (getUserDataFromApiResponseHandler) reads as machine-generated. Trim it.
const properties = result.properties;
for (const key of ['$current_url', '$referrer', '$referring_url']) {
if (key in properties) properties[key] = stripUrlDetails(properties[key]);
Expand Down
Loading