Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: session replay RN #261

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ yalc.lock

# Generated at build time
posthog-react-native/src/version.ts

.DS_Store
3 changes: 2 additions & 1 deletion examples/example_rn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"react": "18.2.0",
"react-native": "0.73.2",
"react-native-device-info": "^10.12.0",
"react-native-navigation": "^7.37.2"
"react-native-navigation": "^7.37.2",
"posthog-react-native-session-replay": "^0.0.5"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
3 changes: 2 additions & 1 deletion examples/example_rn/posthog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import PostHog, {PostHogProvider} from 'posthog-react-native';
import React from 'react';

export const posthog = new PostHog(
'phc_pQ70jJhZKHRvDIL5ruOErnPy6xiAiWCqlL4ayELj4X8',
'phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D',
{
// host: 'https://us.i.posthog.com',
// persistence: 'memory',
sessionReplay: true,
},
);

Expand Down
9 changes: 9 additions & 0 deletions posthog-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,15 @@ export abstract class PostHogCore extends PostHogCoreStateless {
Object.entries(newFeatureFlagPayloads || {}).map(([k, v]) => [k, this._parsePayload(v)])
)
)

const sessionReplay = res?.sessionRecording
if (sessionReplay) {
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, sessionReplay)
console.log('PostHog Debug', 'Session replay config: ', JSON.stringify(sessionReplay))
} else {
console.info('PostHog Debug', 'Session replay config disabled.')
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null)
}
}

return res
Expand Down
7 changes: 6 additions & 1 deletion posthog-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export enum PostHogPersistedProperty {
GroupProperties = 'group_properties',
InstalledAppBuild = 'installed_app_build', // only used by posthog-react-native
InstalledAppVersion = 'installed_app_version', // only used by posthog-react-native
SessionReplay = 'session_replay', // only used by posthog-react-native
}

export type PostHogFetchOptions = {
Expand Down Expand Up @@ -116,7 +117,11 @@ export type PostHogDecideResponse = {
[key: string]: JsonType
}
errorsWhileComputingFlags: boolean
sessionRecording: boolean
sessionRecording?:
| boolean
| {
[key: string]: JsonType
}
}

export type PostHogFlagsAndPayloadsResponse = {
Expand Down
11 changes: 8 additions & 3 deletions posthog-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
"expo-file-system": "^13.0.0",
"expo-localization": "~11.0.0",
"jest-expo": "^46.0.1",
"react": "^18.2.0",
"react": "18.2.0",
"react-native": "^0.69.1",
"react-native-device-info": "^10.3.0",
"react-native-navigation": "^6.0.0"
"react-native-navigation": "^6.0.0",
"posthog-react-native-session-replay": "^0.0.5"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": ">=1.0.0",
Expand All @@ -42,7 +43,8 @@
"expo-file-system": ">= 13.0.0",
"expo-localization": ">= 11.0.0",
"react-native-device-info": ">= 10.0.0",
"react-native-navigation": ">=6.0.0"
"react-native-navigation": ">=6.0.0",
"posthog-react-native-session-replay": "^0.0.5"
},
"peerDependenciesMeta": {
"@react-native-async-storage/async-storage": {
Expand All @@ -68,6 +70,9 @@
},
"react-native-navigation": {
"optional": true
},
"posthog-react-native-session-replay": {
"optional": true
}
}
}
16 changes: 16 additions & 0 deletions posthog-react-native/src/optional/OptionalSessionReplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Platform } from 'react-native'

import type PostHogReactNativeSessionReplay from 'posthog-react-native-session-replay'

export let OptionalReactNativeSessionReplay: typeof PostHogReactNativeSessionReplay | undefined = undefined

try {
OptionalReactNativeSessionReplay = Platform.select({
macos: undefined,
web: undefined,
default: require('posthog-react-native-session-replay'), // Only Android and iOS
})
} catch (e) {
// do nothing
console.warn('PostHog Debug', `Error loading posthog-react-native-session-replay: ${e}`)
}
99 changes: 98 additions & 1 deletion posthog-react-native/src/posthog-rn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppState, Dimensions, Linking, Platform } from 'react-native'

