Add metrics client
This commit is contained in:
@@ -28,6 +28,9 @@ EXPO_PUBLIC_CHAT_PROXY_DID=
|
||||
#
|
||||
#
|
||||
|
||||
# Bluesky's metrics API
|
||||
EXPO_PUBLIC_METRICS_API_HOST=
|
||||
|
||||
# Growthbook config
|
||||
EXPO_PUBLIC_GROWTHBOOK_API_HOST=
|
||||
EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY=
|
||||
|
||||
@@ -8,8 +8,7 @@ import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {logger, type Metrics} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
@@ -450,12 +449,11 @@ export function ProfileGrid({
|
||||
const seenProfilesRef = useRef<Set<string>>(new Set())
|
||||
const containerRef = useRef<View>(null)
|
||||
const hasTrackedRef = useRef(false)
|
||||
const logContext: MetricEvents['suggestedUser:seen']['logContext'] =
|
||||
isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: isProfileHeaderContext
|
||||
? 'Profile'
|
||||
: 'InterstitialProfile'
|
||||
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: isProfileHeaderContext
|
||||
? 'Profile'
|
||||
: 'InterstitialProfile'
|
||||
|
||||
// Callback to fire seen events
|
||||
const fireSeen = useCallback(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {t} from '@lingui/macro'
|
||||
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logEvent} from '#/logger/growthbook'
|
||||
import {logger} from '#/logger'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Toast from '#/components/Toast'
|
||||
@@ -33,7 +33,7 @@ export function DiscoverDebug({
|
||||
style={[a.absolute, {zIndex: 1000, maxWidth: 65, bottom: -4}, a.left_0]}
|
||||
onPress={e => {
|
||||
e.stopPropagation()
|
||||
logEvent('discover_debug:copy_feed_context', {feedContext})
|
||||
logger.metric('debug', {feedContext})
|
||||
Clipboard.setStringAsync(feedContext)
|
||||
Toast.show(t`Copied to clipboard`)
|
||||
}}>
|
||||
|
||||
Vendored
+8
-2
@@ -84,11 +84,17 @@ export const BLUESKY_PROXY_DID: Did =
|
||||
export const CHAT_PROXY_DID: Did =
|
||||
process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
|
||||
|
||||
/**
|
||||
* Metrics API host
|
||||
*/
|
||||
export const METRICS_API_HOST: string =
|
||||
process.env.EXPO_PUBLIC_METRICS_API_HOST || 'https://events.bsky.app'
|
||||
|
||||
/**
|
||||
* Growthbook API host
|
||||
*/
|
||||
export const GROWTHBOOK_API_HOST: string | undefined =
|
||||
process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST
|
||||
export const GROWTHBOOK_API_HOST: string =
|
||||
process.env.EXPO_PUBLIC_GROWTHBOOK_API_HOST || `${METRICS_API_HOST}/gb`
|
||||
|
||||
/**
|
||||
* Growthbook client key
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
|
||||
export const getCurrentState = () => AppState.currentState
|
||||
|
||||
export function onAppStateChange(cb: (state: AppStateStatus) => void) {
|
||||
let prev = AppState.currentState
|
||||
return AppState.addEventListener('change', next => {
|
||||
if (next === prev) return
|
||||
cb(next)
|
||||
})
|
||||
}
|
||||
|
||||
export function useAppState() {
|
||||
const [state, setState] = useState(AppState.currentState)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = onAppStateChange(next => {
|
||||
setState(next)
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [])
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {useAppState as useAppStateBase} from '#/lib/appState'
|
||||
|
||||
export function useAppState() {
|
||||
const [state, setState] = useState(AppState.currentState)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = AppState.addEventListener('change', nextAppState => {
|
||||
setState(nextAppState)
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [])
|
||||
|
||||
return state
|
||||
}
|
||||
/**
|
||||
* @deprecated use `useAppState` from `#/lib/appState` instead
|
||||
*/
|
||||
export const useAppState = useAppStateBase
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {useCallback, useRef} from 'react'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {logger, type Metrics} from '#/logger'
|
||||
|
||||
/**
|
||||
* Hook that returns a callback to track post:view events.
|
||||
@@ -12,7 +11,7 @@ import {type MetricEvents} from '#/logger/metrics'
|
||||
* @returns A callback that accepts a post and logs the view event
|
||||
*/
|
||||
export function usePostViewTracking(
|
||||
logContext: MetricEvents['post:view']['logContext'],
|
||||
logContext: Metrics['post:view']['logContext'],
|
||||
) {
|
||||
const seenUrisRef = useRef(new Set<string>())
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import {Platform} from 'react-native'
|
||||
import {AppState, type AppStateStatus} from 'react-native'
|
||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {logger, type Metrics} from '#/logger'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {IS_WEB} from '#/env'
|
||||
import * as env from '#/env'
|
||||
@@ -43,7 +42,7 @@ if (IS_WEB && typeof window !== 'undefined') {
|
||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||
}
|
||||
|
||||
export type {MetricEvents as LogEvents}
|
||||
export type {Metrics as LogEvents}
|
||||
|
||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||
return {
|
||||
@@ -94,9 +93,9 @@ export function toClout(n: number | null | undefined): number | undefined {
|
||||
/**
|
||||
* @deprecated use `logger.metric()` instead
|
||||
*/
|
||||
export function logEvent<E extends keyof MetricEvents>(
|
||||
export function logEvent<E extends keyof Metrics>(
|
||||
eventName: E & string,
|
||||
rawMetadata: MetricEvents[E] & FlatJSONRecord,
|
||||
rawMetadata: Metrics[E] & FlatJSONRecord,
|
||||
options: {
|
||||
/**
|
||||
* Send to our data lake only, not to StatSig
|
||||
@@ -127,8 +126,8 @@ export function logEvent<E extends keyof MetricEvents>(
|
||||
}
|
||||
}
|
||||
|
||||
function toStringRecord<E extends keyof MetricEvents>(
|
||||
metadata: MetricEvents[E] & FlatJSONRecord,
|
||||
function toStringRecord<E extends keyof Metrics>(
|
||||
metadata: Metrics[E] & FlatJSONRecord,
|
||||
): Record<string, string> {
|
||||
const record: Record<string, string> = {}
|
||||
for (let key in metadata) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {growthbookTrackingPlugin} from '@growthbook/growthbook/plugins'
|
||||
import {GrowthBook} from '@growthbook/growthbook-react'
|
||||
|
||||
import {BSKY_SERVICE} from '#/lib/constants'
|
||||
@@ -15,6 +14,13 @@ import {type SessionAccount} from '#/state/session'
|
||||
import * as env from '#/env'
|
||||
import {device} from '#/storage'
|
||||
|
||||
const debugEnabled = env.IS_DEV && true
|
||||
const debug = (message: string, attributes?: Record<string, any>) => {
|
||||
if (debugEnabled) {
|
||||
console.debug(`(growthbook) ${message}`, attributes || {})
|
||||
}
|
||||
}
|
||||
|
||||
const TIMEOUT_INIT = 500 // TODO should base on p99 or something
|
||||
const TIMEOUT_PREFER_LOW_LATENCY = 250
|
||||
const TIMEOUT_PREFER_FRESH_GATES = 1500
|
||||
@@ -60,21 +66,17 @@ type UserAttributes = GrowthBookDefaultUserAttributes & {
|
||||
appLanguage: string
|
||||
contentLanguages: string[]
|
||||
}
|
||||
type Attributes = DefaultAttributes & UserAttributes
|
||||
export type Attributes = DefaultAttributes & Partial<UserAttributes>
|
||||
|
||||
const gb = new GrowthBook({
|
||||
apiHost: env.GROWTHBOOK_API_HOST,
|
||||
clientKey: env.GROWTHBOOK_CLIENT_KEY,
|
||||
plugins: [growthbookTrackingPlugin()],
|
||||
trackingCallback: (experiment, result) => {
|
||||
console.debug('Experiment Viewed', {
|
||||
experimentId: experiment.key,
|
||||
variationId: result.key,
|
||||
})
|
||||
gb.logEvent('Experiment Viewed', {
|
||||
debug(`Experiment Viewed`, {
|
||||
experimentId: experiment.key,
|
||||
variationId: result.key,
|
||||
})
|
||||
// TODO
|
||||
},
|
||||
attributes: getDefaultAttributes(),
|
||||
})
|
||||
@@ -101,6 +103,14 @@ export const initializer = new Promise<void>(async y => {
|
||||
y()
|
||||
})
|
||||
|
||||
export function getGrowthBook() {
|
||||
return gb
|
||||
}
|
||||
|
||||
export function getGrowthBookAttributes(): Attributes {
|
||||
return gb.getAttributes() as Attributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh feature gates from GrowthBook. Updates attributes based on the
|
||||
* provided account, if any.
|
||||
@@ -112,6 +122,7 @@ export async function refresh({
|
||||
account?: SessionAccount
|
||||
strategy: FeatureFetchStrategy
|
||||
}) {
|
||||
debug(`refresh`, {account: !!account, strategy})
|
||||
setAttributesForAccount(account)
|
||||
await gb.refreshFeatures({
|
||||
timeout:
|
||||
@@ -121,13 +132,6 @@ export async function refresh({
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a custom event to our backend, using GrowthBook's event logging system.
|
||||
*/
|
||||
export function logEvent(eventName: string, metadata?: Record<string, any>) {
|
||||
gb.logEvent(eventName, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a feature gate is enabled
|
||||
*/
|
||||
@@ -160,11 +164,11 @@ function setAttributesForAccount(account?: SessionAccount) {
|
||||
...(getUserAttributes(account) || {}),
|
||||
}
|
||||
gb.setAttributes(attr)
|
||||
console.debug(`setAttributesForAccount: has account`, {attributes: attr})
|
||||
debug(`setAttributesForAccount: has account`, {attributes: attr})
|
||||
} else {
|
||||
const attr = getDefaultAttributes()
|
||||
gb.setAttributes(attr)
|
||||
console.debug(`setAttributesForAccount: no account`, {attributes: attr})
|
||||
debug(`setAttributesForAccount: no account`, {attributes: attr})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-10
@@ -1,8 +1,7 @@
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {logEvent} from '#/logger/growthbook'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {type Metrics, metrics} from '#/logger/metrics'
|
||||
import {consoleTransport} from '#/logger/transports/console'
|
||||
import {sentryTransport} from '#/logger/transports/sentry'
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
import {enabledLogLevels} from '#/logger/util'
|
||||
import {ENV} from '#/env'
|
||||
|
||||
export {type MetricEvents as Metrics} from '#/logger/metrics'
|
||||
export {type Metrics} from '#/logger/metrics'
|
||||
|
||||
const TRANSPORTS: Transport[] = (function configureTransports() {
|
||||
switch (ENV) {
|
||||
@@ -95,20 +94,17 @@ export class Logger {
|
||||
this.transport({level: LogLevel.Error, message: error, metadata})
|
||||
}
|
||||
|
||||
metric<E extends keyof MetricEvents>(
|
||||
metric<E extends keyof Metrics>(
|
||||
event: E & string,
|
||||
metadata: MetricEvents[E],
|
||||
options: {
|
||||
metadata: Metrics[E],
|
||||
_: {
|
||||
/**
|
||||
* Optionally also send to StatSig
|
||||
*/
|
||||
statsig?: boolean
|
||||
} = {statsig: true},
|
||||
) {
|
||||
logEvent(event, metadata, {
|
||||
lake: !options.statsig,
|
||||
})
|
||||
|
||||
metrics.track(event, metadata)
|
||||
for (const transport of this.transports) {
|
||||
transport(LogLevel.Info, LogContext.Metric, event, metadata, Date.now())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||
|
||||
export type MetricEvents = {
|
||||
export type Metrics = {
|
||||
// App events
|
||||
init: {
|
||||
initMs: number
|
||||
@@ -374,7 +374,6 @@ export type MetricEvents = {
|
||||
| 'AvatarButton'
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
| 'ExploreSuggestedAccounts'
|
||||
@@ -468,7 +467,6 @@ export type MetricEvents = {
|
||||
| 'AvatarButton'
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
| 'ExploreSuggestedAccounts'
|
||||
@@ -0,0 +1,124 @@
|
||||
import {getCurrentState, onAppStateChange} from '#/lib/appState'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {
|
||||
type Attributes,
|
||||
getGrowthBook,
|
||||
getGrowthBookAttributes,
|
||||
} from '#/logger/growthbook'
|
||||
import {type Metrics} from '#/logger/metrics/events'
|
||||
import {Sentry} from '#/logger/sentry/lib'
|
||||
import * as env from '#/env'
|
||||
|
||||
export {type Metrics} from '#/logger/metrics/events'
|
||||
|
||||
type Event<M extends Metrics> = {
|
||||
time: number
|
||||
event: keyof M
|
||||
payload: M[keyof M]
|
||||
metadata: Attributes
|
||||
}
|
||||
|
||||
const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/track'
|
||||
|
||||
export const metrics = new (class Tracker {
|
||||
private started: boolean = false
|
||||
private queue: Event<Metrics>[] = []
|
||||
private failedQueue: Event<Metrics>[] = []
|
||||
private flushInterval: NodeJS.Timeout | null = null
|
||||
|
||||
start() {
|
||||
if (this.started) return
|
||||
if (!getGrowthBook().ready) return
|
||||
this.started = true
|
||||
this.flushInterval = setInterval(() => {
|
||||
this.flush()
|
||||
}, 10_000)
|
||||
onAppStateChange(state => {
|
||||
if (state === 'active') {
|
||||
this.retryFailedLogs()
|
||||
} else {
|
||||
this.flush()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
track<E extends keyof Metrics>(event: E, payload: Metrics[E]) {
|
||||
this.start()
|
||||
|
||||
this.queue.push({
|
||||
time: Date.now(),
|
||||
event,
|
||||
payload,
|
||||
metadata: getGrowthBookAttributes(),
|
||||
})
|
||||
|
||||
if (this.queue.length > 100) {
|
||||
this.flush()
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (!this.queue.length) return
|
||||
const events = this.queue.splice(0, this.queue.length)
|
||||
this.queue = []
|
||||
this.sendBatch(events)
|
||||
}
|
||||
|
||||
private async sendBatch(events: Event<Metrics>[], isRetry: boolean = false) {
|
||||
try {
|
||||
const body = JSON.stringify(events)
|
||||
if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) {
|
||||
const success = navigator.sendBeacon(
|
||||
TRACKING_ENDPOINT,
|
||||
new Blob([body], {type: 'application/json'}),
|
||||
)
|
||||
console.log({success})
|
||||
} else {
|
||||
const res = await fetch(TRACKING_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
keepalive: true,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text().catch(() => 'Unknown error')
|
||||
// construct a "network error" for `isNetworkError` to work
|
||||
throw new Error(`${res.status} Failed to fetch — ${error}`)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
if (isRetry) return // retry once
|
||||
this.failedQueue.push(...events)
|
||||
return
|
||||
}
|
||||
Sentry.captureException(`Failed to send metrics`, {
|
||||
extra: {
|
||||
safeMessage: e.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private retryFailedLogs() {
|
||||
if (!this.failedQueue.length) return
|
||||
const events = this.failedQueue.splice(0, this.failedQueue.length)
|
||||
this.failedQueue = []
|
||||
this.sendBatch(events, true)
|
||||
}
|
||||
})()
|
||||
|
||||
let lastActive = getCurrentState() === 'active' ? performance.now() : null
|
||||
onAppStateChange(state => {
|
||||
if (state === 'active') {
|
||||
lastActive = performance.now()
|
||||
metrics.track('state:foreground', {})
|
||||
} else if (lastActive !== null) {
|
||||
metrics.track('state:background', {
|
||||
secondsActive: Math.round((performance.now() - lastActive) / 1e3),
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -15,7 +15,6 @@ export enum LogContext {
|
||||
AgeAssurance = 'age-assurance',
|
||||
PolicyUpdate = 'policy-update',
|
||||
Geolocation = 'geolocation',
|
||||
GrowthBook = 'growthbook',
|
||||
|
||||
/**
|
||||
* METRIC IS FOR INTERNAL USE ONLY, don't create any other loggers using this
|
||||
|
||||
@@ -13,8 +13,7 @@ import * as bcp47Match from 'bcp-47-match'
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {logger, type Metrics} from '#/logger'
|
||||
import {useLanguagePrefs} from '#/state/preferences/languages'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {RQKEY_ROOT as useActorSearchQueryKeyRoot} from '#/state/queries/actor-search'
|
||||
@@ -124,7 +123,7 @@ type ExploreScreenItems =
|
||||
bottomBorder?: boolean
|
||||
searchButton?: {
|
||||
label: string
|
||||
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
||||
metricsTag: Metrics['explore:module:searchButtonPress']['module']
|
||||
tab: 'user' | 'profile' | 'feed'
|
||||
}
|
||||
}
|
||||
@@ -135,7 +134,7 @@ type ExploreScreenItems =
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
searchButton?: {
|
||||
label: string
|
||||
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
|
||||
metricsTag: Metrics['explore:module:searchButtonPress']['module']
|
||||
tab: 'user' | 'profile' | 'feed'
|
||||
}
|
||||
hideDefaultTab?: boolean
|
||||
@@ -729,12 +728,7 @@ export function Explore({
|
||||
<ModuleHeader.SearchButton
|
||||
{...item.searchButton}
|
||||
onPress={() =>
|
||||
focusSearchInput(
|
||||
(item.searchButton?.tab || 'user') as
|
||||
| 'user'
|
||||
| 'profile'
|
||||
| 'feed',
|
||||
)
|
||||
focusSearchInput(item.searchButton?.tab || 'user')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -751,12 +745,7 @@ export function Explore({
|
||||
<ModuleHeader.SearchButton
|
||||
{...item.searchButton}
|
||||
onPress={() =>
|
||||
focusSearchInput(
|
||||
(item.searchButton?.tab || 'user') as
|
||||
| 'user'
|
||||
| 'profile'
|
||||
| 'feed',
|
||||
)
|
||||
focusSearchInput(item.searchButton?.tab || 'user')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -1043,7 +1032,7 @@ export function Explore({
|
||||
const seenProfilesRef = useRef<Set<string>>(new Set())
|
||||
const onItemSeen = useCallback(
|
||||
(item: ExploreScreenItems) => {
|
||||
let module: MetricEvents['explore:module:seen']['module']
|
||||
let module: Metrics['explore:module:seen']['module']
|
||||
if (item.type === 'trendingTopics' || item.type === 'trendingVideos') {
|
||||
module = item.type
|
||||
} else if (item.type === 'profile') {
|
||||
|
||||
@@ -170,6 +170,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
SessionApiContext['logoutCurrentAccount']
|
||||
>(
|
||||
logContext => {
|
||||
// TODO
|
||||
// const gates = refreshGates({account, strategy: 'prefer-fresh-gates'})
|
||||
addSessionDebugLog({type: 'method:start', method: 'logout'})
|
||||
cancelPendingTask()
|
||||
const prevState = store.getState()
|
||||
|
||||
Reference in New Issue
Block a user