From 4e3c3e9905f4926b0533e56d23664c4e381ee93c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 19 Feb 2026 15:31:35 -0600 Subject: [PATCH] [APP-1805] Analytics tweaks and tracking callbacks (#9861) --- package.json | 3 +- src/analytics/identifiers/session.ts | 10 +++++++ src/analytics/identifiers/session.web.ts | 10 +++++++ src/analytics/index.tsx | 35 ++++++++++++++---------- src/analytics/metrics/client.ts | 4 ++- src/analytics/metrics/types.ts | 8 ++++++ src/geolocation/util.ts | 24 ++++++++++++++++ yarn.lock | 17 ++++++++---- 8 files changed, 89 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 0c49dc24da..f4d1cedb8c 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@growthbook/growthbook-react": "^1.6.2", + "@growthbook/growthbook": "^1.6.5", + "@growthbook/growthbook-react": "^1.6.5", "@haileyok/bluesky-video": "0.3.2", "@ipld/dag-cbor": "^9.2.0", "@lingui/react": "^4.14.1", diff --git a/src/analytics/identifiers/session.ts b/src/analytics/identifiers/session.ts index e94ece704f..1699d474a2 100644 --- a/src/analytics/identifiers/session.ts +++ b/src/analytics/identifiers/session.ts @@ -18,6 +18,16 @@ export function getInitialSessionId() { return sessionId } +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return device.get(['nativeSessionId']) +} + export function useSessionId() { const [id, setId] = useState(() => sessionId) diff --git a/src/analytics/identifiers/session.web.ts b/src/analytics/identifiers/session.web.ts index c2b2090cf0..c126a54cc1 100644 --- a/src/analytics/identifiers/session.web.ts +++ b/src/analytics/identifiers/session.web.ts @@ -21,6 +21,16 @@ export function getInitialSessionId() { return sessionId } +/** + * Gets the current session ID. Freshness depends on `useSessionId` being + * mounted, which handles refreshing this value between foreground/background + * transitions. Since that's mounted in `analytics/index.tsx`, this value can + * generally be trusted to be up to date. + */ +export function getSessionId() { + return window.sessionStorage.getItem(SESSION_ID_KEY) +} + export function useSessionId() { const [id, setId] = useState(() => sessionId) diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index 47e024ae88..3d39f464cd 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -1,4 +1,4 @@ -import {createContext, useContext, useEffect, useMemo} from 'react' +import {createContext, useContext, useMemo} from 'react' import {Platform} from 'react-native' import {Logger} from '#/logger' @@ -24,7 +24,7 @@ import { import {type Metrics, metrics} from '#/analytics/metrics' import * as refParams from '#/analytics/misc/refParams' import * as env from '#/env' -import {useGeolocation} from '#/geolocation' +import {useGeolocationServiceResponse} from '#/geolocation/service' import {device} from '#/storage' export * as utils from '#/analytics/utils' @@ -104,7 +104,7 @@ const Context = createContext({ referrerSrc: refParams.src, referrerUrl: refParams.url, }, - geolocation: device.get(['mergedGeolocation']) || { + geolocation: device.get(['geolocationServiceResponse']) || { countryCode: '', regionCode: '', }, @@ -137,7 +137,7 @@ export function AnalyticsContext({ } } const sessionId = useSessionId() - const geolocation = useGeolocation() + const geolocation = useGeolocationServiceResponse() const parentContext = useContext(Context) const childContext = useMemo(() => { const combinedMetadata = { @@ -181,20 +181,25 @@ export function AnalyticsFeaturesContext({ const parentContext = useContext(Context) /** - * Side-effect: we need to synchronously set this during the - * same render cycle. It does not trigger a re-render, it just - * sets properties on the singleton GrowthBook instance. + * Side-effects: we need to synchronously set these during the same render + * cycle. These calls do not trigger re-renders, they just set properties on + * the singleton GrowthBook instance. */ setAttributes(parentContext.metadata) - - useEffect(() => { - feats.setTrackingCallback((experiment, result) => { - parentContext.metric('experiment:viewed', { - experimentId: experiment.key, - variationId: result.key, - }) + feats.setTrackingCallback((experiment, result) => { + parentContext.metric('experiment:viewed', { + experimentId: experiment.key, + variationId: result.key, }) - }, [parentContext.metric]) + }) + feats.setFeatureUsageCallback((feature, result) => { + parentContext.metric('feature:viewed', { + featureId: feature, + featureResultValue: result.value, + experimentId: result.experiment?.key, + variationId: result.experimentResult?.key, + }) + }) const childContext = useMemo(() => { return { diff --git a/src/analytics/metrics/client.ts b/src/analytics/metrics/client.ts index 868cbc93e6..2aab90f265 100644 --- a/src/analytics/metrics/client.ts +++ b/src/analytics/metrics/client.ts @@ -4,6 +4,7 @@ import {Logger} from '#/logger' import * as env from '#/env' type Event> = { + source: 'app' time: number event: keyof M payload: M[keyof M] @@ -43,7 +44,8 @@ export class MetricsClient> { ) { this.start() - const e = { + const e: Event = { + source: 'app', time: Date.now(), event, payload, diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 65f2a5d39f..636c0287a2 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -15,6 +15,14 @@ export type Events = { experimentId: string variationId: string } + 'feature:viewed': { + featureId: string + featureResultValue: unknown + /** Only available if feature has experiment rules applied */ + experimentId?: string + /** Only available if feature has experiment rules applied */ + variationId?: string + } 'account:loggedIn': { logContext: diff --git a/src/geolocation/util.ts b/src/geolocation/util.ts index e9250b0b4c..fb099ac321 100644 --- a/src/geolocation/util.ts +++ b/src/geolocation/util.ts @@ -3,6 +3,7 @@ import {type LocationGeocodedAddress} from 'expo-location' import {IS_ANDROID} from '#/env' import {logger} from '#/geolocation/logger' import {type Geolocation} from '#/geolocation/types' +import {device} from '#/storage' /** * Maps full US region names to their short codes. @@ -125,3 +126,26 @@ export function mergeGeolocations( }) return geolocation } + +/** + * Gets the IP-based geolocation as a string in the format of + * "countryCode-regionCode", or just "countryCode" if regionCode is not + * available. + * + * IMPORTANT: this method should only return IP-based data, not the user's GPS + * based data. IP-based data we can already infer from requests, but for + * consistency between frontend and backend, we sometimes want to share the + * value we have on the frontend with the backend. + */ +export function getIPGeolocationString() { + const geo = device.get(['geolocationServiceResponse']) + if (!geo) return + const {countryCode, regionCode} = geo + if (countryCode) { + if (regionCode) { + return `${countryCode}-${regionCode}` + } else { + return countryCode + } + } +} diff --git a/yarn.lock b/yarn.lock index 4524ecd665..24ec12908c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4592,12 +4592,12 @@ dependencies: nanoid "^3.3.1" -"@growthbook/growthbook-react@^1.6.2": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-1.6.2.tgz#847135be0c46b167f980dbe6015e5f4ed010475c" - integrity sha512-96Bo2Jwd4NBn/kBLN4ceF299PhTw8fQltRykD32hu2xMW8/LXhB8swxbPshGK+Xfa2gjgt24kpZ5oSvaVLLT7w== +"@growthbook/growthbook-react@^1.6.5": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@growthbook/growthbook-react/-/growthbook-react-1.6.5.tgz#e849cff64a54e56d5c566512bdc839148ae613ed" + integrity sha512-afi/RUbwazVNKv2acn6wDQz4BJNRAEpwIuHfggQup2/aE5PLAxy3+95gjjRMgCcPR0Pf3sFmhYGvOmxLD0ZRbQ== dependencies: - "@growthbook/growthbook" "^1.6.2" + "@growthbook/growthbook" "^1.6.5" "@growthbook/growthbook@^1.6.2": version "1.6.2" @@ -4606,6 +4606,13 @@ dependencies: dom-mutator "^0.6.0" +"@growthbook/growthbook@^1.6.5": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz#c9e2119187ee3288525a77a7c64353276f5ba91b" + integrity sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A== + dependencies: + dom-mutator "^0.6.0" + "@grpc/grpc-js@^1.8.20": version "1.13.3" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.13.3.tgz#6ad08d186c2a8651697085f790c5c68eaca45904"