Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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}