import {
JsonType,
PostHogCaptureOptions,
PostHogCore,
PostHogCoreOptions,
Expand All @@ -12,8 +13,14 @@ import { getLegacyValues } from './legacy'
import { PostHogRNStorage, PostHogRNSyncMemoryStorage } from './storage'
import { version } from './version'
import { buildOptimisiticAsyncStorage, getAppProperties } from './native-deps'
import { PostHogAutocaptureOptions, PostHogCustomAppProperties, PostHogCustomStorage } from './types'
import {
PostHogAutocaptureOptions,
PostHogCustomAppProperties,
PostHogCustomStorage,
PostHogSessionReplayConfig,
} from './types'
import { withReactNativeNavigation } from './frameworks/wix-navigation'
import { OptionalReactNativeSessionReplay } from './optional/OptionalSessionReplay'

export type PostHogOptions = PostHogCoreOptions & {
/** Allows you to provide the storage type. By default 'file'.
Expand All @@ -35,12 +42,29 @@ export type PostHogOptions = PostHogCoreOptions & {
* If you're already using the 'captureLifecycleEvents' options with 'withReactNativeNavigation' or 'PostHogProvider, you should not set this to true, otherwise you may see duplicated events.
*/
captureNativeAppLifecycleEvents?: boolean

/**
* Enable Recording of Session Replays for Android and iOS
* Requires Record user sessions to be enabled in the PostHog Project Settings
* Experimental support
* Defaults to false
*/
sessionReplay?: boolean

/**
* Enable Recording of Session Replays for Android and iOS
* Requires Record user sessions to be enabled in the PostHog Project Settings
* Experimental support
* Defaults to false
*/
sessionReplayConfig?: PostHogSessionReplayConfig
}

export class PostHog extends PostHogCore {
private _persistence: PostHogOptions['persistence']
private _storage: PostHogRNStorage
private _appProperties: PostHogCustomAppProperties = {}
private _currentSessionId?: string | undefined

constructor(apiKey: string, options?: PostHogOptions) {
super(apiKey, options)
Expand Down Expand Up @@ -100,6 +124,8 @@ export class PostHog extends PostHogCore {
}

void this.persistAppVersion()

void this.startSessionReplay(options)
}

// For async storage, we wait for the storage to be ready before we start the SDK
Expand Down Expand Up @@ -169,10 +195,81 @@ export class PostHog extends PostHogCore {
)
}

getSessionId(): string {
const sessionId = super.getSessionId()

// only rotate if there is a new sessionId and it is different from the current one
if (sessionId.length > 0 && this._currentSessionId && sessionId !== this._currentSessionId) {
if (OptionalReactNativeSessionReplay) {
try {
OptionalReactNativeSessionReplay.endSession()
OptionalReactNativeSessionReplay.startSession(sessionId)
console.info('PostHog Debug', `Session replay started with sessionId ${sessionId}.`)
} catch (e) {
console.error('PostHog Debug', `Session replay failed to start with sessionId: ${e}.`)
}
}
this._currentSessionId = sessionId
}

return sessionId
}

initReactNativeNavigation(options: PostHogAutocaptureOptions): boolean {
return withReactNativeNavigation(this, options)
}

private async startSessionReplay(options?: PostHogOptions): Promise<void> {
if (!options?.sessionReplay) {
console.info('PostHog Debug', 'Session replay is not enabled.')
return
}

const replayOptions = options?.sessionReplayConfig ?? {
maskAllTextInputs: true,
maskAllImages: true,
captureLog: true,
captureNetworkTelemetry: true,
iOSdebouncerDelayMs: 1000,
androidDebouncerDelayMs: 500,
}

console.log('PostHog Debug', `Session replay sdk config: ${JSON.stringify(replayOptions)}`)

// if Decide has not returned yet, we will start session replay with default config.
const sessionReplay = this.getPersistedProperty(PostHogPersistedProperty.SessionReplay) ?? {}

// sessionReplay is always an object, if its a boolean, its false if disabled
if (sessionReplay) {
const sessionReplayConfig = (sessionReplay as { [key: string]: JsonType }) ?? {}
console.log('PostHog Debug', `Session replay cached config: ${JSON.stringify(sessionReplayConfig)}`)

if (OptionalReactNativeSessionReplay) {
const sessionId = this.getSessionId()

if (sessionId.length === 0) {
console.warn('PostHog Debug', 'Session replay enabled but no sessionId found.')
return
}

try {
if (!(await OptionalReactNativeSessionReplay.isEnabled())) {
await OptionalReactNativeSessionReplay.start(sessionId, sessionReplayConfig, replayOptions)
console.info('PostHog Debug', `Session replay started with sessionId ${sessionId}.`)
} else {
console.log('PostHog Debug', `Session replay already started.`)
}
} catch (e) {
console.error('PostHog Debug', `Session replay failed to start: ${e}.`)
}
} else {
console.warn('PostHog Debug', 'Session replay enabled but not installed.')
}
} else {
console.info('PostHog Debug', 'Session replay disabled.')
}
}

private async captureNativeAppLifecycleEvents(): Promise<void> {
// See the other implementations for reference:
// ios: https://github.com/PostHog/posthog-ios/blob/3a6afc24d6bde730a19470d4e6b713f44d076ad9/PostHog/Classes/PHGPostHog.m#L140
Expand Down
43 changes: 43 additions & 0 deletions posthog-react-native/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,49 @@ export interface PostHogCustomAppProperties {
$timezone?: string | null
}

export type PostHogSessionReplayConfig = {
/**
* Enable masking of all text input fields
* Experimental support
* Default: true
*/
maskAllTextInputs?: boolean
/**
* Enable masking of all images to a placeholder
* Experimental support
* Default: true
*/
maskAllImages?: boolean
/**
* Enable capturing of logcat as console events
* Android only
* Experimental support
* Default: true
*/
captureLog?: boolean
/**
* Deboucer delay used to reduce the number of snapshots captured and reduce performance impact
* This is used for capturing the view as a screenshot
* The lower the number more snapshots will be captured but higher the performance impact
* Defaults to 1s on iOS
*/
iOSdebouncerDelayMs?: number
/**
* Deboucer delay used to reduce the number of snapshots captured and reduce performance impact
* This is used for capturing the view as a screenshot
* The lower the number more snapshots will be captured but higher the performance impact
* Defaults to 0.5s
*/
androidDebouncerDelayMs?: number
/**
* Enable capturing network telemetry
* iOS only
* Experimental support
* Default: true
*/
captureNetworkTelemetry?: boolean
}

export interface PostHogCustomStorage {
getItem: (key: string) => string | null | Promise<string | null>
setItem: (key: string, value: string) => void | Promise<void>
Expand Down
2 changes: 1 addition & 1 deletion posthog-react-native/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"jsx": "react-native",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "node16",
"noEmitOnError": true,
"outDir": "./lib",
"skipLibCheck": true,
Expand Down
Loading