diff --git a/frontend/sentry-init.js b/frontend/sentry-init.js index 9e7dd76f..34c13767 100644 --- a/frontend/sentry-init.js +++ b/frontend/sentry-init.js @@ -9,6 +9,7 @@ import * as Sentry from '@sentry/vue'; import { onAppEvent } from '@/utils/app-events'; import { isValidIP } from '@/utils/ip/valid-ip.js'; +import { readAccountId } from '@/utils/auth-hint.js'; const env = import.meta.env ?? {}; @@ -20,6 +21,13 @@ const SCREENSHOT_CSS_NOISE = [ 'Error inlining remote css file', ]; +// Direct captures carry the FirebaseError as originalException; console +// captures (`console.error('… sign-in failed:', error)`) surface it as the +// event's exception type. +const isFirebaseError = (event, hint) => + hint?.originalException?.name === 'FirebaseError' + || (event.exception?.values ?? []).some((value) => value?.type === 'FirebaseError'); + // `earlyErrors` is main.js's pre-init buffer: ErrorEvent / // PromiseRejectionEvent entries from its temporary window listeners, plus // the raw Error from the mount chain's catch. Flushed right after init; @@ -144,6 +152,11 @@ const initSentry = (app, router, earlyErrors = []) => { // per-source console.error lines remain the health signal. if (String(hint?.originalException?.message ?? '') .startsWith('All sources failed to fetch IP details')) return null; + // Firebase errors carry account id + if (isFirebaseError(event, hint)) { + const accountId = readAccountId(); + if (accountId) event.user = { ...event.user, id: accountId }; + } if (event.logger === 'console') { const firstArg = event.extra?.arguments?.[0]; if (typeof firstArg === 'string' && firstArg.trim()) { diff --git a/frontend/store.js b/frontend/store.js index e1f813e4..9e7b54ec 100644 --- a/frontend/store.js +++ b/frontend/store.js @@ -1,7 +1,7 @@ // store.js import { defineStore } from 'pinia'; import { isFireBaseSet, loadFirebaseAuth } from './firebase-init.js'; -import { writeAuthHint } from './utils/auth-hint.js'; +import { rememberAccountId, writeAuthHint } from './utils/auth-hint.js'; import { markPendingRedirectSignIn, shouldFallBackToRedirect, takePendingRedirectSignIn } from './utils/auth-redirect.js'; import { isRunningAsPwa } from './utils/pwa.js'; import i18n from './locales/i18n.js'; @@ -275,6 +275,7 @@ export const useMainStore = defineStore('main', { const result = await fb.signInWithPopup(fb.auth, descriptor.build(fb)); this.user = result.user; writeAuthHint(true); + rememberAccountId(result.user?.uid); window.location.reload(); return; } catch (error) { @@ -302,6 +303,7 @@ export const useMainStore = defineStore('main', { this.user = result.user; this.isSignedIn = true; writeAuthHint(true); + rememberAccountId(result.user.uid); return true; } catch (error) { this.handleSignInError(error, descriptor); @@ -354,6 +356,7 @@ export const useMainStore = defineStore('main', { this.user = currentUser; if (currentUser) { this.isSignedIn = true; + rememberAccountId(currentUser.uid); } writeAuthHint(!!currentUser); unsubscribe(); // unsubscribe immediately after getting user state diff --git a/frontend/utils/auth-hint.js b/frontend/utils/auth-hint.js index 7c6f09fc..0c46bf11 100644 --- a/frontend/utils/auth-hint.js +++ b/frontend/utils/auth-hint.js @@ -20,3 +20,25 @@ export const writeAuthHint = (signedIn) => { /* storage disabled — every boot stays on the unknown path */ } }; + +// Last account id signed in on this browser. +const ACCOUNT_ID_KEY = 'lastAccountId'; +const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/; + +export const readAccountId = () => { + try { + const value = localStorage.getItem(ACCOUNT_ID_KEY); + return value && ACCOUNT_ID_PATTERN.test(value) ? value : null; + } catch { + return null; + } +}; + +export const rememberAccountId = (uid) => { + if (typeof uid !== 'string' || !ACCOUNT_ID_PATTERN.test(uid)) return; + try { + localStorage.setItem(ACCOUNT_ID_KEY, uid); + } catch { + /* storage disabled — Firebase errors just go without an account id */ + } +};