mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 09:14:46 +01:00
feat: dedupe incremental mutations for mobile replay (#19974)
* start passing context around instead of multiple parameters * start passing a result and context back from conversions * even more using the context and the results * get id sequences under control * manually run prettier * add lint staged rules for ee TS code * remove console logs * start tracking ids as they are processedD * Add a new test case and so update _all_ of the ids :/ * don't process the same add or update id more than once * refactor similar closer together * move keyboard style override into context * snapshots * remove constant * need to fangle context in case select options ever starts to change it
This commit is contained in:
parent
0dd78c59c0
commit
0fdb1e0fe3
@ -0,0 +1,210 @@
|
||||
{
|
||||
"data": {
|
||||
"adds": [
|
||||
{
|
||||
"parentId": 183891344,
|
||||
"wireframe": {
|
||||
"childWireframes": [
|
||||
{
|
||||
"childWireframes": [
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 19,
|
||||
"id": 52129787,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "left",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"text": "PostHog/posthog-ios",
|
||||
"type": "text",
|
||||
"width": 368,
|
||||
"x": 66,
|
||||
"y": 556
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 19,
|
||||
"id": 99571736,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "left",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"text": "PostHog iOS integration",
|
||||
"type": "text",
|
||||
"width": 150,
|
||||
"x": 10,
|
||||
"y": 584
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 32,
|
||||
"id": 240124529,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "center",
|
||||
"paddingBottom": 6,
|
||||
"paddingLeft": 32,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 6,
|
||||
"verticalAlign": "center"
|
||||
},
|
||||
"text": "20",
|
||||
"type": "text",
|
||||
"width": 48,
|
||||
"x": 10,
|
||||
"y": 548
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"height": 62,
|
||||
"id": 209272202,
|
||||
"style": {},
|
||||
"width": 406,
|
||||
"x": 2,
|
||||
"y": 540
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"height": 70,
|
||||
"id": 142908405,
|
||||
"style": {
|
||||
"backgroundImage": "iVBORw0KGgoAAAANSUhEUgAABDgAAAC5CAYAAADNs4/hAAAAAXNSR0IArs4c6QAAAARzQklUCAgI\nCHwIZIgAAAWeSURBVHic7dyxqh1lGIbR77cIBuzTWKUShDSChVh7ITaWFoH0Emsr78D7sBVCBFOZ\nQMo0XoAEEshvZeeZ8cR9PDywVjsfm7d+mNkzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAADA7Vn/5mjv/fHMfDszX83MgxtdBAAAADDzbGZ+npkf1lqvzo5PA8fe++uZ+XFm\n7v73bQAAAADX8npmvllr/XR0dBg49t73Z+b3mblzwWEAAAAA1/FmZj5da7286uCDkx94POIGAAAA\ncLvuzMx3RwdngeOzy20BAAAAeG+HjeLKT1T23h/OzJ9zHkEAAAAAbtq7mflorfX6nx6e/QfHvpFJ\nAAAAANe01rqyY3g7AwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgT\nOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACA\nPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAA\nAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgA\nAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyB\nAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADI\nEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAA\ngDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMA\nAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4\nAAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8\ngQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAA\nyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAA\nAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIED\nAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgT\nOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACA\nPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAA\nAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgA\nAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIOwscL/6XFQAAAADHDhvFWeD47YJD\nAAAAAN7XYaNYRw/33p/PzC/jUxYAAADg9rydmS/WWk+vOjgMF2utJzPz+NKrAAAAAK7h0VHcmDl5\ng+Nve+8vZ+b7mflkZu5dYBgAAADAkT9m5vnMPFxr/XrbYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAgCN/AW0xMqHnNQceAAAAAElFTkSuQmCC\n"
|
||||
},
|
||||
"width": 411,
|
||||
"x": 0,
|
||||
"y": 536
|
||||
}
|
||||
},
|
||||
{
|
||||
"parentId": 142908405,
|
||||
"wireframe": {
|
||||
"childWireframes": [
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 19,
|
||||
"id": 52129787,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "left",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"text": "PostHog/posthog-ios",
|
||||
"type": "text",
|
||||
"width": 368,
|
||||
"x": 66,
|
||||
"y": 556
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 19,
|
||||
"id": 99571736,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "left",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"text": "PostHog iOS integration",
|
||||
"type": "text",
|
||||
"width": 150,
|
||||
"x": 10,
|
||||
"y": 584
|
||||
},
|
||||
{
|
||||
"disabled": false,
|
||||
"height": 32,
|
||||
"id": 240124529,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "center",
|
||||
"paddingBottom": 6,
|
||||
"paddingLeft": 32,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 6,
|
||||
"verticalAlign": "center"
|
||||
},
|
||||
"text": "20",
|
||||
"type": "text",
|
||||
"width": 48,
|
||||
"x": 10,
|
||||
"y": 548
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"height": 62,
|
||||
"id": 209272202,
|
||||
"style": {},
|
||||
"width": 406,
|
||||
"x": 2,
|
||||
"y": 540
|
||||
}
|
||||
},
|
||||
{
|
||||
"parentId": 209272202,
|
||||
"wireframe": {
|
||||
"disabled": false,
|
||||
"height": 19,
|
||||
"id": 52129787,
|
||||
"style": {
|
||||
"color": "#000000",
|
||||
"fontFamily": "sans-serif",
|
||||
"fontSize": 14,
|
||||
"horizontalAlign": "left",
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 0,
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
"text": "PostHog/posthog-ios",
|
||||
"type": "text",
|
||||
"width": 368,
|
||||
"x": 66,
|
||||
"y": 556
|
||||
}
|
||||
}
|
||||
],
|
||||
"removes": [
|
||||
{
|
||||
"id": 149659273,
|
||||
"parentId": 47740111
|
||||
},
|
||||
{
|
||||
"id": 151255663,
|
||||
"parentId": 149659273
|
||||
}
|
||||
],
|
||||
"source": 0
|
||||
},
|
||||
"timestamp": 1706104140861,
|
||||
"type": 3
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ 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 { makeCustomEvent, makeFullEvent, makeIncrementalEvent, makeMetaEvent } from './transformers'
|
||||
import { makeCustomEvent, makeFullEvent, makeIncrementalEvent, makeMetaEvent } from './transformer/transformers'
|
||||
|
||||
const ajv = new Ajv({
|
||||
allowUnionTypes: true,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import posthogEE from '@posthog/ee/exports'
|
||||
import {EventType} from '@rrweb/types'
|
||||
import {ifEeDescribe} from 'lib/ee.test'
|
||||
import { EventType } from '@rrweb/types'
|
||||
import { ifEeDescribe } from 'lib/ee.test'
|
||||
|
||||
import {PostHogEE} from '../../../frontend/@posthog/ee/types'
|
||||
import {validateAgainstWebSchema, validateFromMobile} from './index'
|
||||
import { PostHogEE } from '../../../frontend/@posthog/ee/types'
|
||||
import * as incrementalSnapshotJson from './__mocks__/increment-with-child-duplication.json'
|
||||
import { validateAgainstWebSchema, validateFromMobile } from './index'
|
||||
|
||||
const unspecifiedBase64ImageURL = '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='
|
||||
const unspecifiedBase64ImageURL =
|
||||
'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='
|
||||
|
||||
const heartEyesEmojiURL =
|
||||
'data:image/png;base64,' + unspecifiedBase64ImageURL
|
||||
const heartEyesEmojiURL = 'data:image/png;base64,' + unspecifiedBase64ImageURL
|
||||
|
||||
describe('replay/transform', () => {
|
||||
describe('validation', () => {
|
||||
@ -23,7 +24,7 @@ describe('replay/transform', () => {
|
||||
|
||||
test('example of validating mobile meta event', () => {
|
||||
const validData = {
|
||||
data: {width: 1, height: 1},
|
||||
data: { width: 1, height: 1 },
|
||||
timestamp: 1,
|
||||
type: EventType.Meta,
|
||||
}
|
||||
@ -40,7 +41,7 @@ describe('replay/transform', () => {
|
||||
})
|
||||
|
||||
test('should be valid when...', () => {
|
||||
expect(validateAgainstWebSchema({data: {}, timestamp: 12345, type: 0})).toBeTruthy()
|
||||
expect(validateAgainstWebSchema({ data: {}, timestamp: 12345, type: 0 })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -54,16 +55,16 @@ describe('replay/transform', () => {
|
||||
expect(
|
||||
posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {width: 300, height: 600},
|
||||
data: { width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
data: {href: 'included when present', width: 300, height: 600},
|
||||
data: { href: 'included when present', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{type: 9999},
|
||||
{ type: 9999 },
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
@ -87,7 +88,7 @@ describe('replay/transform', () => {
|
||||
test('can ignore unknown wireframe types', () => {
|
||||
const unexpectedWireframeType = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {screen: 'App Home Page', width: 300, height: 600},
|
||||
data: { screen: 'App Home Page', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
@ -114,14 +115,14 @@ describe('replay/transform', () => {
|
||||
test('can short-circuit non-mobile full snapshot', () => {
|
||||
const allWeb = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {href: 'https://my-awesome.site', width: 300, height: 600},
|
||||
data: { href: 'https://my-awesome.site', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
data: {
|
||||
node: {the: 'payload'},
|
||||
node: { the: 'payload' },
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
@ -235,7 +236,7 @@ describe('replay/transform', () => {
|
||||
test('child wireframes are processed', () => {
|
||||
const textEvent = posthogEEModule.mobileReplay?.transformToWeb([
|
||||
{
|
||||
data: {screen: 'App Home Page', width: 300, height: 600},
|
||||
data: { screen: 'App Home Page', width: 300, height: 600 },
|
||||
timestamp: 1,
|
||||
type: 4,
|
||||
},
|
||||
@ -349,6 +350,11 @@ describe('replay/transform', () => {
|
||||
expect(textEvent).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('incremental mutations de-duplicate the tree', () => {
|
||||
const conversion = posthogEEModule.mobileReplay?.transformEventToWeb(incrementalSnapshotJson)
|
||||
expect(conversion).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('omitting x and y is equivalent to setting them to 0', () => {
|
||||
expect(
|
||||
posthogEEModule.mobileReplay?.transformToWeb([
|
||||
@ -486,7 +492,7 @@ describe('replay/transform', () => {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 30,
|
||||
style: {backgroundImage: heartEyesEmojiURL,}
|
||||
style: { backgroundImage: heartEyesEmojiURL },
|
||||
},
|
||||
{
|
||||
id: 12346,
|
||||
@ -494,7 +500,7 @@ describe('replay/transform', () => {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 30,
|
||||
style: {backgroundImage: unspecifiedBase64ImageURL,}
|
||||
style: { backgroundImage: unspecifiedBase64ImageURL },
|
||||
},
|
||||
{
|
||||
id: 12346,
|
||||
@ -502,7 +508,7 @@ describe('replay/transform', () => {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 30,
|
||||
style: {backgroundImage: unspecifiedBase64ImageURL, backgroundSize: 'cover'}
|
||||
style: { backgroundImage: unspecifiedBase64ImageURL, backgroundSize: 'cover' },
|
||||
},
|
||||
{
|
||||
id: 12346,
|
||||
@ -511,7 +517,7 @@ describe('replay/transform', () => {
|
||||
y: 0,
|
||||
height: 30,
|
||||
// should be ignored
|
||||
style: {backgroundImage: null,}
|
||||
style: { backgroundImage: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -620,7 +626,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'rating'},
|
||||
style: { bar: 'rating' },
|
||||
max: '12',
|
||||
value: '6.5',
|
||||
},
|
||||
@ -636,7 +642,7 @@ describe('replay/transform', () => {
|
||||
posthogEEModule.mobileReplay?.transformEventToWeb({
|
||||
timestamp: 1,
|
||||
type: EventType.Custom,
|
||||
data: {tag: 'keyboard', payload: {open: true, height: 150}},
|
||||
data: { tag: 'keyboard', payload: { open: true, height: 150 } },
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
@ -657,7 +663,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'rating'},
|
||||
style: { bar: 'rating' },
|
||||
max: '12',
|
||||
value: '6.5',
|
||||
},
|
||||
@ -675,7 +681,7 @@ describe('replay/transform', () => {
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: 0,
|
||||
removes: [{parentId: 54321, id: 12345}],
|
||||
removes: [{ parentId: 54321, id: 12345 }],
|
||||
},
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
@ -699,7 +705,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'rating'},
|
||||
style: { bar: 'rating' },
|
||||
max: '12',
|
||||
value: '6.5',
|
||||
},
|
||||
@ -715,7 +721,7 @@ describe('replay/transform', () => {
|
||||
posthogEEModule.mobileReplay?.transformEventToWeb({
|
||||
timestamp: 1,
|
||||
type: EventType.Custom,
|
||||
data: {tag: 'keyboard', payload: {open: false}},
|
||||
data: { tag: 'keyboard', payload: { open: false } },
|
||||
})
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
@ -989,7 +995,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'circular'},
|
||||
style: { bar: 'circular' },
|
||||
},
|
||||
{
|
||||
id: 12365,
|
||||
@ -997,7 +1003,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'horizontal'},
|
||||
style: { bar: 'horizontal' },
|
||||
},
|
||||
{
|
||||
id: 12365,
|
||||
@ -1005,7 +1011,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'horizontal'},
|
||||
style: { bar: 'horizontal' },
|
||||
value: 0.75,
|
||||
},
|
||||
{
|
||||
@ -1014,7 +1020,7 @@ describe('replay/transform', () => {
|
||||
height: 30,
|
||||
type: 'input',
|
||||
inputType: 'progress',
|
||||
style: {bar: 'horizontal'},
|
||||
style: { bar: 'horizontal' },
|
||||
value: 0.75,
|
||||
max: 2.5,
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {NodeType, serializedNodeWithId, wireframeStatusBar} from "./mobile.types";
|
||||
import {NodeType, serializedNodeWithId, wireframeStatusBar} from "../mobile.types";
|
||||
import {STATUS_BAR_ID} from "./transformers";
|
||||
import {ConversionContext, ConversionResult} from "./types";
|
||||
import {makeStylesString} from "./wireframeStyle";
|
||||
|
||||
function spacerDiv(idSequence: Generator<number>): serializedNodeWithId {
|
||||
@ -19,10 +20,10 @@ function spacerDiv(idSequence: Generator<number>): serializedNodeWithId {
|
||||
/**
|
||||
* tricky: we need to accept children because that's the interface of converters, but we don't use them
|
||||
*/
|
||||
export function makeStatusBar(wireframe: wireframeStatusBar, _children: serializedNodeWithId[], timestamp: number, idSequence: Generator<number>): serializedNodeWithId {
|
||||
const clockId = idSequence.next().value;
|
||||
export function makeStatusBar(wireframe: wireframeStatusBar, _children: serializedNodeWithId[], context: ConversionContext): ConversionResult<serializedNodeWithId> {
|
||||
const clockId = context.idSequence.next().value;
|
||||
// convert the wireframe timestamp to a date time, then get just the hour and minute of the time from that
|
||||
const clockTime = timestamp ? new Date(timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) : ""
|
||||
const clockTime = context.timestamp ? new Date(context.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) : ""
|
||||
const clock: serializedNodeWithId = {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
@ -34,12 +35,12 @@ export function makeStatusBar(wireframe: wireframeStatusBar, _children: serializ
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: clockTime,
|
||||
id: idSequence.next().value,
|
||||
id: context.idSequence.next().value,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
return {result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
@ -48,8 +49,8 @@ export function makeStatusBar(wireframe: wireframeStatusBar, _children: serializ
|
||||
},
|
||||
id: STATUS_BAR_ID,
|
||||
childNodes: [
|
||||
spacerDiv(idSequence),
|
||||
spacerDiv(context.idSequence),
|
||||
clock
|
||||
],
|
||||
}
|
||||
}, context }
|
||||
}
|
@ -9,8 +9,8 @@ import {
|
||||
mutationData,
|
||||
removedNodeMutation,
|
||||
} from '@rrweb/types'
|
||||
import {captureMessage} from '@sentry/react'
|
||||
import {isObject} from 'lib/utils'
|
||||
import { captureMessage } from '@sentry/react'
|
||||
import { isObject } from 'lib/utils'
|
||||
|
||||
import {
|
||||
attributes,
|
||||
@ -39,8 +39,9 @@ import {
|
||||
wireframeSelect,
|
||||
wireframeText,
|
||||
wireframeToggle,
|
||||
} from './mobile.types'
|
||||
import {makeStatusBar} from "./status-bar";
|
||||
} from '../mobile.types'
|
||||
import { makeStatusBar } from './status-bar'
|
||||
import { ConversionContext, ConversionResult, StyleOverride } from './types'
|
||||
import {
|
||||
makeBodyStyles,
|
||||
makeColorStyles,
|
||||
@ -50,7 +51,6 @@ import {
|
||||
makeMinimalStyles,
|
||||
makePositionStyles,
|
||||
makeStylesString,
|
||||
StyleOverride,
|
||||
} from './wireframeStyle'
|
||||
|
||||
const BACKGROUND = '#f3f4ef'
|
||||
@ -77,8 +77,7 @@ function* ids(): Generator<number> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this is shared for the lifetime of the page, so a very, very long-lived session could exhaust the ids
|
||||
const idSequence = ids()
|
||||
let globalIdSequence = ids()
|
||||
|
||||
// there are some fixed ids that we need to use for fixed elements or artificial mutations
|
||||
const DOCUMENT_ID = 1
|
||||
@ -93,6 +92,10 @@ function isKeyboardEvent(x: unknown): x is keyboardEvent {
|
||||
return isObject(x) && 'data' in x && isObject(x.data) && 'tag' in x.data && x.data.tag === 'keyboard'
|
||||
}
|
||||
|
||||
export function _isPositiveInteger(id: unknown): id is number {
|
||||
return typeof id === 'number' && id > 0 && id % 1 === 0
|
||||
}
|
||||
|
||||
export const makeCustomEvent = (
|
||||
mobileCustomEvent: (customEvent | keyboardEvent) & {
|
||||
timestamp: number
|
||||
@ -123,23 +126,28 @@ export const makeCustomEvent = (
|
||||
: '100vw',
|
||||
},
|
||||
[],
|
||||
styleOverride
|
||||
{
|
||||
timestamp: mobileCustomEvent.timestamp,
|
||||
idSequence: globalIdSequence,
|
||||
skippableNodes: new Set(),
|
||||
styleOverride,
|
||||
}
|
||||
)
|
||||
if (keyboardPlaceHolder) {
|
||||
adds.push({
|
||||
parentId: BODY_ID,
|
||||
nextId: null,
|
||||
node: keyboardPlaceHolder,
|
||||
node: keyboardPlaceHolder.result,
|
||||
})
|
||||
// mutations seem not to want a tree of nodes to add
|
||||
// so even though `keyboardPlaceholder` is a tree with content
|
||||
// we have to add the text content as well
|
||||
adds.push({
|
||||
parentId: keyboardPlaceHolder.id,
|
||||
parentId: keyboardPlaceHolder.result.id,
|
||||
nextId: null,
|
||||
node: {
|
||||
type: NodeType.Text,
|
||||
id: idSequence.next().value,
|
||||
id: globalIdSequence.next().value,
|
||||
textContent: 'keyboard',
|
||||
},
|
||||
})
|
||||
@ -181,25 +189,32 @@ export const makeMetaEvent = (
|
||||
timestamp: mobileMetaEvent.timestamp,
|
||||
})
|
||||
|
||||
export function _isPositiveInteger(id: unknown): id is number {
|
||||
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
|
||||
function makeDivElement(
|
||||
wireframe: wireframeDiv,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const _id = _isPositiveInteger(wireframe.id) ? wireframe.id : context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
'data-rrweb-id': _id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
'data-rrweb-id': _id,
|
||||
},
|
||||
id: _id,
|
||||
childNodes: children,
|
||||
},
|
||||
id: _id,
|
||||
childNodes: children,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeTextElement(wireframe: wireframeText, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeTextElement(
|
||||
wireframe: wireframeText,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
if (wireframe.type !== 'text') {
|
||||
console.error('Passed incorrect wireframe type to makeTextElement')
|
||||
return null
|
||||
@ -207,92 +222,109 @@ function makeTextElement(wireframe: wireframeText, children: serializedNodeWithI
|
||||
|
||||
// because we might have to style the text, we always wrap it in a div
|
||||
// and apply styles to that
|
||||
const id = idSequence.next().value
|
||||
const id = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: wireframe.text,
|
||||
// since the text node is wrapped, we assign it a synthetic id
|
||||
id: id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;',
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: wireframe.text,
|
||||
// since the text node is wrapped, we assign it a synthetic id
|
||||
id: id,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeWebViewElement(wireframe: wireframe, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeWebViewElement(
|
||||
wireframe: wireframe,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const labelledWireframe: wireframePlaceholder = { ...wireframe } as wireframePlaceholder
|
||||
if ('url' in wireframe) {
|
||||
labelledWireframe.label = wireframe.url
|
||||
}
|
||||
|
||||
return makePlaceholderElement(labelledWireframe, children)
|
||||
return makePlaceholderElement(labelledWireframe, children, context)
|
||||
}
|
||||
|
||||
function makePlaceholderElement(
|
||||
wireframe: wireframe,
|
||||
children: serializedNodeWithId[],
|
||||
styleOverride?: StyleOverride
|
||||
): serializedNodeWithId | null {
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const txt = 'label' in wireframe && wireframe.label ? wireframe.label : wireframe.type || 'PLACEHOLDER'
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe, {
|
||||
verticalAlign: 'center',
|
||||
horizontalAlign: 'center',
|
||||
backgroundColor: wireframe.style?.backgroundColor || BACKGROUND,
|
||||
color: wireframe.style?.color || FOREGROUND,
|
||||
...styleOverride,
|
||||
}),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
// since the text node is wrapped, we assign it a synthetic id
|
||||
id: idSequence.next().value,
|
||||
textContent: txt,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe, {
|
||||
verticalAlign: 'center',
|
||||
horizontalAlign: 'center',
|
||||
backgroundColor: wireframe.style?.backgroundColor || BACKGROUND,
|
||||
color: wireframe.style?.color || FOREGROUND,
|
||||
...context.styleOverride,
|
||||
}),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
// since the text node is wrapped, we assign it a synthetic id
|
||||
id: context.idSequence.next().value,
|
||||
textContent: txt,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
export function dataURIOrPNG(src: string):string {
|
||||
export function dataURIOrPNG(src: string): string {
|
||||
if (!src.startsWith('data:image/')) {
|
||||
return 'data:image/png;base64,' + src
|
||||
}
|
||||
return src;
|
||||
return src
|
||||
}
|
||||
|
||||
function makeImageElement(wireframe: wireframeImage, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeImageElement(
|
||||
wireframe: wireframeImage,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
if (!wireframe.base64) {
|
||||
return makePlaceholderElement(wireframe, children)
|
||||
return makePlaceholderElement(wireframe, children, context)
|
||||
}
|
||||
const src = dataURIOrPNG(wireframe.base64);
|
||||
const src = dataURIOrPNG(wireframe.base64)
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'img',
|
||||
attributes: {
|
||||
src: src,
|
||||
width: wireframe.width,
|
||||
height: wireframe.height,
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'img',
|
||||
attributes: {
|
||||
src: src,
|
||||
width: wireframe.width,
|
||||
height: wireframe.height,
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,7 +386,11 @@ function inputAttributes<T extends wireframeInputComponent>(wireframe: T): attri
|
||||
}
|
||||
}
|
||||
|
||||
function makeButtonElement(wireframe: wireframeButton, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeButtonElement(
|
||||
wireframe: wireframeButton,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const buttonText: textNode | null = wireframe.value
|
||||
? {
|
||||
type: NodeType.Text,
|
||||
@ -363,44 +399,68 @@ function makeButtonElement(wireframe: wireframeButton, children: serializedNodeW
|
||||
: null
|
||||
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'button',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: buttonText ? [{ ...buttonText, id: idSequence.next().value }, ...children] : children,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSelectOptionElement(option: string, selected: boolean): serializedNodeWithId {
|
||||
const optionId = idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'option',
|
||||
attributes: {
|
||||
...(selected ? { selected: selected } : {}),
|
||||
'data-rrweb-id': optionId,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'button',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: buttonText ? [{ ...buttonText, id: context.idSequence.next().value }, ...children] : children,
|
||||
},
|
||||
id: optionId,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: option,
|
||||
id: idSequence.next().value,
|
||||
},
|
||||
],
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSelectElement(wireframe: wireframeSelect, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeSelectOptionElement(
|
||||
option: string,
|
||||
selected: boolean,
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> {
|
||||
const optionId = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'select',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
...(wireframe.options?.map((option) => makeSelectOptionElement(option, wireframe.value === option)) || []),
|
||||
...children,
|
||||
],
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'option',
|
||||
attributes: {
|
||||
...(selected ? { selected: selected } : {}),
|
||||
'data-rrweb-id': optionId,
|
||||
},
|
||||
id: optionId,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: option,
|
||||
id: context.idSequence.next().value,
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSelectElement(
|
||||
wireframe: wireframeSelect,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const selectOptions: serializedNodeWithId[] = []
|
||||
if (wireframe.options) {
|
||||
let optionContext = context
|
||||
for (let i = 0; i < wireframe.options.length; i++) {
|
||||
const option = wireframe.options[i]
|
||||
const conversion = makeSelectOptionElement(option, wireframe.value === option, optionContext)
|
||||
selectOptions.push(conversion.result)
|
||||
optionContext = conversion.context
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'select',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: [...selectOptions, ...children],
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,25 +482,29 @@ function groupRadioButtons(children: serializedNodeWithId[], radioGroupName: str
|
||||
|
||||
function makeRadioGroupElement(
|
||||
wireframe: wireframeRadioGroup,
|
||||
children: serializedNodeWithId[]
|
||||
): serializedNodeWithId | null {
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
const radioGroupName = 'radio_group_' + wireframe.id
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: groupRadioButtons(children, radioGroupName),
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: groupRadioButtons(children, radioGroupName),
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeStar(title: string, path: string): serializedNodeWithId {
|
||||
const svgId = idSequence.next().value
|
||||
const titleId = idSequence.next().value
|
||||
const pathId = idSequence.next().value
|
||||
function makeStar(title: string, path: string, context: ConversionContext): serializedNodeWithId {
|
||||
const svgId = context.idSequence.next().value
|
||||
const titleId = context.idSequence.next().value
|
||||
const pathId = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'svg',
|
||||
@ -465,7 +529,7 @@ function makeStar(title: string, path: string): serializedNodeWithId {
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: title,
|
||||
id: idSequence.next().value,
|
||||
id: context.idSequence.next().value,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -484,33 +548,40 @@ function makeStar(title: string, path: string): serializedNodeWithId {
|
||||
}
|
||||
}
|
||||
|
||||
function filledStar(): serializedNodeWithId {
|
||||
function filledStar(context: ConversionContext): serializedNodeWithId {
|
||||
return makeStar(
|
||||
'filled star',
|
||||
'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z'
|
||||
'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z',
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
function halfStar(): serializedNodeWithId {
|
||||
function halfStar(context: ConversionContext): serializedNodeWithId {
|
||||
return makeStar(
|
||||
'half-filled star',
|
||||
'M12,15.4V6.1L13.71,10.13L18.09,10.5L14.77,13.39L15.76,17.67M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z'
|
||||
'M12,15.4V6.1L13.71,10.13L18.09,10.5L14.77,13.39L15.76,17.67M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
function emptyStar(): serializedNodeWithId {
|
||||
function emptyStar(context: ConversionContext): serializedNodeWithId {
|
||||
return makeStar(
|
||||
'empty star',
|
||||
'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z'
|
||||
'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
function makeRatingBar(wireframe: wireframeProgress, children: serializedNodeWithId[]): serializedNodeWithId | null {
|
||||
function makeRatingBar(
|
||||
wireframe: wireframeProgress,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
// max is the number of stars... and value is the number of stars to fill
|
||||
|
||||
// deliberate double equals, because we want to allow null and undefined
|
||||
if (wireframe.value == null || wireframe.max == null) {
|
||||
return makePlaceholderElement(wireframe, children)
|
||||
return makePlaceholderElement(wireframe, children, context)
|
||||
}
|
||||
|
||||
const numberOfFilledStars = Math.floor(wireframe.value)
|
||||
@ -519,15 +590,15 @@ function makeRatingBar(wireframe: wireframeProgress, children: serializedNodeWit
|
||||
|
||||
const filledStars = Array(numberOfFilledStars)
|
||||
.fill(undefined)
|
||||
.map(() => filledStar())
|
||||
.map(() => filledStar(context))
|
||||
const halfStars = Array(numberOfHalfStars)
|
||||
.fill(undefined)
|
||||
.map(() => halfStar())
|
||||
.map(() => halfStar(context))
|
||||
const emptyStars = Array(numberOfEmptyStars)
|
||||
.fill(undefined)
|
||||
.map(() => emptyStar())
|
||||
.map(() => emptyStar(context))
|
||||
|
||||
const ratingBarId = idSequence.next().value
|
||||
const ratingBarId = context.idSequence.next().value
|
||||
const ratingBar = {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
@ -542,21 +613,25 @@ function makeRatingBar(wireframe: wireframeProgress, children: serializedNodeWit
|
||||
} as serializedNodeWithId
|
||||
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [ratingBar, ...children],
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [ratingBar, ...children],
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeProgressElement(
|
||||
wireframe: wireframeProgress,
|
||||
children: serializedNodeWithId[]
|
||||
): serializedNodeWithId | null {
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
if (wireframe.style?.bar === 'circular') {
|
||||
// value needs to be expressed as a number between 0 and 100
|
||||
const max = wireframe.max || 1
|
||||
@ -583,61 +658,67 @@ function makeProgressElement(
|
||||
attributes: {
|
||||
type: 'text/css',
|
||||
},
|
||||
id: idSequence.next().value,
|
||||
id: context.idSequence.next().value,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Text,
|
||||
textContent: `@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`,
|
||||
id: idSequence.next().value,
|
||||
id: context.idSequence.next().value,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const wrappingDivId = idSequence.next().value
|
||||
const wrappingDivId = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeMinimalStyles(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// with no provided value we render a spinner
|
||||
style: _isPositiveInteger(value)
|
||||
? makeDeterminateProgressStyles(wireframe, styleOverride)
|
||||
: makeIndeterminateProgressStyles(wireframe, styleOverride),
|
||||
'data-rrweb-id': wrappingDivId,
|
||||
},
|
||||
id: wrappingDivId,
|
||||
childNodes: stylingChildren,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeMinimalStyles(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// with no provided value we render a spinner
|
||||
style: _isPositiveInteger(value)
|
||||
? makeDeterminateProgressStyles(wireframe, styleOverride)
|
||||
: makeIndeterminateProgressStyles(wireframe, styleOverride),
|
||||
'data-rrweb-id': wrappingDivId,
|
||||
},
|
||||
id: wrappingDivId,
|
||||
childNodes: stylingChildren,
|
||||
},
|
||||
...children,
|
||||
],
|
||||
},
|
||||
context,
|
||||
}
|
||||
} else if (wireframe.style?.bar === 'rating') {
|
||||
return makeRatingBar(wireframe, children)
|
||||
return makeRatingBar(wireframe, children, context)
|
||||
} else {
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'progress',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'progress',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeToggleParts(wireframe: wireframeToggle): serializedNodeWithId[] {
|
||||
function makeToggleParts(wireframe: wireframeToggle, context: ConversionContext): serializedNodeWithId[] {
|
||||
const togglePosition = wireframe.checked ? 'right' : 'left'
|
||||
const defaultColor = wireframe.checked ? '#1d4aff' : BACKGROUND
|
||||
const sliderPartId = idSequence.next().value
|
||||
const handlePartId = idSequence.next().value
|
||||
const sliderPartId = context.idSequence.next().value
|
||||
const handlePartId = context.idSequence.next().value
|
||||
return [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
@ -670,88 +751,106 @@ function makeToggleParts(wireframe: wireframeToggle): serializedNodeWithId[] {
|
||||
]
|
||||
}
|
||||
|
||||
function makeToggleElement(wireframe: wireframeToggle): (elementNode & { id: number }) | null {
|
||||
function makeToggleElement(
|
||||
wireframe: wireframeToggle,
|
||||
context: ConversionContext
|
||||
): ConversionResult<
|
||||
elementNode & {
|
||||
id: number
|
||||
}
|
||||
> | null {
|
||||
const isLabelled = 'label' in wireframe
|
||||
const wrappingDivId = idSequence.next().value
|
||||
const wrappingDivId = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// if labelled take up available space, otherwise use provided positioning
|
||||
style: isLabelled ? `height:100%;flex:1` : makePositionStyles(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// relative position, fills parent
|
||||
style: 'position:relative;width:100%;height:100%;',
|
||||
'data-rrweb-id': wrappingDivId,
|
||||
},
|
||||
id: wrappingDivId,
|
||||
childNodes: makeToggleParts(wireframe),
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// if labelled take up available space, otherwise use provided positioning
|
||||
style: isLabelled ? `height:100%;flex:1` : makePositionStyles(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
],
|
||||
id: wireframe.id,
|
||||
childNodes: [
|
||||
{
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
// relative position, fills parent
|
||||
style: 'position:relative;width:100%;height:100%;',
|
||||
'data-rrweb-id': wrappingDivId,
|
||||
},
|
||||
id: wrappingDivId,
|
||||
childNodes: makeToggleParts(wireframe, context),
|
||||
},
|
||||
],
|
||||
},
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeLabelledInput(
|
||||
wireframe: wireframeCheckBox | wireframeRadio | wireframeToggle,
|
||||
theInputElement: serializedNodeWithId
|
||||
): serializedNodeWithId {
|
||||
theInputElement: serializedNodeWithId,
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> {
|
||||
const theLabel: serializedNodeWithId = {
|
||||
type: NodeType.Text,
|
||||
textContent: wireframe.label || '',
|
||||
id: idSequence.next().value,
|
||||
id: context.idSequence.next().value,
|
||||
}
|
||||
|
||||
const orderedChildren = wireframe.inputType === 'toggle' ? [theLabel, theInputElement] : [theInputElement, theLabel]
|
||||
|
||||
const labelId = idSequence.next().value
|
||||
const labelId = context.idSequence.next().value
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'label',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': labelId,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'label',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': labelId,
|
||||
},
|
||||
id: labelId,
|
||||
childNodes: orderedChildren,
|
||||
},
|
||||
id: labelId,
|
||||
childNodes: orderedChildren,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function makeInputElement(
|
||||
wireframe: wireframeInputComponent,
|
||||
children: serializedNodeWithId[]
|
||||
): serializedNodeWithId | null {
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
if (!wireframe.inputType) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (wireframe.inputType === 'button') {
|
||||
return makeButtonElement(wireframe, children)
|
||||
return makeButtonElement(wireframe, children, context)
|
||||
}
|
||||
|
||||
if (wireframe.inputType === 'select') {
|
||||
return makeSelectElement(wireframe, children)
|
||||
return makeSelectElement(wireframe, children, context)
|
||||
}
|
||||
|
||||
if (wireframe.inputType === 'progress') {
|
||||
return makeProgressElement(wireframe, children)
|
||||
return makeProgressElement(wireframe, children, context)
|
||||
}
|
||||
|
||||
const theInputElement: serializedNodeWithId | null =
|
||||
const theInputElement: ConversionResult<serializedNodeWithId> | null =
|
||||
wireframe.inputType === 'toggle'
|
||||
? makeToggleElement(wireframe)
|
||||
? makeToggleElement(wireframe, context)
|
||||
: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'input',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'input',
|
||||
attributes: inputAttributes(wireframe),
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
},
|
||||
context,
|
||||
}
|
||||
|
||||
if (!theInputElement) {
|
||||
@ -759,45 +858,48 @@ function makeInputElement(
|
||||
}
|
||||
|
||||
if ('label' in wireframe) {
|
||||
return makeLabelledInput(wireframe, theInputElement)
|
||||
return makeLabelledInput(wireframe, theInputElement.result, theInputElement.context)
|
||||
} else {
|
||||
return {
|
||||
...theInputElement,
|
||||
attributes: {
|
||||
...theInputElement.attributes,
|
||||
// when labelled no styles are needed, when un-labelled as here - we add the styling in.
|
||||
style: makeStylesString(wireframe),
|
||||
},
|
||||
}
|
||||
// when labelled no styles are needed, when un-labelled as here - we add the styling in.
|
||||
;(theInputElement.result as elementNode).attributes.style = makeStylesString(wireframe)
|
||||
return theInputElement
|
||||
}
|
||||
}
|
||||
|
||||
function makeRectangleElement(
|
||||
wireframe: wireframeRectangle,
|
||||
children: serializedNodeWithId[]
|
||||
): serializedNodeWithId | null {
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
return {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
result: {
|
||||
type: NodeType.Element,
|
||||
tagName: 'div',
|
||||
attributes: {
|
||||
style: makeStylesString(wireframe),
|
||||
'data-rrweb-id': wireframe.id,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
},
|
||||
id: wireframe.id,
|
||||
childNodes: children,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
function chooseConverter<T extends wireframe>(
|
||||
wireframe: T
|
||||
): (wireframe: T, children: serializedNodeWithId[], timestamp?: number, idSequence?: Generator<number>) => serializedNodeWithId | null {
|
||||
): (
|
||||
wireframe: T,
|
||||
children: serializedNodeWithId[],
|
||||
context: ConversionContext
|
||||
) => ConversionResult<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: MobileNodeType = wireframe.type || 'div'
|
||||
const converterMapping: Record<
|
||||
MobileNodeType,
|
||||
(wireframe: T, children: serializedNodeWithId[]) => serializedNodeWithId | null
|
||||
(wireframe: T, children: serializedNodeWithId[]) => ConversionResult<serializedNodeWithId> | null
|
||||
> = {
|
||||
// KLUDGE: TS can't tell that the wireframe type of each function is safe based on the converter type
|
||||
text: makeTextElement as any,
|
||||
@ -815,24 +917,41 @@ function chooseConverter<T extends wireframe>(
|
||||
return converterMapping[converterType]
|
||||
}
|
||||
|
||||
function convertWireframe(wireframe: wireframe, timestamp?: number, idSequence?: Generator<number>): serializedNodeWithId | null {
|
||||
const children = convertWireframesFor(wireframe.childWireframes, timestamp)
|
||||
const converter = chooseConverter(wireframe)
|
||||
return converter?.(wireframe, children, timestamp, idSequence) || null
|
||||
}
|
||||
|
||||
function convertWireframesFor(wireframes: wireframe[] | undefined, timestamp?: number): serializedNodeWithId[] {
|
||||
if (!wireframes) {
|
||||
return []
|
||||
function convertWireframe(
|
||||
wireframe: wireframe,
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId> | null {
|
||||
if (context.skippableNodes?.has(wireframe.id)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return wireframes.reduce((acc, wireframe) => {
|
||||
const convertedEl = convertWireframe(wireframe, timestamp, idSequence)
|
||||
if (convertedEl !== null) {
|
||||
acc.push(convertedEl)
|
||||
const children = convertWireframesFor(wireframe.childWireframes, context)
|
||||
const converter = chooseConverter(wireframe)
|
||||
// every wireframe comes through this converter,
|
||||
// so to track which ones we want to skip,
|
||||
// we can add them here
|
||||
context.skippableNodes?.add(wireframe.id)
|
||||
const converted = converter?.(wireframe, children.result, children.context)
|
||||
return converted || null
|
||||
}
|
||||
|
||||
function convertWireframesFor(
|
||||
wireframes: wireframe[] | undefined,
|
||||
context: ConversionContext
|
||||
): ConversionResult<serializedNodeWithId[]> {
|
||||
if (!wireframes) {
|
||||
return { result: [], context }
|
||||
}
|
||||
|
||||
const result: serializedNodeWithId[] = []
|
||||
for (const wireframe of wireframes) {
|
||||
const converted = convertWireframe(wireframe, context)
|
||||
if (converted) {
|
||||
result.push(converted.result)
|
||||
context = converted.context
|
||||
}
|
||||
return acc
|
||||
}, [] as serializedNodeWithId[])
|
||||
}
|
||||
return { result, context }
|
||||
}
|
||||
|
||||
function isMobileIncrementalSnapshotEvent(x: unknown): x is MobileIncrementalSnapshotEvent {
|
||||
@ -854,8 +973,8 @@ function isMobileIncrementalSnapshotEvent(x: unknown): x is MobileIncrementalSna
|
||||
return hasMutationSource && (hasAddedWireframe || hasUpdatedWireframe)
|
||||
}
|
||||
|
||||
function makeIncrementalAdd(add: MobileNodeMutation): addedNodeMutation[] | null {
|
||||
const converted = convertWireframe(add.wireframe, undefined, idSequence)
|
||||
function makeIncrementalAdd(add: MobileNodeMutation, context: ConversionContext): addedNodeMutation[] | null {
|
||||
const converted = convertWireframe(add.wireframe, context)
|
||||
if (!converted) {
|
||||
return null
|
||||
}
|
||||
@ -863,7 +982,7 @@ function makeIncrementalAdd(add: MobileNodeMutation): addedNodeMutation[] | null
|
||||
const addition: addedNodeMutation = {
|
||||
parentId: add.parentId,
|
||||
nextId: null,
|
||||
node: converted,
|
||||
node: converted.result,
|
||||
}
|
||||
const adds: addedNodeMutation[] = []
|
||||
if (addition) {
|
||||
@ -964,17 +1083,28 @@ export const makeIncrementalEvent = (
|
||||
const adds: addedNodeMutation[] = []
|
||||
const removes: removedNodeMutation[] = mobileEvent.data.removes || []
|
||||
if ('adds' in mobileEvent.data && Array.isArray(mobileEvent.data.adds)) {
|
||||
const addsContext = {
|
||||
timestamp: mobileEvent.timestamp,
|
||||
idSequence: globalIdSequence,
|
||||
skippableNodes: new Set<number>(),
|
||||
}
|
||||
|
||||
mobileEvent.data.adds.forEach((add) => {
|
||||
makeIncrementalAdd(add)?.forEach((x) => adds.push(x))
|
||||
makeIncrementalAdd(add, addsContext)?.forEach((x) => adds.push(x))
|
||||
})
|
||||
}
|
||||
if ('updates' in mobileEvent.data && Array.isArray(mobileEvent.data.updates)) {
|
||||
const updatesContext = {
|
||||
timestamp: mobileEvent.timestamp,
|
||||
idSequence: globalIdSequence,
|
||||
skippableNodes: new Set<number>(),
|
||||
}
|
||||
mobileEvent.data.updates.forEach((update) => {
|
||||
const removal = makeIncrementalRemoveForUpdate(update)
|
||||
if (removal) {
|
||||
removes.push(removal)
|
||||
}
|
||||
makeIncrementalAdd(update)?.forEach((x) => adds.push(x))
|
||||
makeIncrementalAdd(update, updatesContext)?.forEach((x) => adds.push(x))
|
||||
})
|
||||
}
|
||||
|
||||
@ -1000,6 +1130,9 @@ export const makeFullEvent = (
|
||||
timestamp: number
|
||||
delay?: number
|
||||
} => {
|
||||
// we can restart the id sequence on each full snapshot
|
||||
globalIdSequence = ids()
|
||||
|
||||
if (!('wireframes' in mobileEvent.data)) {
|
||||
return mobileEvent as unknown as fullSnapshotEvent & {
|
||||
timestamp: number
|
||||
@ -1039,7 +1172,11 @@ export const makeFullEvent = (
|
||||
tagName: 'body',
|
||||
attributes: { style: makeBodyStyles(), 'data-rrweb-id': BODY_ID },
|
||||
id: BODY_ID,
|
||||
childNodes: convertWireframesFor(mobileEvent.data.wireframes, mobileEvent.timestamp) || [],
|
||||
childNodes:
|
||||
convertWireframesFor(mobileEvent.data.wireframes, {
|
||||
timestamp: mobileEvent.timestamp,
|
||||
idSequence: globalIdSequence,
|
||||
}).result || [],
|
||||
},
|
||||
],
|
||||
},
|
25
ee/frontend/mobile-replay/transformer/types.ts
Normal file
25
ee/frontend/mobile-replay/transformer/types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { MobileStyles } from '../mobile.types'
|
||||
|
||||
export interface ConversionResult<T> {
|
||||
result: T
|
||||
context: ConversionContext
|
||||
}
|
||||
|
||||
export interface ConversionContext {
|
||||
timestamp: number
|
||||
idSequence: Generator<number>
|
||||
// in some contexts we want to be able to skip nodes that have already been processed
|
||||
// for example updates are processed as a remove and then an add of the whole tree
|
||||
// this means the mobile app doesn't have to store and diff the tree
|
||||
// it can just send the whole thing over
|
||||
// but multiple nearby updates can result in the same node being present
|
||||
// in the tree multiple times
|
||||
// we track which nodes have been processed to avoid adding them multiple times
|
||||
skippableNodes?: Set<number>
|
||||
styleOverride?: StyleOverride
|
||||
}
|
||||
|
||||
// StyleOverride is defined here and not in the schema
|
||||
// because these are overrides that the transformer is allowed to make
|
||||
// not that clients are allowed to request
|
||||
export type StyleOverride = MobileStyles & { bottom?: true }
|
@ -1,10 +1,6 @@
|
||||
import { MobileStyles, wireframe, wireframeProgress } from './mobile.types'
|
||||
import { wireframe, wireframeProgress } from '../mobile.types'
|
||||
import {dataURIOrPNG} from "./transformers";
|
||||
|
||||
// StyleOverride is defined here and not in the schema
|
||||
// because these are overrides that the transformer is allowed to make
|
||||
// not that clients are allowed to request
|
||||
export type StyleOverride = MobileStyles & { bottom?: true }
|
||||
import {StyleOverride} from "./types";
|
||||
|
||||
function isNumber(candidate: unknown): candidate is number {
|
||||
return typeof candidate === 'number'
|
@ -319,6 +319,10 @@
|
||||
"eslint -c .eslintrc.js --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"ee/frontend/**/*.{js,jsx,mjs,ts,tsx}": [
|
||||
"eslint -c .eslintrc.js --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"plugin-server/**/*.{js,jsx,mjs,ts,tsx}": [
|
||||
"pnpm --dir plugin-server exec eslint --fix",
|
||||
"pnpm --dir plugin-server exec prettier --write"
|
||||
|
Loading…
Reference in New Issue
Block a user