WIP revamp
This commit is contained in:
+27
-25
@@ -23,7 +23,6 @@ import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {initializer as growthbookInitializer} from '#/logger/growthbook'
|
||||
import {setupDeviceId} from '#/logger/metadata'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
@@ -71,6 +70,7 @@ import {
|
||||
prefetchAgeAssuranceConfig,
|
||||
Provider as AgeAssuranceV2Provider,
|
||||
} from '#/ageAssurance'
|
||||
import {AnalyticsContext, setupDeviceId} from '#/analytics'
|
||||
import {IS_ANDROID, IS_IOS} from '#/env'
|
||||
import {
|
||||
prefetchLiveEvents,
|
||||
@@ -228,30 +228,32 @@ function App() {
|
||||
<A11yProvider>
|
||||
<KeyboardControllerProvider>
|
||||
<OnboardingProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
<AnalyticsContext>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<BottomSheetProvider>
|
||||
<StarterPackProvider>
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<InnerApp />
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
</BottomSheetProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</AnalyticsContext>
|
||||
</OnboardingProvider>
|
||||
</KeyboardControllerProvider>
|
||||
</A11yProvider>
|
||||
|
||||
+22
-20
@@ -14,7 +14,6 @@ import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {initializer as growthbookInitializer} from '#/logger/growthbook'
|
||||
import {setupDeviceId} from '#/logger/metadata'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
@@ -61,6 +60,7 @@ import {
|
||||
prefetchAgeAssuranceConfig,
|
||||
Provider as AgeAssuranceV2Provider,
|
||||
} from '#/ageAssurance'
|
||||
import {AnalyticsContext, setupDeviceId} from '#/analytics'
|
||||
import {
|
||||
prefetchLiveEvents,
|
||||
Provider as LiveEventsProvider,
|
||||
@@ -202,25 +202,27 @@ function App() {
|
||||
<Geo.Provider>
|
||||
<A11yProvider>
|
||||
<OnboardingProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
<AnalyticsContext>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</SessionProvider>
|
||||
</AnalyticsContext>
|
||||
</OnboardingProvider>
|
||||
</A11yProvider>
|
||||
</Geo.Provider>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid from 'react-native-uuid'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {device} from '#/storage'
|
||||
|
||||
const LEGACY_STABLE_ID = 'STATSIG_LOCAL_STORAGE_STABLE_ID'
|
||||
|
||||
export async function getAndMigrateDeviceId() {
|
||||
const migrated = getDeviceId()
|
||||
if (migrated) return migrated
|
||||
const id = (await AsyncStorage.getItem(LEGACY_STABLE_ID)) || uuid.v4()
|
||||
device.set(['deviceId'], id)
|
||||
return id
|
||||
}
|
||||
|
||||
export function getDeviceId() {
|
||||
return device.get(['deviceId'])
|
||||
}
|
||||
|
||||
export function getDeviceIdOrThrow() {
|
||||
const id = device.get(['deviceId'])
|
||||
if (!id) {
|
||||
throw new Error(`deviceId is not set, call getAndMigrateDeviceId first`)
|
||||
}
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '#/analytics/identifiers/device'
|
||||
export * from '#/analytics/identifiers/session'
|
||||
@@ -0,0 +1,40 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import uuid from 'react-native-uuid'
|
||||
|
||||
import {onAppStateChange} from '#/lib/appState'
|
||||
import {isSessionIdExpired} from '#/analytics/identifiers/util'
|
||||
import {device} from '#/storage'
|
||||
|
||||
let sessionId = (() => {
|
||||
const existing = device.get(['nativeSessionId'])
|
||||
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
|
||||
const id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4()
|
||||
device.set(['nativeSessionId'], id)
|
||||
return id
|
||||
})()
|
||||
|
||||
export function getInitialSessionId() {
|
||||
return sessionId
|
||||
}
|
||||
|
||||
export function useSessionId() {
|
||||
const [id, setId] = useState(() => sessionId)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = onAppStateChange(state => {
|
||||
if (state === 'active') {
|
||||
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
|
||||
if (isSessionIdExpired(lastEvent)) {
|
||||
sessionId = uuid.v4()
|
||||
device.set(['nativeSessionId'], sessionId)
|
||||
setId(sessionId)
|
||||
}
|
||||
} else {
|
||||
device.set(['nativeSessionIdLastEventAt'], Date.now())
|
||||
}
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [])
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import uuid from 'react-native-uuid'
|
||||
|
||||
import {onAppStateChange} from '#/lib/appState'
|
||||
import {isSessionIdExpired} from '#/analytics/identifiers/util'
|
||||
|
||||
const SESSION_ID_KEY = 'bsky_session_id'
|
||||
const LAST_EVENT_KEY = 'bsky_session_id_last_event_at'
|
||||
|
||||
let sessionId = (() => {
|
||||
const existing = window.sessionStorage.getItem(SESSION_ID_KEY)
|
||||
const lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY)
|
||||
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
|
||||
const id = existing && !isSessionIdExpired(lastEvent) ? existing : uuid.v4()
|
||||
window.sessionStorage.setItem(SESSION_ID_KEY, id)
|
||||
return id
|
||||
})()
|
||||
|
||||
export function getInitialSessionId() {
|
||||
return sessionId
|
||||
}
|
||||
|
||||
export function useSessionId() {
|
||||
const [id, setId] = useState(() => sessionId)
|
||||
|
||||
useEffect(() => {
|
||||
const sub = onAppStateChange(state => {
|
||||
if (state === 'active') {
|
||||
const lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY)
|
||||
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
|
||||
if (isSessionIdExpired(lastEvent)) {
|
||||
sessionId = uuid.v4()
|
||||
window.sessionStorage.setItem(SESSION_ID_KEY, sessionId)
|
||||
setId(sessionId)
|
||||
}
|
||||
} else {
|
||||
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()))
|
||||
}
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [])
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as env from '#/env'
|
||||
|
||||
const ONE_MIN = 60 * 1e3
|
||||
const TTL = (env.IS_NATIVE ? 5 : 30) * ONE_MIN // 5 min on native
|
||||
|
||||
export function isSessionIdExpired(since: number | undefined) {
|
||||
if (since === undefined) return false
|
||||
return Date.now() - since >= TTL
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
import {
|
||||
getAndMigrateDeviceId,
|
||||
getDeviceIdOrThrow,
|
||||
getInitialSessionId,
|
||||
useSessionId,
|
||||
} from '#/analytics/identifiers'
|
||||
import {type Metrics, metrics} from '#/analytics/metrics'
|
||||
import * as referrer from '#/analytics/misc/referrer'
|
||||
import {type MergeableMetadata, type Metadata} from '#/analytics/types'
|
||||
import * as env from '#/env'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import {device} from '#/storage'
|
||||
|
||||
export * as utils from '#/analytics/utils'
|
||||
|
||||
type ContextType = {
|
||||
metric: <E extends keyof Metrics>(
|
||||
event: E,
|
||||
payload: Metrics[E],
|
||||
metadata?: MergeableMetadata,
|
||||
) => void
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
const Context = createContext<ContextType>({
|
||||
metric: (event, payload, metadata) => {
|
||||
metrics.track(event, payload, metadata)
|
||||
},
|
||||
metadata: {
|
||||
base: {
|
||||
deviceId: getDeviceIdOrThrow() ?? 'unknown',
|
||||
sessionId: getInitialSessionId(),
|
||||
platform: Platform.OS,
|
||||
appVersion: env.APP_VERSION,
|
||||
bundleIdentifier: env.BUNDLE_IDENTIFIER,
|
||||
bundleDate: env.BUNDLE_DATE,
|
||||
referrerSrc: referrer.src,
|
||||
referrerUrl: referrer.url,
|
||||
},
|
||||
geolocation: device.get(['mergedGeolocation']) || {
|
||||
countryCode: '',
|
||||
regionCode: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const setupDeviceId = getAndMigrateDeviceId()
|
||||
|
||||
export function AnalyticsContext({
|
||||
children,
|
||||
metadata,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
metadata?: MergeableMetadata
|
||||
}) {
|
||||
if (metadata) {
|
||||
// @ts-ignore
|
||||
if (metadata.__meta !== true) {
|
||||
throw new Error(
|
||||
'Use the meta() helper when passing metadata to AnalyticsContext',
|
||||
)
|
||||
}
|
||||
}
|
||||
const sessionId = useSessionId()
|
||||
const geolocation = useGeolocation()
|
||||
const parentContext = useContext(Context)
|
||||
const childContext = useMemo(() => {
|
||||
const combinedMetadata = {
|
||||
...parentContext.metadata,
|
||||
...metadata,
|
||||
}
|
||||
combinedMetadata.base.sessionId = sessionId
|
||||
combinedMetadata.geolocation = geolocation
|
||||
const context: ContextType = {
|
||||
metadata: combinedMetadata,
|
||||
metric: (event, payload, extraMetadata) => {
|
||||
parentContext.metric(event, payload, {
|
||||
...combinedMetadata,
|
||||
...extraMetadata,
|
||||
})
|
||||
},
|
||||
}
|
||||
return context
|
||||
}, [sessionId, geolocation, parentContext, metadata])
|
||||
return <Context.Provider value={childContext}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function useAnalytics() {
|
||||
return useContext(Context)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import {onAppStateChange} from '#/lib/appState'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {Sentry} from '#/logger/sentry/lib'
|
||||
import * as env from '#/env'
|
||||
|
||||
// TODO just fucken use logger in here
|
||||
|
||||
type Event<M extends Record<string, any>> = {
|
||||
time: number
|
||||
event: keyof M
|
||||
payload: M[keyof M]
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
const TRACKING_ENDPOINT = env.METRICS_API_HOST + '/track'
|
||||
|
||||
export class MetricsClient<M extends Record<string, any>> {
|
||||
private started: boolean = false
|
||||
private queue: Event<M>[] = []
|
||||
private failedQueue: Event<M>[] = []
|
||||
private flushInterval: NodeJS.Timeout | null = null
|
||||
|
||||
start() {
|
||||
if (this.started) 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 M>(
|
||||
event: E,
|
||||
payload: M[E],
|
||||
metadata: Record<string, any> = {},
|
||||
) {
|
||||
this.start()
|
||||
|
||||
this.queue.push({
|
||||
time: Date.now(),
|
||||
event,
|
||||
payload,
|
||||
metadata,
|
||||
})
|
||||
|
||||
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<M>[], isRetry: boolean = false) {
|
||||
try {
|
||||
const body = JSON.stringify(events)
|
||||
if (env.IS_WEB && 'navigator' in globalThis && navigator.sendBeacon) {
|
||||
navigator.sendBeacon(
|
||||
TRACKING_ENDPOINT,
|
||||
new Blob([body], {type: 'application/json'}),
|
||||
)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {MetricsClient} from '#/analytics/metrics/client'
|
||||
import {type Events} from '#/analytics/metrics/types'
|
||||
|
||||
export type {Events as Metrics} from '#/analytics/metrics/types'
|
||||
export const metrics = new MetricsClient<Events>()
|
||||
|
||||
/*
|
||||
* TODO
|
||||
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),
|
||||
})
|
||||
}
|
||||
})
|
||||
*/
|
||||
@@ -0,0 +1,826 @@
|
||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||
|
||||
export type Events = {
|
||||
// App events
|
||||
init: {
|
||||
initMs: number
|
||||
}
|
||||
'experiment:viewed': {
|
||||
experimentId: string
|
||||
variationId: string
|
||||
}
|
||||
|
||||
'account:loggedIn': {
|
||||
logContext:
|
||||
| 'LoginForm'
|
||||
| 'SwitchAccount'
|
||||
| 'ChooseAccountForm'
|
||||
| 'Settings'
|
||||
| 'Notification'
|
||||
withPassword: boolean
|
||||
}
|
||||
'account:loggedOut': {
|
||||
logContext:
|
||||
| 'SwitchAccount'
|
||||
| 'Settings'
|
||||
| 'SignupQueued'
|
||||
| 'Deactivated'
|
||||
| 'Takendown'
|
||||
| 'AgeAssuranceNoAccessScreen'
|
||||
scope: 'current' | 'every'
|
||||
}
|
||||
'notifications:openApp': {
|
||||
reason: NotificationReason
|
||||
causedBoot: boolean
|
||||
}
|
||||
'notifications:request': {
|
||||
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
|
||||
status: 'granted' | 'denied' | 'undetermined'
|
||||
}
|
||||
'state:background': {
|
||||
secondsActive: number
|
||||
}
|
||||
'state:foreground': {}
|
||||
'router:navigate': {
|
||||
from?: string
|
||||
}
|
||||
'deepLink:referrerReceived': {
|
||||
to: string
|
||||
referrer: string
|
||||
hostname: string
|
||||
}
|
||||
|
||||
// Screen events
|
||||
'splash:signInPressed': {}
|
||||
'splash:createAccountPressed': {}
|
||||
'welcomeModal:signupClicked': {}
|
||||
'welcomeModal:exploreClicked': {}
|
||||
'welcomeModal:signinClicked': {}
|
||||
'welcomeModal:dismissed': {}
|
||||
'welcomeModal:presented': {}
|
||||
'signup:nextPressed': {
|
||||
activeStep: number
|
||||
phoneVerificationRequired?: boolean
|
||||
}
|
||||
'signup:backPressed': {
|
||||
activeStep: number
|
||||
}
|
||||
'signup:captchaSuccess': {}
|
||||
'signup:captchaFailure': {}
|
||||
'signup:fieldError': {
|
||||
field: string
|
||||
errorCount: number
|
||||
errorMessage: string
|
||||
activeStep: number
|
||||
}
|
||||
'signup:backgrounded': {
|
||||
activeStep: number
|
||||
backgroundCount: number
|
||||
}
|
||||
'signup:handleTaken': {typeahead?: boolean}
|
||||
'signup:handleAvailable': {typeahead?: boolean}
|
||||
'signup:handleSuggestionSelected': {method: string}
|
||||
'signin:hostingProviderPressed': {
|
||||
hostingProviderDidChange: boolean
|
||||
}
|
||||
'signin:hostingProviderFailedResolution': {}
|
||||
'signin:success': {
|
||||
failedAttemptsCount: number
|
||||
isUsingCustomProvider: boolean
|
||||
timeTakenSeconds: number
|
||||
}
|
||||
'signin:backPressed': {
|
||||
failedAttemptsCount: number
|
||||
}
|
||||
'signin:forgotPasswordPressed': {}
|
||||
'signin:passwordReset': {}
|
||||
'signin:passwordResetSuccess': {}
|
||||
'signin:passwordResetFailure': {}
|
||||
'onboarding:interests:nextPressed': {
|
||||
selectedInterests: string[]
|
||||
selectedInterestsLength: number
|
||||
}
|
||||
'onboarding:suggestedAccounts:tabPressed': {
|
||||
tab: string
|
||||
}
|
||||
'onboarding:suggestedAccounts:followAllPressed': {
|
||||
tab: string
|
||||
numAccounts: number
|
||||
}
|
||||
'onboarding:suggestedAccounts:nextPressed': {
|
||||
selectedAccountsLength: number
|
||||
skipped: boolean
|
||||
}
|
||||
'onboarding:followingFeed:nextPressed': {}
|
||||
'onboarding:algoFeeds:nextPressed': {
|
||||
selectedPrimaryFeeds: string[]
|
||||
selectedPrimaryFeedsLength: number
|
||||
selectedSecondaryFeeds: string[]
|
||||
selectedSecondaryFeedsLength: number
|
||||
}
|
||||
'onboarding:topicalFeeds:nextPressed': {
|
||||
selectedFeeds: string[]
|
||||
selectedFeedsLength: number
|
||||
}
|
||||
'onboarding:moderation:nextPressed': {}
|
||||
'onboarding:profile:nextPressed': {}
|
||||
'onboarding:finished:nextPressed': {
|
||||
usedStarterPack: boolean
|
||||
starterPackName?: string
|
||||
starterPackCreator?: string
|
||||
starterPackUri?: string
|
||||
profilesFollowed: number
|
||||
feedsPinned: number
|
||||
}
|
||||
'onboarding:finished:avatarResult': {
|
||||
avatarResult: 'default' | 'created' | 'uploaded'
|
||||
}
|
||||
'onboarding:valueProp:stepOne:nextPressed': {}
|
||||
'onboarding:valueProp:stepTwo:nextPressed': {}
|
||||
'onboarding:valueProp:skipPressed': {}
|
||||
'home:feedDisplayed': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
index: number
|
||||
}
|
||||
'feed:endReached': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
itemCount: number
|
||||
}
|
||||
'feed:refresh': {
|
||||
feedUrl: string
|
||||
feedType: string
|
||||
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
|
||||
}
|
||||
'feed:save': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:unsave': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:pin': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:unpin': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:like': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:unlike': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:share': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:suggestion:seen': {
|
||||
feedUrl: string
|
||||
}
|
||||
'feed:suggestion:press': {
|
||||
feedUrl: string
|
||||
}
|
||||
'post:showMore': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:showLess': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'feed:clickthrough': {
|
||||
feed: string
|
||||
count: number
|
||||
}
|
||||
'feed:engaged': {
|
||||
feed: string
|
||||
count: number
|
||||
}
|
||||
'feed:seen': {
|
||||
feed: string
|
||||
count: number
|
||||
}
|
||||
|
||||
'feed:discover:emptyError': {
|
||||
userDid: string
|
||||
}
|
||||
|
||||
'composer:gif:open': {}
|
||||
'composer:gif:select': {}
|
||||
'composerPrompt:press': {}
|
||||
'composerPrompt:camera:press': {}
|
||||
'composerPrompt:gallery:press': {}
|
||||
|
||||
'composer:threadgate:open': {
|
||||
nudged: boolean
|
||||
}
|
||||
'composer:threadgate:save': {
|
||||
replyOptions: string
|
||||
quotesEnabled: boolean
|
||||
persist: boolean
|
||||
hasChanged: boolean
|
||||
}
|
||||
|
||||
// Data events
|
||||
'account:create:begin': {}
|
||||
'account:create:success': {
|
||||
signupDuration: number
|
||||
fieldErrorsTotal: number
|
||||
backgroundCount: number
|
||||
}
|
||||
'post:create': {
|
||||
imageCount: number
|
||||
isReply: boolean
|
||||
isPartOfThread: boolean
|
||||
hasLink: boolean
|
||||
hasQuote: boolean
|
||||
langs: string
|
||||
logContext: 'Composer'
|
||||
}
|
||||
'thread:create': {
|
||||
postCount: number
|
||||
isReply: boolean
|
||||
}
|
||||
'post:like': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
doesLikerFollowPoster: boolean | undefined
|
||||
doesPosterFollowLiker: boolean | undefined
|
||||
likerClout: number | undefined
|
||||
postClout: number | undefined
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'post:repost': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'post:unlike': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'post:unrepost': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
}
|
||||
'post:mute': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:unmute': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:pin': {}
|
||||
'post:unpin': {}
|
||||
'post:bookmark': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:unbookmark': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:clickReply': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:clickQuotePost': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:clickthroughAuthor': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:clickthroughItem': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:clickthroughEmbed': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'post:view': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext:
|
||||
| 'FeedItem'
|
||||
| 'PostThreadItem'
|
||||
| 'Post'
|
||||
| 'ImmersiveVideo'
|
||||
| 'SearchResults'
|
||||
| 'Bookmarks'
|
||||
| 'Notifications'
|
||||
| 'Hashtag'
|
||||
| 'Topic'
|
||||
| 'PostQuotes'
|
||||
feedDescriptor?: string
|
||||
position?: number
|
||||
}
|
||||
'bookmarks:view': {}
|
||||
'bookmarks:post-clicked': {}
|
||||
'profile:follow': {
|
||||
contextProfileDid?: string
|
||||
didBecomeMutual: boolean | undefined
|
||||
followeeClout: number | undefined
|
||||
followeeDid: string
|
||||
followerClout: number | undefined
|
||||
position?: number
|
||||
logContext:
|
||||
| 'RecommendedFollowsItem'
|
||||
| 'PostThreadItem'
|
||||
| 'ProfileCard'
|
||||
| 'ProfileHeader'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'ProfileMenu'
|
||||
| 'ProfileHoverCard'
|
||||
| 'AvatarButton'
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
| 'ExploreSuggestedAccounts'
|
||||
| 'OnboardingSuggestedAccounts'
|
||||
| 'FindContacts'
|
||||
}
|
||||
'profile:followers:view': {
|
||||
contextProfileDid: string
|
||||
isOwnProfile: boolean
|
||||
}
|
||||
'profile:followers:paginate': {
|
||||
contextProfileDid: string
|
||||
itemCount: number
|
||||
page: number
|
||||
}
|
||||
'profile:following:view': {
|
||||
contextProfileDid: string
|
||||
isOwnProfile: boolean
|
||||
}
|
||||
'profile:following:paginate': {
|
||||
contextProfileDid: string
|
||||
itemCount: number
|
||||
page: number
|
||||
}
|
||||
'profileCard:seen': {
|
||||
contextProfileDid?: string
|
||||
profileDid: string
|
||||
position?: number
|
||||
}
|
||||
'suggestedUser:follow': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
location: 'Card' | 'Profile'
|
||||
recId?: number
|
||||
position: number
|
||||
suggestedDid: string
|
||||
category: string | null
|
||||
}
|
||||
'suggestedUser:press': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Onboarding'
|
||||
recId?: number
|
||||
position: number
|
||||
suggestedDid: string
|
||||
category: string | null
|
||||
}
|
||||
'suggestedUser:seen': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
| 'ProgressGuide'
|
||||
recId?: number
|
||||
position: number
|
||||
suggestedDid: string
|
||||
category: string | null
|
||||
}
|
||||
'suggestedUser:seeMore': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
}
|
||||
'suggestedUser:dismiss': {
|
||||
logContext: 'InterstitialDiscover' | 'InterstitialProfile'
|
||||
recId?: number
|
||||
position: number
|
||||
suggestedDid: string
|
||||
}
|
||||
'profile:unfollow': {
|
||||
logContext:
|
||||
| 'RecommendedFollowsItem'
|
||||
| 'PostThreadItem'
|
||||
| 'ProfileCard'
|
||||
| 'ProfileHeader'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'ProfileMenu'
|
||||
| 'ProfileHoverCard'
|
||||
| 'Chat'
|
||||
| 'AvatarButton'
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'PostOnboardingFindFollows'
|
||||
| 'ImmersiveVideo'
|
||||
| 'ExploreSuggestedAccounts'
|
||||
| 'OnboardingSuggestedAccounts'
|
||||
| 'FindContacts'
|
||||
}
|
||||
'chat:create': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||
}
|
||||
'chat:open': {
|
||||
logContext:
|
||||
| 'ProfileHeader'
|
||||
| 'NewChatDialog'
|
||||
| 'ChatsList'
|
||||
| 'SendViaChatDialog'
|
||||
}
|
||||
'starterPack:addUser': {
|
||||
starterPack?: string
|
||||
}
|
||||
'starterPack:removeUser': {
|
||||
starterPack?: string
|
||||
}
|
||||
'starterPack:share': {
|
||||
starterPack: string
|
||||
shareType: 'link' | 'qrcode'
|
||||
qrShareType?: 'save' | 'copy' | 'share'
|
||||
}
|
||||
'starterPack:followAll': {
|
||||
logContext: 'StarterPackProfilesList' | 'Onboarding'
|
||||
starterPack: string
|
||||
count: number
|
||||
}
|
||||
'starterPack:delete': {}
|
||||
'starterPack:create': {
|
||||
setName: boolean
|
||||
setDescription: boolean
|
||||
profilesCount: number
|
||||
feedsCount: number
|
||||
}
|
||||
'starterPack:ctaPress': {
|
||||
starterPack: string
|
||||
}
|
||||
'starterPack:opened': {
|
||||
starterPack: string
|
||||
}
|
||||
'link:clicked': {
|
||||
url: string
|
||||
domain: string
|
||||
}
|
||||
|
||||
'feed:interstitial:feedCard:press': {}
|
||||
'desktopFeeds:feed:click': {
|
||||
feedUri: string
|
||||
feedDescriptor: string
|
||||
}
|
||||
|
||||
'profile:header:suggestedFollowsCard:press': {}
|
||||
'profile:addToStarterPack': {}
|
||||
|
||||
'test:all:always': {}
|
||||
'test:all:sometimes': {}
|
||||
'test:all:boosted_by_gate1': {reason: 'base' | 'gate1'}
|
||||
'test:all:boosted_by_gate2': {reason: 'base' | 'gate2'}
|
||||
'test:all:boosted_by_both': {reason: 'base' | 'gate1' | 'gate2'}
|
||||
'test:gate1:always': {}
|
||||
'test:gate1:sometimes': {}
|
||||
'test:gate2:always': {}
|
||||
'test:gate2:sometimes': {}
|
||||
|
||||
'tmd:share': {}
|
||||
'tmd:download': {}
|
||||
'tmd:post': {}
|
||||
|
||||
'trendingTopics:show': {
|
||||
context: 'settings'
|
||||
}
|
||||
'trendingTopics:hide': {
|
||||
context: 'settings' | 'sidebar' | 'interstitial' | 'explore:trending'
|
||||
}
|
||||
'trendingTopic:click': {
|
||||
context: 'sidebar' | 'interstitial' | 'explore'
|
||||
}
|
||||
'recommendedTopic:click': {
|
||||
context: 'explore'
|
||||
}
|
||||
'trendingVideos:show': {
|
||||
context: 'settings'
|
||||
}
|
||||
'trendingVideos:hide': {
|
||||
context: 'settings' | 'interstitial:discover' | 'interstitial:explore'
|
||||
}
|
||||
'videoCard:click': {
|
||||
context: 'interstitial:discover' | 'interstitial:explore' | 'feed'
|
||||
}
|
||||
|
||||
'explore:module:seen': {
|
||||
module:
|
||||
| 'trendingTopics'
|
||||
| 'trendingVideos'
|
||||
| 'suggestedAccounts'
|
||||
| 'suggestedFeeds'
|
||||
| 'suggestedStarterPacks'
|
||||
| `feed:${FeedDescriptor}`
|
||||
}
|
||||
'explore:module:searchButtonPress': {
|
||||
module: 'suggestedAccounts' | 'suggestedFeeds'
|
||||
}
|
||||
'explore:suggestedAccounts:tabPressed': {
|
||||
tab: string
|
||||
}
|
||||
|
||||
'progressGuide:hide': {}
|
||||
'progressGuide:followDialog:open': {}
|
||||
|
||||
'moderation:subscribedToLabeler': {}
|
||||
'moderation:unsubscribedFromLabeler': {}
|
||||
'moderation:changeLabelPreference': {
|
||||
preference: string
|
||||
}
|
||||
|
||||
'moderation:subscribedToList': {
|
||||
listType: 'mute' | 'block'
|
||||
}
|
||||
'moderation:unsubscribedFromList': {
|
||||
listType: 'mute' | 'block'
|
||||
}
|
||||
|
||||
'reportDialog:open': {
|
||||
subjectType: string
|
||||
}
|
||||
'reportDialog:close': {}
|
||||
'reportDialog:success': {
|
||||
reason: string
|
||||
labeler: string
|
||||
details: boolean
|
||||
}
|
||||
'reportDialog:failure': {}
|
||||
|
||||
translate: {
|
||||
sourceLanguages: string[]
|
||||
targetLanguage: string
|
||||
textLength: number
|
||||
}
|
||||
|
||||
'verification:create': {}
|
||||
'verification:revoke': {}
|
||||
'verification:badge:click': {}
|
||||
'verification:learn-more': {
|
||||
location:
|
||||
| 'initialAnnouncementeNux'
|
||||
| 'verificationsDialog'
|
||||
| 'verifierDialog'
|
||||
| 'verificationSettings'
|
||||
}
|
||||
'verification:settings:hideBadges': {}
|
||||
'verification:settings:unHideBadges': {}
|
||||
|
||||
'live:create': {duration: number}
|
||||
'live:edit': {}
|
||||
'live:remove': {}
|
||||
'live:card:open': {subject: string; from: 'post' | 'profile'}
|
||||
'live:card:watch': {subject: string}
|
||||
'live:card:openProfile': {subject: string}
|
||||
'live:view:profile': {subject: string}
|
||||
'live:view:post': {subject: string; feed?: string}
|
||||
|
||||
'post:share': {
|
||||
uri: string
|
||||
authorDid: string
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
feedDescriptor?: string
|
||||
postContext: 'feed' | 'thread'
|
||||
position?: number
|
||||
}
|
||||
'share:press:copyLink': {}
|
||||
'share:press:nativeShare': {}
|
||||
'share:press:openDmSearch': {}
|
||||
'share:press:dmSelected': {}
|
||||
'share:press:recentDm': {}
|
||||
'share:press:embed': {}
|
||||
|
||||
'thread:click:showOtherReplies': {}
|
||||
'thread:click:hideReplyForMe': {}
|
||||
'thread:click:hideReplyForEveryone': {}
|
||||
'thread:preferences:load': {
|
||||
[key: string]: any
|
||||
}
|
||||
'thread:preferences:update': {
|
||||
[key: string]: any
|
||||
}
|
||||
'thread:click:headerMenuOpen': {}
|
||||
'thread:click:editOwnThreadgate': {}
|
||||
'thread:click:viewSomeoneElsesThreadgate': {}
|
||||
'activitySubscription:enable': {
|
||||
setting: 'posts' | 'posts_and_replies'
|
||||
}
|
||||
'activitySubscription:disable': {}
|
||||
'activityPreference:changeChannels': {
|
||||
name: string
|
||||
push: boolean
|
||||
list: boolean
|
||||
}
|
||||
'activityPreference:changeFilter': {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
'ageAssurance:navigateToSettings': {}
|
||||
'ageAssurance:dismissFeedBanner': {}
|
||||
'ageAssurance:dismissSettingsNotice': {}
|
||||
'ageAssurance:initDialogOpen': {
|
||||
hasInitiatedPreviously: boolean
|
||||
}
|
||||
'ageAssurance:initDialogSubmit': {}
|
||||
'ageAssurance:api:begin': {
|
||||
platform: string
|
||||
countryCode: string
|
||||
regionCode?: string
|
||||
}
|
||||
'ageAssurance:initDialogError': {
|
||||
code: string
|
||||
}
|
||||
'ageAssurance:redirectDialogOpen': {}
|
||||
'ageAssurance:redirectDialogSuccess': {}
|
||||
'ageAssurance:redirectDialogFail': {}
|
||||
'ageAssurance:appealDialogOpen': {}
|
||||
'ageAssurance:appealDialogSubmit': {}
|
||||
'ageAssurance:noAccessScreen:shown': {
|
||||
accountCreatedAt: string
|
||||
isAARegion: boolean
|
||||
hasDeclaredAge: boolean
|
||||
canUpdateBirthday: boolean
|
||||
}
|
||||
'ageAssurance:noAccessScreen:openBirthdateDialog': {}
|
||||
|
||||
/*
|
||||
* Specifically for the `BlockedGeoOverlay`
|
||||
*/
|
||||
'blockedGeoOverlay:shown': {}
|
||||
|
||||
'geo:debug': {}
|
||||
|
||||
/*
|
||||
* Find Contacts stuff
|
||||
*/
|
||||
|
||||
// user presses the button on the new feature NUX
|
||||
'contacts:nux:ctaPressed': {}
|
||||
// user presses the banner NUX
|
||||
'contacts:nux:bannerPressed': {}
|
||||
// user dismisses the banner
|
||||
'contacts:nux:bannerDismissed': {}
|
||||
|
||||
// user lands on the contacts step
|
||||
'onboarding:contacts:presented': {}
|
||||
// user pressed "Import Contacts" button to begin flow
|
||||
'onboarding:contacts:begin': {}
|
||||
// skips the step entirely
|
||||
'onboarding:contacts:skipPressed': {}
|
||||
// user shared their contacts
|
||||
'onboarding:contacts:contactsShared': {}
|
||||
// user leaves the matches page
|
||||
'onboarding:contacts:nextPressed': {
|
||||
matchCount: number
|
||||
followCount: number
|
||||
dismissedMatchCount: number
|
||||
}
|
||||
|
||||
// user entered a number
|
||||
'contacts:phone:phoneEntered': {
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user entered the correct one-time-code
|
||||
'contacts:phone:phoneVerified': {
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user responded to the contacts permission prompt
|
||||
'contacts:permission:request': {
|
||||
status: 'granted' | 'denied'
|
||||
accessLevelIOS?: 'all' | 'limited' | 'none'
|
||||
}
|
||||
// contacts were successfully imported and matched
|
||||
'contacts:import:success': {
|
||||
contactCount: number
|
||||
matchCount: number
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// contacts import failed
|
||||
'contacts:import:failure': {
|
||||
reason: 'noValidNumbers' | 'networkError' | 'unknown'
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user followed a single match
|
||||
'contacts:matches:follow': {
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user pressed "Follow All" on matches
|
||||
'contacts:matches:followAll': {
|
||||
followCount: number
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user dismissed a match
|
||||
'contacts:matches:dismiss': {
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user pressed invite to send an SMS to a non-match
|
||||
'contacts:matches:invite': {
|
||||
entryPoint: 'Onboarding' | 'Standalone'
|
||||
}
|
||||
// user opened the Find Contacts settings screen
|
||||
'contacts:settings:presented': {
|
||||
hasPreviouslySynced: boolean
|
||||
matchCount?: number
|
||||
}
|
||||
// user followed a single match from settings
|
||||
'contacts:settings:follow': {}
|
||||
// user pressed "Follow All" from settings
|
||||
'contacts:settings:followAll': {
|
||||
followCount: number
|
||||
}
|
||||
// user dismissed a match from settings
|
||||
'contacts:settings:dismiss': {}
|
||||
// user re-entered the flow via the resync button
|
||||
'contacts:settings:resync': {
|
||||
daysSinceLastSync: number
|
||||
}
|
||||
// user pressed the remove all data button
|
||||
'contacts:settings:removeData': {}
|
||||
|
||||
'liveEvents:feedBanner:seen': {
|
||||
feed: string
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
'liveEvents:feedBanner:click': {
|
||||
feed: string
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
'liveEvents:feedBanner:hide': {
|
||||
feed: string
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
'liveEvents:feedBanner:unhide': {
|
||||
feed: string
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
'liveEvents:hideAllFeedBanners': {
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
'liveEvents:unhideAllFeedBanners': {
|
||||
context: LiveEventFeedMetricContext
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as env from '#/env'
|
||||
|
||||
let refSrc = ''
|
||||
let refUrl = ''
|
||||
if (env.IS_WEB) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
refSrc = params.get('ref_src') ?? ''
|
||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||
}
|
||||
|
||||
export const src = refSrc
|
||||
export const url = refUrl
|
||||
@@ -0,0 +1,34 @@
|
||||
import {type Geolocation} from '#/geolocation'
|
||||
|
||||
export type BaseMetadata = {
|
||||
deviceId: string
|
||||
sessionId: string
|
||||
platform: string
|
||||
appVersion: string
|
||||
bundleIdentifier: string
|
||||
bundleDate: number
|
||||
referrerSrc: string
|
||||
referrerUrl: string
|
||||
}
|
||||
|
||||
export type GeolocationMetadata = Geolocation
|
||||
|
||||
export type SessionMetadata = {
|
||||
did: string
|
||||
isBskyPds: boolean
|
||||
}
|
||||
|
||||
export type PreferencesMetadata = {
|
||||
appLanguage: string
|
||||
contentLanguages: string[]
|
||||
}
|
||||
|
||||
export type MergeableMetadata = {
|
||||
session?: SessionMetadata
|
||||
preferences?: PreferencesMetadata
|
||||
}
|
||||
|
||||
export type Metadata = {
|
||||
base: BaseMetadata
|
||||
geolocation: GeolocationMetadata
|
||||
} & MergeableMetadata
|
||||
@@ -0,0 +1,26 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {BSKY_SERVICE} from '#/lib/constants'
|
||||
import {type SessionAccount} from '#/state/session'
|
||||
import {type MergeableMetadata, type SessionMetadata} from '#/analytics/types'
|
||||
|
||||
export function meta(metadata: MergeableMetadata) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const m = useMemo(() => metadata, [metadata])
|
||||
// @ts-ignore
|
||||
m.__meta = true
|
||||
return m
|
||||
}
|
||||
|
||||
export function accountToSessionMetadata(
|
||||
account: SessionAccount | undefined,
|
||||
): SessionMetadata | undefined {
|
||||
if (!account) {
|
||||
return
|
||||
} else {
|
||||
return {
|
||||
did: account.did,
|
||||
isBskyPds: account.service.startsWith(BSKY_SERVICE),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import uuid from 'react-native-uuid'
|
||||
|
||||
import {onAppStateChange} from '#/lib/appState'
|
||||
import {updateBaseMetadata} from '#/logger/metadata'
|
||||
|
||||
const TTL = 30 * 60 * 1e3 // 30 min on web
|
||||
const SESSION_ID_KEY = 'bsky_session_id'
|
||||
@@ -18,7 +17,7 @@ let sessionId = (() => {
|
||||
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
|
||||
const id = existing && !expired(lastEvent) ? existing : uuid.v4()
|
||||
window.sessionStorage.setItem(SESSION_ID_KEY, id)
|
||||
updateBaseMetadata({sessionId: id})
|
||||
// updateBaseMetadata({sessionId: id})
|
||||
return id
|
||||
})()
|
||||
|
||||
@@ -29,7 +28,7 @@ onAppStateChange(state => {
|
||||
if (expired(lastEvent)) {
|
||||
sessionId = uuid.v4()
|
||||
window.sessionStorage.setItem(SESSION_ID_KEY, sessionId)
|
||||
updateBaseMetadata({sessionId})
|
||||
// updateBaseMetadata({sessionId})
|
||||
}
|
||||
} else {
|
||||
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()))
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
|
||||
import {type AppLanguage} from '#/locale/languages'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {AnalyticsContext, utils} from '#/analytics'
|
||||
|
||||
type SetStateCb = (
|
||||
s: persisted.Schema['languagePrefs'],
|
||||
@@ -124,7 +125,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
<apiContext.Provider value={api}>{children}</apiContext.Provider>
|
||||
<apiContext.Provider value={api}>
|
||||
<AnalyticsContext
|
||||
metadata={utils.meta({
|
||||
preferences: {
|
||||
appLanguage: state.appLanguage,
|
||||
contentLanguages: state.contentLanguages,
|
||||
},
|
||||
})}>
|
||||
{children}
|
||||
</AnalyticsContext>
|
||||
</apiContext.Provider>
|
||||
</stateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
+38
-19
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
|
||||
|
||||
import {setUserMetadata} from '#/logger/metadata'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {AnalyticsContext, useAnalytics, utils} from '#/analytics'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {emitSessionDropped} from '../events'
|
||||
import {
|
||||
@@ -19,7 +19,6 @@ import {type Action, getInitialState, reducer, type State} from './reducer'
|
||||
export {isSignupQueued} from './util'
|
||||
import {addSessionDebugLog} from './logging'
|
||||
export type {SessionAccount} from '#/state/session/types'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
type SessionApiContext,
|
||||
type SessionStateContext,
|
||||
@@ -93,6 +92,7 @@ class SessionStore {
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const ax = useAnalytics()
|
||||
const cancelPendingTask = useOneTaskAtATime()
|
||||
const [store] = React.useState(() => new SessionStore())
|
||||
const state = React.useSyncExternalStore(store.subscribe, store.getState)
|
||||
@@ -111,9 +111,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
accountDid,
|
||||
sessionEvent,
|
||||
})
|
||||
if (!refreshedAccount) {
|
||||
setUserMetadata(null)
|
||||
}
|
||||
},
|
||||
[store],
|
||||
)
|
||||
@@ -122,7 +119,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
async (params, metrics) => {
|
||||
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
|
||||
const signal = cancelPendingTask()
|
||||
logger.metric('account:create:begin', {}, {statsig: true})
|
||||
ax.metric('account:create:begin', {})
|
||||
const {agent, account} = await createAgentAndCreateAccount(
|
||||
params,
|
||||
onAgentSessionChange,
|
||||
@@ -136,10 +133,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
newAgent: agent,
|
||||
newAccount: account,
|
||||
})
|
||||
logger.metric('account:create:success', metrics, {statsig: true})
|
||||
ax.metric('account:create:success', metrics, {
|
||||
session: utils.accountToSessionMetadata(account),
|
||||
})
|
||||
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
|
||||
},
|
||||
[store, onAgentSessionChange, cancelPendingTask],
|
||||
[ax, store, onAgentSessionChange, cancelPendingTask],
|
||||
)
|
||||
|
||||
const login = React.useCallback<SessionApiContext['login']>(
|
||||
@@ -159,14 +158,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
newAgent: agent,
|
||||
newAccount: account,
|
||||
})
|
||||
logger.metric(
|
||||
ax.metric(
|
||||
'account:loggedIn',
|
||||
{logContext, withPassword: true},
|
||||
{statsig: true},
|
||||
{session: utils.accountToSessionMetadata(account)},
|
||||
)
|
||||
addSessionDebugLog({type: 'method:end', method: 'login', account})
|
||||
},
|
||||
[store, onAgentSessionChange, cancelPendingTask],
|
||||
[ax, store, onAgentSessionChange, cancelPendingTask],
|
||||
)
|
||||
|
||||
const logoutCurrentAccount = React.useCallback<
|
||||
@@ -179,20 +178,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
store.dispatch({
|
||||
type: 'logged-out-current-account',
|
||||
})
|
||||
logger.metric(
|
||||
ax.metric(
|
||||
'account:loggedOut',
|
||||
{logContext, scope: 'current'},
|
||||
{statsig: true},
|
||||
{
|
||||
session: utils.accountToSessionMetadata(
|
||||
prevState.accounts.find(
|
||||
a => a.did === prevState.currentAgentState.did,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||
setUserMetadata(null)
|
||||
if (prevState.currentAgentState.did) {
|
||||
clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did})
|
||||
}
|
||||
// reset onboarding flow on logout
|
||||
onboardingDispatch({type: 'skip'})
|
||||
},
|
||||
[store, cancelPendingTask, onboardingDispatch],
|
||||
[ax, store, cancelPendingTask, onboardingDispatch],
|
||||
)
|
||||
|
||||
const logoutEveryAccount = React.useCallback<
|
||||
@@ -201,16 +205,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
logContext => {
|
||||
addSessionDebugLog({type: 'method:start', method: 'logout'})
|
||||
cancelPendingTask()
|
||||
const prevState = store.getState()
|
||||
store.dispatch({
|
||||
type: 'logged-out-every-account',
|
||||
})
|
||||
logger.metric(
|
||||
ax.metric(
|
||||
'account:loggedOut',
|
||||
{logContext, scope: 'every'},
|
||||
{statsig: true},
|
||||
{
|
||||
session: utils.accountToSessionMetadata(
|
||||
prevState.accounts.find(
|
||||
a => a.did === prevState.currentAgentState.did,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||
setUserMetadata(null)
|
||||
clearAgeAssuranceData()
|
||||
// reset onboarding flow on logout
|
||||
onboardingDispatch({type: 'skip'})
|
||||
@@ -364,7 +374,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<AgentContext.Provider value={agent}>
|
||||
<StateContext.Provider value={stateContext}>
|
||||
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
||||
<ApiContext.Provider value={api}>
|
||||
<AnalyticsContext
|
||||
metadata={utils.meta({
|
||||
session: utils.accountToSessionMetadata(
|
||||
stateContext.currentAccount,
|
||||
),
|
||||
})}>
|
||||
{children}
|
||||
</AnalyticsContext>
|
||||
</ApiContext.Provider>
|
||||
</StateContext.Provider>
|
||||
</AgentContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user