[APP-511] metrics overhaul: frontend work (#506)
* WIP * fix types and update imports * wip * tagged events that should be server side * remove server-side analytics * remove useless import * add additional profile header events * remove useless import * track follow/unfollow clicks * add missing types
This commit is contained in:
parent
1695ae34db
commit
17e7590bcd
33 changed files with 156 additions and 41 deletions
128
src/lib/analytics/analytics.tsx
Normal file
128
src/lib/analytics/analytics.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
65
src/lib/analytics/analytics.web.tsx
Normal file
65
src/lib/analytics/analytics.web.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
98
src/lib/analytics/types.ts
Normal file
98
src/lib/analytics/types.ts
Normal file
|
@ -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: {}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue