Skip to content

Commit

Permalink
feat: allow exporting untransformed mobile replay
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra committed Dec 11, 2023
1 parent 4381ea2 commit 39ce9fe
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 23 deletions.
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export type ExportedSessionRecordingFileV2 = {
}

export const createExportedSessionRecording = (
logic: BuiltLogic<sessionRecordingDataLogicType>
logic: BuiltLogic<sessionRecordingDataLogicType>,
// DEBUG signal only, to be removed before release
exportUntransformedMobileSnapshotData: boolean
): ExportedSessionRecordingFileV2 => {
const { sessionPlayerMetaData, sessionPlayerSnapshotData } = logic.values

Expand All @@ -42,7 +44,9 @@ export const createExportedSessionRecording = (
data: {
id: sessionPlayerMetaData?.id ?? '',
person: sessionPlayerMetaData?.person,
snapshots: sessionPlayerSnapshotData?.snapshots || [],
snapshots: exportUntransformedMobileSnapshotData
? sessionPlayerSnapshotData?.untransformed_snapshots || []
: sessionPlayerSnapshotData?.snapshots || [],
},
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -126,6 +128,18 @@ export function PlayerController(): JSX.Element {
Export to file
</LemonButton>

<FlaggedFeature flag={FEATURE_FLAGS.SESSION_REPLAY_EXPORT_MOBILE_DATA} match={true}>
<LemonButton
status="stealth"
onClick={() => exportRecordingToFile(true)}
fullWidth
sideIcon={<IconExport />}
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
</LemonButton>
</FlaggedFeature>

<LemonButton
status="stealth"
onClick={() => openExplorer()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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<sessionRecordingDataLogicType>([
path((key) => ['scenes', 'session-recordings', 'sessionRecordingDataLogic', key]),
props({} as SessionRecordingDataLogicProps),
Expand Down Expand Up @@ -349,14 +381,14 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
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,
Expand All @@ -365,14 +397,14 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
})
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const sessionRecordingPlayerLogic = kea<sessionRecordingPlayerLogicType>(
incrementErrorCount: true,
incrementWarningCount: (count: number = 1) => ({ count }),
updateFromMetadata: true,
exportRecordingToFile: true,
exportRecordingToFile: (exportUntransformedMobileData?: boolean) => ({ exportUntransformedMobileData }),
deleteRecording: true,
openExplorer: true,
closeExplorer: true,
Expand Down Expand Up @@ -881,7 +881,7 @@ export const sessionRecordingPlayerLogic = kea<sessionRecordingPlayerLogicType>(
cache.pausedMediaElements = []
},

exportRecordingToFile: async () => {
exportRecordingToFile: async ({ exportUntransformedMobileData }) => {
if (!values.sessionPlayerData) {
return
}
Expand All @@ -906,11 +906,16 @@ export const sessionRecordingPlayerLogic = kea<sessionRecordingPlayerLogicType>(
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' }
)

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 39ce9fe

Please sign in to comment.