mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 13:39:22 +01:00
feat: add ee licensed replay transformer (#18874)
first pass through using the EE licensed replay transformer in playback Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: David Newell <d.newell1@outlook.com>
This commit is contained in:
parent
bda9166711
commit
16323959fd
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
.eslintrc.js
|
||||
jest.config.ts
|
14
.eslintrc.js
14
.eslintrc.js
@ -54,6 +54,7 @@ module.exports = {
|
||||
'compat',
|
||||
'posthog',
|
||||
'simple-import-sort',
|
||||
'import',
|
||||
],
|
||||
rules: {
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
@ -261,6 +262,19 @@ module.exports = {
|
||||
'no-constant-condition': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-irregular-whitespace': 'off',
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
{
|
||||
zones: [
|
||||
{
|
||||
target: './frontend/**',
|
||||
from: './ee/frontend/**',
|
||||
message:
|
||||
"EE licensed TypeScript should only be accessed via the posthogEE objects. Use `import posthogEE from '@posthog/ee/exports'`",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
4
.github/workflows/ci-frontend.yml
vendored
4
.github/workflows/ci-frontend.yml
vendored
@ -107,6 +107,10 @@ jobs:
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
run: pnpm schema:build:json && git diff --exit-code
|
||||
|
||||
- name: Check if mobile replay "schema.json" is up to date
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
run: pnpm mobile-replay:schema:build:json && git diff --exit-code
|
||||
|
||||
- name: Check toolbar bundle size
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
uses: preactjs/compressed-size-action@v2
|
||||
|
@ -17,7 +17,7 @@
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
<env name="SESSION_RECORDING_KAFKA_COMPRESSION" value="gzip" />
|
||||
<env name="SESSION_RECORDING_KAFKA_HOSTS" value="localhost" />
|
||||
<env name="SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES" value="524288" />
|
||||
<env name="SESSION_RECORDING_KAFKA_MAX_REQUEST_SIZE_BYTES" value="20971520" />
|
||||
<env name="SKIP_SERVICE_VERSION_REQUIREMENTS" value="1" />
|
||||
<env name="REPLAY_EVENTS_NEW_CONSUMER_RATIO" value="1" />
|
||||
</envs>
|
||||
@ -48,4 +48,4 @@
|
||||
<option name="customRunCommand" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { PostHogEE } from '@posthog/ee/types'
|
||||
|
||||
const myTestCode = (): void => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('it works!')
|
||||
}
|
||||
import { transformEventToWeb, transformToWeb } from './mobile-replay'
|
||||
|
||||
const postHogEE: PostHogEE = {
|
||||
enabled: true,
|
||||
myTestCode,
|
||||
}
|
||||
|
||||
export default postHogEE
|
||||
export default async (): Promise<PostHogEE> =>
|
||||
Promise.resolve({
|
||||
enabled: true,
|
||||
mobileReplay: {
|
||||
transformEventToWeb,
|
||||
transformToWeb,
|
||||
},
|
||||
})
|
||||
|
429
ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap
Normal file
429
ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap
Normal file
@ -0,0 +1,429 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`replay/transform transform can convert images 1`] = `
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"height": 600,
|
||||
"href": "",
|
||||
"width": 300,
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 4,
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"initialOffset": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
},
|
||||
"node": {
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "html",
|
||||
"publicId": "",
|
||||
"systemId": "",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [],
|
||||
"id": 4,
|
||||
"tagName": "head",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"style": "color: #ffffff;width: 100px;height: 30px;position: absolute;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 12345,
|
||||
"textContent": "Ⱏ遲䩞㡛쓯잘ጫ䵤㥦鷁끞鈅毅┌빯湌Თ",
|
||||
"type": 3,
|
||||
},
|
||||
],
|
||||
"id": 102,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"height": 30,
|
||||
"src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=",
|
||||
"style": "width: 100px;height: 30px;position: absolute;left: 25px;top: 42px;",
|
||||
"width": 100,
|
||||
},
|
||||
"childNodes": [],
|
||||
"id": 12345,
|
||||
"tagName": "img",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 101,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 5,
|
||||
"tagName": "body",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 3,
|
||||
"tagName": "html",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
"type": 0,
|
||||
},
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 2,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`replay/transform transform can convert rect with text 1`] = `
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"height": 600,
|
||||
"href": "",
|
||||
"width": 300,
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 4,
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"initialOffset": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
},
|
||||
"node": {
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "html",
|
||||
"publicId": "",
|
||||
"systemId": "",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [],
|
||||
"id": 4,
|
||||
"tagName": "head",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"style": "width: 100px;height: 30px;position: absolute;left: 11px;top: 12px;",
|
||||
"viewBox": "0 0 100 30",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"fill": "transparent",
|
||||
"height": 30,
|
||||
"rx": "10px",
|
||||
"stroke": "#ee3ee4",
|
||||
"stroke-width": "4",
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"childNodes": [],
|
||||
"id": 104,
|
||||
"tagName": "rect",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 12345,
|
||||
"tagName": "svg",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"style": "width: 100px;height: 30px;position: absolute;left: 13px;top: 17px;overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 12345,
|
||||
"textContent": "i am in the box",
|
||||
"type": 3,
|
||||
},
|
||||
],
|
||||
"id": 105,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 103,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 5,
|
||||
"tagName": "body",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 3,
|
||||
"tagName": "html",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
"type": 0,
|
||||
},
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 2,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`replay/transform transform can ignore unknown wireframe types 1`] = `
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"height": 600,
|
||||
"href": "",
|
||||
"width": 300,
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 4,
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"initialOffset": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
},
|
||||
"node": {
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "html",
|
||||
"publicId": "",
|
||||
"systemId": "",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [],
|
||||
"id": 4,
|
||||
"tagName": "head",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [],
|
||||
"id": 100,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 5,
|
||||
"tagName": "body",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 3,
|
||||
"tagName": "html",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
"type": 0,
|
||||
},
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 2,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`replay/transform transform can short-circuit non-mobile full snapshot 1`] = `
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"height": 600,
|
||||
"href": "https://my-awesome.site",
|
||||
"width": 300,
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 4,
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"the": "payload",
|
||||
},
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 2,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`replay/transform transform child wireframes are processed 1`] = `
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"height": 600,
|
||||
"href": "",
|
||||
"width": 300,
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 4,
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"initialOffset": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
},
|
||||
"node": {
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "html",
|
||||
"publicId": "",
|
||||
"systemId": "",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [],
|
||||
"id": 4,
|
||||
"tagName": "head",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"style": "overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"style": "overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"attributes": {
|
||||
"style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: absolute;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 12345,
|
||||
"textContent": "first nested",
|
||||
"type": 3,
|
||||
},
|
||||
],
|
||||
"id": 107,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: absolute;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 12345,
|
||||
"textContent": "second nested",
|
||||
"type": 3,
|
||||
},
|
||||
],
|
||||
"id": 108,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 98765,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: absolute;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;",
|
||||
},
|
||||
"childNodes": [
|
||||
{
|
||||
"id": 12345,
|
||||
"textContent": "third (different level) nested",
|
||||
"type": 3,
|
||||
},
|
||||
],
|
||||
"id": 109,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 123456789,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 106,
|
||||
"tagName": "div",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 5,
|
||||
"tagName": "body",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 3,
|
||||
"tagName": "html",
|
||||
"type": 2,
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
"type": 0,
|
||||
},
|
||||
},
|
||||
"timestamp": 1,
|
||||
"type": 2,
|
||||
},
|
||||
]
|
||||
`;
|
73
ee/frontend/mobile-replay/index.ts
Normal file
73
ee/frontend/mobile-replay/index.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { eventWithTime } from '@rrweb/types'
|
||||
import Ajv, { ErrorObject } from 'ajv'
|
||||
|
||||
import { mobileEventWithTime } from './mobile.types'
|
||||
import mobileSchema from './schema/mobile/rr-mobile-schema.json'
|
||||
import webSchema from './schema/web/rr-web-schema.json'
|
||||
import { makeFullEvent, makeMetaEvent } from './transformers'
|
||||
|
||||
const ajv = new Ajv({
|
||||
allowUnionTypes: true,
|
||||
}) // options can be passed, e.g. {allErrors: true}
|
||||
|
||||
const transformers: Record<number, (x: any) => eventWithTime> = {
|
||||
4: makeMetaEvent,
|
||||
2: makeFullEvent,
|
||||
}
|
||||
|
||||
const mobileSchemaValidator = ajv.compile(mobileSchema)
|
||||
|
||||
export function validateFromMobile(data: unknown): {
|
||||
isValid: boolean
|
||||
errors: ErrorObject[] | null | undefined
|
||||
} {
|
||||
const isValid = mobileSchemaValidator(data)
|
||||
return {
|
||||
isValid,
|
||||
errors: isValid ? null : mobileSchemaValidator.errors,
|
||||
}
|
||||
}
|
||||
|
||||
const webSchemaValidator = ajv.compile(webSchema)
|
||||
|
||||
function couldBeEventWithTime(x: unknown): x is eventWithTime | mobileEventWithTime {
|
||||
return typeof x === 'object' && x !== null && 'type' in x && 'timestamp' in x
|
||||
}
|
||||
|
||||
export function transformEventToWeb(event: unknown): eventWithTime | null {
|
||||
if (!couldBeEventWithTime(event)) {
|
||||
console.warn(`No type in event: ${JSON.stringify(event)}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const transformer = transformers[event.type]
|
||||
if (transformer) {
|
||||
const transformed = transformer(event)
|
||||
validateAgainstWebSchema(transformed)
|
||||
return transformed
|
||||
} else {
|
||||
console.warn(`No transformer for event type ${event.type}`)
|
||||
return event as eventWithTime
|
||||
}
|
||||
}
|
||||
|
||||
export function transformToWeb(mobileData: (eventWithTime | mobileEventWithTime)[]): eventWithTime[] {
|
||||
return mobileData.reduce((acc, event) => {
|
||||
const transformed = transformEventToWeb(event)
|
||||
if (transformed) {
|
||||
acc.push(transformed)
|
||||
}
|
||||
return acc
|
||||
}, [] as eventWithTime[])
|
||||
}
|
||||
|
||||
export function validateAgainstWebSchema(data: unknown): boolean {
|
||||
const validationResult = webSchemaValidator(data)
|
||||
if (!validationResult) {
|
||||
// we are passing all data through this validation now and don't know how safe the schema is
|
||||
// TODO would we ever want to reject here?
|
||||
console.error(webSchemaValidator.errors)
|
||||
}
|
||||
|
||||
return validationResult
|
||||
}
|
172
ee/frontend/mobile-replay/mobile.types.ts
Normal file
172
ee/frontend/mobile-replay/mobile.types.ts
Normal file
@ -0,0 +1,172 @@
|
||||
// copied from rrweb-snapshot, not included in rrweb types
|
||||
import { customEvent, EventType } from '@rrweb/types'
|
||||
|
||||
export enum NodeType {
|
||||
Document = 0,
|
||||
DocumentType = 1,
|
||||
Element = 2,
|
||||
Text = 3,
|
||||
CDATA = 4,
|
||||
Comment = 5,
|
||||
}
|
||||
|
||||
export type documentNode = {
|
||||
type: NodeType.Document
|
||||
childNodes: serializedNodeWithId[]
|
||||
compatMode?: string
|
||||
}
|
||||
|
||||
export type documentTypeNode = {
|
||||
type: NodeType.DocumentType
|
||||
name: string
|
||||
publicId: string
|
||||
systemId: string
|
||||
}
|
||||
|
||||
export type attributes = {
|
||||
[key: string]: string | number | true | null
|
||||
}
|
||||
|
||||
export type elementNode = {
|
||||
type: NodeType.Element
|
||||
tagName: string
|
||||
attributes: attributes
|
||||
childNodes: serializedNodeWithId[]
|
||||
isSVG?: true
|
||||
needBlock?: boolean
|
||||
// This is a custom element or not.
|
||||
isCustom?: true
|
||||
}
|
||||
|
||||
export type textNode = {
|
||||
type: NodeType.Text
|
||||
textContent: string
|
||||
isStyle?: true
|
||||
}
|
||||
|
||||
export type cdataNode = {
|
||||
type: NodeType.CDATA
|
||||
textContent: ''
|
||||
}
|
||||
|
||||
export type commentNode = {
|
||||
type: NodeType.Comment
|
||||
textContent: string
|
||||
}
|
||||
|
||||
export type serializedNode = (documentNode | documentTypeNode | elementNode | textNode | cdataNode | commentNode) & {
|
||||
rootId?: number
|
||||
isShadowHost?: boolean
|
||||
isShadow?: boolean
|
||||
}
|
||||
|
||||
export type serializedNodeWithId = serializedNode & { id: number }
|
||||
|
||||
// end copied section
|
||||
|
||||
export type MobileNodeType = 'text' | 'image' | 'rectangle' | 'div'
|
||||
|
||||
export type MobileStyles = {
|
||||
/**
|
||||
* @description maps to CSS color. Accepts any valid CSS color value. Expects a #RGB value e.g. #000 or #000000
|
||||
*/
|
||||
color?: string
|
||||
/**
|
||||
* @description maps to CSS background-color. Accepts any valid CSS color value. Expects a #RGB value e.g. #000 or #000000
|
||||
*/
|
||||
backgroundColor?: string
|
||||
/**
|
||||
* @description if borderWidth is present, then border style is assumed to be solid
|
||||
*/
|
||||
borderWidth?: string | number
|
||||
/**
|
||||
* @description if borderRadius is present, then border style is assumed to be solid
|
||||
*/
|
||||
borderRadius?: string | number
|
||||
/**
|
||||
* @description if borderColor is present, then border style is assumed to be solid
|
||||
*/
|
||||
borderColor?: string
|
||||
/**
|
||||
* @description vertical alignment with respect to its parent
|
||||
*/
|
||||
verticalAlign?: 'top' | 'bottom' | 'center'
|
||||
/**
|
||||
* @description horizontal alignment with respect to its parent
|
||||
*/
|
||||
horizontalAlign?: 'left' | 'right' | 'center'
|
||||
}
|
||||
|
||||
type wireframeBase = {
|
||||
id: number
|
||||
/**
|
||||
* @description x and y are the top left corner of the element, if they are present then the element is absolutely positioned, if they are not present this is equivalent to setting them to 0
|
||||
*/
|
||||
x: number
|
||||
y: number
|
||||
/*
|
||||
* @description width and height are the dimensions of the element, the only accepted units is pixels. You can omit the unit.
|
||||
*/
|
||||
width: number
|
||||
height: number
|
||||
childWireframes?: wireframe[]
|
||||
type: MobileNodeType
|
||||
style?: MobileStyles
|
||||
}
|
||||
|
||||
export type wireframeText = wireframeBase & {
|
||||
type: 'text'
|
||||
text: string
|
||||
}
|
||||
|
||||
export type wireframeImage = wireframeBase & {
|
||||
type: 'image'
|
||||
/**
|
||||
* @description this will be used as base64 encoded image source, with no other attributes it is assumed to be a PNG
|
||||
*/
|
||||
base64: string
|
||||
}
|
||||
|
||||
export type wireframeRectangle = wireframeBase & {
|
||||
type: 'rectangle'
|
||||
}
|
||||
|
||||
export type wireframeDiv = wireframeBase & {
|
||||
/*
|
||||
* @description this is the default type, if no type is specified then it is assumed to be a div
|
||||
*/
|
||||
type: 'div'
|
||||
}
|
||||
|
||||
export type wireframe = wireframeText | wireframeImage | wireframeRectangle | wireframeDiv
|
||||
|
||||
// the rrweb full snapshot event type, but it contains wireframes not html
|
||||
export type fullSnapshotEvent = {
|
||||
type: EventType.FullSnapshot
|
||||
data: {
|
||||
/**
|
||||
* @description This mimics the RRWeb full snapshot event type, except instead of reporting a serialized DOM it reports a wireframe representation of the screen.
|
||||
*/
|
||||
wireframes: wireframe[]
|
||||
initialOffset: {
|
||||
top: number
|
||||
left: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type metaEvent = {
|
||||
type: EventType.Meta
|
||||
data: {
|
||||
href?: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
export type mobileEvent = fullSnapshotEvent | metaEvent | customEvent
|
||||
|
||||
export type mobileEventWithTime = mobileEvent & {
|
||||
timestamp: number
|
||||
delay?: number
|
||||
}
|
324
ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json
Normal file
324
ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"initialOffset": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"left": {
|
||||
"type": "number"
|
||||
},
|
||||
"top": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["top", "left"],
|
||||
"type": "object"
|
||||
},
|
||||
"wireframes": {
|
||||
"description": "This mimics the RRWeb full snapshot event type, except instead of reporting a serialized DOM it reports a wireframe representation of the screen.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/wireframe"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": ["wireframes", "initialOffset"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.FullSnapshot"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"href": {
|
||||
"type": "string"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["width", "height"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Meta"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"payload": {},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["tag", "payload"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Custom"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"EventType.Custom": {
|
||||
"const": 5,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.FullSnapshot": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.Meta": {
|
||||
"const": 4,
|
||||
"type": "number"
|
||||
},
|
||||
"MobileNodeType": {
|
||||
"enum": ["text", "image", "rectangle", "div"],
|
||||
"type": "string"
|
||||
},
|
||||
"MobileStyles": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"backgroundColor": {
|
||||
"description": "maps to CSS background-color. Accepts any valid CSS color value. Expects a #RGB value e.g. #000 or #000000",
|
||||
"type": "string"
|
||||
},
|
||||
"borderColor": {
|
||||
"description": "if borderColor is present, then border style is assumed to be solid",
|
||||
"type": "string"
|
||||
},
|
||||
"borderRadius": {
|
||||
"description": "if borderRadius is present, then border style is assumed to be solid",
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"borderWidth": {
|
||||
"description": "if borderWidth is present, then border style is assumed to be solid",
|
||||
"type": ["string", "number"]
|
||||
},
|
||||
"color": {
|
||||
"description": "maps to CSS color. Accepts any valid CSS color value. Expects a #RGB value e.g. #000 or #000000",
|
||||
"type": "string"
|
||||
},
|
||||
"horizontalAlign": {
|
||||
"description": "horizontal alignment with respect to its parent",
|
||||
"enum": ["left", "right", "center"],
|
||||
"type": "string"
|
||||
},
|
||||
"verticalAlign": {
|
||||
"description": "vertical alignment with respect to its parent",
|
||||
"enum": ["top", "bottom", "center"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"wireframe": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/wireframeText"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/wireframeImage"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/wireframeRectangle"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/wireframeDiv"
|
||||
}
|
||||
]
|
||||
},
|
||||
"wireframeDiv": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"childWireframes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/wireframe"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"style": {
|
||||
"$ref": "#/definitions/MobileStyles"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MobileNodeType"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "x and y are the top left corner of the element, if they are present then the element is absolutely positioned, if they are not present this is equivalent to setting them to 0",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height", "id", "type", "width", "x", "y"],
|
||||
"type": "object"
|
||||
},
|
||||
"wireframeImage": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"base64": {
|
||||
"description": "this will be used as base64 encoded image source, with no other attributes it is assumed to be a PNG",
|
||||
"type": "string"
|
||||
},
|
||||
"childWireframes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/wireframe"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"style": {
|
||||
"$ref": "#/definitions/MobileStyles"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MobileNodeType"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "x and y are the top left corner of the element, if they are present then the element is absolutely positioned, if they are not present this is equivalent to setting them to 0",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["base64", "height", "id", "type", "width", "x", "y"],
|
||||
"type": "object"
|
||||
},
|
||||
"wireframeRectangle": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"childWireframes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/wireframe"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"style": {
|
||||
"$ref": "#/definitions/MobileStyles"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MobileNodeType"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "x and y are the top left corner of the element, if they are present then the element is absolutely positioned, if they are not present this is equivalent to setting them to 0",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height", "id", "type", "width", "x", "y"],
|
||||
"type": "object"
|
||||
},
|
||||
"wireframeText": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"childWireframes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/wireframe"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"style": {
|
||||
"$ref": "#/definitions/MobileStyles"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MobileNodeType"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "x and y are the top left corner of the element, if they are present then the element is absolutely positioned, if they are not present this is equivalent to setting them to 0",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height", "id", "text", "type", "width", "x", "y"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
951
ee/frontend/mobile-replay/schema/web/rr-web-schema.json
Normal file
951
ee/frontend/mobile-replay/schema/web/rr-web-schema.json
Normal file
@ -0,0 +1,951 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.DomContentLoaded"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Load"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"initialOffset": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"left": {
|
||||
"type": "number"
|
||||
},
|
||||
"top": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["top", "left"],
|
||||
"type": "object"
|
||||
},
|
||||
"node": {}
|
||||
},
|
||||
"required": ["node", "initialOffset"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.FullSnapshot"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/incrementalData"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.IncrementalSnapshot"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"href": {
|
||||
"type": "string"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["href", "width", "height"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Meta"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"payload": {},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["tag", "payload"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Custom"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"payload": {},
|
||||
"plugin": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["plugin", "payload"],
|
||||
"type": "object"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/EventType.Plugin"
|
||||
}
|
||||
},
|
||||
"required": ["data", "timestamp", "type"],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"CanvasContext": {
|
||||
"enum": [0, 1, 2],
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.Custom": {
|
||||
"const": 5,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.DomContentLoaded": {
|
||||
"const": 0,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.FullSnapshot": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.IncrementalSnapshot": {
|
||||
"const": 3,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.Load": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.Meta": {
|
||||
"const": 4,
|
||||
"type": "number"
|
||||
},
|
||||
"EventType.Plugin": {
|
||||
"const": 6,
|
||||
"type": "number"
|
||||
},
|
||||
"FontDisplay": {
|
||||
"enum": ["auto", "block", "fallback", "optional", "swap"],
|
||||
"type": "string"
|
||||
},
|
||||
"FontFaceDescriptors": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ascentOverride": {
|
||||
"type": "string"
|
||||
},
|
||||
"descentOverride": {
|
||||
"type": "string"
|
||||
},
|
||||
"display": {
|
||||
"$ref": "#/definitions/FontDisplay"
|
||||
},
|
||||
"featureSettings": {
|
||||
"type": "string"
|
||||
},
|
||||
"lineGapOverride": {
|
||||
"type": "string"
|
||||
},
|
||||
"stretch": {
|
||||
"type": "string"
|
||||
},
|
||||
"style": {
|
||||
"type": "string"
|
||||
},
|
||||
"unicodeRange": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"weight": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"IncrementalSource.AdoptedStyleSheet": {
|
||||
"const": 15,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.CanvasMutation": {
|
||||
"const": 9,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Drag": {
|
||||
"const": 12,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Font": {
|
||||
"const": 10,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Input": {
|
||||
"const": 5,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.MediaInteraction": {
|
||||
"const": 7,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.MouseInteraction": {
|
||||
"const": 2,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.MouseMove": {
|
||||
"const": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Mutation": {
|
||||
"const": 0,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Scroll": {
|
||||
"const": 3,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.Selection": {
|
||||
"const": 14,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.StyleDeclaration": {
|
||||
"const": 13,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.StyleSheetRule": {
|
||||
"const": 8,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.TouchMove": {
|
||||
"const": 6,
|
||||
"type": "number"
|
||||
},
|
||||
"IncrementalSource.ViewportResize": {
|
||||
"const": 4,
|
||||
"type": "number"
|
||||
},
|
||||
"MediaInteractions": {
|
||||
"enum": [0, 1, 2, 3, 4],
|
||||
"type": "number"
|
||||
},
|
||||
"MouseInteractions": {
|
||||
"enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
"type": "number"
|
||||
},
|
||||
"PointerTypes": {
|
||||
"enum": [0, 1, 2],
|
||||
"type": "number"
|
||||
},
|
||||
"SelectionRange": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"end": {
|
||||
"type": "number"
|
||||
},
|
||||
"endOffset": {
|
||||
"type": "number"
|
||||
},
|
||||
"start": {
|
||||
"type": "number"
|
||||
},
|
||||
"startOffset": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["start", "startOffset", "end", "endOffset"],
|
||||
"type": "object"
|
||||
},
|
||||
"addedNodeMutation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nextId": {
|
||||
"type": ["number", "null"]
|
||||
},
|
||||
"node": {},
|
||||
"parentId": {
|
||||
"type": "number"
|
||||
},
|
||||
"previousId": {
|
||||
"type": ["number", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["parentId", "nextId", "node"],
|
||||
"type": "object"
|
||||
},
|
||||
"adoptedStyleSheetData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.AdoptedStyleSheet"
|
||||
},
|
||||
"styleIds": {
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"styles": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/styleSheetAddRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"styleId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["styleId", "rules"],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": ["id", "source", "styleIds"],
|
||||
"type": "object"
|
||||
},
|
||||
"attributeMutation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"attributes": {
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/styleOMValue"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["id", "attributes"],
|
||||
"type": "object"
|
||||
},
|
||||
"canvasMutationCommand": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"args": {
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"setter": {
|
||||
"const": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["property", "args"],
|
||||
"type": "object"
|
||||
},
|
||||
"canvasMutationData": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"commands": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/canvasMutationCommand"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.CanvasMutation"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/CanvasContext"
|
||||
}
|
||||
},
|
||||
"required": ["commands", "id", "source", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"args": {
|
||||
"items": {},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"setter": {
|
||||
"const": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.CanvasMutation"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/CanvasContext"
|
||||
}
|
||||
},
|
||||
"required": ["args", "id", "property", "source", "type"],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fontData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"buffer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"descriptors": {
|
||||
"$ref": "#/definitions/FontFaceDescriptors"
|
||||
},
|
||||
"family": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontSource": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.Font"
|
||||
}
|
||||
},
|
||||
"required": ["buffer", "family", "fontSource", "source"],
|
||||
"type": "object"
|
||||
},
|
||||
"incrementalData": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/mutationData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/mousemoveData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/mouseInteractionData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/scrollData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/viewportResizeData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/inputData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/mediaInteractionData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/styleSheetRuleData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/canvasMutationData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/fontData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/selectionData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/styleDeclarationData"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/adoptedStyleSheetData"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"isChecked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.Input"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"userTriggered": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["id", "isChecked", "source", "text"],
|
||||
"type": "object"
|
||||
},
|
||||
"mediaInteractionData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"currentTime": {
|
||||
"type": "number"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"muted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"playbackRate": {
|
||||
"type": "number"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.MediaInteraction"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MediaInteractions"
|
||||
},
|
||||
"volume": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["id", "source", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
"mouseInteractionData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"pointerType": {
|
||||
"$ref": "#/definitions/PointerTypes"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.MouseInteraction"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/MouseInteractions"
|
||||
},
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["id", "source", "type", "x", "y"],
|
||||
"type": "object"
|
||||
},
|
||||
"mousePosition": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeOffset": {
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["x", "y", "id", "timeOffset"],
|
||||
"type": "object"
|
||||
},
|
||||
"mousemoveData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"positions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/mousePosition"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/IncrementalSource.MouseMove"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/IncrementalSource.TouchMove"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/IncrementalSource.Drag"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["source", "positions"],
|
||||
"type": "object"
|
||||
},
|
||||
"mutationData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"adds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/addedNodeMutation"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"attributes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/attributeMutation"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"isAttachIframe": {
|
||||
"const": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"removes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/removedNodeMutation"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.Mutation"
|
||||
},
|
||||
"texts": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/textMutation"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": ["adds", "attributes", "removes", "source", "texts"],
|
||||
"type": "object"
|
||||
},
|
||||
"removedNodeMutation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"isShadow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parentId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["parentId", "id"],
|
||||
"type": "object"
|
||||
},
|
||||
"scrollData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.Scroll"
|
||||
},
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["id", "source", "x", "y"],
|
||||
"type": "object"
|
||||
},
|
||||
"selectionData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ranges": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/SelectionRange"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.Selection"
|
||||
}
|
||||
},
|
||||
"required": ["ranges", "source"],
|
||||
"type": "object"
|
||||
},
|
||||
"styleDeclarationData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"index": {
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"remove": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"property": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["property"],
|
||||
"type": "object"
|
||||
},
|
||||
"set": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"priority": {
|
||||
"type": "string"
|
||||
},
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["property", "value"],
|
||||
"type": "object"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.StyleDeclaration"
|
||||
},
|
||||
"styleId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["index", "source"],
|
||||
"type": "object"
|
||||
},
|
||||
"styleOMValue": {
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/styleValueWithPriority"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"const": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"styleSheetAddRule": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"index": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rule": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["rule"],
|
||||
"type": "object"
|
||||
},
|
||||
"styleSheetDeleteRule": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"index": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["index"],
|
||||
"type": "object"
|
||||
},
|
||||
"styleSheetRuleData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"adds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/styleSheetAddRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"removes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/styleSheetDeleteRule"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"replace": {
|
||||
"type": "string"
|
||||
},
|
||||
"replaceSync": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.StyleSheetRule"
|
||||
},
|
||||
"styleId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["source"],
|
||||
"type": "object"
|
||||
},
|
||||
"styleValueWithPriority": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array"
|
||||
},
|
||||
"textMutation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"value": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
},
|
||||
"required": ["id", "value"],
|
||||
"type": "object"
|
||||
},
|
||||
"viewportResizeData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/IncrementalSource.ViewportResize"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height", "source", "width"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
301
ee/frontend/mobile-replay/transform.test.ts
Normal file
301
ee/frontend/mobile-replay/transform.test.ts
Normal file
@ -0,0 +1,301 @@
|
||||
import posthogEE from '@posthog/ee/exports'
|
||||
import { EventType } from '@rrweb/types'
|
||||
import { ifEeDescribe } from 'lib/ee.test'
|
||||
|
||||
import { PostHogEE } from '../../../frontend/@posthog/ee/types'
|
||||
import { validateAgainstWebSchema, validateFromMobile } from './index'
|
||||
|
||||
const heartEyesEmojiURL =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII='
|
||||
|
||||
describe('replay/transform', () => {
|
||||
describe('validation', () => {
|
||||
test('example of validating incoming _invalid_ data', () => {
|
||||
const invalidData = {
|
||||
foo: 'abc',
|
||||
bar: 'abc',
|
||||
}
|
||||
|
||||
expect(validateFromMobile(invalidData).isValid).toBe(false)
|
||||
})
|
||||
|
||||
test('example of validating mobile meta event', () => {
|
||||
const validData = {
|
||||
data: { width: 1, height: 1 },
|
||||
timestamp: 1,
|
||||
type: EventType.Meta,
|
||||
}
|
||||
|
||||
expect(validateFromMobile(validData)).toStrictEqual({
|
||||
isValid: true,
|
||||
errors: null,
|
||||
})
|
||||
})
|
||||
|
||||
describe('validate web schema', () => {
|
||||
test('does not block when invalid', () => {
|
||||
expect(validateAgainstWebSchema({})).toBeFalsy()
|
||||
})
|
||||
|
||||
test('should be valid when...', () => {
|
||||
expect(validateAgainstWebSchema({ data: {}, timestamp: 12345, type: 0 })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ifEeDescribe('transform', () => {
|
||||
let posthogEEModule: PostHogEE
|
||||
beforeEach(async () => {
|
||||
posthogEEModule = await posthogEE()
|
||||
})
|
||||
test('can ignore unknown types', () => {
|
||||
expect(
|
||||
posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: { width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
data: { href: 'included when present', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{ type: 9999 },
|
||||
])
|
||||
).toStrictEqual([
|
||||
{ type: 4, data: { href: '', width: 300, height: 600 }, timestamp: 1 },
|
||||
{ type: 4, data: { href: 'included when present', width: 300, height: 600 }, timestamp: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
test('can ignore unknown wireframe types', () => {
|
||||
const unexpectedWireframeType = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: { screen: 'App Home Page', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
wireframes: [
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
type: 'something in the SDK but not yet the transformer',
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
])
|
||||
expect(unexpectedWireframeType).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('can short-circuit non-mobile full snapshot', () => {
|
||||
const allWeb = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: { href: 'https://my-awesome.site', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
node: { the: 'payload' },
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
])
|
||||
expect(allWeb).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('can convert images', () => {
|
||||
const exampleWithImage = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {
|
||||
screen: 'App Home Page',
|
||||
width: 300,
|
||||
height: 600,
|
||||
},
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
wireframes: [
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
// clip: {
|
||||
// bottom: 83,
|
||||
// right: 44,
|
||||
// },
|
||||
type: 'text',
|
||||
text: 'Ⱏ遲䩞㡛쓯잘ጫ䵤㥦鷁끞鈅毅┌빯湌Თ',
|
||||
style: {
|
||||
// family: '疴ꖻ䖭㋑⁃⻋ꑧٹ㧕Ⓖ',
|
||||
// size: 4220431756569966319,
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12345,
|
||||
x: 25,
|
||||
y: 42,
|
||||
width: 100,
|
||||
height: 30,
|
||||
// clip: {
|
||||
// bottom: 83,
|
||||
// right: 44,
|
||||
// },
|
||||
type: 'image',
|
||||
base64: heartEyesEmojiURL,
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
])
|
||||
expect(exampleWithImage).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('can convert rect with text', () => {
|
||||
const exampleWithRectAndText = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {
|
||||
width: 300,
|
||||
height: 600,
|
||||
},
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
wireframes: [
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
type: 'rectangle',
|
||||
style: {
|
||||
color: '#ee3ee4',
|
||||
borderColor: '#ee3ee4',
|
||||
borderWidth: '4',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12345,
|
||||
x: 13,
|
||||
y: 17,
|
||||
width: 100,
|
||||
height: 30,
|
||||
verticalAlign: 'top',
|
||||
horizontalAlign: 'right',
|
||||
type: 'text',
|
||||
text: 'i am in the box',
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
])
|
||||
expect(exampleWithRectAndText).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('child wireframes are processed', () => {
|
||||
const textEvent = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: { screen: 'App Home Page', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
wireframes: [
|
||||
{
|
||||
id: 123456789,
|
||||
childWireframes: [
|
||||
{
|
||||
id: 98765,
|
||||
childWireframes: [
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
type: 'text',
|
||||
text: 'first nested',
|
||||
style: {
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
borderWidth: '4px',
|
||||
borderColor: '#000ddd',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
type: 'text',
|
||||
text: 'second nested',
|
||||
style: {
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
borderWidth: '4px',
|
||||
borderColor: '#000ddd',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12345,
|
||||
x: 11,
|
||||
y: 12,
|
||||
width: 100,
|
||||
height: 30,
|
||||
// clip: {
|
||||
// bottom: 83,
|
||||
// right: 44,
|
||||
// },
|
||||
type: 'text',
|
||||
text: 'third (different level) nested',
|
||||
style: {
|
||||
// family: '疴ꖻ䖭㋑⁃⻋ꑧٹ㧕Ⓖ',
|
||||
// size: 4220431756569966319,
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
borderWidth: '4px',
|
||||
borderColor: '#000ddd',
|
||||
borderRadius: '10', // you can omit the pixels
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
])
|
||||
expect(textEvent).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
269
ee/frontend/mobile-replay/transformers.ts
Normal file
269
ee/frontend/mobile-replay/transformers.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import { EventType, fullSnapshotEvent, metaEvent } from '@rrweb/types'
|
||||
|
||||
import {
|
||||
fullSnapshotEvent as MobileFullSnapshotEvent,
|
||||
metaEvent as MobileMetaEvent,
|
||||
NodeType,
|
||||
serializedNodeWithId,
|
||||
wireframe,
|
||||
wireframeDiv,
|
||||
wireframeImage,
|
||||
wireframeRectangle,
|
||||
wireframeText,
|
||||
} from './mobile.types'
|
||||
import { makePositionStyles, makeStylesString, makeSvgBorder } from './wireframeStyle'
|
||||
|
||||
/**
|
||||
* generates a sequence of ids
|
||||
* from 100 to 9,999,999
|
||||
* the transformer reserves ids in the range 0 to 9,999,999
|
||||
* we reserve a range of ids because we need nodes to have stable ids across snapshots
|
||||
* in order for incremental snapshots to work
|
||||
* some mobile elements have to be wrapped in other elements in order to be styled correctly
|
||||
* which means the web version of a mobile replay will use ids that don't exist in the mobile replay,
|
||||
* and we need to ensure they don't clash
|
||||
* -----
|
||||
* id is typed as a number in rrweb
|
||||
* and there's a few places in their code where rrweb uses a check for `id === -1` to bail out of processing
|
||||
* so, it's safest to assume that id is expected to be a positive integer
|
||||
*/
|
||||
function* ids(): Generator<number> {
|
||||
let i = 100
|
||||
while (i < 9999999) {
|
||||
yield i++
|
||||
}
|
||||
}
|
||||
const idSequence = ids()
|
||||
|
||||
export const makeMetaEvent = (
|
||||
mobileMetaEvent: MobileMetaEvent & {
|
||||
timestamp: number
|
||||
}
|
||||
): metaEvent & {
|
||||
timestamp: number
|
||||
delay?: number
|
||||
} => ({
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: mobileMetaEvent.data.href || '', // the replay doesn't use the href, so we safely ignore any absence
|
||||
// mostly we need width and height in order to size the viewport
|
||||
width: mobileMetaEvent.data.width,
|
||||
height: mobileMetaEvent.data.height,
|
||||
},
|
||||
timestamp: mobileMetaEvent.timestamp,
|
||||
})
|
||||
|
||||
function _isPositiveInteger(id: unknown): boolean {
|
||||
return typeof id === 'number' && id > 0 && id % 1 === 0
|
||||
}
|
||||
|
||||
function makeDivElement(wireframe: wireframeDiv, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
const _id = _isPositiveInteger(wireframe.id) ? wireframe.id : idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
},
|
||||
id: _id,
|
||||
childNodes: children,
|
||||
}
|
||||
}
|
||||
|
||||
function makeTextElement(wireframe: wireframeText, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
if (wireframe.type !== 'text') {
|
||||
console.error('Passed incorrect wireframe type to makeTextElement')
|
||||
return null
|
||||
}
|
||||
|
||||
// because we might have to style the text, we always wrap it in a div
|
||||
// and apply styles to that
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
},
|
||||
id: idSequence.next().value,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: wireframe.text,
|
||||
id: wireframe.id,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function makeImageElement(wireframe: wireframeImage, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
let src = wireframe.base64
|
||||
if (!src.startsWith('data:image/')) {
|
||||
src = 'data:image/png;base64,' + src
|
||||
}
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'img',
|
||||
attributes: {
|
||||
src: src,
|
||||
width: wireframe.width,
|
||||
height: wireframe.height,
|
||||
style: makeStylesString(wireframe),
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
}
|
||||
}
|
||||
|
||||
function makeRectangleElement(
|
||||
wireframe: wireframeRectangle,
|
||||
children: serializedNodeWithId[]
|
||||
): serializedNodeWithId | null {
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'svg',
|
||||
attributes: {
|
||||
style: makePositionStyles(wireframe),
|
||||
viewBox: `0 0 ${wireframe.width} ${wireframe.height}`,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'rect',
|
||||
attributes: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: wireframe.width,
|
||||
height: wireframe.height,
|
||||
fill: wireframe.style?.backgroundColor || 'transparent',
|
||||
...makeSvgBorder(wireframe.style),
|
||||
},
|
||||
id: idSequence.next().value,
|
||||
childNodes: children,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function chooseConverter<T extends wireframe>(
|
||||
wireframe: T
|
||||
): (wireframe: T, children: serializedNodeWithId[]) => serializedNodeWithId | null {
|
||||
// in theory type is always present
|
||||
// but since this is coming over the wire we can't really be sure
|
||||
// and so we default to div
|
||||
const converterType = wireframe.type || 'div'
|
||||
switch (converterType) {
|
||||
case 'text':
|
||||
return makeTextElement as unknown as (
|
||||
wireframe: T,
|
||||
children: serializedNodeWithId[]
|
||||
) => serializedNodeWithId | null
|
||||
case 'image':
|
||||
return makeImageElement as unknown as (
|
||||
wireframe: T,
|
||||
children: serializedNodeWithId[]
|
||||
) => serializedNodeWithId | null
|
||||
case 'rectangle':
|
||||
return makeRectangleElement as unknown as (
|
||||
wireframe: T,
|
||||
children: serializedNodeWithId[]
|
||||
) => serializedNodeWithId | null
|
||||
case 'div':
|
||||
return makeDivElement as unknown as (
|
||||
wireframe: T,
|
||||
children: serializedNodeWithId[]
|
||||
) => serializedNodeWithId | null
|
||||
}
|
||||
}
|
||||
|
||||
function convertWireframesFor(wireframes: wireframe[] | undefined): serializedNodeWithId[] {
|
||||
if (!wireframes) {
|
||||
return []
|
||||
}
|
||||
|
||||
return wireframes.reduce((acc, wireframe) => {
|
||||
const children = convertWireframesFor(wireframe.childWireframes)
|
||||
const converter = chooseConverter(wireframe)
|
||||
if (!converter) {
|
||||
console.error(`No converter for wireframe type ${wireframe.type}`)
|
||||
return acc
|
||||
}
|
||||
const convertedEl = converter(wireframe, children)
|
||||
if (convertedEl !== null) {
|
||||
acc.push(convertedEl)
|
||||
}
|
||||
return acc
|
||||
}, [] as serializedNodeWithId[])
|
||||
}
|
||||
|
||||
export const makeFullEvent = (
|
||||
mobileEvent: MobileFullSnapshotEvent & {
|
||||
timestamp: number
|
||||
delay?: number
|
||||
}
|
||||
): fullSnapshotEvent & {
|
||||
timestamp: number
|
||||
delay?: number
|
||||
} => {
|
||||
if (!('wireframes' in mobileEvent.data)) {
|
||||
return mobileEvent as unknown as fullSnapshotEvent & {
|
||||
timestamp: number
|
||||
delay?: number
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: EventType.FullSnapshot,
|
||||
timestamp: mobileEvent.timestamp,
|
||||
data: {
|
||||
node: {
|
||||
type: NodeType.Document,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.DocumentType,
|
||||
name: 'html',
|
||||
publicId: '',
|
||||
systemId: '',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'html',
|
||||
attributes: {},
|
||||
id: 3,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'head',
|
||||
attributes: {},
|
||||
id: 4,
|
||||
childNodes: [],
|
||||
},
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'body',
|
||||
attributes: {},
|
||||
id: 5,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {},
|
||||
id: idSequence.next().value,
|
||||
childNodes: convertWireframesFor(mobileEvent.data.wireframes),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
id: 1,
|
||||
},
|
||||
initialOffset: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
95
ee/frontend/mobile-replay/wireframeStyle.ts
Normal file
95
ee/frontend/mobile-replay/wireframeStyle.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { MobileStyles, wireframe } from './mobile.types'
|
||||
|
||||
function ensureUnit(value: string | number): string {
|
||||
return typeof value === 'number' ? `${value}px` : value.replace(/px$/g, '') + 'px'
|
||||
}
|
||||
|
||||
function makeBorderStyles(wireframe: wireframe): string {
|
||||
let styles = ''
|
||||
|
||||
if (wireframe.style?.borderWidth) {
|
||||
const borderWidth = ensureUnit(wireframe.style.borderWidth)
|
||||
styles += `border-width: ${borderWidth};`
|
||||
}
|
||||
if (wireframe.style?.borderRadius) {
|
||||
const borderRadius = ensureUnit(wireframe.style.borderRadius)
|
||||
styles += `border-radius: ${borderRadius};`
|
||||
}
|
||||
if (wireframe.style?.borderColor) {
|
||||
styles += `border-color: ${wireframe.style.borderColor};`
|
||||
}
|
||||
|
||||
if (styles.length > 0) {
|
||||
styles += `border-style: solid;`
|
||||
}
|
||||
|
||||
return styles
|
||||
}
|
||||
|
||||
export function makeSvgBorder(style: MobileStyles | undefined): Record<string, string> {
|
||||
const svgBorderStyles: Record<string, string> = {}
|
||||
|
||||
if (style?.borderWidth) {
|
||||
svgBorderStyles['stroke-width'] = style.borderWidth.toString()
|
||||
}
|
||||
if (style?.borderColor) {
|
||||
svgBorderStyles.stroke = style.borderColor
|
||||
}
|
||||
if (style?.borderRadius) {
|
||||
svgBorderStyles.rx = ensureUnit(style.borderRadius)
|
||||
}
|
||||
|
||||
return svgBorderStyles
|
||||
}
|
||||
|
||||
export function makePositionStyles(wireframe: wireframe): string {
|
||||
let styles = ''
|
||||
if (wireframe.width) {
|
||||
styles += `width: ${ensureUnit(wireframe.width)};`
|
||||
}
|
||||
if (wireframe.height) {
|
||||
styles += `height: ${ensureUnit(wireframe.height)};`
|
||||
}
|
||||
if (wireframe.x || wireframe.y) {
|
||||
styles += `position: absolute;`
|
||||
if (wireframe.x) {
|
||||
styles += `left: ${ensureUnit(wireframe.x)};`
|
||||
}
|
||||
if (wireframe.y) {
|
||||
styles += `top: ${ensureUnit(wireframe.y)};`
|
||||
}
|
||||
}
|
||||
return styles
|
||||
}
|
||||
|
||||
function makeLayoutStyles(wireframe: wireframe): string {
|
||||
let styles = ''
|
||||
if (wireframe.style?.verticalAlign) {
|
||||
styles += `align-items: ${
|
||||
{ top: 'flex-start', center: 'center', bottom: 'flex-end' }[wireframe.style.verticalAlign]
|
||||
};`
|
||||
}
|
||||
if (wireframe.style?.horizontalAlign) {
|
||||
styles += `justify-content: ${
|
||||
{ left: 'flex-start', center: 'center', right: 'flex-end' }[wireframe.style.horizontalAlign]
|
||||
};`
|
||||
}
|
||||
if (styles.length) {
|
||||
styles += `display: flex;`
|
||||
}
|
||||
return styles
|
||||
}
|
||||
|
||||
export function makeStylesString(wireframe: wireframe): string {
|
||||
let styles = ''
|
||||
if (wireframe.style?.color) {
|
||||
styles += `color: ${wireframe.style.color};`
|
||||
}
|
||||
if (wireframe.style?.backgroundColor) {
|
||||
styles += `background-color: ${wireframe.style.backgroundColor};`
|
||||
}
|
||||
styles += makeBorderStyles(wireframe)
|
||||
styles += makePositionStyles(wireframe)
|
||||
styles += makeLayoutStyles(wireframe)
|
||||
return styles
|
||||
}
|
@ -1,7 +1,14 @@
|
||||
import { PostHogEE } from './types'
|
||||
|
||||
const posthogEE: PostHogEE = {
|
||||
enabled: false,
|
||||
export default async (): Promise<PostHogEE> => {
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
return import('../../../ee/frontend/exports')
|
||||
.then((ee) => {
|
||||
return ee.default()
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
enabled: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default posthogEE
|
||||
|
@ -1,5 +1,12 @@
|
||||
// NOTE: All exported items from the EE module _must_ be optionally defined to ensure we work well with FOSS
|
||||
|
||||
import { eventWithTime } from '@rrweb/types'
|
||||
|
||||
export type PostHogEE = {
|
||||
enabled: boolean
|
||||
myTestCode?: () => void
|
||||
mobileReplay?: {
|
||||
// defined as unknown while the mobileEventWithTime type is in the ee folder
|
||||
transformEventToWeb(x: unknown): eventWithTime | null
|
||||
transformToWeb(x: unknown[]): eventWithTime[]
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,24 @@ import fs from 'fs'
|
||||
const eeFolderExists = fs.existsSync('ee/frontend/exports.ts')
|
||||
export const ifEeIt = eeFolderExists ? it : it.skip
|
||||
export const ifFossIt = !eeFolderExists ? it : it.skip
|
||||
export const ifEeDescribe = eeFolderExists ? describe : describe.skip
|
||||
export const ifFossDescribe = !eeFolderExists ? describe : describe.skip
|
||||
|
||||
import posthogEE from '@posthog/ee/exports'
|
||||
|
||||
import { PostHogEE } from '../../@posthog/ee/types'
|
||||
|
||||
describe('ee importing', () => {
|
||||
let posthogEEModule: PostHogEE
|
||||
|
||||
beforeEach(async () => {
|
||||
posthogEEModule = await posthogEE()
|
||||
})
|
||||
ifEeIt('should import actual ee code', () => {
|
||||
expect(posthogEE.enabled).toBe(true)
|
||||
expect(posthogEEModule.enabled).toBe(true)
|
||||
})
|
||||
|
||||
ifFossIt('should import actual ee code', () => {
|
||||
expect(posthogEE.enabled).toBe(false)
|
||||
expect(posthogEEModule.enabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,8 @@
|
||||
export async function formatSource(filename: string, source: string): Promise<string> {
|
||||
// Lazy-load prettier, as it's pretty big and its only use is formatting app source code
|
||||
// @ts-expect-error
|
||||
const prettier = (await import('prettier/standalone')).default
|
||||
// @ts-expect-error
|
||||
const parserTypeScript = (await import('prettier/parser-typescript')).default
|
||||
|
||||
if (filename.endsWith('.json')) {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { eventWithTime } from '@rrweb/types'
|
||||
import { prepareRecordingSnapshots } from 'scenes/session-recordings/player/sessionRecordingDataLogic'
|
||||
|
||||
import { RecordingSnapshot } from '~/types'
|
||||
|
||||
const lineOne =
|
||||
@ -7,6 +10,23 @@ const lineTwo =
|
||||
|
||||
export const snapshotsAsJSONLines = (): string => `${lineOne}\n${lineTwo}\n`
|
||||
|
||||
export const convertSnapshotsByWindowId = (snapshotsByWindowId: {
|
||||
[key: string]: eventWithTime[]
|
||||
}): RecordingSnapshot[] =>
|
||||
Object.entries(snapshotsByWindowId).flatMap(([windowId, snapshots]) => {
|
||||
return snapshots.map((snapshot) => ({
|
||||
...snapshot,
|
||||
windowId,
|
||||
}))
|
||||
})
|
||||
|
||||
export const convertSnapshotsResponse = (
|
||||
snapshotsByWindowId: { [key: string]: eventWithTime[] },
|
||||
existingSnapshots?: RecordingSnapshot[]
|
||||
): RecordingSnapshot[] => {
|
||||
return prepareRecordingSnapshots(convertSnapshotsByWindowId(snapshotsByWindowId), existingSnapshots)
|
||||
}
|
||||
|
||||
export const sortedRecordingSnapshots = (): { snapshot_data_by_window_id: Record<string, RecordingSnapshot[]> } => {
|
||||
const sortedRecordingSnapshotsJson = { snapshot_data_by_window_id: {} }
|
||||
|
||||
|
@ -21,7 +21,12 @@ import { PlayerInspectorListItem } from './components/PlayerInspectorListItem'
|
||||
import { playerInspectorLogic } from './playerInspectorLogic'
|
||||
|
||||
function isLocalhost(url: string | null | undefined): boolean {
|
||||
return !!url && ['localhost', '127.0.0.1'].includes(new URL(url).hostname)
|
||||
try {
|
||||
return !!url && ['localhost', '127.0.0.1'].includes(new URL(url).hostname)
|
||||
} catch (e) {
|
||||
// for e.g. mobile doesn't have a URL, so we can swallow this and move on
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function EmptyNetworkTab({
|
||||
|
@ -1,8 +1,8 @@
|
||||
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 {
|
||||
convertSnapshotsByWindowId,
|
||||
prepareRecordingSnapshots,
|
||||
sessionRecordingDataLogic,
|
||||
} from 'scenes/session-recordings/player/sessionRecordingDataLogic'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import posthogEE from '@posthog/ee/exports'
|
||||
import { EventType, eventWithTime } from '@rrweb/types'
|
||||
import { captureException } from '@sentry/react'
|
||||
import { actions, connect, defaults, kea, key, listeners, path, props, reducers, selectors } from 'kea'
|
||||
@ -29,22 +30,35 @@ import {
|
||||
SessionRecordingUsageType,
|
||||
} from '~/types'
|
||||
|
||||
import { PostHogEE } from '../../../../@posthog/ee/types'
|
||||
import type { sessionRecordingDataLogicType } from './sessionRecordingDataLogicType'
|
||||
import { createSegments, mapSnapshotsToWindowId } from './utils/segmenter'
|
||||
|
||||
const IS_TEST_MODE = process.env.NODE_ENV === 'test'
|
||||
const BUFFER_MS = 60000 // +- before and after start and end of a recording to query for.
|
||||
|
||||
const parseEncodedSnapshots = (items: (EncodedRecordingSnapshot | string)[], sessionId: string): RecordingSnapshot[] =>
|
||||
items.flatMap((l) => {
|
||||
let postHogEEModule: PostHogEE
|
||||
|
||||
const parseEncodedSnapshots = async (
|
||||
items: (EncodedRecordingSnapshot | string)[],
|
||||
sessionId: string
|
||||
): Promise<RecordingSnapshot[]> => {
|
||||
if (!postHogEEModule) {
|
||||
postHogEEModule = await posthogEE()
|
||||
}
|
||||
return items.flatMap((l) => {
|
||||
try {
|
||||
const snapshotLine = typeof l === 'string' ? (JSON.parse(l) as EncodedRecordingSnapshot) : l
|
||||
const snapshotData = snapshotLine['data']
|
||||
|
||||
return snapshotData.map((d: any) => ({
|
||||
windowId: snapshotLine['window_id'],
|
||||
...d,
|
||||
}))
|
||||
// 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)
|
||||
return {
|
||||
windowId: snapshotLine['window_id'],
|
||||
...(snap || (d as eventWithTime)),
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
posthog.capture('session recording had unparseable line', {
|
||||
sessionId,
|
||||
@ -54,6 +68,7 @@ const parseEncodedSnapshots = (items: (EncodedRecordingSnapshot | string)[], ses
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getHrefFromSnapshot = (snapshot: RecordingSnapshot): string | undefined => {
|
||||
return (snapshot.data as any)?.href || (snapshot.data as any)?.payload?.href
|
||||
@ -65,7 +80,7 @@ export const prepareRecordingSnapshots = (
|
||||
): RecordingSnapshot[] => {
|
||||
const seenHashes: Record<string, (RecordingSnapshot | string)[]> = {}
|
||||
|
||||
const prepared = (newSnapshots || [])
|
||||
return (newSnapshots || [])
|
||||
.concat(existingSnapshots ? existingSnapshots ?? [] : [])
|
||||
.filter((snapshot) => {
|
||||
// For a multitude of reasons, there can be duplicate snapshots in the same recording.
|
||||
@ -89,27 +104,6 @@ export const prepareRecordingSnapshots = (
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
|
||||
return prepared
|
||||
}
|
||||
|
||||
export const convertSnapshotsByWindowId = (snapshotsByWindowId: {
|
||||
[key: string]: eventWithTime[]
|
||||
}): RecordingSnapshot[] => {
|
||||
return Object.entries(snapshotsByWindowId).flatMap(([windowId, snapshots]) => {
|
||||
return snapshots.map((snapshot) => ({
|
||||
...snapshot,
|
||||
windowId,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// Until we change the API to return a simple list of snapshots, we need to convert this ourselves
|
||||
export const convertSnapshotsResponse = (
|
||||
snapshotsByWindowId: { [key: string]: eventWithTime[] },
|
||||
existingSnapshots?: RecordingSnapshot[]
|
||||
): RecordingSnapshot[] => {
|
||||
return prepareRecordingSnapshots(convertSnapshotsByWindowId(snapshotsByWindowId), existingSnapshots)
|
||||
}
|
||||
|
||||
const generateRecordingReportDurations = (
|
||||
@ -346,8 +340,9 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
|
||||
props.sessionRecordingId,
|
||||
source.blob_key
|
||||
)
|
||||
|
||||
data.snapshots = prepareRecordingSnapshots(
|
||||
parseEncodedSnapshots(encodedResponse, props.sessionRecordingId),
|
||||
await parseEncodedSnapshots(encodedResponse, props.sessionRecordingId),
|
||||
values.sessionPlayerSnapshotData?.snapshots ?? []
|
||||
)
|
||||
} else {
|
||||
@ -359,7 +354,7 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
|
||||
const response = await api.recordings.listSnapshots(props.sessionRecordingId, params)
|
||||
if (response.snapshots) {
|
||||
data.snapshots = prepareRecordingSnapshots(
|
||||
parseEncodedSnapshots(response.snapshots, props.sessionRecordingId),
|
||||
await parseEncodedSnapshots(response.snapshots, props.sessionRecordingId),
|
||||
values.sessionPlayerSnapshotData?.snapshots ?? []
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json'
|
||||
import { sortedRecordingSnapshots } from 'scenes/session-recordings/__mocks__/recording_snapshots'
|
||||
import {
|
||||
convertSnapshotsResponse,
|
||||
sortedRecordingSnapshots,
|
||||
} from 'scenes/session-recordings/__mocks__/recording_snapshots'
|
||||
|
||||
import { RecordingSnapshot } from '~/types'
|
||||
|
||||
import { convertSnapshotsResponse } from '../sessionRecordingDataLogic'
|
||||
import { createSegments } from './segmenter'
|
||||
|
||||
describe('segmenter', () => {
|
||||
|
@ -309,7 +309,7 @@ export async function buildOrWatch(config) {
|
||||
|
||||
if (isDev) {
|
||||
chokidar
|
||||
.watch(path.resolve(absWorkingDir, 'src'), {
|
||||
.watch([path.resolve(absWorkingDir, 'src'), path.resolve(absWorkingDir, '../ee/frontend')], {
|
||||
ignored: /.*(Type|\.test\.stories)\.[tj]sx$/,
|
||||
ignoreInitial: true,
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { Config } from 'jest'
|
||||
import fs from 'fs'
|
||||
|
||||
process.env.TZ = process.env.TZ || 'UTC'
|
||||
|
||||
@ -8,6 +9,14 @@ process.env.TZ = process.env.TZ || 'UTC'
|
||||
*/
|
||||
|
||||
const esmModules = ['query-selector-shadow-dom', 'react-syntax-highlighter', '@react-hook', '@medv']
|
||||
const eeFolderExists = fs.existsSync('ee/frontend/exports.ts')
|
||||
function rootDirectories() {
|
||||
const rootDirectories = ['<rootDir>/frontend/src']
|
||||
if (eeFolderExists) {
|
||||
rootDirectories.push('<rootDir>/ee/frontend')
|
||||
}
|
||||
return rootDirectories
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
@ -85,16 +94,16 @@ const config: Config = {
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'^.+\\.(css|less|scss|svg|png|lottie)$': '<rootDir>/test/mocks/styleMock.js',
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
'^@posthog/lemon-ui(|/.*)$': '<rootDir>/../@posthog/lemon-ui/src/$1',
|
||||
'^@posthog/apps-common(|/.*)$': '<rootDir>/../@posthog/apps-common/src/$1',
|
||||
'^@posthog/ee/exports': ['<rootDir>/../../ee/frontend/exports', '<rootDir>/../@posthog/ee/exports'],
|
||||
'^lib/(.*)$': '<rootDir>/lib/$1',
|
||||
'^scenes/(.*)$': '<rootDir>/scenes/$1',
|
||||
'^.+\\.(css|less|scss|svg|png|lottie)$': '<rootDir>/frontend/src/test/mocks/styleMock.js',
|
||||
'^~/(.*)$': '<rootDir>/frontend/src/$1',
|
||||
'^@posthog/lemon-ui(|/.*)$': '<rootDir>/frontend/@posthog/lemon-ui/src/$1',
|
||||
'^@posthog/apps-common(|/.*)$': '<rootDir>/frontend/@posthog/apps-common/src/$1',
|
||||
'^@posthog/ee/exports': ['<rootDir>/ee/frontend/exports', '<rootDir>/frontend/@posthog/ee/exports'],
|
||||
'^lib/(.*)$': '<rootDir>/frontend/src/lib/$1',
|
||||
'^scenes/(.*)$': '<rootDir>/frontend/src/scenes/$1',
|
||||
'^antd/es/(.*)$': 'antd/lib/$1',
|
||||
'^react-virtualized/dist/es/(.*)$': 'react-virtualized/dist/commonjs/$1',
|
||||
d3: '<rootDir>/../../node_modules/d3/dist/d3.min.js',
|
||||
d3: '<rootDir>/node_modules/d3/dist/d3.min.js',
|
||||
'^d3-(.*)$': `d3-$1/dist/d3-$1`,
|
||||
},
|
||||
|
||||
@ -129,22 +138,19 @@ const config: Config = {
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
rootDir: 'frontend/src',
|
||||
modulePaths: ['<rootDir>/'],
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
roots: rootDirectories(),
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: ['../../jest.setup.ts'],
|
||||
setupFiles: ['<rootDir>/jest.setup.ts'],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['../../jest.setupAfterEnv.ts', 'givens/setup', './mocks/jest.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setupAfterEnv.ts', 'givens/setup', '<rootDir>/frontend/src/mocks/jest.ts'],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
11
package.json
11
package.json
@ -63,7 +63,10 @@
|
||||
"build-storybook": "storybook build",
|
||||
"dev:migrate:postgres": "export DEBUG=1 && source env/bin/activate && python manage.py migrate",
|
||||
"dev:migrate:clickhouse": "export DEBUG=1 && source env/bin/activate && python manage.py migrate_clickhouse",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"mobile-replay:web:schema:build:json": "ts-json-schema-generator -f tsconfig.json --path 'node_modules/@rrweb/types/dist/index.d.ts' --type 'eventWithTime' --expose all --no-top-ref --out ee/frontend/mobile-replay/schema/web/rr-web-schema.json && prettier --write ee/frontend/mobile-replay/schema/web/rr-web-schema.json",
|
||||
"mobile-replay:mobile:schema:build:json": "ts-json-schema-generator -f tsconfig.json --path 'ee/frontend/mobile-replay/mobile.types.ts' --type 'mobileEventWithTime' --expose all --no-top-ref --out ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json && prettier --write ee/frontend/mobile-replay/schema/mobile/rr-mobile-schema.json",
|
||||
"mobile-replay:schema:build:json": "pnpm mobile-replay:web:schema:build:json && pnpm mobile-replay:mobile:schema:build:json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
@ -95,6 +98,7 @@
|
||||
"@types/md5": "^2.3.0",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
"@types/react-virtualized": "^9.21.23",
|
||||
"ajv": "^8.12.0",
|
||||
"antd": "^4.17.1",
|
||||
"antd-dayjs-webpack-plugin": "^1.0.6",
|
||||
"babel-preset-nano-react-app": "^0.1.0",
|
||||
@ -246,6 +250,7 @@
|
||||
"eslint-plugin-compat": "^4.2.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-jest": "^27.4.3",
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
"eslint-plugin-posthog": "link:./eslint-rules",
|
||||
@ -258,7 +263,7 @@
|
||||
"history": "^5.0.1",
|
||||
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"jest": "^29.3.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-canvas-mock": "^2.4.0",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"jest-image-snapshot": "^6.1.0",
|
||||
@ -286,7 +291,7 @@
|
||||
"stylelint-order": "^6.0.3",
|
||||
"sucrase": "^3.29.0",
|
||||
"timekeeper": "^2.2.0",
|
||||
"ts-json-schema-generator": "^1.2.0",
|
||||
"ts-json-schema-generator": "^1.4.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.9.5",
|
||||
"webpack": "^5.88.2",
|
||||
|
739
pnpm-lock.yaml
739
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -31,8 +31,10 @@
|
||||
"noUnusedParameters": true, // Report errors on unused parameters
|
||||
"experimentalDecorators": true, // Enables experimental support for ES decorators
|
||||
"noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement
|
||||
// TODO this will be deprecated in TS 5. and we have _many_ of these
|
||||
"suppressImplicitAnyIndexErrors": true, // Index objects by number
|
||||
"lib": ["dom", "es2019"]
|
||||
"lib": ["dom", "es2019"],
|
||||
"ignoreDeprecations": "5.0"
|
||||
},
|
||||
"include": ["frontend/**/*", ".storybook/**/*", "ee/frontend/**/*"],
|
||||
"exclude": ["frontend/dist/**/*"],
|
||||
|
Loading…
Reference in New Issue
Block a user