0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 00:47:50 +01:00

fix: patch mobile recordings that are missing their meta event (#24840)

This commit is contained in:
Paul D'Ambra 2024-09-06 16:24:37 +01:00 committed by GitHub
parent a17ca6f070
commit 9a4e9a81da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 656 additions and 11 deletions

View File

@ -0,0 +1,6 @@
export const encodedWebSnapshotData: string[] = [
// first item could be a network event or something else
'{"windowId":"0191C63B-03FF-73B5-96BE-40BE2761621C","data":{"payload":{"requests":[{"duration":28,"entryType":"resource","initiatorType":"fetch","method":"GET","name":"https://1.bp.blogspot.com/-hkNkoCjc5UA/T4JTlCjhhfI/AAAAAAAAB98/XxQwZ-QPkI8/s1600/Free+Google+Wallpapers+3.jpg","responseStatus":200,"timestamp":1725369200216,"transferSize":82375}]},"plugin":"rrweb/network@1"},"timestamp":1725369200216,"type":6,"seen":8833798676917222}',
'{"windowId":"0191C63B-03FF-73B5-96BE-40BE2761621C","data":{"height":852,"width":393},"timestamp":1725607643113,"type":4,"seen":4930607506458337}',
'{"windowId":"0191C63B-03FF-73B5-96BE-40BE2761621C","data":{"initialOffset":{"left":0,"top":0},"wireframes":[{"base64":"data:image/jpeg;base64,/9j/4AAQSkZJR","height":852,"id":4324378400,"type":"screenshot","width":393,"x":0,"y":0}]},"timestamp":1725607643113,"type":2,"seen":2118469619185818}',
]

View File

@ -0,0 +1,339 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot parsing handles mobile data with no meta event 1`] = `
[
{
"data": {
"payload": {
"requests": [
{
"duration": 28,
"entryType": "resource",
"initiatorType": "fetch",
"method": "GET",
"name": "https://1.bp.blogspot.com/-hkNkoCjc5UA/T4JTlCjhhfI/AAAAAAAAB98/XxQwZ-QPkI8/s1600/Free+Google+Wallpapers+3.jpg",
"responseStatus": 200,
"timestamp": 1725369200216,
"transferSize": 82375,
},
],
},
"plugin": "rrweb/network@1",
},
"seen": 8833798676917222,
"timestamp": 1725369200216,
"type": 6,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
{
"data": {
"height": 852,
"href": "",
"width": 393,
},
"timestamp": 1725607643113,
"type": 4,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
{
"data": {
"initialOffset": {
"left": 0,
"top": 0,
},
"node": {
"childNodes": [
{
"id": 2,
"name": "html",
"publicId": "",
"systemId": "",
"type": 1,
},
{
"attributes": {
"data-rrweb-id": 3,
"style": "height: 100vh; width: 100vw;",
},
"childNodes": [
{
"attributes": {
"data-rrweb-id": 4,
},
"childNodes": [
{
"attributes": {
"type": "text/css",
},
"childNodes": [
{
"id": 101,
"textContent": "
body {
margin: unset;
}
input, button, select, textarea {
font: inherit;
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: transparent;
padding-block: 0 !important;
}
.input:focus {
outline: none;
}
img {
border-style: none;
}
",
"type": 3,
},
],
"id": 100,
"tagName": "style",
"type": 2,
},
],
"id": 4,
"tagName": "head",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 5,
"style": "height: 100vh; width: 100vw;",
},
"childNodes": [
{
"attributes": {
"data-rrweb-id": 4324378400,
"height": 852,
"src": "data:image/jpeg;base64,/9j/4AAQSkZJR",
"style": "width: 393px;height: 852px;position: fixed;left: 0px;top: 0px;",
"width": 393,
},
"childNodes": [],
"id": 4324378400,
"tagName": "img",
"type": 2,
},
{
"attributes": {
"data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position",
"data-rrweb-id": 9,
},
"childNodes": [],
"id": 9,
"tagName": "div",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 7,
},
"childNodes": [],
"id": 7,
"tagName": "div",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 11,
},
"childNodes": [],
"id": 11,
"tagName": "div",
"type": 2,
},
],
"id": 5,
"tagName": "body",
"type": 2,
},
],
"id": 3,
"tagName": "html",
"type": 2,
},
],
"id": 1,
"type": 0,
},
},
"timestamp": 1725607643113,
"type": 2,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
]
`;
exports[`snapshot parsing handles normal mobile data 1`] = `
[
{
"data": {
"payload": {
"requests": [
{
"duration": 28,
"entryType": "resource",
"initiatorType": "fetch",
"method": "GET",
"name": "https://1.bp.blogspot.com/-hkNkoCjc5UA/T4JTlCjhhfI/AAAAAAAAB98/XxQwZ-QPkI8/s1600/Free+Google+Wallpapers+3.jpg",
"responseStatus": 200,
"timestamp": 1725369200216,
"transferSize": 82375,
},
],
},
"plugin": "rrweb/network@1",
},
"seen": 8833798676917222,
"timestamp": 1725369200216,
"type": 6,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
{
"data": {
"height": 852,
"href": "",
"width": 393,
},
"timestamp": 1725607643113,
"type": 4,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
{
"data": {
"initialOffset": {
"left": 0,
"top": 0,
},
"node": {
"childNodes": [
{
"id": 2,
"name": "html",
"publicId": "",
"systemId": "",
"type": 1,
},
{
"attributes": {
"data-rrweb-id": 3,
"style": "height: 100vh; width: 100vw;",
},
"childNodes": [
{
"attributes": {
"data-rrweb-id": 4,
},
"childNodes": [
{
"attributes": {
"type": "text/css",
},
"childNodes": [
{
"id": 101,
"textContent": "
body {
margin: unset;
}
input, button, select, textarea {
font: inherit;
margin: 0;
padding: 0;
border: 0;
outline: 0;
background: transparent;
padding-block: 0 !important;
}
.input:focus {
outline: none;
}
img {
border-style: none;
}
",
"type": 3,
},
],
"id": 100,
"tagName": "style",
"type": 2,
},
],
"id": 4,
"tagName": "head",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 5,
"style": "height: 100vh; width: 100vw;",
},
"childNodes": [
{
"attributes": {
"data-rrweb-id": 4324378400,
"height": 852,
"src": "data:image/jpeg;base64,/9j/4AAQSkZJR",
"style": "width: 393px;height: 852px;position: fixed;left: 0px;top: 0px;",
"width": 393,
},
"childNodes": [],
"id": 4324378400,
"tagName": "img",
"type": 2,
},
{
"attributes": {
"data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position",
"data-rrweb-id": 9,
},
"childNodes": [],
"id": 9,
"tagName": "div",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 7,
},
"childNodes": [],
"id": 7,
"tagName": "div",
"type": 2,
},
{
"attributes": {
"data-rrweb-id": 11,
},
"childNodes": [],
"id": 11,
"tagName": "div",
"type": 2,
},
],
"id": 5,
"tagName": "body",
"type": 2,
},
],
"id": 3,
"tagName": "html",
"type": 2,
},
],
"id": 1,
"type": 0,
},
},
"timestamp": 1725607643113,
"type": 2,
"windowId": "0191C63B-03FF-73B5-96BE-40BE2761621C",
},
]
`;

View File

@ -0,0 +1,20 @@
import { parseEncodedSnapshots } from 'scenes/session-recordings/player/sessionRecordingDataLogic'
import { encodedWebSnapshotData } from './__mocks__/encoded-snapshot-data'
describe('snapshot parsing', () => {
const sessionId = '12345'
const numberOfParsedLinesInData = 3
it('handles normal mobile data', async () => {
const parsed = await parseEncodedSnapshots(encodedWebSnapshotData, sessionId, true)
expect(parsed.length).toEqual(numberOfParsedLinesInData)
expect(parsed).toMatchSnapshot()
})
it('handles mobile data with no meta event', async () => {
const withoutMeta = [encodedWebSnapshotData[0], encodedWebSnapshotData[2]]
const parsed = await parseEncodedSnapshots(withoutMeta, sessionId, true)
expect(parsed.length).toEqual(numberOfParsedLinesInData)
expect(parsed).toMatchSnapshot()
})
})

View File

@ -0,0 +1,4 @@
export const encodedWebSnapshotData: string[] = [
'{"window_id":"0191c366-dd75-708c-bb41-c0d5bd2bb0dc","data":[{"type":4,"data":{"href":"http://localhost:8000/project/1","width":719,"height":914},"timestamp":1725560859629},{"type":3,"data":{"source":2,"type":0,"id":320,"x":21.41059112548828,"y":28.776042938232422},"timestamp":1725560861395},{"type":3,"data":{"source":2,"type":2,"id":320,"x":21,"y":28,"pointerType":0},"timestamp":1725560861398},{"type":3,"data":{"source":0,"texts":[],"attributes":[{"id":59,"attributes":{"class":"Navbar3000"}},{"id":313,"attributes":{"class":"Navbar3000__overlay"}}],"removes":[],"adds":[]},"timestamp":1725560861402}]}',
'{"window_id":"0191c366-dd75-708c-bb41-c0d5bd2bb0dc","data":[{"type":4,"data":{"href":"http://localhost:8000/project/1","width":719,"height":914},"timestamp":1725560859629},{"type":3,"data":{"source":2,"type":0,"id":320,"x":21.41059112548828,"y":28.776042938232422},"timestamp":1725560861395},{"type":3,"data":{"source":2,"type":2,"id":320,"x":21,"y":28,"pointerType":0},"timestamp":1725560861398},{"type":3,"data":{"source":0,"texts":[],"attributes":[{"id":59,"attributes":{"class":"Navbar3000"}},{"id":313,"attributes":{"class":"Navbar3000__overlay"}}],"removes":[],"adds":[]},"timestamp":1725560861402}]}',
]

View File

@ -2078,3 +2078,193 @@ exports[`sessionRecordingDataLogic deduplicateSnapshots should match snapshot 1`
},
]
`;
exports[`sessionRecordingDataLogic snapshot parsing handles data with unparseable lines 1`] = `
[
{
"data": {
"height": 914,
"href": "http://localhost:8000/project/1",
"width": 719,
},
"timestamp": 1725560859629,
"type": 4,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"source": 2,
"type": 0,
"x": 21.41059112548828,
"y": 28.776042938232422,
},
"timestamp": 1725560861395,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"pointerType": 0,
"source": 2,
"type": 2,
"x": 21,
"y": 28,
},
"timestamp": 1725560861398,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"adds": [],
"attributes": [
{
"attributes": {
"class": "Navbar3000",
},
"id": 59,
},
{
"attributes": {
"class": "Navbar3000__overlay",
},
"id": 313,
},
],
"removes": [],
"source": 0,
"texts": [],
},
"timestamp": 1725560861402,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
]
`;
exports[`sessionRecordingDataLogic snapshot parsing handles normal web data 1`] = `
[
{
"data": {
"height": 914,
"href": "http://localhost:8000/project/1",
"width": 719,
},
"timestamp": 1725560859629,
"type": 4,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"source": 2,
"type": 0,
"x": 21.41059112548828,
"y": 28.776042938232422,
},
"timestamp": 1725560861395,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"pointerType": 0,
"source": 2,
"type": 2,
"x": 21,
"y": 28,
},
"timestamp": 1725560861398,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"adds": [],
"attributes": [
{
"attributes": {
"class": "Navbar3000",
},
"id": 59,
},
{
"attributes": {
"class": "Navbar3000__overlay",
},
"id": 313,
},
],
"removes": [],
"source": 0,
"texts": [],
},
"timestamp": 1725560861402,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"height": 914,
"href": "http://localhost:8000/project/1",
"width": 719,
},
"timestamp": 1725560859629,
"type": 4,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"source": 2,
"type": 0,
"x": 21.41059112548828,
"y": 28.776042938232422,
},
"timestamp": 1725560861395,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"id": 320,
"pointerType": 0,
"source": 2,
"type": 2,
"x": 21,
"y": 28,
},
"timestamp": 1725560861398,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
{
"data": {
"adds": [],
"attributes": [
{
"attributes": {
"class": "Navbar3000",
},
"id": 59,
},
{
"attributes": {
"class": "Navbar3000__overlay",
},
"id": 313,
},
],
"removes": [],
"source": 0,
"texts": [],
},
"timestamp": 1725560861402,
"type": 3,
"windowId": "0191c366-dd75-708c-bb41-c0d5bd2bb0dc",
},
]
`;

View File

@ -2,8 +2,10 @@ import { expectLogic } from 'kea-test-utils'
import { api, MOCK_TEAM_ID } from 'lib/api.mock'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { convertSnapshotsByWindowId } from 'scenes/session-recordings/__mocks__/recording_snapshots'
import { encodedWebSnapshotData } from 'scenes/session-recordings/player/__mocks__/encoded-snapshot-data'
import {
deduplicateSnapshots,
parseEncodedSnapshots,
sessionRecordingDataLogic,
} from 'scenes/session-recordings/player/sessionRecordingDataLogic'
import { teamLogic } from 'scenes/teamLogic'
@ -404,4 +406,30 @@ describe('sessionRecordingDataLogic', () => {
])
})
})
describe('snapshot parsing', () => {
const sessionId = '12345'
const numberOfParsedLinesInData = 8
it('handles normal web data', async () => {
const parsed = await parseEncodedSnapshots(encodedWebSnapshotData, sessionId, false)
expect(parsed.length).toEqual(numberOfParsedLinesInData)
expect(parsed).toMatchSnapshot()
})
it('handles data with unparseable lines', async () => {
const parsed = await parseEncodedSnapshots(
encodedWebSnapshotData.map((line, index) => {
return index == 0 ? line.substring(0, line.length / 2) : line
}),
sessionId,
false
)
// unparseable lines are not returned
expect(encodedWebSnapshotData.length).toEqual(2)
expect(parsed.length).toEqual(numberOfParsedLinesInData / 2)
expect(parsed).toMatchSnapshot()
})
})
})

