Bluesky app fork with some witchin' additions 💫

[APP-1805] Analytics tweaks and tracking callbacks (#9861)

authored by

Eric Bailey and committed by
GitHub
4e3c3e99 1565e071

+89 -22
+2 -1
package.json
··· 93 93 "@fortawesome/free-regular-svg-icons": "^6.1.1", 94 94 "@fortawesome/free-solid-svg-icons": "^6.1.1", 95 95 "@fortawesome/react-native-fontawesome": "^0.3.2", 96 - "@growthbook/growthbook-react": "^1.6.2", 96 + "@growthbook/growthbook": "^1.6.5", 97 + "@growthbook/growthbook-react": "^1.6.5", 97 98 "@haileyok/bluesky-video": "0.3.2", 98 99 "@ipld/dag-cbor": "^9.2.0", 99 100 "@lingui/react": "^4.14.1",
+10
src/analytics/identifiers/session.ts
··· 18 18 return sessionId 19 19 } 20 20 21 + /** 22 + * Gets the current session ID. Freshness depends on `useSessionId` being 23 + * mounted, which handles refreshing this value between foreground/background 24 + * transitions. Since that's mounted in `analytics/index.tsx`, this value can 25 + * generally be trusted to be up to date. 26 + */ 27 + export function getSessionId() { 28 + return device.get(['nativeSessionId']) 29 + } 30 + 21 31 export function useSessionId() { 22 32 const [id, setId] = useState(() => sessionId) 23 33
+10
src/analytics/identifiers/session.web.ts
··· 21 21 return sessionId 22 22 } 23 23 24 + /** 25 + * Gets the current session ID. Freshness depends on `useSessionId` being 26 + * mounted, which handles refreshing this value between foreground/background 27 + * transitions. Since that's mounted in `analytics/index.tsx`, this value can 28 + * generally be trusted to be up to date. 29 + */ 30 + export function getSessionId() { 31 + return window.sessionStorage.getItem(SESSION_ID_KEY) 32 + } 33 + 24 34 export function useSessionId() { 25 35 const [id, setId] = useState(() => sessionId) 26 36
+20 -15
src/analytics/index.tsx
··· 1 - import {createContext, useContext, useEffect, useMemo} from 'react' 1 + import {createContext, useContext, useMemo} from 'react' 2 2 import {Platform} from 'react-native' 3 3 4 4 import {Logger} from '#/logger' ··· 24 24 import {type Metrics, metrics} from '#/analytics/metrics' 25 25 import * as refParams from '#/analytics/misc/refParams' 26 26 import * as env from '#/env' 27 - import {useGeolocation} from '#/geolocation' 27 + import {useGeolocationServiceResponse} from '#/geolocation/service' 28 28 import {device} from '#/storage' 29 29 30 30 export * as utils from '#/analytics/utils' ··· 104 104 referrerSrc: refParams.src, 105 105 referrerUrl: refParams.url, 106 106 }, 107 - geolocation: device.get(['mergedGeolocation']) || { 107 + geolocation: device.get(['geolocationServiceResponse']) || { 108 108 countryCode: '', 109 109 regionCode: '', 110 110 }, ··· 137 137 } 138 138 } 139 139 const sessionId = useSessionId() 140 - const geolocation = useGeolocation() 140 + const geolocation = useGeolocationServiceResponse() 141 141 const parentContext = useContext(Context) 142 142 const childContext = useMemo(() => { 143 143 const combinedMetadata = { ··· 181 181 const parentContext = useContext(Context) 182 182 183 183 /** 184 - * Side-effect: we need to synchronously set this during the 185 - * same render cycle. It does not trigger a re-render, it just 186 - * sets properties on the singleton GrowthBook instance. 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 187 */ 188 188 setAttributes(parentContext.metadata) 189 - 190 - useEffect(() => { 191 - feats.setTrackingCallback((experiment, result) => { 192 - parentContext.metric('experiment:viewed', { 193 - experimentId: experiment.key, 194 - variationId: result.key, 195 - }) 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, 196 201 }) 197 - }, [parentContext.metric]) 202 + }) 198 203 199 204 const childContext = useMemo<AnalyticsContextType>(() => { 200 205 return {
+3 -1
src/analytics/metrics/client.ts
··· 4 4 import * as env from '#/env' 5 5 6 6 type Event<M extends Record<string, any>> = { 7 + source: 'app' 7 8 time: number 8 9 event: keyof M 9 10 payload: M[keyof M] ··· 43 44 ) { 44 45 this.start() 45 46 46 - const e = { 47 + const e: Event<M> = { 48 + source: 'app', 47 49 time: Date.now(), 48 50 event, 49 51 payload,
+8
src/analytics/metrics/types.ts
··· 15 15 experimentId: string 16 16 variationId: string 17 17 } 18 + 'feature:viewed': { 19 + featureId: string 20 + featureResultValue: unknown 21 + /** Only available if feature has experiment rules applied */ 22 + experimentId?: string 23 + /** Only available if feature has experiment rules applied */ 24 + variationId?: string 25 + } 18 26 19 27 'account:loggedIn': { 20 28 logContext:
+24
src/geolocation/util.ts
··· 3 3 import {IS_ANDROID} from '#/env' 4 4 import {logger} from '#/geolocation/logger' 5 5 import {type Geolocation} from '#/geolocation/types' 6 + import {device} from '#/storage' 6 7 7 8 /** 8 9 * Maps full US region names to their short codes. ··· 125 126 }) 126 127 return geolocation 127 128 } 129 + 130 + /** 131 + * Gets the IP-based geolocation as a string in the format of 132 + * "countryCode-regionCode", or just "countryCode" if regionCode is not 133 + * available. 134 + * 135 + * IMPORTANT: this method should only return IP-based data, not the user's GPS 136 + * based data. IP-based data we can already infer from requests, but for 137 + * consistency between frontend and backend, we sometimes want to share the 138 + * value we have on the frontend with the backend. 139 + */ 140 + export function getIPGeolocationString() { 141 + const geo = device.get(['geolocationServiceResponse']) 142 + if (!geo) return 143 + const {countryCode, regionCode} = geo 144 + if (countryCode) { 145 + if (regionCode) { 146 + return `${countryCode}-${regionCode}` 147 + } else { 148 + return countryCode 149 + } 150 + } 151 + }
+12 -5
yarn.lock
··· 4592 4592 dependencies: 4593 4593 nanoid "^3.3.1" 4594 4594 4595 - "@growthbook/growthbook-react@^1.6.2": 4596 - version "1.6.2" 4597 - resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-1.6.2.tgz#847135be0c46b167f980dbe6015e5f4ed010475c" 4598 - integrity sha512-96Bo2Jwd4NBn/kBLN4ceF299PhTw8fQltRykD32hu2xMW8/LXhB8swxbPshGK+Xfa2gjgt24kpZ5oSvaVLLT7w== 4595 + "@growthbook/growthbook-react@^1.6.5": 4596 + version "1.6.5" 4597 + resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-1.6.5.tgz#e849cff64a54e56d5c566512bdc839148ae613ed" 4598 + integrity sha512-afi/RUbwazVNKv2acn6wDQz4BJNRAEpwIuHfggQup2/aE5PLAxy3+95gjjRMgCcPR0Pf3sFmhYGvOmxLD0ZRbQ== 4599 4599 dependencies: 4600 - "@growthbook/growthbook" "^1.6.2" 4600 + "@growthbook/growthbook" "^1.6.5" 4601 4601 4602 4602 "@growthbook/growthbook@^1.6.2": 4603 4603 version "1.6.2" 4604 4604 resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-1.6.2.tgz#6a25122deac8a09955f6bddeb134af62b209809b" 4605 4605 integrity sha512-x3sK6Lff4BVusIzcdBeHZqA3B4kLs3kM/pJ7vvLTJwb6N/+Yn99EF1yc0XU6cfDVqFq6uFkvIFhMwDWUaKD73g== 4606 + dependencies: 4607 + dom-mutator "^0.6.0" 4608 + 4609 + "@growthbook/growthbook@^1.6.5": 4610 + version "1.6.5" 4611 + resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz#c9e2119187ee3288525a77a7c64353276f5ba91b" 4612 + integrity sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A== 4606 4613 dependencies: 4607 4614 dom-mutator "^0.6.0" 4608 4615