From 58fa69d7f12e324b8cbee47e2dbfc25598854966 Mon Sep 17 00:00:00 2001 From: Ben White Date: Wed, 20 Nov 2024 18:42:19 -0800 Subject: [PATCH] Added embeddable endpoint --- frontend/src/exporter/Exporter.tsx | 18 +++-- .../exporter/replay/EmbeddedReplayHelper.tsx | 9 +++ .../exporter/replay/embeddedReplayLogic.ts | 67 +++++++++++++++++++ .../sessionRecordingFilePlaybackSceneLogic.ts | 2 +- posthog/api/sharing.py | 10 +++ 5 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 frontend/src/exporter/replay/EmbeddedReplayHelper.tsx create mode 100644 frontend/src/exporter/replay/embeddedReplayLogic.ts diff --git a/frontend/src/exporter/Exporter.tsx b/frontend/src/exporter/Exporter.tsx index 0b152399ec9..01bdece907e 100644 --- a/frontend/src/exporter/Exporter.tsx +++ b/frontend/src/exporter/Exporter.tsx @@ -19,6 +19,7 @@ import { Logo } from '~/toolbar/assets/Logo' import { DashboardPlacement } from '~/types' import { exporterViewLogic } from './exporterViewLogic' +import { EmbeddedReplayHelper } from './replay/EmbeddedReplayHelper' export function Exporter(props: ExportedData): JSX.Element { // NOTE: Mounting the logic is important as it is used by sub-logics @@ -83,13 +84,16 @@ export function Exporter(props: ExportedData): JSX.Element { placement={type === ExportType.Image ? DashboardPlacement.Export : DashboardPlacement.Public} /> ) : recording ? ( - + <> + + + ) : (

Something went wrong...

)} diff --git a/frontend/src/exporter/replay/EmbeddedReplayHelper.tsx b/frontend/src/exporter/replay/EmbeddedReplayHelper.tsx new file mode 100644 index 00000000000..f51d2851233 --- /dev/null +++ b/frontend/src/exporter/replay/EmbeddedReplayHelper.tsx @@ -0,0 +1,9 @@ +import { useMountedLogic } from 'kea' + +import { embeddedReplayLogic } from './embeddedReplayLogic' + +export function EmbeddedReplayHelper(): JSX.Element { + // NOTE: This is a helper component to avoid circular imports from the logic + useMountedLogic(embeddedReplayLogic) + return <> +} diff --git a/frontend/src/exporter/replay/embeddedReplayLogic.ts b/frontend/src/exporter/replay/embeddedReplayLogic.ts new file mode 100644 index 00000000000..a159860bf58 --- /dev/null +++ b/frontend/src/exporter/replay/embeddedReplayLogic.ts @@ -0,0 +1,67 @@ +import { actions, afterMount, connect, kea, listeners, path, selectors } from 'kea' +import { dayjs } from 'lib/dayjs' +import { waitForDataLogic } from 'scenes/session-recordings/file-playback/sessionRecordingFilePlaybackSceneLogic' +import { deduplicateSnapshots, parseEncodedSnapshots } from 'scenes/session-recordings/player/sessionRecordingDataLogic' + +import { exporterViewLogic } from '../exporterViewLogic' +import { ExportedData } from '../types' +import type { embeddedReplayLogicType } from './embeddedReplayLogicType' + +export const embeddedReplayLogic = kea([ + path(() => ['scenes', 'exporter', 'embeddedReplayLogic']), + connect({ + values: [exporterViewLogic, ['exportedData']], + }), + actions({ + loadReplayFromData: (data: any[]) => ({ data }), + }), + selectors(() => ({ + isEmbeddedRecording: [ + (s) => [s.exportedData], + (exportedData: ExportedData): boolean => !!(exportedData.recording && exportedData.recording.id === ''), + ], + })), + + listeners(() => ({ + loadReplayFromData: async ({ data }) => { + const dataLogic = await waitForDataLogic('exporter') + if (!dataLogic || !data) { + return + } + + // Add a window ID to the snapshots so that we can identify them + data.forEach((snapshot: any) => { + snapshot.window_id = 'window-1' + }) + + const snapshots = deduplicateSnapshots(await parseEncodedSnapshots(data, 'embedded')) + // Simulate a loaded source and sources so that nothing extra gets loaded + dataLogic.actions.loadSnapshotsForSourceSuccess({ + snapshots: snapshots, + untransformed_snapshots: snapshots, + source: { source: 'file' }, + }) + dataLogic.actions.loadSnapshotSourcesSuccess([{ source: 'file' }]) + dataLogic.actions.loadRecordingMetaSuccess({ + id: 'embedded', + viewed: false, + recording_duration: snapshots[snapshots.length - 1].timestamp - snapshots[0].timestamp, + person: undefined, + start_time: dayjs(snapshots[0].timestamp).toISOString(), + end_time: dayjs(snapshots[snapshots.length - 1].timestamp).toISOString(), + snapshot_source: 'unknown', // TODO: we should be able to detect this from the file + }) + }, + })), + + afterMount(({ values, actions }) => { + if (values.isEmbeddedRecording) { + window.addEventListener('message', (event) => { + if (event.data.type === 'session-replay-data') { + actions.loadReplayFromData(event.data.snapshots) + return + } + }) + } + }), +]) diff --git a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackSceneLogic.ts b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackSceneLogic.ts index 118614cfa6c..980498149e7 100644 --- a/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackSceneLogic.ts +++ b/frontend/src/scenes/session-recordings/file-playback/sessionRecordingFilePlaybackSceneLogic.ts @@ -58,7 +58,7 @@ export const parseExportedSessionRecording = (fileData: string): ExportedSession * in practice, it will only wait for 1-2 retries * but a timeout is provided to avoid waiting forever when something breaks */ -const waitForDataLogic = async (playerKey: string): Promise> => { +export const waitForDataLogic = async (playerKey: string): Promise> => { const maxRetries = 20 // 2 seconds / 100 ms per retry let retries = 0 let dataLogic = null diff --git a/posthog/api/sharing.py b/posthog/api/sharing.py index d0cf5af56ba..7a113113923 100644 --- a/posthog/api/sharing.py +++ b/posthog/api/sharing.py @@ -235,6 +235,16 @@ class SharingViewerPageViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSe @xframe_options_exempt def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Any: + if self.kwargs.get("access_token") == "embedded-player": + # NOTE: This is a shortcut for the embedded player rendered on posthog.com + return render_template( + "exporter.html", + request=request, + context={ + "exported_data": {"recording": {"id": ""}}, + }, + ) + resource = self.get_object() if not resource: