Skip to content

Commit

Permalink
chore: session rep;ay
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto committed Sep 16, 2024
1 parent 489b6aa commit c8fa399
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 1 deletion.
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('Session replay config:', sessionReplay)
} else {
// TODO: add removePersistedProperty
this.setPersistedProperty(PostHogPersistedProperty.SessionReplay, null)
}
}

return res
Expand Down
5 changes: 4 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,9 @@ export type PostHogDecideResponse = {
[key: string]: JsonType
}
errorsWhileComputingFlags: boolean
sessionRecording: boolean
sessionRecording?: boolean | {
[key: string]: JsonType
}
}

export type PostHogFlagsAndPayloadsResponse = {
Expand Down
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 ReactNativeSessionReplay from 'posthog-react-native-session-replay'
import type ReactNativeSessionReplay from '../replay/ReactNativeSessionReplay'

export let OptionalReactNativeSessionReplay: typeof ReactNativeSessionReplay | undefined = undefined

try {
// macos not supported
OptionalReactNativeSessionReplay = Platform.select({
macos: undefined,
web: undefined,
windows: undefined,
native: undefined,
default: require('posthog-react-native-session-replay'), // Only Android and iOS
})
} catch (e) {}
69 changes: 69 additions & 0 deletions 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 @@ -14,6 +15,7 @@ import { version } from './version'
import { buildOptimisiticAsyncStorage, getAppProperties } from './native-deps'
import { PostHogAutocaptureOptions, PostHogCustomAppProperties, PostHogCustomStorage } 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 +37,20 @@ 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
* Defaults to false
*/
sessionReplay?: boolean
}

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 +110,8 @@ export class PostHog extends PostHogCore {
}

void this.persistAppVersion()

void this.startSessionReplay()
}

// For async storage, we wait for the storage to be ready before we start the SDK
Expand Down Expand Up @@ -169,10 +181,67 @@ 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(`Session replay enabled with sessionId ${sessionId}.`)
} catch (e) {
console.error(`Session replay failed to rotate sessionId: ${e}.`)
}
}
this._currentSessionId = sessionId
}

return sessionId
}

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

private async startSessionReplay(): Promise<void> {
const sessionReplay = this.getPersistedProperty(PostHogPersistedProperty.SessionReplay)

// sessionReplay is always an object, if its a boolean, its false if disabled
if (sessionReplay) {
if (OptionalReactNativeSessionReplay) {
const sessionReplayConfig = (sessionReplay as { [key: string]: JsonType }) ?? {}
const endpoint = (sessionReplayConfig['endpoint'] as string) ?? '' // use default instead

const sessionId = this.getSessionId()

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

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

private async endSessionReplay(): Promise<void> {
}

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
5 changes: 5 additions & 0 deletions posthog-react-native/src/replay/ReactNativeSessionReplay.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// auto generated by posthog-react-native-session-replay

import type { ReactNativeSessionReplayStatic } from './ReactNativeSessionReplayStatic';
declare const ReactNativeSessionReplay: ReactNativeSessionReplayStatic;
export default ReactNativeSessionReplay;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// auto generated by posthog-react-native-session-replay

export declare type ReactNativeSessionReplayStatic = {

start: (sessionId: string, endpoint: string) => Promise<void>;
startSession: (sessionId: string) => Promise<void>;
endSession: () => Promise<void>;
isEnabled: () => Promise<boolean>;
}

0 comments on commit c8fa399

Please sign in to comment.