From c8fa399f9fc96f278362262cb5da7d3c7ac75fa8 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 16 Sep 2024 10:02:29 +0100 Subject: [PATCH] chore: session rep;ay --- posthog-core/src/index.ts | 9 +++ posthog-core/src/types.ts | 5 +- .../src/optional/OptionalSessionReplay.ts | 16 +++++ posthog-react-native/src/posthog-rn.ts | 69 +++++++++++++++++++ .../src/replay/ReactNativeSessionReplay.d.ts | 5 ++ .../replay/ReactNativeSessionReplayStatic.ts | 9 +++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 posthog-react-native/src/optional/OptionalSessionReplay.ts create mode 100644 posthog-react-native/src/replay/ReactNativeSessionReplay.d.ts create mode 100644 posthog-react-native/src/replay/ReactNativeSessionReplayStatic.ts diff --git a/posthog-core/src/index.ts b/posthog-core/src/index.ts index 713a801f..20761c52 100644 --- a/posthog-core/src/index.ts +++ b/posthog-core/src/index.ts @@ -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 diff --git a/posthog-core/src/types.ts b/posthog-core/src/types.ts index ed06fb3f..ac1b3a28 100644 --- a/posthog-core/src/types.ts +++ b/posthog-core/src/types.ts @@ -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 = { @@ -116,7 +117,9 @@ export type PostHogDecideResponse = { [key: string]: JsonType } errorsWhileComputingFlags: boolean - sessionRecording: boolean + sessionRecording?: boolean | { + [key: string]: JsonType + } } export type PostHogFlagsAndPayloadsResponse = { diff --git a/posthog-react-native/src/optional/OptionalSessionReplay.ts b/posthog-react-native/src/optional/OptionalSessionReplay.ts new file mode 100644 index 00000000..ce921668 --- /dev/null +++ b/posthog-react-native/src/optional/OptionalSessionReplay.ts @@ -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) {} diff --git a/posthog-react-native/src/posthog-rn.ts b/posthog-react-native/src/posthog-rn.ts index 520c6ffe..c0abe449 100644 --- a/posthog-react-native/src/posthog-rn.ts +++ b/posthog-react-native/src/posthog-rn.ts @@ -1,6 +1,7 @@ import { AppState, Dimensions, Linking, Platform } from 'react-native' import { + JsonType, PostHogCaptureOptions, PostHogCore, PostHogCoreOptions, @@ -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'. @@ -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) @@ -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 @@ -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 { + 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 { + } + private async captureNativeAppLifecycleEvents(): Promise { // See the other implementations for reference: // ios: https://github.com/PostHog/posthog-ios/blob/3a6afc24d6bde730a19470d4e6b713f44d076ad9/PostHog/Classes/PHGPostHog.m#L140 diff --git a/posthog-react-native/src/replay/ReactNativeSessionReplay.d.ts b/posthog-react-native/src/replay/ReactNativeSessionReplay.d.ts new file mode 100644 index 00000000..44f73c2d --- /dev/null +++ b/posthog-react-native/src/replay/ReactNativeSessionReplay.d.ts @@ -0,0 +1,5 @@ +// auto generated by posthog-react-native-session-replay + +import type { ReactNativeSessionReplayStatic } from './ReactNativeSessionReplayStatic'; +declare const ReactNativeSessionReplay: ReactNativeSessionReplayStatic; +export default ReactNativeSessionReplay; diff --git a/posthog-react-native/src/replay/ReactNativeSessionReplayStatic.ts b/posthog-react-native/src/replay/ReactNativeSessionReplayStatic.ts new file mode 100644 index 00000000..ad352eec --- /dev/null +++ b/posthog-react-native/src/replay/ReactNativeSessionReplayStatic.ts @@ -0,0 +1,9 @@ +// auto generated by posthog-react-native-session-replay + +export declare type ReactNativeSessionReplayStatic = { + + start: (sessionId: string, endpoint: string) => Promise; + startSession: (sessionId: string) => Promise; + endSession: () => Promise; + isEnabled: () => Promise; +}