forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}