@openreplay/tracker 14.0.10-beta.1 → 14.0.10-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/app/index.d.ts +23 -3
- package/cjs/app/index.js +76 -45
- package/cjs/app/nodes.d.ts +3 -2
- package/cjs/app/nodes.js +17 -16
- package/cjs/app/observer/iframe_observer.d.ts +1 -1
- package/cjs/app/observer/iframe_observer.js +2 -2
- package/cjs/app/observer/observer.d.ts +4 -0
- package/cjs/app/observer/observer.js +37 -6
- package/cjs/app/observer/top_observer.d.ts +1 -1
- package/cjs/app/observer/top_observer.js +2 -2
- package/cjs/index.js +1 -1
- package/cjs/modules/img.js +1 -1
- package/cjs/utils.d.ts +3 -3
- package/cjs/utils.js +28 -9
- package/lib/app/index.d.ts +23 -3
- package/lib/app/index.js +76 -45
- package/lib/app/nodes.d.ts +3 -2
- package/lib/app/nodes.js +17 -16
- package/lib/app/observer/iframe_observer.d.ts +1 -1
- package/lib/app/observer/iframe_observer.js +2 -2
- package/lib/app/observer/observer.d.ts +4 -0
- package/lib/app/observer/observer.js +37 -6
- package/lib/app/observer/top_observer.d.ts +1 -1
- package/lib/app/observer/top_observer.js +2 -2
- package/lib/index.js +1 -1
- package/lib/modules/img.js +1 -1
- package/lib/utils.d.ts +3 -3
- package/lib/utils.js +28 -9
- package/package.json +1 -1
package/cjs/utils.js
CHANGED
|
@@ -119,34 +119,53 @@ function ngSafeBrowserMethod(method) {
|
|
|
119
119
|
: method;
|
|
120
120
|
}
|
|
121
121
|
exports.ngSafeBrowserMethod = ngSafeBrowserMethod;
|
|
122
|
-
function createMutationObserver(cb) {
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
function createMutationObserver(cb, angularMode) {
|
|
123
|
+
if (angularMode) {
|
|
124
|
+
const mObserver = ngSafeBrowserMethod('MutationObserver');
|
|
125
|
+
return new window[mObserver](cb);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
return new MutationObserver(cb);
|
|
129
|
+
}
|
|
125
130
|
}
|
|
126
131
|
exports.createMutationObserver = createMutationObserver;
|
|
127
|
-
function createEventListener(target, event, cb, capture) {
|
|
128
|
-
|
|
132
|
+
function createEventListener(target, event, cb, capture, angularMode) {
|
|
133
|
+
let safeAddEventListener;
|
|
134
|
+
if (angularMode) {
|
|
135
|
+
safeAddEventListener = ngSafeBrowserMethod('addEventListener');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
safeAddEventListener = 'addEventListener';
|
|
139
|
+
}
|
|
129
140
|
try {
|
|
130
141
|
target[safeAddEventListener](event, cb, capture);
|
|
142
|
+
target.addEventListener(event, cb, capture);
|
|
131
143
|
}
|
|
132
144
|
catch (e) {
|
|
133
145
|
const msg = e.message;
|
|
134
146
|
console.error(
|
|
135
147
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
136
|
-
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
|
|
148
|
+
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event, target);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
exports.createEventListener = createEventListener;
|
|
140
|
-
function deleteEventListener(target, event, cb, capture) {
|
|
141
|
-
|
|
152
|
+
function deleteEventListener(target, event, cb, capture, angularMode) {
|
|
153
|
+
let safeRemoveEventListener;
|
|
154
|
+
if (angularMode) {
|
|
155
|
+
safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
safeRemoveEventListener = 'removeEventListener';
|
|
159
|
+
}
|
|
142
160
|
try {
|
|
143
161
|
target[safeRemoveEventListener](event, cb, capture);
|
|
162
|
+
target.removeEventListener(event, cb, capture);
|
|
144
163
|
}
|
|
145
164
|
catch (e) {
|
|
146
165
|
const msg = e.message;
|
|
147
166
|
console.error(
|
|
148
167
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
149
|
-
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
|
|
168
|
+
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event, target);
|
|
150
169
|
}
|
|
151
170
|
}
|
|
152
171
|
exports.deleteEventListener = deleteEventListener;
|
package/lib/app/index.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface StartOptions {
|
|
|
18
18
|
forceNew?: boolean;
|
|
19
19
|
sessionHash?: string;
|
|
20
20
|
assistOnly?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated We strongly advise to use .start().then instead.
|
|
23
|
+
*
|
|
24
|
+
* This method is kept for snippet compatibility only
|
|
25
|
+
* */
|
|
26
|
+
startCallback?: (result: StartPromiseReturn) => void;
|
|
21
27
|
}
|
|
22
28
|
interface OnStartInfo {
|
|
23
29
|
sessionID: string;
|
|
@@ -100,6 +106,12 @@ type AppOptions = {
|
|
|
100
106
|
parentDomain?: string;
|
|
101
107
|
};
|
|
102
108
|
network?: NetworkOptions;
|
|
109
|
+
/**
|
|
110
|
+
* use this flag if you're using Angular
|
|
111
|
+
* basically goes around window.Zone api changes to mutation observer
|
|
112
|
+
* and event listeners
|
|
113
|
+
* */
|
|
114
|
+
angularMode?: boolean;
|
|
103
115
|
} & WebworkerOptions & SessOptions;
|
|
104
116
|
export type Options = AppOptions & ObserverOptions & SanitizerOptions;
|
|
105
117
|
export declare const DEFAULT_INGEST_POINT = "https://api.openreplay.com/ingest";
|
|
@@ -146,16 +158,24 @@ export default class App {
|
|
|
146
158
|
private rootId;
|
|
147
159
|
private pageFrames;
|
|
148
160
|
private frameOderNumber;
|
|
149
|
-
private readonly initialHostName;
|
|
150
161
|
private features;
|
|
151
162
|
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>, signalError: (error: string, apis: string[]) => void, insideIframe: boolean);
|
|
152
163
|
/** used by child iframes for crossdomain only */
|
|
153
164
|
parentActive: boolean;
|
|
154
165
|
checkStatus: () => boolean;
|
|
155
166
|
parentCrossDomainFrameListener: (event: MessageEvent) => void;
|
|
156
|
-
|
|
167
|
+
/**
|
|
168
|
+
* context ids for iframes,
|
|
169
|
+
* order is not so important as long as its consistent
|
|
170
|
+
* */
|
|
171
|
+
trackedFrames: string[];
|
|
157
172
|
crossDomainIframeListener: (event: MessageEvent) => void;
|
|
158
|
-
|
|
173
|
+
/**
|
|
174
|
+
* { command : [remaining iframes] }
|
|
175
|
+
* + order of commands
|
|
176
|
+
**/
|
|
177
|
+
pollingQueue: Record<string, any>;
|
|
178
|
+
private readonly addCommand;
|
|
159
179
|
bootChildrenFrames: () => Promise<void>;
|
|
160
180
|
killChildrenFrames: () => void;
|
|
161
181
|
signalIframeTracker: () => void;
|
package/lib/app/index.js
CHANGED
|
@@ -67,7 +67,7 @@ export default class App {
|
|
|
67
67
|
this.stopCallbacks = [];
|
|
68
68
|
this.commitCallbacks = [];
|
|
69
69
|
this.activityState = ActivityState.NotActive;
|
|
70
|
-
this.version = '14.0.10-beta.
|
|
70
|
+
this.version = '14.0.10-beta.2'; // TODO: version compatability check inside each plugin.
|
|
71
71
|
this.socketMode = false;
|
|
72
72
|
this.compressionThreshold = 24 * 1000;
|
|
73
73
|
this.bc = null;
|
|
@@ -77,7 +77,6 @@ export default class App {
|
|
|
77
77
|
this.rootId = null;
|
|
78
78
|
this.pageFrames = [];
|
|
79
79
|
this.frameOderNumber = 0;
|
|
80
|
-
this.initialHostName = location.hostname;
|
|
81
80
|
this.features = {
|
|
82
81
|
'feature-flags': true,
|
|
83
82
|
'usability-test': true,
|
|
@@ -112,7 +111,6 @@ export default class App {
|
|
|
112
111
|
this.frameOderNumber = data.frameOrderNumber;
|
|
113
112
|
this.debug.log('starting iframe tracking', data);
|
|
114
113
|
this.allowAppStart();
|
|
115
|
-
this.delay = data.frameTimeOffset;
|
|
116
114
|
}
|
|
117
115
|
if (data.line === proto.killIframe) {
|
|
118
116
|
if (this.active()) {
|
|
@@ -120,6 +118,10 @@ export default class App {
|
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* context ids for iframes,
|
|
123
|
+
* order is not so important as long as its consistent
|
|
124
|
+
* */
|
|
123
125
|
this.trackedFrames = [];
|
|
124
126
|
this.crossDomainIframeListener = (event) => {
|
|
125
127
|
if (!this.active() || event.source === window)
|
|
@@ -130,25 +132,30 @@ export default class App {
|
|
|
130
132
|
if (data.line === proto.iframeSignal) {
|
|
131
133
|
// @ts-ignore
|
|
132
134
|
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
|
|
133
|
-
const childIframeDomain = data.domain;
|
|
134
135
|
const pageIframes = Array.from(document.querySelectorAll('iframe'));
|
|
135
136
|
this.pageFrames = pageIframes;
|
|
136
137
|
const signalId = async () => {
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
if (event.source === null) {
|
|
139
|
+
return console.error('Couldnt connect to event.source for child iframe tracking');
|
|
140
|
+
}
|
|
141
|
+
const id = await this.checkNodeId(pageIframes, event.source);
|
|
142
|
+
if (id && !this.trackedFrames.includes(data.context)) {
|
|
139
143
|
try {
|
|
140
|
-
this.trackedFrames.push(
|
|
144
|
+
this.trackedFrames.push(data.context);
|
|
141
145
|
await this.waitStarted();
|
|
142
146
|
const token = this.session.getSessionToken();
|
|
147
|
+
const order = this.trackedFrames.findIndex((f) => f === data.context) + 1;
|
|
148
|
+
if (order === 0) {
|
|
149
|
+
this.debug.error('Couldnt get order number for iframe', data.context, this.trackedFrames);
|
|
150
|
+
}
|
|
143
151
|
const iframeData = {
|
|
144
152
|
line: proto.iframeId,
|
|
145
|
-
context: this.contextId,
|
|
146
|
-
domain: childIframeDomain,
|
|
147
153
|
id,
|
|
148
154
|
token,
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
// since indexes go from 0 we +1
|
|
156
|
+
frameOrderNumber: order,
|
|
151
157
|
};
|
|
158
|
+
this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
|
|
152
159
|
// @ts-ignore
|
|
153
160
|
event.source?.postMessage(iframeData, '*');
|
|
154
161
|
}
|
|
@@ -156,6 +163,9 @@ export default class App {
|
|
|
156
163
|
console.error(e);
|
|
157
164
|
}
|
|
158
165
|
}
|
|
166
|
+
else {
|
|
167
|
+
this.debug.log('Couldnt get node id for iframe', event.source, pageIframes);
|
|
168
|
+
}
|
|
159
169
|
};
|
|
160
170
|
void signalId();
|
|
161
171
|
}
|
|
@@ -169,7 +179,7 @@ export default class App {
|
|
|
169
179
|
if (msg[0] === 20 /* MType.MouseMove */) {
|
|
170
180
|
let fixedMessage = msg;
|
|
171
181
|
this.pageFrames.forEach((frame) => {
|
|
172
|
-
if (frame.
|
|
182
|
+
if (frame.contentWindow === event.source) {
|
|
173
183
|
const [type, x, y] = msg;
|
|
174
184
|
const { left, top } = frame.getBoundingClientRect();
|
|
175
185
|
fixedMessage = [type, x + left, y + top];
|
|
@@ -180,7 +190,7 @@ export default class App {
|
|
|
180
190
|
if (msg[0] === 68 /* MType.MouseClick */) {
|
|
181
191
|
let fixedMessage = msg;
|
|
182
192
|
this.pageFrames.forEach((frame) => {
|
|
183
|
-
if (frame.
|
|
193
|
+
if (frame.contentWindow === event.source) {
|
|
184
194
|
const [type, id, hesitationTime, label, selector, normX, normY] = msg;
|
|
185
195
|
const { left, top, width, height } = frame.getBoundingClientRect();
|
|
186
196
|
const contentWidth = document.documentElement.scrollWidth;
|
|
@@ -208,34 +218,47 @@ export default class App {
|
|
|
208
218
|
this.messages.push(...mappedMessages);
|
|
209
219
|
}
|
|
210
220
|
if (data.line === proto.polling) {
|
|
211
|
-
if (!this.pollingQueue.length) {
|
|
221
|
+
if (!this.pollingQueue.order.length) {
|
|
212
222
|
return;
|
|
213
223
|
}
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
const nextCommand = this.pollingQueue.order[0];
|
|
225
|
+
if (this.pollingQueue[nextCommand].includes(data.context)) {
|
|
226
|
+
this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter((c) => c !== data.context);
|
|
216
227
|
// @ts-ignore
|
|
217
|
-
event.source?.postMessage({ line:
|
|
228
|
+
event.source?.postMessage({ line: nextCommand }, '*');
|
|
229
|
+
if (this.pollingQueue[nextCommand].length === 0) {
|
|
230
|
+
this.pollingQueue.order.shift();
|
|
231
|
+
}
|
|
218
232
|
}
|
|
219
233
|
}
|
|
220
234
|
};
|
|
221
|
-
|
|
235
|
+
/**
|
|
236
|
+
* { command : [remaining iframes] }
|
|
237
|
+
* + order of commands
|
|
238
|
+
**/
|
|
239
|
+
this.pollingQueue = {
|
|
240
|
+
order: [],
|
|
241
|
+
};
|
|
242
|
+
this.addCommand = (cmd) => {
|
|
243
|
+
this.pollingQueue.order.push(cmd);
|
|
244
|
+
this.pollingQueue[cmd] = [...this.trackedFrames];
|
|
245
|
+
};
|
|
222
246
|
this.bootChildrenFrames = async () => {
|
|
223
247
|
await this.waitStarted();
|
|
224
|
-
this.
|
|
248
|
+
this.addCommand(proto.startIframe);
|
|
225
249
|
};
|
|
226
250
|
this.killChildrenFrames = () => {
|
|
227
|
-
this.
|
|
251
|
+
this.addCommand(proto.killIframe);
|
|
228
252
|
};
|
|
229
253
|
this.signalIframeTracker = () => {
|
|
230
|
-
const domain = this.initialHostName;
|
|
231
254
|
const thisTab = this.session.getTabId();
|
|
232
255
|
const signalToParent = (n) => {
|
|
233
256
|
window.parent.postMessage({
|
|
234
257
|
line: proto.iframeSignal,
|
|
235
258
|
source: thisTab,
|
|
236
259
|
context: this.contextId,
|
|
237
|
-
domain,
|
|
238
260
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
261
|
+
console.log('trying to signal to parent', n);
|
|
239
262
|
setTimeout(() => {
|
|
240
263
|
if (!this.checkStatus() && n < 100) {
|
|
241
264
|
void signalToParent(n + 1);
|
|
@@ -263,8 +286,12 @@ export default class App {
|
|
|
263
286
|
if (useSafe) {
|
|
264
287
|
listener = this.safe(listener);
|
|
265
288
|
}
|
|
266
|
-
const createListener = () => target
|
|
267
|
-
|
|
289
|
+
const createListener = () => target
|
|
290
|
+
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
|
|
291
|
+
: null;
|
|
292
|
+
const deleteListener = () => target
|
|
293
|
+
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
|
|
294
|
+
: null;
|
|
268
295
|
this.attachStartCallback(createListener, useSafe);
|
|
269
296
|
this.attachStopCallback(deleteListener, useSafe);
|
|
270
297
|
};
|
|
@@ -344,6 +371,7 @@ export default class App {
|
|
|
344
371
|
__save_canvas_locally: false,
|
|
345
372
|
useAnimationFrame: false,
|
|
346
373
|
},
|
|
374
|
+
angularMode: false,
|
|
347
375
|
};
|
|
348
376
|
this.options = simpleMerge(defaultOptions, options);
|
|
349
377
|
if (!this.insideIframe &&
|
|
@@ -357,7 +385,7 @@ export default class App {
|
|
|
357
385
|
this.localStorage = this.options.localStorage ?? window.localStorage;
|
|
358
386
|
this.sessionStorage = this.options.sessionStorage ?? window.sessionStorage;
|
|
359
387
|
this.sanitizer = new Sanitizer(this, options);
|
|
360
|
-
this.nodes = new Nodes(this.options.node_id);
|
|
388
|
+
this.nodes = new Nodes(this.options.node_id, Boolean(options.angularMode));
|
|
361
389
|
this.observer = new Observer(this, options);
|
|
362
390
|
this.ticker = new Ticker(this);
|
|
363
391
|
this.ticker.attach(() => this.commit());
|
|
@@ -381,24 +409,25 @@ export default class App {
|
|
|
381
409
|
if (sessionToken != null) {
|
|
382
410
|
this.session.applySessionHash(sessionToken);
|
|
383
411
|
}
|
|
384
|
-
this.initWorker();
|
|
385
412
|
const thisTab = this.session.getTabId();
|
|
386
|
-
/**
|
|
387
|
-
* listen for messages from parent window, so we can signal that we're alive
|
|
388
|
-
* */
|
|
389
413
|
if (this.insideIframe) {
|
|
414
|
+
/**
|
|
415
|
+
* listen for messages from parent window, so we can signal that we're alive
|
|
416
|
+
* */
|
|
390
417
|
window.addEventListener('message', this.parentCrossDomainFrameListener);
|
|
391
418
|
setInterval(() => {
|
|
392
419
|
window.parent.postMessage({
|
|
393
420
|
line: proto.polling,
|
|
421
|
+
context: this.contextId,
|
|
394
422
|
}, '*');
|
|
395
423
|
}, 250);
|
|
396
424
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
425
|
+
else {
|
|
426
|
+
this.initWorker();
|
|
427
|
+
/**
|
|
428
|
+
* if we get a signal from child iframes, we check for their node_id and send it back,
|
|
429
|
+
* so they can act as if it was just a same-domain iframe
|
|
430
|
+
* */
|
|
402
431
|
window.addEventListener('message', this.crossDomainIframeListener);
|
|
403
432
|
}
|
|
404
433
|
if (this.bc !== null) {
|
|
@@ -446,9 +475,9 @@ export default class App {
|
|
|
446
475
|
this.startTimeout = null;
|
|
447
476
|
}
|
|
448
477
|
}
|
|
449
|
-
async checkNodeId(iframes,
|
|
478
|
+
async checkNodeId(iframes, source) {
|
|
450
479
|
for (const iframe of iframes) {
|
|
451
|
-
if (iframe.
|
|
480
|
+
if (iframe.contentWindow && iframe.contentWindow === source) {
|
|
452
481
|
/**
|
|
453
482
|
* Here we're trying to get node id from the iframe (which is kept in observer)
|
|
454
483
|
* because of async nature of dom initialization, we give 100 retries with 100ms delay each
|
|
@@ -585,19 +614,18 @@ export default class App {
|
|
|
585
614
|
this.messages.length = 0;
|
|
586
615
|
return;
|
|
587
616
|
}
|
|
588
|
-
if (this.worker === undefined || !this.messages.length) {
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
617
|
if (this.insideIframe) {
|
|
592
618
|
window.parent.postMessage({
|
|
593
619
|
line: proto.iframeBatch,
|
|
594
620
|
messages: this.messages,
|
|
595
|
-
domain: this.initialHostName,
|
|
596
621
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
597
622
|
this.commitCallbacks.forEach((cb) => cb(this.messages));
|
|
598
623
|
this.messages.length = 0;
|
|
599
624
|
return;
|
|
600
625
|
}
|
|
626
|
+
if (this.worker === undefined || !this.messages.length) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
601
629
|
try {
|
|
602
630
|
requestIdleCb(() => {
|
|
603
631
|
this.messages.unshift(TabData(this.session.getTabId()));
|
|
@@ -989,7 +1017,7 @@ export default class App {
|
|
|
989
1017
|
if (isColdStart && this.coldInterval) {
|
|
990
1018
|
clearInterval(this.coldInterval);
|
|
991
1019
|
}
|
|
992
|
-
if (!this.worker) {
|
|
1020
|
+
if (!this.worker && !this.insideIframe) {
|
|
993
1021
|
const reason = 'No worker found: perhaps, CSP is not set.';
|
|
994
1022
|
this.signalError(reason, []);
|
|
995
1023
|
return Promise.resolve(UnsuccessfulStart(reason));
|
|
@@ -1016,7 +1044,7 @@ export default class App {
|
|
|
1016
1044
|
metadata: startOpts.metadata,
|
|
1017
1045
|
});
|
|
1018
1046
|
const timestamp = now();
|
|
1019
|
-
this.worker
|
|
1047
|
+
this.worker?.postMessage({
|
|
1020
1048
|
type: 'start',
|
|
1021
1049
|
pageNo: this.session.incPageNo(),
|
|
1022
1050
|
ingestPoint: this.options.ingestPoint,
|
|
@@ -1057,7 +1085,7 @@ export default class App {
|
|
|
1057
1085
|
const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
|
|
1058
1086
|
return UnsuccessfulStart(reason);
|
|
1059
1087
|
}
|
|
1060
|
-
if (!this.worker) {
|
|
1088
|
+
if (!this.worker && !this.insideIframe) {
|
|
1061
1089
|
const reason = 'no worker found after start request (this should not happen in real world)';
|
|
1062
1090
|
this.signalError(reason, []);
|
|
1063
1091
|
return UnsuccessfulStart(reason);
|
|
@@ -1095,10 +1123,10 @@ export default class App {
|
|
|
1095
1123
|
});
|
|
1096
1124
|
if (socketOnly) {
|
|
1097
1125
|
this.socketMode = true;
|
|
1098
|
-
this.worker
|
|
1126
|
+
this.worker?.postMessage('stop');
|
|
1099
1127
|
}
|
|
1100
1128
|
else {
|
|
1101
|
-
this.worker
|
|
1129
|
+
this.worker?.postMessage({
|
|
1102
1130
|
type: 'auth',
|
|
1103
1131
|
token,
|
|
1104
1132
|
beaconSizeLimit,
|
|
@@ -1117,6 +1145,9 @@ export default class App {
|
|
|
1117
1145
|
// TODO: start as early as possible (before receiving the token)
|
|
1118
1146
|
/** after start */
|
|
1119
1147
|
this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
|
|
1148
|
+
if (startOpts.startCallback) {
|
|
1149
|
+
startOpts.startCallback(SuccessfulStart(onStartInfo));
|
|
1150
|
+
}
|
|
1120
1151
|
if (this.features['feature-flags']) {
|
|
1121
1152
|
void this.featureFlags.reloadFlags();
|
|
1122
1153
|
}
|
package/lib/app/nodes.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
type NodeCallback = (node: Node, isStart: boolean) => void;
|
|
2
2
|
export default class Nodes {
|
|
3
3
|
private readonly node_id;
|
|
4
|
+
private readonly angularMode;
|
|
4
5
|
private nodes;
|
|
5
6
|
private totalNodeAmount;
|
|
6
7
|
private readonly nodeCallbacks;
|
|
7
8
|
private readonly elementListeners;
|
|
8
9
|
private nextNodeId;
|
|
9
|
-
constructor(node_id: string);
|
|
10
|
+
constructor(node_id: string, angularMode: boolean);
|
|
10
11
|
syntheticMode(frameOrder: number): void;
|
|
11
12
|
attachNodeCallback(nodeCallback: NodeCallback): void;
|
|
12
13
|
scanTree: (cb: (node: Node | void) => void) => void;
|
|
13
|
-
attachNodeListener(node: Node, type: string, listener: EventListener, useCapture?: boolean)
|
|
14
|
+
attachNodeListener: (node: Node, type: string, listener: EventListener, useCapture?: boolean) => void;
|
|
14
15
|
registerNode(node: Node): [/*id:*/ number, /*isNew:*/ boolean];
|
|
15
16
|
unregisterNode(node: Node): number | undefined;
|
|
16
17
|
cleanTree(): void;
|
package/lib/app/nodes.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createEventListener, deleteEventListener } from '../utils.js';
|
|
2
2
|
export default class Nodes {
|
|
3
|
-
constructor(node_id) {
|
|
3
|
+
constructor(node_id, angularMode) {
|
|
4
4
|
this.node_id = node_id;
|
|
5
|
+
this.angularMode = angularMode;
|
|
5
6
|
this.nodes = [];
|
|
6
7
|
this.totalNodeAmount = 0;
|
|
7
8
|
this.nodeCallbacks = [];
|
|
@@ -10,9 +11,22 @@ export default class Nodes {
|
|
|
10
11
|
this.scanTree = (cb) => {
|
|
11
12
|
this.nodes.forEach((node) => cb(node));
|
|
12
13
|
};
|
|
14
|
+
this.attachNodeListener = (node, type, listener, useCapture = true) => {
|
|
15
|
+
const id = this.getID(node);
|
|
16
|
+
if (id === undefined) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
createEventListener(node, type, listener, useCapture, this.angularMode);
|
|
20
|
+
let listeners = this.elementListeners.get(id);
|
|
21
|
+
if (listeners === undefined) {
|
|
22
|
+
listeners = [];
|
|
23
|
+
this.elementListeners.set(id, listeners);
|
|
24
|
+
}
|
|
25
|
+
listeners.push([type, listener, useCapture]);
|
|
26
|
+
};
|
|
13
27
|
}
|
|
14
28
|
syntheticMode(frameOrder) {
|
|
15
|
-
const maxSafeNumber =
|
|
29
|
+
const maxSafeNumber = Number.MAX_SAFE_INTEGER;
|
|
16
30
|
const placeholderSize = 99999999;
|
|
17
31
|
const nextFrameId = placeholderSize * frameOrder;
|
|
18
32
|
// I highly doubt that this will ever happen,
|
|
@@ -26,19 +40,6 @@ export default class Nodes {
|
|
|
26
40
|
attachNodeCallback(nodeCallback) {
|
|
27
41
|
this.nodeCallbacks.push(nodeCallback);
|
|
28
42
|
}
|
|
29
|
-
attachNodeListener(node, type, listener, useCapture = true) {
|
|
30
|
-
const id = this.getID(node);
|
|
31
|
-
if (id === undefined) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
createEventListener(node, type, listener, useCapture);
|
|
35
|
-
let listeners = this.elementListeners.get(id);
|
|
36
|
-
if (listeners === undefined) {
|
|
37
|
-
listeners = [];
|
|
38
|
-
this.elementListeners.set(id, listeners);
|
|
39
|
-
}
|
|
40
|
-
listeners.push([type, listener, useCapture]);
|
|
41
|
-
}
|
|
42
43
|
registerNode(node) {
|
|
43
44
|
let id = node[this.node_id];
|
|
44
45
|
const isNew = id === undefined;
|
|
@@ -61,7 +62,7 @@ export default class Nodes {
|
|
|
61
62
|
const listeners = this.elementListeners.get(id);
|
|
62
63
|
if (listeners !== undefined) {
|
|
63
64
|
this.elementListeners.delete(id);
|
|
64
|
-
listeners.forEach((listener) => deleteEventListener(node, listener[0], listener[1], listener[2]));
|
|
65
|
+
listeners.forEach((listener) => deleteEventListener(node, listener[0], listener[1], listener[2], this.angularMode));
|
|
65
66
|
}
|
|
66
67
|
this.totalNodeAmount--;
|
|
67
68
|
}
|
|
@@ -17,13 +17,13 @@ export default class IFrameObserver extends Observer {
|
|
|
17
17
|
this.app.send(CreateIFrameDocument(hostID, docID));
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
syntheticObserve(
|
|
20
|
+
syntheticObserve(rootNodeId, doc) {
|
|
21
21
|
this.observeRoot(doc, (docID) => {
|
|
22
22
|
if (docID === undefined) {
|
|
23
23
|
this.app.debug.log('OpenReplay: Iframe document not bound');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
this.app.send(CreateIFrameDocument(
|
|
26
|
+
this.app.send(CreateIFrameDocument(rootNodeId, docID));
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -10,6 +10,10 @@ export default abstract class Observer {
|
|
|
10
10
|
private readonly textSet;
|
|
11
11
|
constructor(app: App, isTopContext?: boolean);
|
|
12
12
|
private clear;
|
|
13
|
+
/**
|
|
14
|
+
* Unbinds the removed nodes in case of iframe src change.
|
|
15
|
+
*/
|
|
16
|
+
private handleIframeSrcChange;
|
|
13
17
|
private sendNodeAttribute;
|
|
14
18
|
private sendNodeData;
|
|
15
19
|
private bindNode;
|
|
@@ -55,7 +55,6 @@ export default class Observer {
|
|
|
55
55
|
}
|
|
56
56
|
if (type === 'childList') {
|
|
57
57
|
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
58
|
-
// Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
|
|
59
58
|
if (isObservable(mutation.removedNodes[i])) {
|
|
60
59
|
this.bindNode(mutation.removedNodes[i]);
|
|
61
60
|
}
|
|
@@ -77,6 +76,9 @@ export default class Observer {
|
|
|
77
76
|
if (name === null) {
|
|
78
77
|
continue;
|
|
79
78
|
}
|
|
79
|
+
if (target instanceof HTMLIFrameElement && name === 'src') {
|
|
80
|
+
this.handleIframeSrcChange(target);
|
|
81
|
+
}
|
|
80
82
|
let attr = this.attributesMap.get(id);
|
|
81
83
|
if (attr === undefined) {
|
|
82
84
|
this.attributesMap.set(id, (attr = new Set()));
|
|
@@ -86,11 +88,10 @@ export default class Observer {
|
|
|
86
88
|
}
|
|
87
89
|
if (type === 'characterData') {
|
|
88
90
|
this.textSet.add(id);
|
|
89
|
-
continue;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
this.commitNodes();
|
|
93
|
-
}));
|
|
94
|
+
}), this.app.options.angularMode);
|
|
94
95
|
}
|
|
95
96
|
clear() {
|
|
96
97
|
this.commited.length = 0;
|
|
@@ -99,10 +100,40 @@ export default class Observer {
|
|
|
99
100
|
this.attributesMap.clear();
|
|
100
101
|
this.textSet.clear();
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Unbinds the removed nodes in case of iframe src change.
|
|
105
|
+
*/
|
|
106
|
+
handleIframeSrcChange(iframe) {
|
|
107
|
+
const oldContentDocument = iframe.contentDocument;
|
|
108
|
+
if (oldContentDocument) {
|
|
109
|
+
const id = this.app.nodes.getID(oldContentDocument);
|
|
110
|
+
if (id !== undefined) {
|
|
111
|
+
const walker = document.createTreeWalker(oldContentDocument, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
112
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) === undefined
|
|
113
|
+
? NodeFilter.FILTER_REJECT
|
|
114
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
115
|
+
},
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
false);
|
|
118
|
+
let removed = 0;
|
|
119
|
+
const totalBeforeRemove = this.app.nodes.getNodeCount();
|
|
120
|
+
while (walker.nextNode()) {
|
|
121
|
+
if (!iframe.contentDocument.contains(walker.currentNode)) {
|
|
122
|
+
removed += 1;
|
|
123
|
+
this.app.nodes.unregisterNode(walker.currentNode);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const removedPercent = Math.floor((removed / totalBeforeRemove) * 100);
|
|
127
|
+
if (removedPercent > 30) {
|
|
128
|
+
this.app.send(UnbindNodes(removedPercent));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
102
133
|
sendNodeAttribute(id, node, name, value) {
|
|
103
134
|
if (isSVGElement(node)) {
|
|
104
|
-
if (name.
|
|
105
|
-
name = name.
|
|
135
|
+
if (name.substring(0, 6) === 'xlink:') {
|
|
136
|
+
name = name.substring(6);
|
|
106
137
|
}
|
|
107
138
|
if (value === null) {
|
|
108
139
|
this.app.send(RemoveNodeAttribute(id, name));
|
|
@@ -123,7 +154,7 @@ export default class Observer {
|
|
|
123
154
|
name === 'integrity' ||
|
|
124
155
|
name === 'crossorigin' ||
|
|
125
156
|
name === 'autocomplete' ||
|
|
126
|
-
name.
|
|
157
|
+
name.substring(0, 2) === 'on') {
|
|
127
158
|
return;
|
|
128
159
|
}
|
|
129
160
|
if (name === 'value' &&
|
|
@@ -19,7 +19,7 @@ export default class TopObserver extends Observer {
|
|
|
19
19
|
private shadowRootObservers;
|
|
20
20
|
private handleShadowRoot;
|
|
21
21
|
observe(): void;
|
|
22
|
-
crossdomainObserve(
|
|
22
|
+
crossdomainObserve(rootNodeId: number, frameOder: number): void;
|
|
23
23
|
disconnect(): void;
|
|
24
24
|
}
|
|
25
25
|
export {};
|
|
@@ -102,7 +102,7 @@ export default class TopObserver extends Observer {
|
|
|
102
102
|
this.app.nodes.callNodeCallbacks(document, true);
|
|
103
103
|
}, window.document.documentElement);
|
|
104
104
|
}
|
|
105
|
-
crossdomainObserve(
|
|
105
|
+
crossdomainObserve(rootNodeId, frameOder) {
|
|
106
106
|
const observer = this;
|
|
107
107
|
Element.prototype.attachShadow = function () {
|
|
108
108
|
// eslint-disable-next-line
|
|
@@ -114,7 +114,7 @@ export default class TopObserver extends Observer {
|
|
|
114
114
|
this.app.nodes.syntheticMode(frameOder);
|
|
115
115
|
const iframeObserver = new IFrameObserver(this.app);
|
|
116
116
|
this.iframeObservers.push(iframeObserver);
|
|
117
|
-
iframeObserver.syntheticObserve(
|
|
117
|
+
iframeObserver.syntheticObserve(rootNodeId, window.document);
|
|
118
118
|
}
|
|
119
119
|
disconnect() {
|
|
120
120
|
this.iframeOffsets.clear();
|
package/lib/index.js
CHANGED
|
@@ -67,7 +67,7 @@ export default class API {
|
|
|
67
67
|
const orig = this.options.ingestPoint || DEFAULT_INGEST_POINT;
|
|
68
68
|
req.open('POST', orig + '/v1/web/not-started');
|
|
69
69
|
req.send(JSON.stringify({
|
|
70
|
-
trackerVersion: '14.0.10-beta.
|
|
70
|
+
trackerVersion: '14.0.10-beta.2',
|
|
71
71
|
projectKey: this.options.projectKey,
|
|
72
72
|
doNotTrack,
|
|
73
73
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|