Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 238 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}) 113Context.displayName = 'AnalyticsContext' 114 115/** 116 * Ensures that deviceId is set and migrated from legacy storage. Handled on 117 * startup in `App.<platform>.tsx`. This must be awaited prior to the app 118 * booting up. 119 */ 120export const setupDeviceId = getAndMigrateDeviceId() 121 122/** 123 * Analytics context provider. Decorates the parent analytics context with 124 * additional metadata. Nesting should be done carefully and sparingly. 125 */ 126export function AnalyticsContext({ 127 children, 128 metadata, 129}: { 130 children: React.ReactNode 131 metadata?: MergeableMetadata 132}) { 133 if (metadata) { 134 if (!('__meta' in metadata)) { 135 throw new Error( 136 'Use the useMeta() helper when passing metadata to AnalyticsContext', 137 ) 138 } 139 } 140 const sessionId = useSessionId() 141 const geolocation = useGeolocationServiceResponse() 142 const parentContext = useContext(Context) 143 const childContext = useMemo(() => { 144 const combinedMetadata = { 145 ...parentContext.metadata, 146 ...metadata, 147 base: { 148 ...parentContext.metadata.base, 149 sessionId, 150 }, 151 geolocation, 152 } 153 const context: AnalyticsBaseContextType = { 154 ...parentContext, 155 logger: createLogger( 156 Logger.Context.Default, 157 getMetadataForLogger(combinedMetadata), 158 ), 159 metadata: combinedMetadata, 160 metric: (event, payload, extraMetadata) => { 161 parentContext.metric(event, payload, { 162 ...combinedMetadata, 163 ...extraMetadata, 164 }) 165 }, 166 } 167 return context 168 }, [sessionId, geolocation, parentContext, metadata]) 169 return <Context.Provider value={childContext}>{children}</Context.Provider> 170} 171 172/** 173 * Feature gates provider. Decorates the parent analytics context with 174 * feature gate capabilities. Should be mounted within `AnalyticsContext`, 175 * and below the `<Fragment key={did} />` breaker in `App.<platform>.tsx`. 176 */ 177export function AnalyticsFeaturesContext({ 178 children, 179}: { 180 children: React.ReactNode 181}) { 182 const parentContext = useContext(Context) 183 184 /** 185 * Side-effects: we need to synchronously set these during the same render 186 * cycle. These calls do not trigger re-renders, they just set properties on 187 * the singleton GrowthBook instance. 188 */ 189 setAttributes(parentContext.metadata) 190 feats.setTrackingCallback((experiment, result) => { 191 parentContext.metric('experiment:viewed', { 192 experimentId: experiment.key, 193 variationId: result.key, 194 }) 195 }) 196 feats.setFeatureUsageCallback((feature, result) => { 197 parentContext.metric('feature:viewed', { 198 featureId: feature, 199 featureResultValue: result.value, 200 experimentId: result.experiment?.key, 201 variationId: result.experimentResult?.key, 202 }) 203 }) 204 205 const childContext = useMemo<AnalyticsContextType>(() => { 206 return { 207 ...parentContext, 208 features: { 209 enabled: feats.isOn.bind(feats), 210 ...Features, 211 }, 212 } 213 }, [parentContext]) 214 215 return <Context.Provider value={childContext}>{children}</Context.Provider> 216} 217 218/** 219 * Basic analytics context without feature gates. Should really only be used 220 * above the `AnalyticsFeaturesContext` provider. 221 */ 222export function useAnalyticsBase() { 223 return useContext(Context) 224} 225 226/** 227 * The main analytics context, including feature gates. Use this everywhere you 228 * need metrics, features, or logging within the React tree. 229 */ 230export function useAnalytics() { 231 const ctx = useContext(Context) 232 if (!('features' in ctx)) { 233 throw new Error( 234 'useAnalytics must be used within an AnalyticsFeaturesContext', 235 ) 236 } 237 return ctx as AnalyticsContextType 238}