@@ -24,25 +24,53 @@ const BOOTSTRAP_ERROR_MESSAGE = 'Failed to bootstrap user object';
2424
2525const AUTH_BOUNCE_COUNT_KEY = 'wpcom_auth_bounce_count' ;
2626
27- function trackAuthBounce ( ) {
27+ const AUTH_LOOP_WINDOW_MS = 10 * 1000 ;
28+
29+ interface AuthBounceRecord {
30+ count : number ;
31+ at : number ;
32+ }
33+
34+ // bumpStat when we have a login redirect loop.
35+ // We track the time of the last bounce, and if it was within a window we count
36+ // it towards our loop count. This is more reliable than clearing the counter on
37+ // successful auth, because how do we know when it is safe to clear the count?
38+ // It could be that immediately after successful auth, the very next API returns
39+ // 401 and causes a bounce, yet we would have already cleared the count.
40+ function trackAuthBounceLoop ( ) {
2841 try {
29- const count = Number ( window . sessionStorage . getItem ( AUTH_BOUNCE_COUNT_KEY ) ) + 1 ;
30- window . sessionStorage . setItem ( AUTH_BOUNCE_COUNT_KEY , String ( count ) ) ;
42+ const now = Date . now ( ) ;
43+ const storedRecord : unknown = JSON . parse (
44+ window . sessionStorage . getItem ( AUTH_BOUNCE_COUNT_KEY ) ?? 'null'
45+ ) ;
46+ const previousRecord = isAuthBounceRecord ( storedRecord ) ? storedRecord : null ;
47+ const withinWindow = previousRecord !== null && now - previousRecord . at < AUTH_LOOP_WINDOW_MS ;
48+ const count = withinWindow ? previousRecord . count + 1 : 1 ;
49+
50+ window . sessionStorage . setItem (
51+ AUTH_BOUNCE_COUNT_KEY ,
52+ JSON . stringify ( { count, at : now } satisfies AuthBounceRecord )
53+ ) ;
3154
3255 if ( count >= 2 ) {
33- bumpStat ( 'dashboard-auth' , 'loop' ) ;
56+ bumpStat ( 'dashboard-auth-loop ' , String ( count ) ) ;
3457 }
3558 } catch {
36- // sessionStorage can be unavailable in private contexts.
59+ // sessionStorage can be unavailable in private contexts or JSON.parse may fail .
3760 }
3861}
3962
40- function clearAuthBounceCount ( ) {
41- try {
42- window . sessionStorage . removeItem ( AUTH_BOUNCE_COUNT_KEY ) ;
43- } catch {
44- // sessionStorage can be unavailable in private contexts.
45- }
63+ // Checks that what is stored in sessionStorage matches the AuthBounceRecord
64+ // shape we expect.
65+ function isAuthBounceRecord ( value : unknown ) : value is AuthBounceRecord {
66+ return (
67+ typeof value === 'object' &&
68+ value !== null &&
69+ 'count' in value &&
70+ typeof value . count === 'number' &&
71+ 'at' in value &&
72+ typeof value . at === 'number'
73+ ) ;
4674}
4775
4876function getOAuthAuthorizeUrl ( {
@@ -161,7 +189,7 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
161189 authErrorHandled . current = true ;
162190
163191 bumpStat ( 'dashboard-auth' , `bounce:${ reason } ` ) ;
164- trackAuthBounce ( ) ;
192+ trackAuthBounceLoop ( ) ;
165193
166194 if ( config . isEnabled ( 'oauth' ) ) {
167195 const state = crypto . randomUUID ( ) ;
@@ -227,7 +255,6 @@ export function AuthProvider( { children }: { children: React.ReactNode } ) {
227255 if ( ! successStatBumped . current ) {
228256 successStatBumped . current = true ;
229257 bumpStat ( 'dashboard-auth' , shouldUseBootstrap ( ) ? 'success:bootstrap' : 'success:fetch' ) ;
230- clearAuthBounceCount ( ) ;
231258 }
232259 }
233260 } , [ user ] ) ;
0 commit comments