@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/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
- const mObserver = ngSafeBrowserMethod('MutationObserver');
124
- return new window[mObserver](cb);
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
- const safeAddEventListener = ngSafeBrowserMethod('addEventListener');
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
- const safeRemoveEventListener = ngSafeBrowserMethod('removeEventListener');
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;
@@ -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
- trackedFrames: number[];
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
- pollingQueue: string[];
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.1'; // TODO: version compatability check inside each plugin.
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
- const id = await this.checkNodeId(pageIframes, childIframeDomain);
138
- if (id && !this.trackedFrames.includes(id)) {
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(id);
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
- frameOrderNumber: this.trackedFrames.length,
150
- frameTimeOffset: this.timestamp(),
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.dataset.domain === event.data.domain) {
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.dataset.domain === event.data.domain) {
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
- while (this.pollingQueue.length) {
215
- const msg = this.pollingQueue.shift();
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: msg }, '*');
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
- this.pollingQueue = [];
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.pollingQueue.push(proto.startIframe);
248
+ this.addCommand(proto.startIframe);
225
249
  };
226
250
  this.killChildrenFrames = () => {
227
- this.pollingQueue.push(proto.killIframe);
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 ? createEventListener(target, type, listener, useCapture) : null;
267
- const deleteListener = () => target ? deleteEventListener(target, type, listener, useCapture) : null;
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
- * if we get a signal from child iframes, we check for their node_id and send it back,
399
- * so they can act as if it was just a same-domain iframe
400
- * */
401
- if (!this.insideIframe) {
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, domain) {
478
+ async checkNodeId(iframes, source) {
450
479
  for (const iframe of iframes) {
451
- if (iframe.dataset.domain === domain) {
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.postMessage({
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.postMessage('stop');
1126
+ this.worker?.postMessage('stop');
1099
1127
  }
1100
1128
  else {
1101
- this.worker.postMessage({
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
  }
@@ -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): void;
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 = 9007199254740900;
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
  }
@@ -1,5 +1,5 @@
1
1
  import Observer from './observer.js';
2
2
  export default class IFrameObserver extends Observer {
3
3
  observe(iframe: HTMLIFrameElement): void;
4
- syntheticObserve(selfId: number, doc: Document): void;
4
+ syntheticObserve(rootNodeId: number, doc: Document): void;
5
5
  }
@@ -17,13 +17,13 @@ export default class IFrameObserver extends Observer {
17
17
  this.app.send(CreateIFrameDocument(hostID, docID));
18
18
  });
19
19
  }
20
- syntheticObserve(selfId, doc) {
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(selfId, docID));
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.substr(0, 6) === 'xlink:') {
105
- name = name.substr(6);
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.substr(0, 2) === 'on') {
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(selfId: number, frameOder: number): void;
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(selfId, frameOder) {
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(selfId, window.document);
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.1',
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,