Implement our own lifecycle tracking to ensure it never fires while the app is backgrounded (close #193) (#211)
This commit is contained in:
+3
-2
@@ -14,7 +14,7 @@ import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
|||||||
import {MobileShell} from './view/shell/mobile'
|
import {MobileShell} from './view/shell/mobile'
|
||||||
import {s} from './view/lib/styles'
|
import {s} from './view/lib/styles'
|
||||||
import notifee, {EventType} from '@notifee/react-native'
|
import notifee, {EventType} from '@notifee/react-native'
|
||||||
import {segmentClient} from './lib/segmentClient'
|
import * as analytics from './lib/analytics'
|
||||||
import * as Toast from './view/com/util/Toast'
|
import * as Toast from './view/com/util/Toast'
|
||||||
|
|
||||||
const App = observer(() => {
|
const App = observer(() => {
|
||||||
@@ -26,9 +26,10 @@ const App = observer(() => {
|
|||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
view.setup()
|
view.setup()
|
||||||
setSegment(segmentClient)
|
setSegment(analytics.segmentClient)
|
||||||
setupState().then(store => {
|
setupState().then(store => {
|
||||||
setRootStore(store)
|
setRootStore(store)
|
||||||
|
analytics.init(store)
|
||||||
SplashScreen.hide()
|
SplashScreen.hide()
|
||||||
Linking.getInitialURL().then((url: string | null) => {
|
Linking.getInitialURL().then((url: string | null) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {AppState, AppStateStatus} from 'react-native'
|
||||||
|
import {createClient} from '@segment/analytics-react-native'
|
||||||
|
import {RootStoreModel, AppInfo} from '../state/models/root-store'
|
||||||
|
// import {createLogger} from './logger'
|
||||||
|
|
||||||
|
export const segmentClient = createClient({
|
||||||
|
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
||||||
|
trackAppLifecycleEvents: false,
|
||||||
|
// Uncomment to debug:
|
||||||
|
// logger: createLogger(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function init(store: RootStoreModel) {
|
||||||
|
// NOTE
|
||||||
|
// this method 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.onContextLoaded(() => {
|
||||||
|
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') {
|
||||||
|
segmentClient.track('Application Installed', {
|
||||||
|
version: newAppInfo.version,
|
||||||
|
build: newAppInfo.build,
|
||||||
|
})
|
||||||
|
} else if (newAppInfo.version !== oldAppInfo.version) {
|
||||||
|
segmentClient.track('Application Updated', {
|
||||||
|
version: newAppInfo.version,
|
||||||
|
build: newAppInfo.build,
|
||||||
|
previous_version: oldAppInfo.version,
|
||||||
|
previous_build: oldAppInfo.build,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import {createClient} from '@segment/analytics-react-native'
|
|
||||||
// import {createLogger} from './logger'
|
|
||||||
|
|
||||||
export const segmentClient = createClient({
|
|
||||||
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
|
||||||
trackAppLifecycleEvents: true,
|
|
||||||
// Uncomment to debug:
|
|
||||||
// logger: createLogger(),
|
|
||||||
})
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {TABS_ENABLED} from '../../build-flags'
|
import {TABS_ENABLED} from '../../build-flags'
|
||||||
import {segmentClient} from '../../lib/segmentClient'
|
import {segmentClient} from '../../lib/analytics'
|
||||||
|
|
||||||
let __id = 0
|
let __id = 0
|
||||||
function genId() {
|
function genId() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {AtpAgent} from '@atproto/api'
|
|||||||
import {createContext, useContext} from 'react'
|
import {createContext, useContext} from 'react'
|
||||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||||
import BackgroundFetch from 'react-native-background-fetch'
|
import BackgroundFetch from 'react-native-background-fetch'
|
||||||
|
import {z} from 'zod'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
import {LogModel} from './log'
|
import {LogModel} from './log'
|
||||||
import {SessionModel} from './session'
|
import {SessionModel} from './session'
|
||||||
@@ -17,8 +18,17 @@ import {LinkMetasViewModel} from './link-metas-view'
|
|||||||
import {MeModel} from './me'
|
import {MeModel} from './me'
|
||||||
import {OnboardModel} from './onboard'
|
import {OnboardModel} from './onboard'
|
||||||
|
|
||||||
|
export const appInfo = z.object({
|
||||||
|
build: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
namespace: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
})
|
||||||
|
export type AppInfo = z.infer<typeof appInfo>
|
||||||
|
|
||||||
export class RootStoreModel {
|
export class RootStoreModel {
|
||||||
agent: AtpAgent
|
agent: AtpAgent
|
||||||
|
appInfo?: AppInfo
|
||||||
log = new LogModel()
|
log = new LogModel()
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
nav = new NavigationModel(this)
|
nav = new NavigationModel(this)
|
||||||
@@ -42,8 +52,13 @@ export class RootStoreModel {
|
|||||||
return this.agent.api
|
return this.agent.api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAppInfo(info: AppInfo) {
|
||||||
|
this.appInfo = info
|
||||||
|
}
|
||||||
|
|
||||||
serialize(): unknown {
|
serialize(): unknown {
|
||||||
return {
|
return {
|
||||||
|
appInfo: this.appInfo,
|
||||||
log: this.log.serialize(),
|
log: this.log.serialize(),
|
||||||
session: this.session.serialize(),
|
session: this.session.serialize(),
|
||||||
me: this.me.serialize(),
|
me: this.me.serialize(),
|
||||||
@@ -55,6 +70,12 @@ export class RootStoreModel {
|
|||||||
|
|
||||||
hydrate(v: unknown) {
|
hydrate(v: unknown) {
|
||||||
if (isObj(v)) {
|
if (isObj(v)) {
|
||||||
|
if (hasProp(v, 'appInfo')) {
|
||||||
|
const appInfoParsed = appInfo.safeParse(v.appInfo)
|
||||||
|
if (appInfoParsed.success) {
|
||||||
|
this.setAppInfo(appInfoParsed.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (hasProp(v, 'log')) {
|
if (hasProp(v, 'log')) {
|
||||||
this.log.hydrate(v.log)
|
this.log.hydrate(v.log)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user