View File

@ -1,5 +1,5 @@
import posthogEE from '@posthog/ee/exports'
import { customEvent, EventType, eventWithTime } from '@rrweb/types'
import { customEvent, EventType, eventWithTime, fullSnapshotEvent } from '@rrweb/types'
import { captureException, captureMessage } from '@sentry/react'
import {
actions,
@ -21,6 +21,7 @@ import api from 'lib/api'
import { FEATURE_FLAGS } from 'lib/constants'
import { Dayjs, dayjs } from 'lib/dayjs'
import { featureFlagLogic, FeatureFlagsSet } from 'lib/logic/featureFlagLogic'
import { isObject } from 'lib/utils'
import { chainToElements } from 'lib/utils/elements-chain'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import posthog from 'posthog-js'
@ -63,6 +64,54 @@ function isRecordingSnapshot(x: unknown): x is RecordingSnapshot {
return typeof x === 'object' && x !== null && 'type' in x && 'timestamp' in x
}
/*
there was a bug in mobile SDK that didn't consistently send a meta event with a full snapshot.
rrweb player hides itself until it has seen the meta event 🤷
but we can patch a meta event into the recording data to make it work
*/
function patchMetaEventIntoMobileData(parsedLines: RecordingSnapshot[]): RecordingSnapshot[] {
let fullSnapshotIndex: number = -1
let metaIndex: number = -1
try {
fullSnapshotIndex = parsedLines.findIndex((l) => l.type === EventType.FullSnapshot)
metaIndex = parsedLines.findIndex((l) => l.type === EventType.Meta)
// then we need to patch the meta event into the snapshot data
if (fullSnapshotIndex > -1 && metaIndex === -1) {
const fullSnapshot = parsedLines[fullSnapshotIndex] as RecordingSnapshot & fullSnapshotEvent & eventWithTime
// a full snapshot (particularly from the mobile transformer) has a relatively fixed structure,
// but the types exposed by rrweb don't quite cover what we need , so...
const mainNode = fullSnapshot.data.node as any
const targetNode = mainNode.childNodes[1].childNodes[1].childNodes[0]
const { width, height } = targetNode.attributes
const metaEvent: RecordingSnapshot = {
windowId: fullSnapshot.windowId,
type: EventType.Meta,
timestamp: fullSnapshot.timestamp,
data: {
href: getHrefFromSnapshot(fullSnapshot) || '',
width,
height,
},
}
parsedLines.splice(fullSnapshotIndex, 0, metaEvent)
}
} catch (e) {
captureException(e, {
tags: { feature: 'session-recording-missing-meta-patching' },
extra: { fullSnapshotIndex, metaIndex },
})
}
return parsedLines
}
function hasAnyWireframes(snapshotData: Record<string, any>[]): boolean {
return snapshotData.some((d) => {
return isObject(d.data) && 'wireframes' in d.data
})
}
export const parseEncodedSnapshots = async (
items: (RecordingSnapshot | EncodedRecordingSnapshot | string)[],
sessionId: string,
@ -72,9 +121,12 @@ export const parseEncodedSnapshots = async (
if (!postHogEEModule) {
postHogEEModule = await posthogEE()
}
const lineCount = items.length
const unparseableLines: string[] = []
const parsedLines = items.flatMap((l) => {
let isMobileSnapshots = false
const parsedLines: RecordingSnapshot[] = items.flatMap((l) => {
if (!l) {
// blob files have an empty line at the end
return []
@ -83,6 +135,10 @@ export const parseEncodedSnapshots = async (
const snapshotLine = typeof l === 'string' ? (JSON.parse(l) as EncodedRecordingSnapshot) : l
const snapshotData = isRecordingSnapshot(snapshotLine) ? [snapshotLine] : snapshotLine['data']
if (!isMobileSnapshots) {
isMobileSnapshots = hasAnyWireframes(snapshotData)
}
return snapshotData.map((d: unknown) => {
const snap = withMobileTransformer
? postHogEEModule?.mobileReplay?.transformEventToWeb(d) || (d as eventWithTime)
@ -118,11 +174,13 @@ export const parseEncodedSnapshots = async (
})
}
return parsedLines
return isMobileSnapshots ? patchMetaEventIntoMobileData(parsedLines) : parsedLines
}
const getHrefFromSnapshot = (snapshot: RecordingSnapshot): string | undefined => {
return (snapshot.data as any)?.href || (snapshot.data as any)?.payload?.href
const getHrefFromSnapshot = (snapshot: unknown): string | undefined => {
return isObject(snapshot) && 'data' in snapshot
? (snapshot.data as any)?.href || (snapshot.data as any)?.payload?.href
: undefined
}
/*
@ -500,12 +558,12 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
try {
const query: HogQLQuery = {
kind: NodeKind.HogQLQuery,
query: hogql`SELECT properties, uuid
FROM events
WHERE timestamp > ${dayjs(event.timestamp).subtract(1000, 'ms')}
AND timestamp < ${dayjs(event.timestamp).add(1000, 'ms')}
AND event = ${event.event}
AND uuid = ${event.id}`,
query: hogql`SELECT properties, uuid
FROM events
WHERE timestamp > ${dayjs(event.timestamp).subtract(1000, 'ms')}
AND timestamp < ${dayjs(event.timestamp).add(1000, 'ms')}
AND event = ${event.event}
AND uuid = ${event.id}`,
}
const response = await api.query(query)
if (response.error) {