Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 232 lines 6.4 kB view raw
1import {createContext, useContext, useEffect, useMemo} from 'react' 2import {Platform} from 'react-native' 3 4import {Logger} from '#/logger' 5import { 6 Features, 7 features as feats, 8 init, 9 refresh, 10 setAttributes, 11} from '#/analytics/features' 12import { 13 getAndMigrateDeviceId, 14 getDeviceId, 15 getInitialSessionId, 16 useSessionId, 17} from '#/analytics/identifiers' 18import { 19 getMetadataForLogger, 20 getNavigationMetadata, 21 type MergeableMetadata, 22 type Metadata, 23} from '#/analytics/metadata' 24import {type Metrics, metrics} from '#/analytics/metrics' 25import * as refParams from '#/analytics/misc/refParams' 26import * as env from '#/env' 27import {useGeolocation} from '#/geolocation' 28import {device} from '#/storage' 29 30export * as utils from '#/analytics/utils' 31export const features = {init, refresh} 32export {Features} from '#/analytics/features' 33export {type Metrics} from '#/analytics/metrics' 34 35type LoggerType = { 36 debug: Logger['debug'] 37 info: Logger['info'] 38 log: Logger['log'] 39 warn: Logger['warn'] 40 error: Logger['error'] 41 /** 42 * Clones the existing logger and overrides the `context` value. Existing 43 * metadata is inherited. 44 * 45 * ```ts 46 * const ax = useAnalytics() 47 * const logger = ax.logger.useChild(ax.logger.Context.Notifications) 48 * ``` 49 */ 50 useChild: (context: Exclude<Logger['context'], undefined>) => LoggerType 51 Context: typeof Logger.Context 52} 53export type AnalyticsContextType = { 54 metadata: Metadata 55 logger: LoggerType 56 metric: <E extends keyof Metrics>( 57 event: E, 58 payload: Metrics[E], 59 metadata?: MergeableMetadata, 60 ) => void 61 features: typeof Features & { 62 enabled(feature: Features): boolean 63 } 64} 65export type AnalyticsBaseContextType = Omit<AnalyticsContextType, 'features'> 66 67function createLogger( 68 context: Logger['context'], 69 metadata: Partial<Metadata>, 70): LoggerType { 71 const logger = Logger.create(context, metadata) 72 return { 73 debug: logger.debug.bind(logger), 74 info: logger.info.bind(logger), 75 log: logger.log.bind(logger), 76 warn: logger.warn.bind(logger), 77 error: logger.error.bind(logger), 78 useChild: (context: Exclude<Logger['context'], undefined>) => { 79 return useMemo(() => createLogger(context, metadata), [context, metadata]) 80 }, 81 Context: Logger.Context, 82 } 83} 84 85const Context = createContext<AnalyticsBaseContextType>({ 86 logger: createLogger(Logger.Context.Default, {}), 87 metric: (event, payload, metadata) => { 88 if (metadata && '__meta' in metadata) { 89 delete metadata.__meta 90 } 91 metrics.track(event, payload, { 92 ...metadata, 93 navigation: getNavigationMetadata(), 94 }) 95 }, 96 metadata: { 97 base: { 98 deviceId: getDeviceId() ?? 'unknown', 99 sessionId: getInitialSessionId(), 100 platform: Platform.OS, 101 appVersion: env.APP_VERSION, 102 bundleIdentifier: env.BUNDLE_IDENTIFIER, 103 bundleDate: env.BUNDLE_DATE, 104 referrerSrc: refParams.src, 105 referrerUrl: refParams.url, 106 }, 107 geolocation: device.get(['mergedGeolocation']) || { 108 countryCode: '', 109 regionCode: '', 110 }, 111 }, 112}) 113 114/** 115 * Ensures that deviceId is set and migrated from legacy storage. Handled on 116 * startup in `App.<platform>.tsx`. This must be awaited prior to the app 117 * booting up. 118 */ 119export const setupDeviceId = getAndMigrateDeviceId() 120 121/** 122 * Analytics context provider. Decorates the parent analytics context with 123 * additional metadata. Nesting should be done carefully and sparingly. 124 */ 125export function AnalyticsContext({ 126 children, 127 metadata, 128}: { 129 children: React.ReactNode 130 metadata?: MergeableMetadata 131}) { 132 if (metadata) { 133 if (!('__meta' in metadata)) { 134 throw new Error( 135 'Use the useMeta() helper when passing metadata to AnalyticsContext', 136 ) 137 } 138 } 139 const sessionId = useSessionId() 140 const geolocation = useGeolocation() 141 const parentContext = useContext(Context) 142 const childContext = useMemo(() => { 143 const combinedMetadata = { 144 ...parentContext.metadata, 145 ...metadata, 146 base: { 147 ...parentContext.metadata.base, 148 sessionId, 149 }, 150 geolocation, 151 } 152 const context: AnalyticsBaseContextType = { 153 ...parentContext, 154 logger: createLogger( 155 Logger.Context.Default, 156 getMetadataForLogger(combinedMetadata), 157 ), 158 metadata: combinedMetadata, 159 metric: (event, payload, extraMetadata) => { 160 parentContext.metric(event, payload, { 161 ...combinedMetadata, 162 ...extraMetadata, 163 }) 164 }, 165 } 166 return context 167 }, [sessionId, geolocation, parentContext, metadata]) 168 return <Context.Provider value={childContext}>{children}</Context.Provider> 169} 170 171/** 172 * Feature gates provider. Decorates the parent analytics context with 173 * feature gate capabilities. Should be mounted within `AnalyticsContext`, 174 * and below the `<Fragment key={did} />` breaker in `App.<platform>.tsx`. 175 */ 176export function AnalyticsFeaturesContext({ 177 children, 178}: { 179 children: React.ReactNode 180}) { 181 const parentContext = useContext(Context) 182 183 /** 184 * Side-effect: we need to synchronously set this during the 185 * same render cycle. It does not trigger a re-render, it just 186 * sets properties on the singleton GrowthBook instance. 187 */ 188 setAttributes(parentContext.metadata) 189 190 useEffect(() => { 191 feats.setTrackingCallback((experiment, result) => { 192 parentContext.metric('experiment:viewed', { 193 experimentId: experiment.key, 194 variationId: result.key, 195 }) 196 }) 197 }, [parentContext.metric]) 198 199 const childContext = useMemo<AnalyticsContextType>(() => { 200 return { 201 ...parentContext, 202 features: { 203 enabled: feats.isOn.bind(feats), 204 ...Features, 205 }, 206 } 207 }, [parentContext]) 208 209 return <Context.Provider value={childContext}>{children}</Context.Provider> 210} 211 212/** 213 * Basic analytics context without feature gates. Should really only be used 214 * above the `AnalyticsFeaturesContext` provider. 215 */ 216export function useAnalyticsBase() { 217 return useContext(Context) 218} 219 220/** 221 * The main analytics context, including feature gates. Use this everywhere you 222 * need metrics, features, or logging within the React tree. 223 */ 224export function useAnalytics() { 225 const ctx = useContext(Context) 226 if (!('features' in ctx)) { 227 throw new Error( 228 'useAnalytics must be used within an AnalyticsFeaturesContext', 229 ) 230 } 231 return ctx as AnalyticsContextType 232}