/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import type { EnrichmentPlugin, Event, ReactNativeClient, ReactNativeConfig } from '@amplitude/analytics-types'; import { PluginSessionReplayReactNative } from './native-module'; import { VERSION } from './version'; import { SessionReplayConfig, getDefaultConfig } from './session-replay-config'; type ResolvedSessionReplayConfig = Required; export class SessionReplayPlugin implements EnrichmentPlugin { name = '@amplitude/plugin-session-replay-react-native'; type = 'enrichment' as const; // this.config is defined in setup() which will always be called first // @ts-ignore config: ReactNativeConfig; isInitialized = false; sessionReplayConfig: ResolvedSessionReplayConfig; constructor(config: SessionReplayConfig = {}) { this.sessionReplayConfig = { ...getDefaultConfig(), ...config, }; console.log('Initializing SessionReplayPlugin with config: ', this.sessionReplayConfig); } async setup(config: ReactNativeConfig, _: ReactNativeClient): Promise { this.config = config; console.log(`Installing @amplitude/plugin-session-replay-react-native, version ${VERSION}.`); // `apiKey`, `deviceId`, `sessionId`, and `serverZone` are sourced from the // analytics client's `ReactNativeConfig` because the plugin runs inside an // initialized Amplitude SDK and inherits identity from it. // Resolve the effective mask level here — the single source of truth for // the default. `privacyConfig.maskLevel` can be `undefined` when a partial // `privacyConfig` (e.g. `{}`) is supplied, so fall back to `'medium'` rather // than forwarding `undefined` across the native bridge. const resolvedMaskLevel = this.sessionReplayConfig.privacyConfig.maskLevel ?? 'medium'; await PluginSessionReplayReactNative.setup( config.apiKey, config.deviceId, config.sessionId, config.serverZone, this.sessionReplayConfig.sampleRate, this.sessionReplayConfig.enableRemoteConfig, this.sessionReplayConfig.logLevel, this.sessionReplayConfig.autoStart, // TODO(SDKRN-15): Migrate native bridge to accept the full privacyConfig object instead of a flat maskLevel string. resolvedMaskLevel, ); this.isInitialized = true; } async execute(event: Event): Promise { if (!this.isInitialized) { return Promise.resolve(event); } // On event, synchronize the session id to the what's on the browserConfig (source of truth) // Choosing not to read from event object here, concerned about offline/delayed events messing up the state stored // in SR. if (this.config.sessionId && this.config.sessionId !== (await PluginSessionReplayReactNative.getSessionId())) { await PluginSessionReplayReactNative.setSessionId(this.config.sessionId); } // Treating config.sessionId as source of truth, if the event's session id doesn't match, the // event is not of the current session (offline/late events). In that case, don't tag the events if (this.config.sessionId && this.config.sessionId === event.session_id) { const sessionRecordingProperties = await PluginSessionReplayReactNative.getSessionReplayProperties(); event.event_properties = { ...event.event_properties, ...sessionRecordingProperties, }; } return Promise.resolve(event); } async start(): Promise { if (this.isInitialized) { await PluginSessionReplayReactNative.start(); } } async stop(): Promise { if (this.isInitialized) { await PluginSessionReplayReactNative.stop(); } } async teardown(): Promise { if (this.isInitialized) { await PluginSessionReplayReactNative.teardown(); } // the following are initialized in setup() which will always be called first // here we reset them to null to prevent memory leaks // @ts-ignore this.config = null; this.isInitialized = false; } async getSessionReplayProperties() { if (!this.isInitialized) { return {}; } return PluginSessionReplayReactNative.getSessionReplayProperties(); } }