mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {AppState, AppStateStatus} from 'react-native'
3import AsyncStorage from '@react-native-async-storage/async-storage'
4import {createClient, SegmentClient} from '@segment/analytics-react-native'
5import {sha256} from 'js-sha256'
6import {Native} from 'sentry-expo'
7
8import {useSession, SessionAccount} from '#/state/session'
9import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
10import {logger} from '#/logger'
11
12type AppInfo = {
13 build?: string | undefined
14 name?: string | undefined
15 namespace?: string | undefined
16 version?: string | undefined
17}
18
19// Delay creating until first actual use.
20let segmentClient: SegmentClient | null = null
21function getClient(): SegmentClient {
22 if (!segmentClient) {
23 segmentClient = createClient({
24 writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
25 trackAppLifecycleEvents: false,
26 proxy: 'https://api.events.bsky.app/v1',
27 })
28 }
29 return segmentClient
30}
31
32export const track = async <E extends keyof TrackPropertiesMap>(
33 event: E,
34 properties?: TrackPropertiesMap[E],
35) => {
36 await getClient().track(event, properties)
37}
38
39export function useAnalytics() {
40 const {hasSession} = useSession()
41
42 return React.useMemo(() => {
43 if (hasSession) {
44 return {
45 async screen<E extends keyof ScreenPropertiesMap>(
46 event: E,
47 properties?: ScreenPropertiesMap[E],
48 ) {
49 await getClient().screen(event, properties)
50 },
51 async track<E extends keyof TrackPropertiesMap>(
52 event: E,
53 properties?: TrackPropertiesMap[E],
54 ) {
55 await getClient().track(event, properties)
56 },
57 }
58 }
59 // dont send analytics pings for anonymous users
60 return {
61 screen: async () => {},
62 track: async () => {},
63 }
64 }, [hasSession])
65}
66
67export function init(account: SessionAccount | undefined) {
68 setupListenersOnce()
69
70 if (account) {
71 const client = getClient()
72 if (account.did) {
73 const did_hashed = sha256(account.did)
74 client.identify(did_hashed, {did_hashed})
75 Native.setUser({id: did_hashed})
76 logger.debug('Ping w/hash')
77 } else {
78 logger.debug('Ping w/o hash')
79 client.identify()
80 }
81 }
82}
83
84let didSetupListeners = false
85function setupListenersOnce() {
86 if (didSetupListeners) {
87 return
88 }
89 didSetupListeners = true
90 // NOTE
91 // this is a copy of segment's own lifecycle event tracking
92 // we handle it manually to ensure that it never fires while the app is backgrounded
93 // -prf
94 const client = getClient()
95 client.isReady.onChange(async () => {
96 if (AppState.currentState !== 'active') {
97 logger.debug('Prevented a metrics ping while the app was backgrounded')
98 return
99 }
100 const context = client.context.get()
101 if (typeof context?.app === 'undefined') {
102 logger.debug('Aborted metrics ping due to unavailable context')
103 return
104 }
105
106 const oldAppInfo = await readAppInfo()
107 const newAppInfo = context.app as AppInfo
108 writeAppInfo(newAppInfo)
109 logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
110
111 if (typeof oldAppInfo === 'undefined') {
112 client.track('Application Installed', {
113 version: newAppInfo.version,
114 build: newAppInfo.build,
115 })
116 } else if (newAppInfo.version !== oldAppInfo.version) {
117 client.track('Application Updated', {
118 version: newAppInfo.version,
119 build: newAppInfo.build,
120 previous_version: oldAppInfo.version,
121 previous_build: oldAppInfo.build,
122 })
123 }
124 client.track('Application Opened', {
125 from_background: false,
126 version: newAppInfo.version,
127 build: newAppInfo.build,
128 })
129 })
130
131 let lastState: AppStateStatus = AppState.currentState
132 AppState.addEventListener('change', (state: AppStateStatus) => {
133 if (state === 'active' && lastState !== 'active') {
134 const context = client.context.get()
135 client.track('Application Opened', {
136 from_background: true,
137 version: context?.app?.version,
138 build: context?.app?.build,
139 })
140 } else if (state !== 'active' && lastState === 'active') {
141 client.track('Application Backgrounded')
142 }
143 lastState = state
144 })
145}
146
147async function writeAppInfo(value: AppInfo) {
148 await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
149}
150
151async function readAppInfo(): Promise<AppInfo | undefined> {
152 const rawData = await AsyncStorage.getItem('BSKY_APP_INFO')
153 const obj = rawData ? JSON.parse(rawData) : undefined
154 if (!obj || typeof obj !== 'object') {
155 return undefined
156 }
157 return obj
158}