2023-11-29 11:41:18 +01:00
|
|
|
import { eventWithTime } from '@rrweb/types'
|
2023-11-29 19:55:13 +01:00
|
|
|
import { captureException } from '@sentry/react'
|
2023-11-29 11:41:18 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-11-29 19:55:13 +01:00
|
|
|
export function transformEventToWeb(event: unknown): eventWithTime {
|
|
|
|
// the transformation needs to never break a recording itself
|
|
|
|
// so, we default to returning what we received
|
|
|
|
// replacing it only if there's a valid transformation
|
|
|
|
let result = event as eventWithTime
|
|
|
|
try {
|
|
|
|
if (couldBeEventWithTime(event)) {
|
|
|
|
const transformer = transformers[event.type]
|
|
|
|
if (transformer) {
|
|
|
|
const transformed = transformer(event)
|
|
|
|
validateAgainstWebSchema(transformed)
|
|
|
|
result = transformed
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn(`No type in event: ${JSON.stringify(event)}`)
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
captureException(e, { extra: { event } })
|
2023-11-29 11:41:18 +01:00
|
|
|
}
|
2023-11-29 19:55:13 +01:00
|
|
|
return result
|
2023-11-29 11:41:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function transformToWeb(mobileData: (eventWithTime | mobileEventWithTime)[]): eventWithTime[] {
|
|
|
|
return mobileData.reduce((acc, event) => {
|
|
|
|
const transformed = transformEventToWeb(event)
|
2023-11-29 19:55:13 +01:00
|
|
|
acc.push(transformed ? transformed : (event as eventWithTime))
|
2023-11-29 11:41:18 +01:00
|
|
|
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
|
|
|
|
console.error(webSchemaValidator.errors)
|
2023-11-29 19:55:13 +01:00
|
|
|
captureException(new Error('transformation did not match schema'), {
|
|
|
|
extra: { data, errors: webSchemaValidator.errors },
|
|
|
|
})
|
2023-11-29 11:41:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return validationResult
|
|
|
|
}
|