Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Session] Logging (#4476)

* Add session logging (console.log)

* Hook it up for real

* Send type separately

authored by

dan and committed by
GitHub
4c48a1f1 4bba5979

+178 -6
+37 -5
src/state/session/index.tsx
··· 19 19 import {getInitialState, reducer} from './reducer' 20 20 21 21 export {isSignupQueued} from './util' 22 + import {addSessionDebugLog} from './logging' 22 23 export type {SessionAccount} from '#/state/session/types' 23 24 import {SessionApiContext, SessionStateContext} from '#/state/session/types' 24 25 ··· 40 41 41 42 export function Provider({children}: React.PropsWithChildren<{}>) { 42 43 const cancelPendingTask = useOneTaskAtATime() 43 - const [state, dispatch] = React.useReducer(reducer, null, () => 44 - getInitialState(persisted.get('session').accounts), 45 - ) 44 + const [state, dispatch] = React.useReducer(reducer, null, () => { 45 + const initialState = getInitialState(persisted.get('session').accounts) 46 + addSessionDebugLog({type: 'reducer:init', state: initialState}) 47 + return initialState 48 + }) 46 49 47 50 const onAgentSessionChange = React.useCallback( 48 51 (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { ··· 63 66 64 67 const createAccount = React.useCallback<SessionApiContext['createAccount']>( 65 68 async params => { 69 + addSessionDebugLog({type: 'method:start', method: 'createAccount'}) 66 70 const signal = cancelPendingTask() 67 71 track('Try Create Account') 68 72 logEvent('account:create:begin', {}) ··· 81 85 }) 82 86 track('Create Account') 83 87 logEvent('account:create:success', {}) 88 + addSessionDebugLog({type: 'method:end', method: 'createAccount', account}) 84 89 }, 85 90 [onAgentSessionChange, cancelPendingTask], 86 91 ) 87 92 88 93 const login = React.useCallback<SessionApiContext['login']>( 89 94 async (params, logContext) => { 95 + addSessionDebugLog({type: 'method:start', method: 'login'}) 90 96 const signal = cancelPendingTask() 91 97 const {agent, account} = await createAgentAndLogin( 92 98 params, ··· 103 109 }) 104 110 track('Sign In', {resumedSession: false}) 105 111 logEvent('account:loggedIn', {logContext, withPassword: true}) 112 + addSessionDebugLog({type: 'method:end', method: 'login', account}) 106 113 }, 107 114 [onAgentSessionChange, cancelPendingTask], 108 115 ) 109 116 110 117 const logout = React.useCallback<SessionApiContext['logout']>( 111 118 logContext => { 119 + addSessionDebugLog({type: 'method:start', method: 'logout'}) 112 120 cancelPendingTask() 113 121 dispatch({ 114 122 type: 'logged-out', 115 123 }) 116 124 logEvent('account:loggedOut', {logContext}) 125 + addSessionDebugLog({type: 'method:end', method: 'logout'}) 117 126 }, 118 127 [cancelPendingTask], 119 128 ) 120 129 121 130 const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( 122 131 async storedAccount => { 132 + addSessionDebugLog({ 133 + type: 'method:start', 134 + method: 'resumeSession', 135 + account: storedAccount, 136 + }) 123 137 const signal = cancelPendingTask() 124 138 const {agent, account} = await createAgentAndResume( 125 139 storedAccount, ··· 134 148 newAgent: agent, 135 149 newAccount: account, 136 150 }) 151 + addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) 137 152 }, 138 153 [onAgentSessionChange, cancelPendingTask], 139 154 ) 140 155 141 156 const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 142 157 account => { 158 + addSessionDebugLog({ 159 + type: 'method:start', 160 + method: 'removeAccount', 161 + account, 162 + }) 143 163 cancelPendingTask() 144 164 dispatch({ 145 165 type: 'removed-account', 146 166 accountDid: account.did, 147 167 }) 168 + addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) 148 169 }, 149 170 [cancelPendingTask], 150 171 ) ··· 152 173 React.useEffect(() => { 153 174 if (state.needsPersist) { 154 175 state.needsPersist = false 155 - persisted.write('session', { 176 + const persistedData = { 156 177 accounts: state.accounts, 157 178 currentAccount: state.accounts.find( 158 179 a => a.did === state.currentAgentState.did, 159 180 ), 160 - }) 181 + } 182 + addSessionDebugLog({type: 'persisted:broadcast', data: persistedData}) 183 + persisted.write('session', persistedData) 161 184 } 162 185 }, [state]) 163 186 164 187 React.useEffect(() => { 165 188 return persisted.onUpdate(() => { 166 189 const synced = persisted.get('session') 190 + addSessionDebugLog({type: 'persisted:receive', data: synced}) 167 191 dispatch({ 168 192 type: 'synced-accounts', 169 193 syncedAccounts: synced.accounts, ··· 177 201 resumeSession(syncedAccount) 178 202 } else { 179 203 const agent = state.currentAgentState.agent as BskyAgent 204 + const prevSession = agent.session 180 205 agent.session = sessionAccountToSession(syncedAccount) 206 + addSessionDebugLog({ 207 + type: 'agent:patch', 208 + agent, 209 + prevSession, 210 + nextSession: agent.session, 211 + }) 181 212 } 182 213 } 183 214 }) ··· 215 246 // Read the previous value and immediately advance the pointer. 216 247 const prevAgent = currentAgentRef.current 217 248 currentAgentRef.current = agent 249 + addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent}) 218 250 // We never reuse agents so let's fully neutralize the previous one. 219 251 // This ensures it won't try to consume any refresh tokens. 220 252 prevAgent.session = undefined
+137
src/state/session/logging.ts
··· 1 + import {AtpSessionData} from '@atproto/api' 2 + import {sha256} from 'js-sha256' 3 + import {Statsig} from 'statsig-react-native-expo' 4 + 5 + import {Schema} from '../persisted' 6 + import {Action, State} from './reducer' 7 + import {SessionAccount} from './types' 8 + 9 + type Reducer = (state: State, action: Action) => State 10 + 11 + type Log = 12 + | { 13 + type: 'reducer:init' 14 + state: State 15 + } 16 + | { 17 + type: 'reducer:call' 18 + action: Action 19 + prevState: State 20 + nextState: State 21 + } 22 + | { 23 + type: 'method:start' 24 + method: 25 + | 'createAccount' 26 + | 'login' 27 + | 'logout' 28 + | 'resumeSession' 29 + | 'removeAccount' 30 + account?: SessionAccount 31 + } 32 + | { 33 + type: 'method:end' 34 + method: 35 + | 'createAccount' 36 + | 'login' 37 + | 'logout' 38 + | 'resumeSession' 39 + | 'removeAccount' 40 + account?: SessionAccount 41 + } 42 + | { 43 + type: 'persisted:broadcast' 44 + data: Schema['session'] 45 + } 46 + | { 47 + type: 'persisted:receive' 48 + data: Schema['session'] 49 + } 50 + | { 51 + type: 'agent:switch' 52 + prevAgent: object 53 + nextAgent: object 54 + } 55 + | { 56 + type: 'agent:patch' 57 + agent: object 58 + prevSession: AtpSessionData | undefined 59 + nextSession: AtpSessionData 60 + } 61 + 62 + export function wrapSessionReducerForLogging(reducer: Reducer): Reducer { 63 + return function loggingWrapper(prevState: State, action: Action): State { 64 + const nextState = reducer(prevState, action) 65 + addSessionDebugLog({type: 'reducer:call', prevState, action, nextState}) 66 + return nextState 67 + } 68 + } 69 + 70 + let nextMessageIndex = 0 71 + const MAX_SLICE_LENGTH = 1000 72 + 73 + export function addSessionDebugLog(log: Log) { 74 + try { 75 + if (!Statsig.initializeCalled() || !Statsig.getStableID()) { 76 + // Drop these logs for now. 77 + return 78 + } 79 + if (!Statsig.checkGate('debug_session')) { 80 + return 81 + } 82 + const messageIndex = nextMessageIndex++ 83 + const {type, ...content} = log 84 + let payload = JSON.stringify(content, replacer) 85 + 86 + let nextSliceIndex = 0 87 + while (payload.length > 0) { 88 + const sliceIndex = nextSliceIndex++ 89 + const slice = payload.slice(0, MAX_SLICE_LENGTH) 90 + payload = payload.slice(MAX_SLICE_LENGTH) 91 + Statsig.logEvent('session:debug', null, { 92 + realmId, 93 + messageIndex: String(messageIndex), 94 + messageType: type, 95 + sliceIndex: String(sliceIndex), 96 + slice, 97 + }) 98 + } 99 + } catch (e) { 100 + console.error(e) 101 + } 102 + } 103 + 104 + let agentIds = new WeakMap<object, string>() 105 + let realmId = Math.random().toString(36).slice(2) 106 + let nextAgentId = 1 107 + 108 + function getAgentId(agent: object) { 109 + let id = agentIds.get(agent) 110 + if (id === undefined) { 111 + id = realmId + '::' + nextAgentId++ 112 + agentIds.set(agent, id) 113 + } 114 + return id 115 + } 116 + 117 + function replacer(key: string, value: unknown) { 118 + if (typeof value === 'object' && value != null && 'api' in value) { 119 + return getAgentId(value) 120 + } 121 + if ( 122 + key === 'service' || 123 + key === 'email' || 124 + key === 'emailConfirmed' || 125 + key === 'emailAuthFactor' || 126 + key === 'pdsUrl' 127 + ) { 128 + return undefined 129 + } 130 + if ( 131 + typeof value === 'string' && 132 + (key === 'refreshJwt' || key === 'accessJwt') 133 + ) { 134 + return sha256(value) 135 + } 136 + return value 137 + }
+4 -1
src/state/session/reducer.ts
··· 1 1 import {AtpSessionEvent} from '@atproto/api' 2 2 3 3 import {createPublicAgent} from './agent' 4 + import {wrapSessionReducerForLogging} from './logging' 4 5 import {SessionAccount} from './types' 5 6 6 7 // A hack so that the reducer can't read anything from the agent. ··· 64 65 } 65 66 } 66 67 67 - export function reducer(state: State, action: Action): State { 68 + let reducer = (state: State, action: Action): State => { 68 69 switch (action.type) { 69 70 case 'received-agent-event': { 70 71 const {agent, accountDid, refreshedAccount, sessionEvent} = action ··· 166 167 } 167 168 } 168 169 } 170 + reducer = wrapSessionReducerForLogging(reducer) 171 + export {reducer}