feat(tracker): adoptedStyleSheets
This commit is contained in:
parent
3ec74095d7
commit
5c3fc505ab
7 changed files with 316 additions and 18 deletions
|
|
@ -51,6 +51,11 @@ export enum Type {
|
|||
CSSInsertRuleURLBased = 67,
|
||||
MouseClick = 69,
|
||||
CreateIFrameDocument = 70,
|
||||
AdoptedSSReplaceURLBased = 71,
|
||||
AdoptedSSInsertRuleURLBased = 73,
|
||||
AdoptedSSDeleteRule = 75,
|
||||
AdoptedSSAddOwner = 76,
|
||||
AdoptedSSRemoveOwner = 77,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -400,6 +405,39 @@ export type CreateIFrameDocument = [
|
|||
id: number,
|
||||
]
|
||||
|
||||
export type AdoptedSSReplaceURLBased = [
|
||||
type: Type.AdoptedSSReplaceURLBased,
|
||||
sheetID: number,
|
||||
text: string,
|
||||
baseURL: string,
|
||||
]
|
||||
|
||||
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | 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
|
||||
export type AdoptedSSInsertRuleURLBased = [
|
||||
type: Type.AdoptedSSInsertRuleURLBased,
|
||||
sheetID: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
baseURL: string,
|
||||
]
|
||||
|
||||
export type AdoptedSSDeleteRule = [
|
||||
type: Type.AdoptedSSDeleteRule,
|
||||
sheetID: number,
|
||||
index: number,
|
||||
]
|
||||
|
||||
export type AdoptedSSAddOwner = [
|
||||
type: Type.AdoptedSSAddOwner,
|
||||
sheetID: number,
|
||||
id: number,
|
||||
]
|
||||
|
||||
export type AdoptedSSRemoveOwner = [
|
||||
type: Type.AdoptedSSRemoveOwner,
|
||||
sheetID: number,
|
||||
id: number,
|
||||
]
|
||||
|
||||
|
||||
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | 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
|
||||
export default Message
|
||||
|
|
|
|||
|
|
@ -646,3 +646,64 @@ export function CreateIFrameDocument(
|
|||
]
|
||||
}
|
||||
|
||||
export function AdoptedSSReplaceURLBased(
|
||||
sheetID: number,
|
||||
text: string,
|
||||
baseURL: string,
|
||||
): Messages.AdoptedSSReplaceURLBased {
|
||||
return [
|
||||
Messages.Type.AdoptedSSReplaceURLBased,
|
||||
sheetID,
|
||||
text,
|
||||
baseURL,
|
||||
]
|
||||
}
|
||||
|
||||
export function AdoptedSSInsertRuleURLBased(
|
||||
sheetID: number,
|
||||
rule: string,
|
||||
index: number,
|
||||
baseURL: string,
|
||||
): Messages.AdoptedSSInsertRuleURLBased {
|
||||
return [
|
||||
Messages.Type.AdoptedSSInsertRuleURLBased,
|
||||
sheetID,
|
||||
rule,
|
||||
index,
|
||||
baseURL,
|
||||
]
|
||||
}
|
||||
|
||||
export function AdoptedSSDeleteRule(
|
||||
sheetID: number,
|
||||
index: number,
|
||||
): Messages.AdoptedSSDeleteRule {
|
||||
return [
|
||||
Messages.Type.AdoptedSSDeleteRule,
|
||||
sheetID,
|
||||
index,
|
||||
]
|
||||
}
|
||||
|
||||
export function AdoptedSSAddOwner(
|
||||
sheetID: number,
|
||||
id: number,
|
||||
): Messages.AdoptedSSAddOwner {
|
||||
return [
|
||||
Messages.Type.AdoptedSSAddOwner,
|
||||
sheetID,
|
||||
id,
|
||||
]
|
||||
}
|
||||
|
||||
export function AdoptedSSRemoveOwner(
|
||||
sheetID: number,
|
||||
id: number,
|
||||
): Messages.AdoptedSSRemoveOwner {
|
||||
return [
|
||||
Messages.Type.AdoptedSSRemoveOwner,
|
||||
sheetID,
|
||||
id,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export interface Options {
|
|||
captureIFrames: boolean
|
||||
}
|
||||
|
||||
type ContextCallback = (context: Window & typeof globalThis) => void
|
||||
|
||||
const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot()
|
||||
|
||||
export default class TopObserver extends Observer {
|
||||
|
|
@ -44,25 +46,36 @@ export default class TopObserver extends Observer {
|
|||
})
|
||||
}
|
||||
|
||||
private readonly contextCallbacks: Array<ContextCallback> = []
|
||||
|
||||
// Attached once per Tracker instance
|
||||
attachContextCallback(cb: ContextCallback) {
|
||||
this.contextCallbacks.push(cb)
|
||||
}
|
||||
|
||||
private iframeObservers: IFrameObserver[] = []
|
||||
private handleIframe(iframe: HTMLIFrameElement): void {
|
||||
let doc: Document | null = null
|
||||
let win: Window | null = null
|
||||
const handle = this.app.safe(() => {
|
||||
const id = this.app.nodes.getID(iframe)
|
||||
if (id === undefined) {
|
||||
return
|
||||
} //log
|
||||
if (iframe.contentDocument === doc) {
|
||||
return
|
||||
} // How frequently can it happen?
|
||||
doc = iframe.contentDocument
|
||||
if (!doc || !iframe.contentWindow) {
|
||||
//log
|
||||
return
|
||||
}
|
||||
const observer = new IFrameObserver(this.app)
|
||||
|
||||
this.iframeObservers.push(observer)
|
||||
observer.observe(iframe)
|
||||
const currentWin = iframe.contentWindow
|
||||
const currentDoc = iframe.contentDocument
|
||||
if (currentDoc && currentDoc !== doc) {
|
||||
const observer = new IFrameObserver(this.app)
|
||||
this.iframeObservers.push(observer)
|
||||
observer.observe(iframe)
|
||||
doc = currentDoc
|
||||
}
|
||||
if (currentWin && currentWin !== win) {
|
||||
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
||||
this.contextCallbacks.forEach((cb) => cb(currentWin))
|
||||
win = currentWin
|
||||
}
|
||||
})
|
||||
iframe.addEventListener('load', handle) // why app.attachEventListener not working?
|
||||
handle()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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 AdoptedStyleSheets from './modules/adoptedStyleSheets.js'
|
||||
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
|
||||
|
||||
import type { Options as AppOptions } from './app/index.js'
|
||||
|
|
@ -113,6 +114,7 @@ export default class API {
|
|||
if (app !== null) {
|
||||
Viewport(app)
|
||||
CSSRules(app)
|
||||
AdoptedStyleSheets(app)
|
||||
Connection(app)
|
||||
Console(app, options)
|
||||
Exception(app, options)
|
||||
|
|
|
|||
155
tracker/tracker/src/main/modules/adoptedStyleSheets.ts
Normal file
155
tracker/tracker/src/main/modules/adoptedStyleSheets.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import type App from '../app/index.js'
|
||||
import {
|
||||
TechnicalInfo,
|
||||
AdoptedSSReplaceURLBased,
|
||||
AdoptedSSInsertRuleURLBased,
|
||||
AdoptedSSDeleteRule,
|
||||
AdoptedSSAddOwner,
|
||||
AdoptedSSRemoveOwner,
|
||||
} from '../app/messages.gen.js'
|
||||
import { isRootNode } from '../app/guards.js'
|
||||
|
||||
type StyleSheetOwner = (Document | ShadowRoot) & { adoptedStyleSheets: CSSStyleSheet[] }
|
||||
|
||||
function hasAdoptedSS(node: Node): node is StyleSheetOwner {
|
||||
return (
|
||||
isRootNode(node) &&
|
||||
// @ts-ignore
|
||||
typeof node.adoptedStyleSheets !== 'undefined'
|
||||
)
|
||||
}
|
||||
|
||||
export default function (app: App | null) {
|
||||
if (app === null) {
|
||||
return
|
||||
}
|
||||
if (!hasAdoptedSS(document)) {
|
||||
app.attachStartCallback(() => {
|
||||
// MBTODO: pre-start sendQueue app
|
||||
app.send(TechnicalInfo('no_adopted_stylesheets', ''))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let nextID = 0xf
|
||||
const styleSheetIDMap: Map<CSSStyleSheet, number> = new Map()
|
||||
const adoptedStyleSheetsOwnings: Map<number, number[]> = new Map()
|
||||
|
||||
const updateAdoptedStyleSheets = (root: StyleSheetOwner) => {
|
||||
let nodeID = app.nodes.getID(root)
|
||||
if (root === document) {
|
||||
nodeID = 0 // main document doesn't have nodeID. ID count starts from the documentElement
|
||||
}
|
||||
if (!nodeID) {
|
||||
return
|
||||
}
|
||||
let pastOwning = adoptedStyleSheetsOwnings.get(nodeID)
|
||||
if (!pastOwning) {
|
||||
pastOwning = []
|
||||
}
|
||||
const nowOwning: number[] = []
|
||||
const styleSheets = root.adoptedStyleSheets
|
||||
for (const s of styleSheets) {
|
||||
let sheetID = styleSheetIDMap.get(s)
|
||||
const init = !sheetID
|
||||
if (!sheetID) {
|
||||
sheetID = ++nextID
|
||||
}
|
||||
nowOwning.push(sheetID)
|
||||
if (!pastOwning.includes(sheetID)) {
|
||||
app.send(AdoptedSSAddOwner(sheetID, nodeID))
|
||||
}
|
||||
if (init) {
|
||||
const rules = s.cssRules
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, rules[i].cssText, i, app.getBaseHref()))
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const sheetID of pastOwning) {
|
||||
if (!nowOwning.includes(sheetID)) {
|
||||
app.send(AdoptedSSRemoveOwner(sheetID, nodeID))
|
||||
}
|
||||
}
|
||||
adoptedStyleSheetsOwnings.set(nodeID, nowOwning)
|
||||
}
|
||||
|
||||
function patchAdoptedStyleSheets(
|
||||
prototype: typeof Document.prototype | typeof ShadowRoot.prototype,
|
||||
) {
|
||||
const nativeAdoptedStyleSheetsDescriptor = Object.getOwnPropertyDescriptor(
|
||||
prototype,
|
||||
'adoptedStyleSheets',
|
||||
)
|
||||
if (nativeAdoptedStyleSheetsDescriptor) {
|
||||
Object.defineProperty(prototype, 'adoptedStyleSheets', {
|
||||
...nativeAdoptedStyleSheetsDescriptor,
|
||||
set: function (this: StyleSheetOwner, value) {
|
||||
// @ts-ignore
|
||||
const retVal = nativeAdoptedStyleSheetsDescriptor.set.call(this, value)
|
||||
updateAdoptedStyleSheets(this)
|
||||
return retVal
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const patchContext = (context: typeof globalThis): void => {
|
||||
patchAdoptedStyleSheets(context.Document.prototype)
|
||||
patchAdoptedStyleSheets(context.ShadowRoot.prototype)
|
||||
|
||||
//@ts-ignore TODO: configure ts (use necessary lib)
|
||||
const { insertRule, deleteRule, replace, replaceSync } = context.CSSStyleSheet.prototype
|
||||
|
||||
//@ts-ignore
|
||||
context.CSSStyleSheet.prototype.replace = function (text: string) {
|
||||
return replace.call(this, text).then((sheet: CSSStyleSheet) => {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
|
||||
}
|
||||
return sheet
|
||||
})
|
||||
}
|
||||
//@ts-ignore
|
||||
context.CSSStyleSheet.prototype.replaceSync = function (text: string) {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
|
||||
}
|
||||
return replaceSync.call(this, text)
|
||||
}
|
||||
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()))
|
||||
}
|
||||
return insertRule.call(this, rule, index)
|
||||
}
|
||||
context.CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
app.send(AdoptedSSDeleteRule(sheetID, index))
|
||||
}
|
||||
return deleteRule.call(this, index)
|
||||
}
|
||||
}
|
||||
|
||||
patchContext(window)
|
||||
app.observer.attachContextCallback(patchContext)
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
styleSheetIDMap.clear()
|
||||
adoptedStyleSheetsOwnings.clear()
|
||||
})
|
||||
|
||||
// So far main Document is not triggered with nodeCallbacks
|
||||
app.attachStartCallback(() => {
|
||||
updateAdoptedStyleSheets(document as StyleSheetOwner)
|
||||
})
|
||||
app.nodes.attachNodeCallback((node: Node): void => {
|
||||
if (hasAdoptedSS(node)) {
|
||||
updateAdoptedStyleSheets(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
import type App from '../app/index.js'
|
||||
import { SetViewportScroll, SetNodeScroll } from '../app/messages.gen.js'
|
||||
import { isElementNode } from '../app/guards.js'
|
||||
import { isElementNode, isRootNode } from '../app/guards.js'
|
||||
|
||||
export default function (app: App): void {
|
||||
let documentScroll = false
|
||||
const nodeScroll: Map<Element, [number, number]> = new Map()
|
||||
const nodeScroll: Map<Node, [number, number]> = new Map()
|
||||
|
||||
function setNodeScroll(target: EventTarget | null) {
|
||||
if (target instanceof Element) {
|
||||
nodeScroll.set(target, [target.scrollLeft, target.scrollTop])
|
||||
}
|
||||
}
|
||||
|
||||
const sendSetViewportScroll = app.safe((): void =>
|
||||
app.send(
|
||||
|
|
@ -38,18 +44,21 @@ export default function (app: App): void {
|
|||
app.nodes.attachNodeCallback((node, isStart) => {
|
||||
if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
|
||||
nodeScroll.set(node, [node.scrollLeft, node.scrollTop])
|
||||
} else if (isRootNode(node)) {
|
||||
// scroll is not-composed event (https://javascript.info/shadow-dom-events)
|
||||
app.attachEventListener(node, 'scroll', (e: Event): void => {
|
||||
setNodeScroll(e.target)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.attachEventListener(window, 'scroll', (e: Event): void => {
|
||||
app.attachEventListener(document, 'scroll', (e: Event): void => {
|
||||
const target = e.target
|
||||
if (target === document) {
|
||||
documentScroll = true
|
||||
return
|
||||
}
|
||||
if (target instanceof Element) {
|
||||
nodeScroll.set(target, [target.scrollLeft, target.scrollTop])
|
||||
}
|
||||
setNodeScroll(target)
|
||||
})
|
||||
|
||||
app.ticker.attach(
|
||||
|
|
|
|||
|
|
@ -209,6 +209,26 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
|||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
case Messages.Type.AdoptedSSReplaceURLBased:
|
||||
return this.uint(msg[1]) && this.string(msg[2]) && this.string(msg[3])
|
||||
break
|
||||
|
||||
case Messages.Type.AdoptedSSInsertRuleURLBased:
|
||||
return this.uint(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.string(msg[4])
|
||||
break
|
||||
|
||||
case Messages.Type.AdoptedSSDeleteRule:
|
||||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
case Messages.Type.AdoptedSSAddOwner:
|
||||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
case Messages.Type.AdoptedSSRemoveOwner:
|
||||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue