diff --git a/apps/docs/lib/userAuth.ts b/apps/docs/lib/userAuth.ts index 12fec885fe7..973c5eb8bb1 100644 --- a/apps/docs/lib/userAuth.ts +++ b/apps/docs/lib/userAuth.ts @@ -1,6 +1,11 @@ -import { gotrueClient } from 'common' +import * as Sentry from '@sentry/nextjs' +import { gotrueClient, setCaptureException } from 'common' import { useEffect } from 'react' +setCaptureException((e: any) => { + Sentry.captureException(e) +}) + export const auth = gotrueClient export async function getAccessToken() { diff --git a/apps/studio/lib/gotrue.ts b/apps/studio/lib/gotrue.ts index b51fdd29e71..ab350710682 100644 --- a/apps/studio/lib/gotrue.ts +++ b/apps/studio/lib/gotrue.ts @@ -1,6 +1,11 @@ +import * as Sentry from '@sentry/nextjs' import type { JwtPayload } from '@supabase/supabase-js' import { getAccessToken, type User } from 'common/auth' -import { gotrueClient } from 'common/gotrue' +import { gotrueClient, setCaptureException } from 'common/gotrue' + +setCaptureException((e: any) => { + Sentry.captureException(e) +}) export const auth = gotrueClient export { getAccessToken } diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 0456a464e56..6ab6fb2ec72 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -115,12 +115,72 @@ const logIndexedDB = (message: string, ...args: any[]) => { })() } +let captureException: ((e: any) => any) | null = null + +export function setCaptureException(fn: typeof captureException) { + captureException = fn +} + +async function debuggableNavigatorLock( + name: string, + acquireTimeout: number, + fn: () => Promise +): Promise { + let stackException: any + + try { + throw new Error('Lock is being held for over 2s here') + } catch (e: any) { + stackException = e + } + + const debugTimeout = setTimeout(() => { + ;(async () => { + const bc = new BroadcastChannel('who-is-holding-the-lock') + try { + bc.postMessage({}) + } finally { + bc.close() + } + + console.error( + `Waited for over 2s to acquire an Auth client lock`, + await navigator.locks.query(), + stackException + ) + })() + }, 2000) + + try { + return await navigatorLock(name, acquireTimeout, async () => { + clearTimeout(debugTimeout) + + const bc = new BroadcastChannel('who-is-holding-the-lock') + bc.addEventListener('message', () => { + console.error('Lock is held here', stackException) + + if (captureException) { + captureException(stackException) + } + }) + + try { + return await fn() + } finally { + bc.close() + } + }) + } finally { + clearTimeout(debugTimeout) + } +} + export const gotrueClient = new AuthClient({ url: process.env.NEXT_PUBLIC_GOTRUE_URL, storageKey: STORAGE_KEY, detectSessionInUrl: shouldDetectSessionInUrl, debug: debug ? (persistedDebug ? logIndexedDB : true) : false, - lock: navigatorLockEnabled ? navigatorLock : undefined, + lock: navigatorLockEnabled ? debuggableNavigatorLock : undefined, ...('localStorage' in globalThis ? { storage: globalThis.localStorage, userStorage: globalThis.localStorage }