Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 237 lines 6.7 kB view raw
1import {createContext, useContext, 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 {useGeolocationServiceResponse} from '#/geolocation/service' 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(['geolocationServiceResponse']) || { 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 = useGeolocationServiceResponse() 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-effects: we need to synchronously set these during the same render 185 * cycle. These calls do not trigger re-renders, they just set properties on 186 * the singleton GrowthBook instance. 187 */ 188 setAttributes(parentContext.metadata) 189 feats.setTrackingCallback((experiment, result) => { 190 parentContext.metric('experiment:viewed', { 191 experimentId: experiment.key, 192 variationId: result.key, 193 }) 194 }) 195 feats.setFeatureUsageCallback((feature, result) => { 196 parentContext.metric('feature:viewed', { 197 featureId: feature, 198 featureResultValue: result.value, 199 experimentId: result.experiment?.key, 200 variationId: result.experimentResult?.key, 201 }) 202 }) 203 204 const childContext = useMemo<AnalyticsContextType>(() => { 205 return { 206 ...parentContext, 207 features: { 208 enabled: feats.isOn.bind(feats), 209 ...Features, 210 }, 211 } 212 }, [parentContext]) 213 214 return <Context.Provider value={childContext}>{children}</Context.Provider> 215} 216 217/** 218 * Basic analytics context without feature gates. Should really only be used 219 * above the `AnalyticsFeaturesContext` provider. 220 */ 221export function useAnalyticsBase() { 222 return useContext(Context) 223} 224 225/** 226 * The main analytics context, including feature gates. Use this everywhere you 227 * need metrics, features, or logging within the React tree. 228 */ 229export function useAnalytics() { 230 const ctx = useContext(Context) 231 if (!('features' in ctx)) { 232 throw new Error( 233 'useAnalytics must be used within an AnalyticsFeaturesContext', 234 ) 235 } 236 return ctx as AnalyticsContextType 237}