feat(tracker): update message schema with BatchMetadata; separate message-related responsibilities; add message size

This commit is contained in:
Alex Kaminskii 2022-08-02 16:37:30 +02:00
parent 820994b55f
commit 64d481aa4c
31 changed files with 1104 additions and 1050 deletions

View file

@ -1,6 +1,6 @@
# Special one for Batch Meta. Message id could define the version
# Depricated since tracker 3.6.0
message 80, 'BatchMeta', :replayer => false do
# Depricated since tracker 3.6.0 in favor of BatchMetadata
message 80, 'BatchMeta', :js => false, :replayer => false do
uint 'PageNo'
uint 'FirstIndex'
int 'Timestamp'
@ -98,7 +98,6 @@ message 14, 'SetNodeData' do
uint 'ID'
string 'Data'
end
# Depricated starting from 5.5.11 in favor of SetStyleData
message 15, 'SetCSSData', :js => false do
uint 'ID'
string 'Data'
@ -400,10 +399,6 @@ message 64, 'CustomIssue', :replayer => false do
string 'Name'
string 'Payload'
end
# Since 5.6.6; only for websocket (might be probably replaced with ws.close())
# Depricated
message 65, 'PageClose', :replayer => false do
end
message 66, 'AssetCache', :replayer => false, :js => false do
string 'URL'
end

View file

