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: