diff options
Diffstat (limited to 'src/lib/analytics')
-rw-r--r-- | src/lib/analytics/analytics.tsx | 128 | ||||
-rw-r--r-- | src/lib/analytics/analytics.web.tsx | 65 | ||||
-rw-r--r-- | src/lib/analytics/types.ts | 98 |
3 files changed, 291 insertions, 0 deletions
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx new file mode 100644 index 000000000..d9d53e6a9 --- /dev/null +++ b/src/lib/analytics/analytics.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import {AppState, AppStateStatus} from 'react-native' +import { + createClient, + AnalyticsProvider, + useAnalytics as useAnalyticsOrig, + ClientMethods, +} from '@segment/analytics-react-native' +import {RootStoreModel, AppInfo} from 'state/models/root-store' +import {useStores} from 'state/models/root-store' +import {sha256} from 'js-sha256' +import {ScreenEvent, TrackEvent} from './types' + +const segmentClient = createClient({ + writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', + trackAppLifecycleEvents: false, +}) + +export function useAnalytics() { + const store = useStores() + const methods: ClientMethods = useAnalyticsOrig() + return React.useMemo(() => { + if (store.session.hasSession) { + return { + screen: methods.screen as ScreenEvent, // ScreenEvents defines all the possible screen names + track: methods.track as TrackEvent, // TrackEvents defines all the possible track events and their properties + identify: methods.identify, + flush: methods.flush, + group: methods.group, + alias: methods.alias, + reset: methods.reset, + } + } + // dont send analytics pings for anonymous users + return { + screen: () => Promise<void>, + track: () => Promise<void>, + identify: () => Promise<void>, + flush: () => Promise<void>, + group: () => Promise<void>, + alias: () => Promise<void>, + reset: () => Promise<void>, + } + }, [store, methods]) +} + +export function init(store: RootStoreModel) { + store.onSessionLoaded(() => { + const sess = store.session.currentSession + if (sess) { + if (sess.email) { + store.log.debug('Ping w/hash') + const email_hashed = sha256(sess.email) + segmentClient.identify(email_hashed, {email_hashed}) + } else { + store.log.debug('Ping w/o hash') + segmentClient.identify() + } + } + }) + + // NOTE + // this is a copy of segment's own lifecycle event tracking + // we handle it manually to ensure that it never fires while the app is backgrounded + // -prf + segmentClient.isReady.onChange(() => { + if (AppState.currentState !== 'active') { + store.log.debug('Prevented a metrics ping while the app was backgrounded') + return + } + const context = segmentClient.context.get() + if (typeof context?.app === 'undefined') { + store.log.debug('Aborted metrics ping due to unavailable context') + return + } + + const oldAppInfo = store.appInfo + const newAppInfo = context.app as AppInfo + store.setAppInfo(newAppInfo) + store.log.debug('Recording app info', {new: newAppInfo, old: oldAppInfo}) + + if (typeof oldAppInfo === 'undefined') { + if (store.session.hasSession) { + segmentClient.track('Application Installed', { + version: newAppInfo.version, + build: newAppInfo.build, + }) + } + } else if (newAppInfo.version !== oldAppInfo.version) { + if (store.session.hasSession) { + segmentClient.track('Application Updated', { + version: newAppInfo.version, + build: newAppInfo.build, + previous_version: oldAppInfo.version, + previous_build: oldAppInfo.build, + }) + } + } + if (store.session.hasSession) { + segmentClient.track('Application Opened', { + from_background: false, + version: newAppInfo.version, + build: newAppInfo.build, + }) + } + }) + + let lastState: AppStateStatus = AppState.currentState + AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === 'active' && lastState !== 'active') { + const context = segmentClient.context.get() + segmentClient.track('Application Opened', { + from_background: true, + version: context?.app?.version, + build: context?.app?.build, + }) + } else if (state !== 'active' && lastState === 'active') { + segmentClient.track('Application Backgrounded') + } + lastState = state + }) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + return ( + <AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider> + ) +} diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx new file mode 100644 index 000000000..467ae278b --- /dev/null +++ b/src/lib/analytics/analytics.web.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import { + createClient, + AnalyticsProvider, + useAnalytics as useAnalyticsOrig, +} from '@segment/analytics-react' +import {RootStoreModel} from 'state/models/root-store' +import {useStores} from 'state/models/root-store' +import {sha256} from 'js-sha256' + +const segmentClient = createClient( + { + writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', + }, + { + integrations: { + 'Segment.io': { + apiHost: 'api.evt.bsky.app/v1', + }, + }, + }, +) +export const track = segmentClient?.track?.bind?.(segmentClient) + +export function useAnalytics() { + const store = useStores() + const methods = useAnalyticsOrig() + return React.useMemo(() => { + if (store.session.hasSession) { + return methods + } + // dont send analytics pings for anonymous users + return { + screen: () => {}, + track: () => {}, + identify: () => {}, + flush: () => {}, + group: () => {}, + alias: () => {}, + reset: () => {}, + } + }, [store, methods]) +} + +export function init(store: RootStoreModel) { + store.onSessionLoaded(() => { + const sess = store.session.currentSession + if (sess) { + if (sess.email) { + store.log.debug('Ping w/hash') + const email_hashed = sha256(sess.email) + segmentClient.identify(email_hashed, {email_hashed}) + } else { + store.log.debug('Ping w/o hash') + segmentClient.identify() + } + } + }) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + return ( + <AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider> + ) +} diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts new file mode 100644 index 000000000..0638c6b77 --- /dev/null +++ b/src/lib/analytics/types.ts @@ -0,0 +1,98 @@ +export type TrackEvent = ( + event: keyof TrackPropertiesMap, + properties?: TrackPropertiesMap[keyof TrackPropertiesMap], +) => Promise<void> + +export type ScreenEvent = ( + name: keyof ScreenPropertiesMap, + properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap], +) => Promise<void> +interface TrackPropertiesMap { + // LOGIN / SIGN UP events + 'Sign In': {resumedSession: boolean} // CAN BE SERVER + 'Create Account': {} // CAN BE SERVER + 'Signin:PressedForgotPassword': {} + 'Signin:PressedSelectService': {} + // COMPOSER / CREATE POST events + 'Create Post': {imageCount: string} // CAN BE SERVER + 'Composer:PastedPhotos': {} + 'Composer:CameraOpened': {} + 'Composer:GalleryOpened': {} + 'HomeScreen:PressCompose': {} + 'ProfileScreen:PressCompose': {} + // EDIT PROFILE events + 'EditHandle:ViewCustomForm': {} + 'EditHandle:ViewProvidedForm': {} + 'EditHandle:SetNewHandle': {} + 'EditProfile:AvatarSelected': {} + 'EditProfile:BannerSelected': {} + 'EditProfile:Save': {} // CAN BE SERVER + // FEED events + 'Feed:onRefresh': {} + 'Feed:onEndReached': {} + // FEED ITEM events + 'FeedItem:PostReply': {} // CAN BE SERVER + 'FeedItem:PostRepost': {} // CAN BE SERVER + 'FeedItem:PostLike': {} // CAN BE SERVER + 'FeedItem:PostDelete': {} // CAN BE SERVER + 'FeedItem:ThreadMute': {} // CAN BE SERVER + // PROFILE HEADER events + 'ProfileHeader:EditProfileButtonClicked': {} + 'ProfileHeader:FollowersButtonClicked': {} + 'ProfileHeader:FollowsButtonClicked': {} + 'ProfileHeader:ShareButtonClicked': {} + 'ProfileHeader:MuteAccountButtonClicked': {} + 'ProfileHeader:UnmuteAccountButtonClicked': {} + 'ProfileHeader:ReportAccountButtonClicked': {} + 'ProfileHeader:AddToListsButtonClicked': {} + 'ProfileHeader:BlockAccountButtonClicked': {} + 'ProfileHeader:UnblockAccountButtonClicked': {} + 'ProfileHeader:FollowButtonClicked': {} + 'ProfileHeader:UnfollowButtonClicked': {} + 'ViewHeader:MenuButtonClicked': {} + // SETTINGS events + 'Settings:SwitchAccountButtonClicked': {} + 'Settings:AddAccountButtonClicked': {} + 'Settings:ChangeHandleButtonClicked': {} + 'Settings:InvitecodesButtonClicked': {} + 'Settings:ContentfilteringButtonClicked': {} + 'Settings:SignOutButtonClicked': {} + 'Settings:ContentlanguagesButtonClicked': {} + // MENU events + 'Menu:ItemClicked': {url: string} + 'Menu:FeedbackClicked': {} + // MOBILE SHELL events + 'MobileShell:MyProfileButtonPressed': {} + 'MobileShell:HomeButtonPressed': {} + 'MobileShell:SearchButtonPressed': {} + 'MobileShell:NotificationsButtonPressed': {} + 'MobileShell:FeedsButtonPressed': {} + // LISTS events + 'Lists:onRefresh': {} + 'Lists:onEndReached': {} + 'CreateMuteList:AvatarSelected': {} + 'CreateMuteList:Save': {} // CAN BE SERVER + // CUSTOM FEED events + 'MultiFeed:onEndReached': {} + 'MultiFeed:onRefresh': {} + // MODERATION events + 'Moderation:ContentfilteringButtonClicked': {} +} + +interface ScreenPropertiesMap { + Login: {} + CreateAccount: {} + 'Choose Account': {} + 'Signin:ForgotPassword': {} + 'Signin:SetNewPasswordForm': {} + 'Signin:PasswordUpdatedForm': {} + Feed: {} + Notifications: {} + Profile: {} + Settings: {} + AppPasswords: {} + Moderation: {} + BlockedAccounts: {} + MutedAccounts: {} + SavedFeeds: {} +} |