@ -1,31 +1,15 @@
// Auto-generated, do not edit
import type { Writer, Message }from "./types.js";
export default Message
function bindNew<C extends { new(...args: A): T }, A extends any[], T>(
Class: C & { new(...args: A): T }
): C & ((...args: A) => T) {
function _Class(...args: A) {
return new Class(...args);
}
_Class.prototype = Class.prototype;
return <C & ((...args: A) => T)>_Class;
export enum Type {
<%= $messages.select { |msg| msg.js }.map { |msg| "#{ msg.name } = #{ msg.id }," }.join "\n " %>
}
export const classes: Map<number, Function> = new Map();
<% $messages.select { |msg| msg.js }.each do |msg| %>
class _<%= msg.name %> implements Message {
readonly _id: number = <%= msg.id %>;
constructor(
<%= msg.attributes.map { |attr| "public #{attr.name.first_lower}: #{attr.type_js}" }.join ",\n " %>
) {}
encode(writer: Writer): boolean {
return writer.uint(<%= msg.id %>)<%= " &&" if msg.attributes.length() > 0 %>
<%= msg.attributes.map { |attr| "writer.#{attr.type}(this.#{attr.name.first_lower})" }.join " &&\n " %>;
}
}
export const <%= msg.name %> = bindNew(_<%= msg.name %>);
classes.set(<%= msg.id %>, <%= msg.name %>);
export type <%= msg.name %> = [
type: Type.<%= msg.name %>,
<%= msg.attributes.map { |attr| "#{attr.name.first_lower}: #{attr.type_js}," }.join "\n " %>
]
<% end %>
type Message = <%= $messages.select { |msg| msg.js }.map { |msg| "#{msg.name}" }.join " | " %>
export default Message

View file

@ -0,0 +1,15 @@
// Auto-generated, do not edit
import * as Messages from '../../common/messages.js'
export { default } from '../../common/messages.js'
<% $messages.select { |msg| msg.js }.each do |msg| %>
export function <%= msg.name %>(
<%= msg.attributes.map { |attr| "#{attr.name.first_lower}: #{attr.type_js}," }.join "\n " %>
): Messages.<%= msg.name %> {
return [
Messages.Type.<%= msg.name %>,
<%= msg.attributes.map { |attr| "#{attr.name.first_lower}," }.join "\n " %>
]
}
<% end %>

View file

@ -0,0 +1,20 @@
// Auto-generated, do not edit
import * as Messages from '../common/messages.js'
import Message from '../common/messages.js'
import PrimitiveEncoder from './PrimitiveEncoder.js'
export default class MessageEncoder extends PrimitiveEncoder {
encode(msg: Message): boolean {
switch(msg[0]) {
<% $messages.select { |msg| msg.js }.each do |msg| %>
case Messages.Type.<%= msg.name %>:
return <% if msg.attributes.size == 0 %> true <% else %> <%= msg.attributes.map.with_index { |attr, index| "this.#{attr.type}(msg[#{index+1}])" }.join " && " %> <% end %>
break
<% end %>
}
}
}

View file

@ -1,3 +1,5 @@
import Message from './messages.js';
export interface Options {
connAttemptCount?: number;
connAttemptGap?: number;
@ -8,6 +10,7 @@ type Start = {
ingestPoint: string;
pageNo: number;
timestamp: number;
url: string;
} & Options;
type Auth = {
@ -16,4 +19,4 @@ type Auth = {
beaconSizeLimit?: number;
};
export type WorkerMessageData = null | 'stop' | Start | Auth | Array<{ _id: number }>;
export type WorkerMessageData = null | 'stop' | Start | Auth | Array<Message>;

File diff suppressed because it is too large Load diff

View file

@ -1,10 +0,0 @@
export interface Writer {
uint(n: number): boolean
int(n: number): boolean
string(s: string): boolean
boolean(b: boolean): boolean
}
export interface Message {
encode(w: Writer): boolean;
}

View file

@ -1,5 +1,5 @@
import type Message from '../../common/messages.js';
import { Timestamp, Metadata, UserID } from '../../common/messages.js';
import type Message from './messages.js';
import { Timestamp, Metadata, UserID } from './messages.js';
import { timestamp, deprecationWarn } from '../utils.js';
import Nodes from './nodes.js';
import Observer from './observer/top_observer.js';
@ -13,7 +13,7 @@ import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js';
import type { Options as ObserverOptions } from './observer/top_observer.js';
import type { Options as SanitizerOptions } from './sanitizer.js';
import type { Options as LoggerOptions } from './logger.js';
import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/webworker.js';
import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/interaction.js';
// TODO: Unify and clearly describe options logic
export interface StartOptions {
@ -133,10 +133,10 @@ export default class App {
this.session.attachUpdateCallback(({ userID, metadata }) => {
if (userID != null) {
// TODO: nullable userID
this.send(new UserID(userID));
this.send(UserID(userID));
}
if (metadata != null) {
Object.entries(metadata).forEach(([key, value]) => this.send(new Metadata(key, value)));
Object.entries(metadata).forEach(([key, value]) => this.send(Metadata(key, value)));
}
});
this.localStorage = this.options.localStorage;
@ -206,7 +206,7 @@ export default class App {
}
private commit(): void {
if (this.worker && this.messages.length) {
this.messages.unshift(new Timestamp(timestamp()));
this.messages.unshift(Timestamp(timestamp()));
this.worker.postMessage(this.messages);
this.commitCallbacks.forEach((cb) => cb(this.messages));
this.messages.length = 0;
@ -358,6 +358,7 @@ export default class App {
pageNo,
ingestPoint: this.options.ingestPoint,
timestamp: startInfo.timestamp,
url: document.URL,
connAttemptCount: this.options.connAttemptCount,
connAttemptGap: this.options.connAttemptGap,
};

View file

@ -0,0 +1,334 @@
// Auto-generated, do not edit
import * as Messages from '../../common/messages.js';
export { default } from '../../common/messages.js';
export function BatchMetadata(
version: number,
pageNo: number,
firstIndex: number,
timestamp: number,
location: string,
): Messages.BatchMetadata {
return [Messages.Type.BatchMetadata, version, pageNo, firstIndex, timestamp, location];
}
export function PartitionedMessage(partNo: number, partTotal: number): Messages.PartitionedMessage {
return [Messages.Type.PartitionedMessage, partNo, partTotal];
}
export function Timestamp(timestamp: number): Messages.Timestamp {
return [Messages.Type.Timestamp, timestamp];
}
export function SetPageLocation(
url: string,
referrer: string,
navigationStart: number,
): Messages.SetPageLocation {
return [Messages.Type.SetPageLocation, url, referrer, navigationStart];
}
export function SetViewportSize(width: number, height: number): Messages.SetViewportSize {
return [Messages.Type.SetViewportSize, width, height];
}
export function SetViewportScroll(x: number, y: number): Messages.SetViewportScroll {
return [Messages.Type.SetViewportScroll, x, y];
}
export function CreateDocument(): Messages.CreateDocument {
return [Messages.Type.CreateDocument];
}
export function CreateElementNode(
id: number,
parentID: number,
index: number,
tag: string,
svg: boolean,
): Messages.CreateElementNode {
return [Messages.Type.CreateElementNode, id, parentID, index, tag, svg];
}
export function CreateTextNode(
id: number,
parentID: number,
index: number,
): Messages.CreateTextNode {
return [Messages.Type.CreateTextNode, id, parentID, index];
}
export function MoveNode(id: number, parentID: number, index: number): Messages.MoveNode {
return [Messages.Type.MoveNode, id, parentID, index];
}
export function RemoveNode(id: number): Messages.RemoveNode {
return [Messages.Type.RemoveNode, id];
}
export function SetNodeAttribute(
id: number,
name: string,
value: string,
): Messages.SetNodeAttribute {
return [Messages.Type.SetNodeAttribute, id, name, value];
}
export function RemoveNodeAttribute(id: number, name: string): Messages.RemoveNodeAttribute {
return [Messages.Type.RemoveNodeAttribute, id, name];
}
export function SetNodeData(id: number, data: string): Messages.SetNodeData {
return [Messages.Type.SetNodeData, id, data];
}
export function SetNodeScroll(id: number, x: number, y: number): Messages.SetNodeScroll {
return [Messages.Type.SetNodeScroll, id, x, y];
}
export function SetInputTarget(id: number, label: string): Messages.SetInputTarget {
return [Messages.Type.SetInputTarget, id, label];
}
export function SetInputValue(id: number, value: string, mask: number): Messages.SetInputValue {
return [Messages.Type.SetInputValue, id, value, mask];
}
export function SetInputChecked(id: number, checked: boolean): Messages.SetInputChecked {
return [Messages.Type.SetInputChecked, id, checked];
}
export function MouseMove(x: number, y: number): Messages.MouseMove {
return [Messages.Type.MouseMove, x, y];
}
export function ConsoleLog(level: string, value: string): Messages.ConsoleLog {
return [Messages.Type.ConsoleLog, level, value];
}
export function PageLoadTiming(
requestStart: number,
responseStart: number,
responseEnd: number,
domContentLoadedEventStart: number,
domContentLoadedEventEnd: number,
loadEventStart: number,
loadEventEnd: number,
firstPaint: number,
firstContentfulPaint: number,
): Messages.PageLoadTiming {
return [
Messages.Type.PageLoadTiming,
requestStart,
responseStart,
responseEnd,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
loadEventEnd,
firstPaint,
firstContentfulPaint,
];
}
export function PageRenderTiming(
speedIndex: number,
visuallyComplete: number,
timeToInteractive: number,
): Messages.PageRenderTiming {
return [Messages.Type.PageRenderTiming, speedIndex, visuallyComplete, timeToInteractive];
}
export function JSException(name: string, message: string, payload: string): Messages.JSException {
return [Messages.Type.JSException, name, message, payload];
}
export function RawCustomEvent(name: string, payload: string): Messages.RawCustomEvent {
return [Messages.Type.RawCustomEvent, name, payload];
}
export function UserID(id: string): Messages.UserID {
return [Messages.Type.UserID, id];
}
export function UserAnonymousID(id: string): Messages.UserAnonymousID {
return [Messages.Type.UserAnonymousID, id];
}
export function Metadata(key: string, value: string): Messages.Metadata {
return [Messages.Type.Metadata, key, value];
}
export function CSSInsertRule(id: number, rule: string, index: number): Messages.CSSInsertRule {
return [Messages.Type.CSSInsertRule, id, rule, index];
}
export function CSSDeleteRule(id: number, index: number): Messages.CSSDeleteRule {
return [Messages.Type.CSSDeleteRule, id, index];
}
export function Fetch(
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
): Messages.Fetch {
return [Messages.Type.Fetch, method, url, request, response, status, timestamp, duration];
}
export function Profiler(
name: string,
duration: number,
args: string,
result: string,
): Messages.Profiler {
return [Messages.Type.Profiler, name, duration, args, result];
}
export function OTable(key: string, value: string): Messages.OTable {
return [Messages.Type.OTable, key, value];
}
export function StateAction(type: string): Messages.StateAction {
return [Messages.Type.StateAction, type];
}
export function Redux(action: string, state: string, duration: number): Messages.Redux {
return [Messages.Type.Redux, action, state, duration];
}
export function Vuex(mutation: string, state: string): Messages.Vuex {
return [Messages.Type.Vuex, mutation, state];
}
export function MobX(type: string, payload: string): Messages.MobX {
return [Messages.Type.MobX, type, payload];
}
export function NgRx(action: string, state: string, duration: number): Messages.NgRx {
return [Messages.Type.NgRx, action, state, duration];
}
export function GraphQL(
operationKind: string,
operationName: string,
variables: string,
response: string,
): Messages.GraphQL {
return [Messages.Type.GraphQL, operationKind, operationName, variables, response];
}
export function PerformanceTrack(
frames: number,
ticks: number,
totalJSHeapSize: number,
usedJSHeapSize: number,
): Messages.PerformanceTrack {
return [Messages.Type.PerformanceTrack, frames, ticks, totalJSHeapSize, usedJSHeapSize];
}
export function ResourceTiming(
timestamp: number,
duration: number,
ttfb: number,
headerSize: number,
encodedBodySize: number,
decodedBodySize: number,
url: string,
initiator: string,
): Messages.ResourceTiming {
return [
Messages.Type.ResourceTiming,
timestamp,
duration,
ttfb,
headerSize,
encodedBodySize,
decodedBodySize,
url,
initiator,
];
}
export function ConnectionInformation(
downlink: number,
type: string,
): Messages.ConnectionInformation {
return [Messages.Type.ConnectionInformation, downlink, type];
}
export function SetPageVisibility(hidden: boolean): Messages.SetPageVisibility {
return [Messages.Type.SetPageVisibility, hidden];
}
export function LongTask(
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
): Messages.LongTask {
return [
Messages.Type.LongTask,
timestamp,
duration,
context,
containerType,
containerSrc,
containerId,
containerName,
];
}
export function SetNodeAttributeURLBased(
id: number,
name: string,
value: string,
baseURL: string,
): Messages.SetNodeAttributeURLBased {
return [Messages.Type.SetNodeAttributeURLBased, id, name, value, baseURL];
}
export function SetCSSDataURLBased(
id: number,
data: string,
baseURL: string,
): Messages.SetCSSDataURLBased {
return [Messages.Type.SetCSSDataURLBased, id, data, baseURL];
}
export function TechnicalInfo(type: string, value: string): Messages.TechnicalInfo {
return [Messages.Type.TechnicalInfo, type, value];
}
export function CustomIssue(name: string, payload: string): Messages.CustomIssue {
return [Messages.Type.CustomIssue, name, payload];
}
export function CSSInsertRuleURLBased(
id: number,
rule: string,
index: number,
baseURL: string,
): Messages.CSSInsertRuleURLBased {
return [Messages.Type.CSSInsertRuleURLBased, id, rule, index, baseURL];
}
export function MouseClick(
id: number,
hesitationTime: number,
label: string,
selector: string,
): Messages.MouseClick {
return [Messages.Type.MouseClick, id, hesitationTime, label, selector];
}
export function CreateIFrameDocument(frameID: number, id: number): Messages.CreateIFrameDocument {
return [Messages.Type.CreateIFrameDocument, frameID, id];
}

View file

@ -1,5 +1,5 @@
import Observer from './observer.js';
import { CreateIFrameDocument } from '../../../common/messages.js';
import { CreateIFrameDocument } from '../messages.js';
export default class IFrameObserver extends Observer {
observe(iframe: HTMLIFrameElement) {

View file

@ -8,7 +8,7 @@ import {
CreateElementNode,
MoveNode,
RemoveNode,
} from '../../../common/messages.js';
} from '../messages.js';
import App from '../index.js';
import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag } from '../guards.js';
@ -125,14 +125,14 @@ export default abstract class Observer {
name = name.substr(6);
}
if (value === null) {
this.app.send(new RemoveNodeAttribute(id, name));
this.app.send(RemoveNodeAttribute(id, name));
} else if (name === 'href') {
if (value.length > 1e5) {
value = '';
}
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
} else {
this.app.send(new SetNodeAttribute(id, name, value));
this.app.send(SetNodeAttribute(id, name, value));
}
return;
}
@ -156,26 +156,26 @@ export default abstract class Observer {
return;
}
if (value === null) {
this.app.send(new RemoveNodeAttribute(id, name));
this.app.send(RemoveNodeAttribute(id, name));
return;
}
if (name === 'style' || (name === 'href' && hasTag(node, 'LINK'))) {
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
return;
}
if (name === 'href' || value.length > 1e5) {
value = '';
}
this.app.send(new SetNodeAttribute(id, name, value));
this.app.send(SetNodeAttribute(id, name, value));
}
private sendNodeData(id: number, parentElement: Element, data: string): void {
if (hasTag(parentElement, 'STYLE') || hasTag(parentElement, 'style')) {
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref()));
return;
}
data = this.app.sanitizer.sanitize(id, data);
this.app.send(new SetNodeData(id, data));
this.app.send(SetNodeData(id, data));
}
private bindNode(node: Node): void {
@ -221,7 +221,7 @@ export default abstract class Observer {
private unbindNode(node: Node) {
const id = this.app.nodes.unregisterNode(node);
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
this.app.send(new RemoveNode(id));
this.app.send(RemoveNode(id));
}
}
@ -289,7 +289,7 @@ export default abstract class Observer {
(el as HTMLElement | SVGElement).style.height = height + 'px';
}
this.app.send(new CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
}
for (let i = 0; i < el.attributes.length; i++) {
const attr = el.attributes[i];
@ -297,13 +297,13 @@ export default abstract class Observer {
}
} else if (isTextNode(node)) {
// for text node id != 0, hence parentID !== undefined and parent is Element
this.app.send(new CreateTextNode(id, parentID as number, index));
this.app.send(CreateTextNode(id, parentID as number, index));
this.sendNodeData(id, parent as Element, node.data);
}
return true;
}
if (recentsType === RecentsType.Removed && parentID !== undefined) {
this.app.send(new MoveNode(id, parentID, index));
this.app.send(MoveNode(id, parentID, index));
}
const attr = this.attributesMap.get(id);
if (attr !== undefined) {

View file

@ -1,5 +1,5 @@
import Observer from './observer.js';
import { CreateIFrameDocument } from '../../../common/messages.js';
import { CreateIFrameDocument } from '../messages.js';
export default class ShadowRootObserver extends Observer {
observe(el: Element) {

View file

@ -4,7 +4,7 @@ import { isElementNode, hasTag } from '../guards.js';
import IFrameObserver from './iframe_observer.js';
import ShadowRootObserver from './shadow_root_observer.js';
import { CreateDocument } from '../../../common/messages.js';
import { CreateDocument } from '../messages.js';
import App from '../index.js';
import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js';
@ -95,7 +95,7 @@ export default class TopObserver extends Observer {
this.observeRoot(
window.document,
() => {
this.app.send(new CreateDocument());
this.app.send(CreateDocument());
},
window.document.documentElement,
);

View file

@ -1,5 +1,3 @@
import { UserID, UserAnonymousID, Metadata } from '../../common/messages.js';
interface SessionInfo {
sessionID: string | null;
metadata: Record<string, string>;

View file

@ -1,14 +1,8 @@
import App, { DEFAULT_INGEST_POINT } from './app/index.js';
export { default as App } from './app/index.js';
import {
UserID,
UserAnonymousID,
Metadata,
RawCustomEvent,
CustomIssue,
} from '../common/messages.js';
import * as _Messages from '../common/messages.js';
import { UserID, UserAnonymousID, RawCustomEvent, CustomIssue } from './app/messages.js';
import * as _Messages from './app/messages.js';
export const Messages = _Messages;
import Connection from './modules/connection.js';
@ -224,7 +218,7 @@ export default class API {
setUserAnonymousID(id: string): void {
if (typeof id === 'string' && this.app !== null) {
this.app.send(new UserAnonymousID(id));
this.app.send(UserAnonymousID(id));
}
}
userAnonymousID(id: string): void {
@ -252,7 +246,7 @@ export default class API {
} catch (e) {
return;
}
this.app.send(new RawCustomEvent(key, payload));
this.app.send(RawCustomEvent(key, payload));
}
}
}
@ -264,7 +258,7 @@ export default class API {
} catch (e) {
return;
}
this.app.send(new CustomIssue(key, payload));
this.app.send(CustomIssue(key, payload));
}
}

View file

@ -1,5 +1,5 @@
import App from '../app/index.js';
import { ConnectionInformation } from '../../common/messages.js';
import { ConnectionInformation } from '../app/messages.js';
export default function (app: App): void {
const connection:
@ -18,10 +18,7 @@ export default function (app: App): void {
const sendConnectionInformation = (): void =>
app.send(
new ConnectionInformation(
Math.round(connection.downlink * 1000),
connection.type || 'unknown',
),
ConnectionInformation(Math.round(connection.downlink * 1000), connection.type || 'unknown'),
);
sendConnectionInformation();
connection.addEventListener('change', sendConnectionInformation);

View file

@ -1,7 +1,7 @@
import type App from '../app/index.js';
import { hasTag } from '../app/guards.js';
import { IN_BROWSER } from '../utils.js';
import { ConsoleLog } from '../../common/messages.js';
import { ConsoleLog } from '../app/messages.js';
const printError: (e: Error) => string =
IN_BROWSER && 'InstallTrigger' in window // detect Firefox
@ -109,7 +109,7 @@ export default function (app: App, opts: Partial<Options>): void {
}
const sendConsoleLog = app.safe((level: string, args: unknown[]): void =>
app.send(new ConsoleLog(level, printf(args))),
app.send(ConsoleLog(level, printf(args))),
);
let n: number;

View file

@ -1,5 +1,5 @@
import type App from '../app/index.js';
import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../common/messages.js';
import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../app/messages.js';
import { hasTag } from '../app/guards.js';
export default function (app: App | null) {
@ -7,7 +7,7 @@ export default function (app: App | null) {
return;
}
if (!window.CSSStyleSheet) {
app.send(new TechnicalInfo('no_stylesheet_prototype_in_window', ''));
app.send(TechnicalInfo('no_stylesheet_prototype_in_window', ''));
return;
}
@ -15,8 +15,8 @@ export default function (app: App | null) {
const sendMessage =
typeof rule === 'string'
? (nodeID: number) =>
app.send(new CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref()))
: (nodeID: number) => app.send(new CSSDeleteRule(nodeID, index));
app.send(CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref()))
: (nodeID: number) => app.send(CSSDeleteRule(nodeID, index));
// TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule)
if (stylesheet.ownerNode == null) {
throw new Error('Owner Node not found');

View file

@ -1,6 +1,6 @@
import type App from '../app/index.js';
import type Message from '../../common/messages.js';
import { JSException } from '../../common/messages.js';
import type Message from '../app/messages.js';
import { JSException } from '../app/messages.js';
import ErrorStackParser from 'error-stack-parser';
export interface Options {
@ -32,7 +32,7 @@ export function getExceptionMessage(error: Error, fallbackStack: Array<StackFram
try {
stack = ErrorStackParser.parse(error);
} catch (e) {}
return new JSException(error.name, error.message, JSON.stringify(stack));
return JSException(error.name, error.message, JSON.stringify(stack));
}
export function getExceptionMessageFromEvent(
@ -47,7 +47,7 @@ export function getExceptionMessageFromEvent(
name = 'Error';
message = e.message;
}
return new JSException(name, message, JSON.stringify(getDefaultStack(e)));
return JSException(name, message, JSON.stringify(getDefaultStack(e)));
}
} else if ('PromiseRejectionEvent' in window && e instanceof PromiseRejectionEvent) {
if (e.reason instanceof Error) {
@ -59,7 +59,7 @@ export function getExceptionMessageFromEvent(
} catch (_) {
message = String(e.reason);
}
return new JSException('Unhandled Promise Rejection', message, '[]');
return JSException('Unhandled Promise Rejection', message, '[]');
}
}
return null;

View file

@ -1,10 +1,6 @@
import type App from '../app/index.js';
import { timestamp, isURL } from '../utils.js';
import {
ResourceTiming,
SetNodeAttributeURLBased,
SetNodeAttribute,
} from '../../common/messages.js';
import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../app/messages.js';
import { hasTag } from '../app/guards.js';
function resolveURL(url: string, location: Location = document.location) {
@ -26,13 +22,13 @@ const PLACEHOLDER_SRC = 'https://static.openreplay.com/tracker/placeholder.jpeg'
export default function (app: App): void {
function sendPlaceholder(id: number, node: HTMLImageElement): void {
app.send(new SetNodeAttribute(id, 'src', PLACEHOLDER_SRC));
app.send(SetNodeAttribute(id, 'src', PLACEHOLDER_SRC));
const { width, height } = node.getBoundingClientRect();
if (!node.hasAttribute('width')) {
app.send(new SetNodeAttribute(id, 'width', String(width)));
app.send(SetNodeAttribute(id, 'width', String(width)));
}
if (!node.hasAttribute('height')) {
app.send(new SetNodeAttribute(id, 'height', String(height)));
app.send(SetNodeAttribute(id, 'height', String(height)));
}
}
@ -48,18 +44,18 @@ export default function (app: App): void {
const resolvedSrc = resolveURL(src || ''); // Src type is null sometimes. - is it true?
if (naturalWidth === 0 && naturalHeight === 0) {
if (isURL(resolvedSrc)) {
app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img'));
app.send(ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img'));
}
} else if (resolvedSrc.length >= 1e5 || app.sanitizer.isMasked(id)) {
sendPlaceholder(id, this);
} else {
app.send(new SetNodeAttribute(id, 'src', resolvedSrc));
app.send(SetNodeAttribute(id, 'src', resolvedSrc));
if (srcset) {
const resolvedSrcset = srcset
.split(',')
.map((str) => resolveURL(str))
.join(',');
app.send(new SetNodeAttribute(id, 'srcset', resolvedSrcset));
app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset));
}
}
});
@ -74,11 +70,11 @@ export default function (app: App): void {
}
if (mutation.attributeName === 'src') {
const src = target.src;
app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
}
if (mutation.attributeName === 'srcset') {
const srcset = target.srcset;
app.send(new SetNodeAttribute(id, 'srcset', srcset));
app.send(SetNodeAttribute(id, 'srcset', srcset));
}
}
}

View file

@ -1,7 +1,7 @@
import type App from '../app/index.js';
import { normSpaces, IN_BROWSER, getLabelAttribute, hasOpenreplayAttribute } from '../utils.js';
import { hasTag } from '../app/guards.js';
import { SetInputTarget, SetInputValue, SetInputChecked } from '../../common/messages.js';
import { SetInputTarget, SetInputValue, SetInputChecked } from '../app/messages.js';
const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date'];
@ -97,7 +97,7 @@ export default function (app: App, opts: Partial<Options>): void {
function sendInputTarget(id: number, node: TextEditableElement): void {
const label = getInputLabel(node);
if (label !== '') {
app.send(new SetInputTarget(id, label));
app.send(SetInputTarget(id, label));
}
}
function sendInputValue(id: number, node: TextEditableElement | HTMLSelectElement): void {
@ -126,7 +126,7 @@ export default function (app: App, opts: Partial<Options>): void {
break;
}
app.send(new SetInputValue(id, value, mask));
app.send(SetInputValue(id, value, mask));
}
const inputValues: Map<number, string> = new Map();
@ -165,7 +165,7 @@ export default function (app: App, opts: Partial<Options>): void {
}
if (checked !== node.checked) {
checkableValues.set(id, node.checked);
app.send(new SetInputChecked(id, node.checked));
app.send(SetInputChecked(id, node.checked));
}
});
});
@ -191,7 +191,7 @@ export default function (app: App, opts: Partial<Options>): void {
}
if (isCheckable(node)) {
checkableValues.set(id, node.checked);
app.send(new SetInputChecked(id, node.checked));
app.send(SetInputChecked(id, node.checked));
return;
}
}),

View file

@ -1,5 +1,5 @@
import type App from '../app/index.js';
import { LongTask } from '../../common/messages.js';
import { LongTask } from '../app/messages.js';
// https://w3c.github.io/performance-timeline/#the-performanceentry-interface
interface TaskAttributionTiming extends PerformanceEntry {
@ -45,7 +45,7 @@ export default function (app: App): void {
}
app.send(
new LongTask(
LongTask(
entry.startTime + performance.timing.navigationStart,
entry.duration,
Math.max(contexts.indexOf(entry.name), 0),

View file

@ -1,7 +1,7 @@
import type App from '../app/index.js';
import { hasTag, isSVGElement } from '../app/guards.js';
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js';
import { MouseMove, MouseClick } from '../../common/messages.js';
import { MouseMove, MouseClick } from '../app/messages.js';
import { getInputLabel } from './input.js';
function _getSelector(target: Element): string {
@ -115,7 +115,7 @@ export default function (app: App): void {
const sendMouseMove = (): void => {
if (mousePositionChanged) {
app.send(new MouseMove(mousePositionX, mousePositionY));
app.send(MouseMove(mousePositionX, mousePositionY));
mousePositionChanged = false;
}
};
@ -151,7 +151,7 @@ export default function (app: App): void {
if (id !== undefined) {
sendMouseMove();
app.send(
new MouseClick(
MouseClick(
id,
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target),

View file

@ -1,6 +1,6 @@
import type App from '../app/index.js';
import { IN_BROWSER } from '../utils.js';
import { PerformanceTrack } from '../../common/messages.js';
import { PerformanceTrack } from '../app/messages.js';
type Perf = {
memory: {
@ -60,7 +60,7 @@ export default function (app: App, opts: Partial<Options>): void {
return;
}
app.send(
new PerformanceTrack(
PerformanceTrack(
frames,
ticks,
perf.memory.totalJSHeapSize || 0,

View file

@ -1,5 +1,5 @@
import type App from '../app/index.js';
import { SetViewportScroll, SetNodeScroll } from '../../common/messages.js';
import { SetViewportScroll, SetNodeScroll } from '../app/messages.js';
import { isElementNode } from '../app/guards.js';
export default function (app: App): void {
@ -8,7 +8,7 @@ export default function (app: App): void {
const sendSetViewportScroll = app.safe((): void =>
app.send(
new SetViewportScroll(
SetViewportScroll(
window.pageXOffset ||
(document.documentElement && document.documentElement.scrollLeft) ||
(document.body && document.body.scrollLeft) ||
@ -24,7 +24,7 @@ export default function (app: App): void {
const sendSetNodeScroll = app.safe((s: [number, number], node: Node): void => {
const id = app.nodes.getID(node);
if (id !== undefined) {
app.send(new SetNodeScroll(id, s[0], s[1]));
app.send(SetNodeScroll(id, s[0], s[1]));
}
});

View file

@ -1,7 +1,7 @@
import type App from '../app/index.js';
import { hasTag } from '../app/guards.js';
import { isURL } from '../utils.js';
import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../../common/messages.js';
import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.js';
// Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js
@ -109,7 +109,7 @@ export default function (app: App, opts: Partial<Options>): void {
resources[entry.name] = entry.startTime + entry.duration;
}
app.send(
new ResourceTiming(
ResourceTiming(
entry.startTime + performance.timing.navigationStart,
entry.duration,
entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0,
@ -175,7 +175,7 @@ export default function (app: App, opts: Partial<Options>): void {
loadEventEnd,
} = performance.timing;
app.send(
new PageLoadTiming(
PageLoadTiming(
requestStart - navigationStart || 0,
responseStart - navigationStart || 0,
responseEnd - navigationStart || 0,
@ -236,7 +236,7 @@ export default function (app: App, opts: Partial<Options>): void {
)
: 0;
app.send(
new PageRenderTiming(
PageRenderTiming(
speedIndex,
firstContentfulPaint > visuallyComplete ? firstContentfulPaint : visuallyComplete,
timeToInteractive,

View file

@ -1,5 +1,5 @@
import type App from '../app/index.js';
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../../common/messages.js';
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.js';
export default function (app: App): void {
let url: string, width: number, height: number;
@ -9,7 +9,7 @@ export default function (app: App): void {
const { URL } = document;
if (URL !== url) {
url = URL;
app.send(new SetPageLocation(url, document.referrer, navigationStart));
app.send(SetPageLocation(url, document.referrer, navigationStart));
navigationStart = 0;
}
});
@ -19,14 +19,14 @@ export default function (app: App): void {
if (innerWidth !== width || innerHeight !== height) {
width = innerWidth;
height = innerHeight;
app.send(new SetViewportSize(width, height));
app.send(SetViewportSize(width, height));
}
});
const sendSetPageVisibility =
document.hidden === undefined
? Function.prototype
: app.safe(() => app.send(new SetPageVisibility(document.hidden)));
: app.safe(() => app.send(SetPageVisibility(document.hidden)));
app.attachStartCallback(() => {
url = '';

View file

@ -1,33 +1,62 @@
import type Message from '../common/messages.js';
import PrimitiveWriter from './PrimitiveWriter.js';
import { BatchMeta, Timestamp } from '../common/messages.js';
import * as Messages from '../common/messages.js';
import MessageEncoder from './MessageEncoder.js';
import PrimitiveEncoder from './PrimitiveEncoder.js';
const SIZE_RESERVED = 2;
const MAX_M_SIZE = (1 << (SIZE_RESERVED * 8)) - 1;
export default class BatchWriter {
private nextIndex = 0;
private beaconSize = 2 * 1e5; // Default 200kB
private writer = new PrimitiveWriter(this.beaconSize);
private encoder = new MessageEncoder(this.beaconSize);
private readonly sizeEncoder = new PrimitiveEncoder(SIZE_RESERVED);
private isEmpty = true;
constructor(
private readonly pageNo: number,
private timestamp: number,
private url: string,
private readonly onBatch: (batch: Uint8Array) => void,
) {
this.prepare();
}
private prepare(): void {
if (!this.writer.isEmpty()) {
if (!this.encoder.isEmpty()) {
return;
}
new BatchMeta(this.pageNo, this.nextIndex, this.timestamp).encode(this.writer);
// MBTODO: move service-messages creation to webworker
const batchMetadata: Messages.BatchMetadata = [
Messages.Type.BatchMetadata,
1,
this.pageNo,
this.nextIndex,
this.timestamp,
this.url,
];
this.encoder.encode(batchMetadata);
}
private write(message: Message): boolean {
const wasWritten = message.encode(this.writer);
const e = this.encoder;
if (!e.uint(message[0]) || !e.skip(SIZE_RESERVED)) {
return false;
}
const startOffset = e.getCurrentOffset();
const wasWritten = e.encode(message);
if (wasWritten) {
const endOffset = e.getCurrentOffset();
const size = endOffset - startOffset;
if (size > MAX_M_SIZE || !this.sizeEncoder.uint(size)) {
console.warn('OpenReplay: max message size overflow.');
return false;
}
this.sizeEncoder.checkpoint(); // TODO: separate checkpoint logic to an Encoder-inherit class
e.set(this.sizeEncoder.flush(), startOffset - SIZE_RESERVED);
e.checkpoint();
this.isEmpty = false;
this.writer.checkpoint();
this.nextIndex++;
}
return wasWritten;
@ -39,21 +68,24 @@ export default class BatchWriter {
}
writeMessage(message: Message) {
if (message instanceof Timestamp) {
this.timestamp = (<any>message).timestamp;
if (message[0] === Messages.Type.Timestamp) {
this.timestamp = message[1]; // .timestamp
}
if (message[0] === Messages.Type.SetPageLocation) {
this.url = message[1]; // .url
}
while (!this.write(message)) {
this.finaliseBatch();
if (this.beaconSize === this.beaconSizeLimit) {
console.warn('OpenReplay: beacon size overflow. Skipping large message.');
this.writer.reset();
this.encoder.reset();
this.prepare();
this.isEmpty = true;
return;
}
// MBTODO: tempWriter for one message?
this.beaconSize = Math.min(this.beaconSize * 2, this.beaconSizeLimit);
this.writer = new PrimitiveWriter(this.beaconSize);
this.encoder = new MessageEncoder(this.beaconSize);
this.prepare();
this.isEmpty = true;
}
@ -63,12 +95,12 @@ export default class BatchWriter {
if (this.isEmpty) {
return;
}
this.onBatch(this.writer.flush());
this.onBatch(this.encoder.flush());
this.prepare();
this.isEmpty = true;
}
clean() {
this.writer.reset();
this.encoder.reset();
}
}

View file

@ -0,0 +1,264 @@
// Auto-generated, do not edit
import * as Messages from '../common/messages.js';
import Message from '../common/messages.js';
import PrimitiveEncoder from './PrimitiveEncoder.js';
export default class MessageEncoder extends PrimitiveEncoder {
encode(msg: Message): boolean {
switch (msg[0]) {
case Messages.Type.BatchMetadata:
return (
this.uint(msg[1]) &&
this.uint(msg[2]) &&
this.uint(msg[3]) &&
this.int(msg[4]) &&
this.string(msg[5])
);
break;
case Messages.Type.PartitionedMessage:
return this.uint(msg[1]) && this.uint(msg[2]);
break;
case Messages.Type.Timestamp:
return this.uint(msg[1]);
break;
case Messages.Type.SetPageLocation:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.SetViewportSize:
return this.uint(msg[1]) && this.uint(msg[2]);
break;
case Messages.Type.SetViewportScroll:
return this.int(msg[1]) && this.int(msg[2]);
break;
case Messages.Type.CreateDocument:
return true;
break;
case Messages.Type.CreateElementNode:
return (
this.uint(msg[1]) &&
this.uint(msg[2]) &&
this.uint(msg[3]) &&
this.string(msg[4]) &&
this.boolean(msg[5])
);
break;
case Messages.Type.CreateTextNode:
return this.uint(msg[1]) && this.uint(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.MoveNode:
return this.uint(msg[1]) && this.uint(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.RemoveNode:
return this.uint(msg[1]);
break;
case Messages.Type.SetNodeAttribute:
return this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3]);
break;
case Messages.Type.RemoveNodeAttribute:
return this.uint(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.SetNodeData:
return this.uint(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.SetNodeScroll:
return this.uint(msg[1]) && this.int(msg[2]) && this.int(msg[3]);
break;
case Messages.Type.SetInputTarget:
return this.uint(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.SetInputValue:
return this.uint(msg[1]) && this.string(msg[2]) && this.int(msg[3]);
break;
case Messages.Type.SetInputChecked:
return this.uint(msg[1]) && this.boolean(msg[2]);
break;
case Messages.Type.MouseMove:
return this.uint(msg[1]) && this.uint(msg[2]);
break;
case Messages.Type.ConsoleLog:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.PageLoadTiming:
return (
this.uint(msg[1]) &&
this.uint(msg[2]) &&
this.uint(msg[3]) &&
this.uint(msg[4]) &&
this.uint(msg[5]) &&
this.uint(msg[6]) &&
this.uint(msg[7]) &&
this.uint(msg[8]) &&
this.uint(msg[9])
);
break;
case Messages.Type.PageRenderTiming:
return this.uint(msg[1]) && this.uint(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.JSException:
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]);
break;
case Messages.Type.RawCustomEvent:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.UserID:
return this.string(msg[1]);
break;
case Messages.Type.UserAnonymousID:
return this.string(msg[1]);
break;
case Messages.Type.Metadata:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.CSSInsertRule:
return this.uint(msg[1]) && this.string(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.CSSDeleteRule:
return this.uint(msg[1]) && this.uint(msg[2]);
break;
case Messages.Type.Fetch:
return (
this.string(msg[1]) &&
this.string(msg[2]) &&
this.string(msg[3]) &&
this.string(msg[4]) &&
this.uint(msg[5]) &&
this.uint(msg[6]) &&
this.uint(msg[7])
);
break;
case Messages.Type.Profiler:
return (
this.string(msg[1]) && this.uint(msg[2]) && this.string(msg[3]) && this.string(msg[4])
);
break;
case Messages.Type.OTable:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.StateAction:
return this.string(msg[1]);
break;
case Messages.Type.Redux:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.Vuex:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.MobX:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.NgRx:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]);
break;
case Messages.Type.GraphQL:
return (
this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4])
);
break;
case Messages.Type.PerformanceTrack:
return this.int(msg[1]) && this.int(msg[2]) && this.uint(msg[3]) && this.uint(msg[4]);
break;
case Messages.Type.ResourceTiming:
return (
this.uint(msg[1]) &&
this.uint(msg[2]) &&
this.uint(msg[3]) &&
this.uint(msg[4]) &&
this.uint(msg[5]) &&
this.uint(msg[6]) &&
this.string(msg[7]) &&
this.string(msg[8])
);
break;
case Messages.Type.ConnectionInformation:
return this.uint(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.SetPageVisibility:
return this.boolean(msg[1]);
break;
case Messages.Type.LongTask:
return (
this.uint(msg[1]) &&
this.uint(msg[2]) &&
this.uint(msg[3]) &&
this.uint(msg[4]) &&
this.string(msg[5]) &&
this.string(msg[6]) &&
this.string(msg[7])
);
break;
case Messages.Type.SetNodeAttributeURLBased:
return (
this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4])
);
break;
case Messages.Type.SetCSSDataURLBased:
return this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3]);
break;
case Messages.Type.TechnicalInfo:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.CustomIssue:
return this.string(msg[1]) && this.string(msg[2]);
break;
case Messages.Type.CSSInsertRuleURLBased:
return this.uint(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.string(msg[4]);
break;
case Messages.Type.MouseClick:
return this.uint(msg[1]) && this.uint(msg[2]) && this.string(msg[3]) && this.string(msg[4]);
break;
case Messages.Type.CreateIFrameDocument:
return this.uint(msg[1]) && this.uint(msg[2]);
break;
}
}
}

View file

@ -53,19 +53,29 @@ const textEncoder: { encode(str: string): Uint8Array } =
},
};
export default class PrimitiveWriter {
export default class PrimitiveEncoder {
private offset = 0;
private checkpointOffset = 0;
private readonly data: Uint8Array;
constructor(private readonly size: number) {
this.data = new Uint8Array(size);
}
getCurrentOffset(): number {
return this.offset;
}
checkpoint() {
this.checkpointOffset = this.offset;
}
isEmpty(): boolean {
return this.offset === 0;
}
skip(n: number): boolean {
this.offset += n;
return this.offset <= this.size;
}
set(bytes: Uint8Array, offset: number) {
this.data.set(bytes, offset);
}
boolean(value: boolean): boolean {
this.data[this.offset++] = +value;
return this.offset <= this.size;

View file

@ -1,7 +1,7 @@
import type Message from '../common/messages.js';
import { WorkerMessageData } from '../common/webworker.js';
import { Type as MType } from '../common/messages.js';
import { WorkerMessageData } from '../common/interaction.js';
import { classes, SetPageVisibility } from '../common/messages.js';
import QueueSender from './QueueSender.js';
import BatchWriter from './BatchWriter.js';
@ -66,13 +66,11 @@ self.onmessage = ({ data }: MessageEvent<WorkerMessageData>): any => {
}
const w = writer;
// Message[]
data.forEach((data) => {
// @ts-ignore
const message: Message = new (classes.get(data._id))();
data.forEach((message) => {
Object.assign(message, data);
if (message instanceof SetPageVisibility) {
// @ts-ignore
if ((<any>message).hidden) {
if (message[0] === MType.SetPageVisibility) {
if (message[1]) {
// .hidden
restartTimeoutID = setTimeout(() => self.postMessage('restart'), 30 * 60 * 1000);
} else {
clearTimeout(restartTimeoutID);
@ -102,6 +100,7 @@ self.onmessage = ({ data }: MessageEvent<WorkerMessageData>): any => {
writer = new BatchWriter(
data.pageNo,
data.timestamp,
data.url,
// onBatch
(batch) => sender && sender.push(batch),
);