Dev aleksk (#795)

*feat(tracker;backend;frontend/player): focus in elem
*feat(tracker;frontend/player): FontFace load
This commit is contained in:
Alex K 2022-11-04 11:33:29 +01:00 committed by GitHub
parent d8774122b7
commit 822ca53980
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 3070 additions and 2713 deletions

View file

@ -10,5 +10,5 @@ func IsIOSType(id int) bool {
}
func IsDOMType(id int) bool {
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 54 == id || 55 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
}
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -66,12 +66,18 @@ func ResolveCSS(baseURL string, css string) string {
css = rewriteLinks(css, func(rawurl string) string {
return ResolveURL(baseURL, rawurl)
})
return strings.Replace(css, ":hover", ".-openreplay-hover", -1)
return rewritePseudoclasses(css)
}
func (r *Rewriter) RewriteCSS(sessionID uint64, baseurl string, css string) string {
css = rewriteLinks(css, func(rawurl string) string {
return r.RewriteURL(sessionID, baseurl, rawurl)
})
return strings.Replace(css, ":hover", ".-openreplay-hover", -1)
return rewritePseudoclasses(css)
}
func rewritePseudoclasses(css string) string {
css = strings.Replace(css, ":hover", ".-openreplay-hover", -1)
css = strings.Replace(css, ":focus", ".-openreplay-focus", -1)
return css
}

View file

@ -568,6 +568,23 @@ class PerformanceTrackAggr(Message):
self.max_used_js_heap_size = max_used_js_heap_size
class LoadFontFace(Message):
__id__ = 57
def __init__(self, parent_id, family, source, descriptors):
self.parent_id = parent_id
self.family = family
self.source = source
self.descriptors = descriptors
class SetNodeFocus(Message):
__id__ = 58
def __init__(self, id):
self.id = id
class LongTask(Message):
__id__ = 59

View file

@ -524,6 +524,19 @@ class MessageCodec(Codec):
max_used_js_heap_size=self.read_uint(reader)
)
if message_id == 57:
return LoadFontFace(
parent_id=self.read_uint(reader),
family=self.read_string(reader),
source=self.read_string(reader),
descriptors=self.read_string(reader)
)
if message_id == 58:
return SetNodeFocus(
id=self.read_int(reader)
)
if message_id == 59:
return LongTask(
timestamp=self.read_uint(reader),

View file

@ -5,6 +5,7 @@ import type { Message, SetNodeScroll, CreateElementNode } from '../../messages';
import ListWalker from '../ListWalker';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import FocusManager from './FocusManager';
import {
VElement,
VText,
@ -35,9 +36,9 @@ const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
export default class DOMManager extends ListWalker<Message> {
private vTexts: Map<number, VText> = new Map() // map vs object here?
private vElements: Map<number, VElement> = new Map()
private vRoots: Map<number, VShadowRoot | VDocument> = new Map()
private readonly vTexts: Map<number, VText> = new Map() // map vs object here?
private readonly vElements: Map<number, VElement> = new Map()
private readonly vRoots: Map<number, VShadowRoot | VDocument> = new Map()
private activeIframeRoots: Map<number, number> = new Map()
private styleSheets: Map<number, CSSStyleSheet> = new Map()
private ppStyleSheets: Map<number, PostponedStyleSheet> = new Map()
@ -46,6 +47,7 @@ export default class DOMManager extends ListWalker<Message> {
private upperBodyId: number = -1;
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> = new Map()
private stylesManager: StylesManager
private focusManager: FocusManager = new FocusManager(this.vElements)
constructor(
@ -67,6 +69,10 @@ export default class DOMManager extends ListWalker<Message> {
scrollManager.append(m)
return
}
if (m.tp === "set_node_focus") {
this.focusManager.append(m)
return
}
if (m.tp === "create_element_node") {
if(m.tag === "BODY" && this.upperBodyId === -1) {
this.upperBodyId = m.id
@ -126,7 +132,7 @@ export default class DOMManager extends ListWalker<Message> {
parent.insertChildAt(child, index)
}
private applyMessage = (msg: Message): void => {
private applyMessage = (msg: Message): Promise<any> | undefined => {
let node: Node | undefined
let vn: VNode | undefined
let doc: Document | null
@ -145,12 +151,15 @@ export default class DOMManager extends ListWalker<Message> {
fRoot.innerText = '';
vn = new VElement(fRoot)
this.vElements = new Map([[0, vn]])
this.vElements.clear()
this.vElements.set(0, vn)
const vDoc = new VDocument(doc)
vDoc.insertChildAt(vn, 0)
this.vRoots = new Map([[0, vDoc]]) // watchout: id==0 for both Document and documentElement
this.vRoots.clear()
this.vRoots.set(0, vDoc) // watchout: id==0 for both Document and documentElement
// this is done for the AdoptedCSS logic
// todo: start from 0 (sync logic with tracker)
this.vTexts.clear()
this.stylesManager.reset()
this.activeIframeRoots.clear()
return
@ -371,20 +380,37 @@ export default class DOMManager extends ListWalker<Message> {
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets].filter(s => s !== styleSheet)
return
case "load_font_face":
vn = this.vRoots.get(msg.parentID)
if (!vn) { logger.error("Node not found", msg); return }
if (vn instanceof VShadowRoot) { logger.error(`Node ${vn} expected to be a Document`, msg); return }
let descr: Object
try {
descr = JSON.parse(msg.descriptors)
descr = typeof descr === 'object' ? descr : undefined
} catch {
logger.warn("Can't parse font-face descriptors: ", msg)
}
const ff = new FontFace(msg.family, msg.source, descr)
vn.node.fonts.add(ff)
return ff.load()
}
}
moveReady(t: number): Promise<void> {
async moveReady(t: number): Promise<void> {
// MBTODO (back jump optimisation):
// - store intemediate virtual dom state
// - cancel previous moveReady tasks (is it possible?) if new timestamp is less
this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?)
// This function autoresets pointer if necessary (better name?)
await this.moveWait(t, this.applyMessage)
this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.stylesManager.moveReady(t).then(() => {
// Apply focus
this.focusManager.move(t)
// Apply all scrolls after the styles got applied
this.nodeScrollManagers.forEach(manager => {
const msg = manager.moveGetLast(t)

View file

@ -0,0 +1,26 @@
import logger from 'App/logger';
import type { SetNodeFocus } from '../../messages';
import type { VElement } from './VirtualDOM';
import ListWalker from '../ListWalker';
const FOCUS_CLASS = "-openreplay-focus"
export default class FocusManager extends ListWalker<SetNodeFocus> {
constructor(private readonly vElements: Map<number, VElement>) {super()}
private focused: Element | null = null
move(t: number) {
const msg = this.moveGetLast(t)
if (!msg) {return}
this.focused?.classList.remove(FOCUS_CLASS)
if (msg.id === -1) {
this.focused = null
return
}
const vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
this.focused = vn.node
this.focused.classList.add(FOCUS_CLASS)
}
}

View file

@ -9,7 +9,7 @@ export default class ListWalker<T extends Timed> {
console.error("Trying to append message with the less time then the list tail: ", m)
return
}
this._list.push(m);
this.list.push(m);
}
reset(): void {
@ -18,53 +18,53 @@ export default class ListWalker<T extends Timed> {
sort(comparator: (a: T, b: T) => number): void {
// @ts-ignore
this._list.sort((m1,m2) => comparator(m1,m2) || (m1._index - m2._index) ); // indexes for sort stability (TODO: fix types???)
this.list.sort((m1,m2) => comparator(m1,m2) || (m1._index - m2._index) ); // indexes for sort stability (TODO: fix types???)
}
forEach(f: (item: T) => void):void {
this._list.forEach(f);
this.list.forEach(f);
}
get last(): T | null {
if (this._list.length === 0) {
if (this.list.length === 0) {
return null;
}
return this._list[ this._list.length - 1 ];
return this.list[ this.list.length - 1 ];
}
get current(): T | null {
if (this.p === 0) {
return null;
}
return this._list[ this.p - 1 ];
return this.list[ this.p - 1 ];
}
get timeNow(): number {
if (this.p === 0) {
return 0;
}
return this._list[ this.p - 1 ].time;
return this.list[ this.p - 1 ].time;
}
get length(): number {
return this._list.length;
return this.list.length;
}
get maxTime(): number {
if (this.length === 0) {
return 0;
}
return this._list[ this.length - 1 ].time;
return this.list[ this.length - 1 ].time;
}
get minTime(): number {
if (this.length === 0) {
return 0;
}
return this._list[ 0 ].time;
return this.list[ 0 ].time;
}
get listNow(): Array<T> {
return this._list.slice(0, this.p);
return this.list.slice(0, this.p);
}
get list(): Array<T> {
@ -93,28 +93,42 @@ export default class ListWalker<T extends Timed> {
}
let changed = false;
while (this.p < this.length && this._list[this.p][key] <= val) {
while (this.p < this.length && this.list[this.p][key] <= val) {
this.p++;
changed = true;
}
while (this.p > 0 && this._list[ this.p - 1 ][key] > val) {
while (this.p > 0 && this.list[ this.p - 1 ][key] > val) {
this.p--;
changed = true;
}
return changed ? this._list[ this.p - 1 ] : null;
return changed ? this.list[ this.p - 1 ] : null;
}
moveApply(t: number, fn: (T) => void, fnBack?: (T) => void): void {
moveApply(t: number, fn: (msg: T) => void, fnBack?: (msg: T) => void): void {
// Applying only in increment order for now
if (t < this.timeNow) {
this.reset();
}
while (!!this._list[this.p] && this._list[this.p].time <= t) {
fn(this._list[ this.p++ ]);
const list = this.list
while (list[this.p] && list[this.p].time <= t) {
fn(list[ this.p++ ]);
}
while (fnBack && this.p > 0 && this._list[ this.p - 1 ].time > t) {
fnBack(this._list[ --this.p ]);
while (fnBack && this.p > 0 && list[ this.p - 1 ].time > t) {
fnBack(list[ --this.p ]);
}
}
async moveWait(t: number, fn: (msg: T) => Promise<any> | undefined): Promise<void> {
// Applying only in increment order for now
if (t < this.timeNow) {
this.reset();
}
const list = this.list
while (list[this.p] && list[this.p].time <= t) {
const ret = fn(this.list[ this.p++ ]);
if (ret) { await ret }
}
}

View file

@ -366,6 +366,28 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 57: {
const parentID = this.readUint(); if (parentID === null) { return resetPointer() }
const family = this.readString(); if (family === null) { return resetPointer() }
const source = this.readString(); if (source === null) { return resetPointer() }
const descriptors = this.readString(); if (descriptors === null) { return resetPointer() }
return {
tp: "load_font_face",
parentID,
family,
source,
descriptors,
};
}
case 58: {
const id = this.readInt(); if (id === null) { return resetPointer() }
return {
tp: "set_node_focus",
id,
};
}
case 59: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }

View file

@ -35,6 +35,8 @@ import type {
RawPerformanceTrack,
RawConnectionInformation,
RawSetPageVisibility,
RawLoadFontFace,
RawSetNodeFocus,
RawLongTask,
RawSetNodeAttributeURLBased,
RawSetCssDataURLBased,
@ -123,6 +125,10 @@ export type ConnectionInformation = RawConnectionInformation & Timed
export type SetPageVisibility = RawSetPageVisibility & Timed
export type LoadFontFace = RawLoadFontFace & Timed
export type SetNodeFocus = RawSetNodeFocus & Timed
export type LongTask = RawLongTask & Timed
export type SetNodeAttributeURLBased = RawSetNodeAttributeURLBased & Timed

View file

@ -207,6 +207,19 @@ export interface RawSetPageVisibility {
hidden: boolean,
}
export interface RawLoadFontFace {
tp: "load_font_face",
parentID: number,
family: string,
source: string,
descriptors: string,
}
export interface RawSetNodeFocus {
tp: "set_node_focus",
id: number,
}
export interface RawLongTask {
tp: "long_task",
timestamp: number,
@ -378,4 +391,4 @@ export interface RawIosNetworkCall {
}
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawConnectionInformation | RawSetPageVisibility | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;

View file

@ -46,6 +46,8 @@ export const TP_MAP = {
53: "resource_timing",
54: "connection_information",
55: "set_page_visibility",
57: "load_font_face",
58: "set_node_focus",
59: "long_task",
60: "set_node_attribute_url_based",
61: "set_css_data_url_based",

View file

@ -290,6 +290,19 @@ type TrSetPageVisibility = [
hidden: boolean,
]
type TrLoadFontFace = [
type: 57,
parentID: number,
family: string,
source: string,
descriptors: string,
]
type TrSetNodeFocus = [
type: 58,
id: number,
]
type TrLongTask = [
type: 59,
timestamp: number,
@ -398,7 +411,7 @@ type TrJSException = [
]
export type TrackerMessage = TrBatchMetadata | TrPartitionedMessage | TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrJSExceptionDeprecated | TrRawCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrZustand | TrJSException
export type TrackerMessage = TrBatchMetadata | TrPartitionedMessage | TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrJSExceptionDeprecated | TrRawCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrZustand | TrJSException
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
@ -662,6 +675,23 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 57: {
return {
tp: "load_font_face",
parentID: tMsg[1],
family: tMsg[2],
source: tMsg[3],
descriptors: tMsg[4],
}
}
case 58: {
return {
tp: "set_node_focus",
id: tMsg[1],
}
}
case 59: {
return {
tp: "long_task",

View file

@ -50,6 +50,14 @@ function rewriteCSSLinks(css: string, rewriter: (rawurl: string) => string): str
return css
}
function rewritePseudoclasses(css: string): string {
return css
.replace(/:hover/g, ".-openreplay-hover")
.replace(/:focus/g, ".-openreplay-focus")
}
export function resolveCSS(baseURL: string, css: string): string {
return rewriteCSSLinks(css, rawurl => resolveURL(baseURL, rawurl));
return rewritePseudoclasses(
rewriteCSSLinks(css, rawurl => resolveURL(baseURL, rawurl))
);
}

View file

@ -4,11 +4,5 @@
To generate all necessary files for the project:
```sh
ruby run.rb
sh generate.sh
```
In order format generated files run:
```sh
sh format.sh
```
(Otherwise there will be changes in stage)

View file

@ -1 +1,2 @@
ruby run.rb
gofmt -w ../backend/pkg/messages

View file

@ -359,7 +359,19 @@ message 56, 'PerformanceTrackAggr', :tracker => false, :replayer => false do
uint 'AvgUsedJSHeapSize'
uint 'MaxUsedJSHeapSize'
end
## 57 58
# Since 4.1.7 / 1.9.0
message 57, 'LoadFontFace' do
uint 'ParentID'
string 'Family'
string 'Source'
string 'Descriptors'
end
# Since 4.1.7 / 1.9.0
message 58, 'SetNodeFocus' do
int 'ID'
end
#Depricated (since 3.0.?)
message 59, 'LongTask' do
uint 'Timestamp'
@ -466,6 +478,8 @@ message 78, 'JSException', :replayer => false do
string 'Payload'
string 'Metadata'
end
message 126, 'SessionEnd', :tracker => false, :replayer => false do
uint 'Timestamp'
string 'EncryptionKey'

View file

@ -44,6 +44,8 @@ export declare const enum Type {
ResourceTiming = 53,
ConnectionInformation = 54,
SetPageVisibility = 55,
LoadFontFace = 57,
SetNodeFocus = 58,
LongTask = 59,
SetNodeAttributeURLBased = 60,
SetCSSDataURLBased = 61,
@ -348,6 +350,19 @@ export type SetPageVisibility = [
/*hidden:*/ boolean,
]
export type LoadFontFace = [
/*type:*/ Type.LoadFontFace,
/*parentID:*/ number,
/*family:*/ string,
/*source:*/ string,
/*descriptors:*/ string,
]
export type SetNodeFocus = [
/*type:*/ Type.SetNodeFocus,
/*id:*/ number,
]
export type LongTask = [
/*type:*/ Type.LongTask,
/*timestamp:*/ number,
@ -456,5 +471,5 @@ export type JSException = [
]
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSExceptionDeprecated | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | Zustand | JSException
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSExceptionDeprecated | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | Zustand | JSException
export default Message

View file

@ -25,6 +25,7 @@ export function isRootNode(node: Node): node is Document | DocumentFragment {
type TagTypeMap = {
HTML: HTMLHtmlElement
BODY: HTMLBodyElement
IMG: HTMLImageElement
INPUT: HTMLInputElement
TEXTAREA: HTMLTextAreaElement

View file

@ -480,7 +480,8 @@ export default class App {
const onStartInfo = { sessionToken: token, userUUID, sessionID }
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // TODO: start as early as possible (before receiving the token)
// TODO: start as early as possible (before receiving the token)
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // MBTODO: callbacks after DOM "mounted" (observed)
this.observer.observe()
this.ticker.start()
this.activityState = ActivityState.Active

View file

@ -535,6 +535,30 @@ export function SetPageVisibility(
]
}
export function LoadFontFace(
parentID: number,
family: string,
source: string,
descriptors: string,
): Messages.LoadFontFace {
return [
Messages.Type.LoadFontFace,
parentID,
family,
source,
descriptors,
]
}
export function SetNodeFocus(
id: number,
): Messages.SetNodeFocus {
return [
Messages.Type.SetNodeFocus,
id,
]
}
export function LongTask(
timestamp: number,
duration: number,

View file

@ -129,6 +129,8 @@ export default class TopObserver extends Observer {
window.document,
() => {
this.app.send(CreateDocument())
// it has no node_id here
this.app.nodes.callNodeCallbacks(document, true)
},
window.document.documentElement,
)

View file

@ -20,6 +20,8 @@ import Performance from './modules/performance.js'
import Scroll from './modules/scroll.js'
import Viewport from './modules/viewport.js'
import CSSRules from './modules/cssrules.js'
import Focus from './modules/focus.js'
import Fonts from './modules/fonts.js'
import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
@ -123,6 +125,8 @@ export default class API {
Timing(app, options)
Performance(app, options)
Scroll(app)
Focus(app)
Fonts(app)
;(window as any).__OPENREPLAY__ = this
if (options.autoResetOnWindowOpen) {

View file

@ -31,10 +31,6 @@ export default function (app: App | null) {
return
}
if (!hasAdoptedSS(document)) {
app.attachStartCallback(() => {
// MBTODO: pre-start sendQueue app
app.send(TechnicalInfo('no_adopted_stylesheets', ''))
})
return
}

View file

@ -0,0 +1,45 @@
import type App from '../app/index.js'
import { isNode, hasTag } from '../app/guards.js'
import { SetNodeFocus } from '../app/messages.gen.js'
export default function (app: App): void {
function sendSetNodeFocus(n: Node) {
const id = app.nodes.getID(n)
if (id !== undefined) {
app.send(SetNodeFocus(id))
}
}
let blurred = false
app.nodes.attachNodeCallback((node) => {
if (!hasTag(node, 'BODY')) {
return
}
app.nodes.attachNodeListener(node, 'focus', (e: FocusEvent): void => {
if (!isNode(e.target)) {
return
}
sendSetNodeFocus(e.target)
blurred = false
})
app.nodes.attachNodeListener(node, 'blur', (e: FocusEvent): void => {
if (e.relatedTarget === null) {
blurred = true
setTimeout(() => {
if (blurred) {
app.send(SetNodeFocus(-1))
}
}, 0)
}
})
})
app.attachStartCallback(() => {
let elem = document.activeElement
while (elem && hasTag(elem, 'IFRAME') && elem.contentDocument) {
elem = elem.contentDocument.activeElement
}
if (elem && elem !== elem.ownerDocument.body) {
sendSetNodeFocus(elem)
}
}, true)
}

View file

@ -0,0 +1,66 @@
import type App from '../app/index.js'
import { isDocument } from '../app/guards.js'
import { LoadFontFace } from '../app/messages.gen.js'
type FFData = [string, string, string]
export default function (app: App) {
if (!window.FontFace) {
return
}
const docFonts: Map<Document, FFData[]> = new Map()
const patchWindow = (wnd: typeof globalThis) => {
class FontFaceInterceptor extends wnd.FontFace {
constructor(...args: ConstructorParameters<typeof FontFace>) {
//maybe do this on load(). In this case check if the document.fonts.load(...) function calls the font's load()
if (typeof args[1] === 'string') {
let desc = ''
if (args[2]) {
app.safe(() => {
desc = JSON.stringify(args[2])
})
}
const ffData: FFData = [args[0], args[1], desc]
const ffDataArr = docFonts.get(wnd.document) || []
ffDataArr.push(ffData)
docFonts.set(wnd.document, ffDataArr)
const parentID = wnd === window ? 0 : app.nodes.getID(wnd.document)
if (parentID === undefined) {
return
}
if (app.active()) {
app.send(LoadFontFace(parentID, ...ffData))
}
}
super(...args)
}
}
wnd.FontFace = FontFaceInterceptor
}
app.observer.attachContextCallback(patchWindow)
patchWindow(window)
app.nodes.attachNodeCallback((node) => {
if (!isDocument(node)) {
return
}
const ffDataArr = docFonts.get(node)
if (!ffDataArr) {
return
}
const parentID = node.defaultView === window ? 0 : app.nodes.getID(node)
if (parentID === undefined) {
return
}
ffDataArr.forEach((ffData) => {
app.send(LoadFontFace(parentID, ...ffData))
})
})
}

View file

@ -178,6 +178,14 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.boolean(msg[1])
break
case Messages.Type.LoadFontFace:
return this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4])
break
case Messages.Type.SetNodeFocus:
return this.int(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