Shared metadata cache

This commit is contained in:
Eric Bailey
2026-01-20 17:44:21 -06:00
parent aadc71bb3b
commit af8fada3c1
20 changed files with 275 additions and 203 deletions
+26
View File
@@ -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
}
+104
View File
@@ -0,0 +1,104 @@
import {Platform} from 'react-native'
import {BSKY_SERVICE} from '#/lib/constants'
import {setGrowthBookAttributes} from '#/logger/growthbook'
import {getAndMigrateDeviceId, getDeviceId} from '#/logger/metadata/deviceId'
import {getSessionId} from '#/logger/metadata/sessionId'
import * as persisted from '#/state/persisted'
import * as env from '#/env'
import {device} from '#/storage'
export type BaseMetadata = {
deviceId: string
sessionId: string
country: string
}
export type UserMetadata = {
did: string
isBskyPds: boolean
platform: string
appVersion: string
bundleIdentifier: string
bundleDate: number
refSrc: string
refUrl: string
appLanguage: string
contentLanguages: string[]
}
export type Metadata = BaseMetadata & Partial<UserMetadata>
/**
* Ensures that deviceId is set and migrated from legacy storage. Handled on
* startup in `App.<platform>.tsx`
*/
export const setupDeviceId = getAndMigrateDeviceId()
let baseMetadata: BaseMetadata = {
deviceId: getDeviceId() || 'unknown',
sessionId: getSessionId(),
country: device.get(['mergedGeolocation'])?.countryCode || 'unknown',
}
export function updateBaseMetadata(
metadata: Omit<BaseMetadata, 'deviceId' | 'sessionId'>,
) {
baseMetadata = {
deviceId: getDeviceId() || 'unknown',
sessionId: getSessionId(),
...metadata,
}
__onMetadataChange()
}
export function getBaseMetadata() {
return {
...baseMetadata,
sessionId: getSessionId(), // may have changed
}
}
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') ?? '')
}
let userMetadata: UserMetadata | null = null
export function updateUserMetadata(account: persisted.PersistedAccount | null) {
if (account === null) {
userMetadata = null
} else {
const languagePrefs = persisted.get('languagePrefs')
userMetadata = {
did: account.did,
isBskyPds: account.service.startsWith(BSKY_SERVICE),
platform: Platform.OS,
appVersion: env.RELEASE_VERSION,
bundleIdentifier: env.BUNDLE_IDENTIFIER,
bundleDate: env.BUNDLE_DATE,
appLanguage: languagePrefs.appLanguage,
contentLanguages: languagePrefs.contentLanguages,
refSrc,
refUrl,
}
}
__onMetadataChange()
}
export function getUserMetadata() {
return userMetadata
}
export function getMetadata(): Metadata {
return {
...getBaseMetadata(),
...(getUserMetadata() || {}),
}
}
function __onMetadataChange() {
const metadata = getMetadata()
setGrowthBookAttributes(metadata)
}
__onMetadataChange()
+34
View File
@@ -0,0 +1,34 @@
import uuid from 'react-native-uuid'
import {onAppStateChange} from '#/lib/appState'
import {device} from '#/storage'
const TTL = 5 * 60 * 1e3 // 5 min on native
function expired(since: number | undefined) {
if (since === undefined) return false
return Date.now() - since >= TTL
}
let sessionId = (() => {
const existing = device.get(['nativeSessionId'])
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
const id = existing && !expired(lastEvent) ? existing : uuid.v4()
device.set(['nativeSessionId'], id)
return id
})()
onAppStateChange(state => {
if (state === 'active') {
const lastEvent = device.get(['nativeSessionIdLastEventAt'])
if (expired(lastEvent)) {
sessionId = uuid.v4()
device.set(['nativeSessionId'], sessionId)
}
} else {
device.set(['nativeSessionIdLastEventAt'], Date.now())
}
})
export function getSessionId() {
return sessionId
}
+38
View File
@@ -0,0 +1,38 @@
import uuid from 'react-native-uuid'
import {onAppStateChange} from '#/lib/appState'
const TTL = 30 * 60 * 1e3 // 30 min on web
const SESSION_ID_KEY = 'bsky_session_id'
const LAST_EVENT_KEY = 'bsky_session_id_last_event_at'
function expired(since: number | undefined) {
if (since === undefined) return false
return Date.now() - since >= TTL
}
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 && !expired(lastEvent) ? existing : uuid.v4()
window.sessionStorage.setItem(SESSION_ID_KEY, id)
return id
})()
onAppStateChange(state => {
if (state === 'active') {
const lastEventStr = window.sessionStorage.getItem(LAST_EVENT_KEY)
const lastEvent = lastEventStr ? Number(lastEventStr) : undefined
if (expired(lastEvent)) {
sessionId = uuid.v4()
window.sessionStorage.setItem(SESSION_ID_KEY, sessionId)
}
} else {
window.sessionStorage.setItem(LAST_EVENT_KEY, String(Date.now()))
}
})
export function getSessionId() {
return sessionId
}