From 39ce9fe1433b9d9464bd1a5dc6239dd931fb0d12 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Mon, 11 Dec 2023 23:04:57 +0000 Subject: [PATCH] feat: allow exporting untransformed mobile replay --- frontend/src/lib/constants.tsx | 1 + .../sessionRecordingFilePlaybackLogic.ts | 8 ++- .../player/controller/PlayerController.tsx | 14 ++++ .../player/sessionRecordingDataLogic.ts | 66 ++++++++++++++----- .../player/sessionRecordingPlayerLogic.ts | 13 ++-- frontend/src/types.ts | 3 + 6 files changed, 82 insertions(+), 23 deletions(-) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index b98404efa5779..8f26033d048a6 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -190,6 +190,7 @@ export const FEATURE_FLAGS = { SESSION_REPLAY_MOBILE: 'session-replay-mobile', // owner: #team-replay SESSION_REPLAY_IOS: 'session-replay-ios', // owner: #team-replay YEAR_IN_HOG: 'year-in-hog', // owner: #team-replay + SESSION_REPLAY_EXPORT_MOBILE_DATA: 'session-replay-export-mobile-data', // owner: #team-replay } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts index 8126bf6a97c35..1e400fdd3780e 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackLogic.ts @@ -33,7 +33,9 @@ export type ExportedSessionRecordingFileV2 = { } export const createExportedSessionRecording = ( - logic: BuiltLogic + logic: BuiltLogic, + // DEBUG signal only, to be removed before release + exportUntransformedMobileSnapshotData: boolean ): ExportedSessionRecordingFileV2 => { const { sessionPlayerMetaData, sessionPlayerSnapshotData } = logic.values @@ -42,7 +44,9 @@ export const createExportedSessionRecording = ( data: { id: sessionPlayerMetaData?.id ?? '', person: sessionPlayerMetaData?.person, - snapshots: sessionPlayerSnapshotData?.snapshots || [], + snapshots: exportUntransformedMobileSnapshotData + ? sessionPlayerSnapshotData?.untransformed_snapshots || [] + : sessionPlayerSnapshotData?.snapshots || [], }, } } diff --git a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx index 465fe05a39377..c0bb442cde79f 100644 --- a/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/PlayerController.tsx @@ -1,5 +1,7 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' +import { FlaggedFeature } from 'lib/components/FlaggedFeature' +import { FEATURE_FLAGS } from 'lib/constants' import { IconExport, IconFullScreen, IconMagnifier, IconPause, IconPlay, IconSkipInactivity } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' @@ -126,6 +128,18 @@ export function PlayerController(): JSX.Element { Export to file + + exportRecordingToFile(true)} + fullWidth + sideIcon={} + tooltip="DEBUG ONLY - Export untransformed recording to a file. This can be loaded later into PostHog for playback." + > + DEBUG Export mobile replay to file DEBUG + + + openExplorer()} diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index b2eb8d58516d8..c58d2373cc0e3 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -6,7 +6,7 @@ import { loaders } from 'kea-loaders' import api from 'lib/api' import { FEATURE_FLAGS } from 'lib/constants' import { Dayjs, dayjs } from 'lib/dayjs' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic' import { toParams } from 'lib/utils' import { chainToElements } from 'lib/utils/elements-chain' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' @@ -58,9 +58,10 @@ const parseEncodedSnapshots = async ( const snapshotLine = typeof l === 'string' ? (JSON.parse(l) as EncodedRecordingSnapshot) : l const snapshotData = snapshotLine['data'] - // TODO can we type this better and still have mobileEventWithTime in ee folder? return snapshotData.map((d: unknown) => { - const snap = postHogEEModule?.mobileReplay?.transformEventToWeb(d) || (d as eventWithTime) + const snap = withMobileTransformer + ? postHogEEModule?.mobileReplay?.transformEventToWeb(d) || (d as eventWithTime) + : (d as eventWithTime) return { windowId: snapshotLine['window_id'], ...(snap || (d as eventWithTime)), @@ -168,6 +169,37 @@ function makeEventsQuery( }) } +async function processEncodedResponse( + encodedResponse: (EncodedRecordingSnapshot | string)[], + props: SessionRecordingDataLogicProps, + existingData: SessionPlayerSnapshotData | null, + featureFlags: FeatureFlagsSet +): Promise<{ transformed: RecordingSnapshot[]; untransformed: RecordingSnapshot[] | null }> { + let untransformed: RecordingSnapshot[] | null = null + + const transformed = prepareRecordingSnapshots( + await parseEncodedSnapshots( + encodedResponse, + props.sessionRecordingId, + !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE] + ), + existingData?.snapshots ?? [] + ) + + if (featureFlags[FEATURE_FLAGS.SESSION_REPLAY_EXPORT_MOBILE_DATA]) { + untransformed = prepareRecordingSnapshots( + await parseEncodedSnapshots( + encodedResponse, + props.sessionRecordingId, + false // don't transform mobile data + ), + existingData?.untransformed_snapshots ?? [] + ) + } + + return { transformed, untransformed } +} + export const sessionRecordingDataLogic = kea([ path((key) => ['scenes', 'session-recordings', 'sessionRecordingDataLogic', key]), props({} as SessionRecordingDataLogicProps), @@ -349,14 +381,14 @@ export const sessionRecordingDataLogic = kea([ source.blob_key ) - data.snapshots = prepareRecordingSnapshots( - await parseEncodedSnapshots( - encodedResponse, - props.sessionRecordingId, - !!values.featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE] - ), - values.sessionPlayerSnapshotData?.snapshots ?? [] + const { transformed, untransformed } = await processEncodedResponse( + encodedResponse, + props, + values.sessionPlayerSnapshotData, + values.featureFlags ) + data.snapshots = transformed + data.untransformed_snapshots = untransformed ?? undefined } else { const params = toParams({ source: source?.source, @@ -365,14 +397,14 @@ export const sessionRecordingDataLogic = kea([ }) const response = await api.recordings.listSnapshots(props.sessionRecordingId, params) if (response.snapshots) { - data.snapshots = prepareRecordingSnapshots( - await parseEncodedSnapshots( - response.snapshots, - props.sessionRecordingId, - !!values.featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE] - ), - values.sessionPlayerSnapshotData?.snapshots ?? [] + const { transformed, untransformed } = await processEncodedResponse( + response.snapshots, + props, + values.sessionPlayerSnapshotData, + values.featureFlags ) + data.snapshots = transformed + data.untransformed_snapshots = untransformed ?? undefined } if (response.sources) { diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index 5d865c9dda4c8..fcbadb8f175ac 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -170,7 +170,7 @@ export const sessionRecordingPlayerLogic = kea( incrementErrorCount: true, incrementWarningCount: (count: number = 1) => ({ count }), updateFromMetadata: true, - exportRecordingToFile: true, + exportRecordingToFile: (exportUntransformedMobileData?: boolean) => ({ exportUntransformedMobileData }), deleteRecording: true, openExplorer: true, closeExplorer: true, @@ -881,7 +881,7 @@ export const sessionRecordingPlayerLogic = kea( cache.pausedMediaElements = [] }, - exportRecordingToFile: async () => { + exportRecordingToFile: async ({ exportUntransformedMobileData }) => { if (!values.sessionPlayerData) { return } @@ -906,11 +906,16 @@ export const sessionRecordingPlayerLogic = kea( await delay(delayTime) } - const payload = createExportedSessionRecording(sessionRecordingDataLogic(props)) + const payload = createExportedSessionRecording( + sessionRecordingDataLogic(props), + !!exportUntransformedMobileData + ) const recordingFile = new File( [JSON.stringify(payload, null, 2)], - `export-${props.sessionRecordingId}.ph-recording.json`, + `export-${props.sessionRecordingId}.${ + exportUntransformedMobileData ? 'mobile.' : '' + }ph-recording.json`, { type: 'application/json' } ) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 964452da01fea..8632f899f54d8 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -702,6 +702,9 @@ export interface SessionPlayerSnapshotData { snapshots?: RecordingSnapshot[] sources?: SessionRecordingSnapshotSource[] blob_keys?: string[] + // used for a debug signal only for PostHog team, controlled by a feature flag + // DO NOT RELY ON THIS FOR NON DEBUG PURPOSES + untransformed_snapshots?: RecordingSnapshot[] } export interface SessionPlayerData {