fix: give the dashboard's auth errors a real stack and stop reporting an ended session as a crash, by building AuthError per throw instead of sharing two module-level instances whose stack was captured at module evaluation, so every report pointed at "module code" rather than the call that failed, and by dropping AuthError in before_send since normalizeError already turns it into a redirect and UserProvider sends the user to sign in (#527)

This commit is contained in:
Matthew Meszaros
2026-09-15 01:52:35 -07:00
committed by GitHub
parent 0a1ed6f04e
commit d40a95dff5
3 changed files with 21 additions and 9 deletions
+7 -7
View File
@@ -2,7 +2,7 @@ import type { AxiosRequestConfig } from "axios"
import Client from "./Client"
import getToken from "@/lib/helper/getToken"
import isExpired from "@/lib/helper/isExpired";
import { NoToken, SessionExpired } from "@/lib/errors/auth";
import { noToken, sessionExpired } from "@/lib/errors/auth";
import refreshTokenFn from "./auth/refreshToken";
import setToken from "@/lib/helper/setToken";
import reviveDates from "@/lib/helper/reviveDates";
@@ -20,7 +20,7 @@ let refreshPromise: Promise<Token> | null = null;
async function ensureValidToken(): Promise<Token> {
const token = getToken();
if (!token) {
throw NoToken;
throw noToken();
}
if (token.access_token && !isExpired(token.access_token_expires_at)) {
@@ -30,7 +30,7 @@ async function ensureValidToken(): Promise<Token> {
// Access token expired — need to refresh
if (!token.refresh_token || isExpired(token.refresh_token_expires_at)) {
clearTokens();
throw SessionExpired;
throw sessionExpired();
}
// If a refresh is already in progress, wait for it
@@ -41,9 +41,9 @@ async function ensureValidToken(): Promise<Token> {
if (updated && updated.access_token && !isExpired(updated.access_token_expires_at)) {
return updated;
}
throw SessionExpired;
throw sessionExpired();
} catch {
throw SessionExpired;
throw sessionExpired();
}
}
@@ -55,7 +55,7 @@ async function ensureValidToken(): Promise<Token> {
return newToken;
} catch {
clearTokens();
throw SessionExpired;
throw sessionExpired();
} finally {
refreshPromise = null;
}
@@ -90,7 +90,7 @@ export default async function Request<T>(config: AuthRequestConfig): Promise<T>
return reviveDates(res.data)
} catch {
clearTokens();
throw SessionExpired;
throw sessionExpired();
}
}
+5 -2
View File
@@ -5,5 +5,8 @@ export class AuthError extends Error {
}
}
export const SessionExpired = new AuthError("Session expired")
export const NoToken = new AuthError("No authorization token")
// Built per throw, not shared. A module-level instance captures its stack once,
// at module evaluation, so every report pointed at "module code" instead of the
// call that failed, and one mutable Error was shared across concurrent requests.
export const sessionExpired = () => new AuthError("Session expired")
export const noToken = () => new AuthError("No authorization token")
+9
View File
@@ -166,7 +166,16 @@ const NOISE = [
"ResizeObserver loop limit exceeded",
];
// A session ending is a lifecycle event, not a crash: normalizeError turns an
// AuthError into a redirect and UserProvider sends the user to sign in. It
// reached error tracking only by also escaping to the global rejection handler.
const NOISE_TYPES = ["AuthError"];
function isNoise(properties: Properties): boolean {
const types = properties.$exception_types;
if (Array.isArray(types) && types.some((t) => typeof t === "string" && NOISE_TYPES.includes(t))) {
return true;
}
const values = properties.$exception_values;
if (!Array.isArray(values)) return false;
return values.some((v) => typeof v === "string" && NOISE.includes(v.trim()));