@openreplay/tracker 14.0.8 → 14.0.9

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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 14.0.9
2
+
3
+ - more stable crossdomain iframe tracking (refactored child/parent process discovery)
4
+ - checks for bad start error
5
+
1
6
  # 14.0.8
2
7
 
3
8
  - use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy))
package/cjs/app/canvas.js CHANGED
@@ -106,10 +106,8 @@ class CanvasRecorder {
106
106
  startTracking() {
107
107
  setTimeout(() => {
108
108
  this.app.nodes.scanTree(this.captureCanvas);
109
- this.app.nodes.attachNodeCallback((node) => {
110
- this.captureCanvas(node);
111
- });
112
- }, 500);
109
+ this.app.nodes.attachNodeCallback(this.captureCanvas);
110
+ }, 250);
113
111
  }
114
112
  sendSnaps(images, canvasId, createdAt) {
115
113
  if (Object.keys(this.snapshots).length === 0) {
@@ -143,6 +143,13 @@ export default class App {
143
143
  private readonly initialHostName;
144
144
  private features;
145
145
  constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>, signalError: (error: string, apis: string[]) => void, insideIframe: boolean);
146
+ /** used by child iframes for crossdomain only */
147
+ parentActive: boolean;
148
+ checkStatus: () => boolean;
149
+ /** used by child iframes for crossdomain only */
150
+ /** track app instances in crossdomain child iframes */
151
+ crossdomainIframesModule: () => void;
152
+ signalIframeTracker: () => void;
146
153
  startTimeout: ReturnType<typeof setTimeout> | null;
147
154
  private allowAppStart;
148
155
  private checkNodeId;
package/cjs/app/index.js CHANGED
@@ -78,6 +78,8 @@ const proto = {
78
78
  iframeId: 'never-gonna-say-goodbye',
79
79
  // batch of messages from an iframe window
80
80
  iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you',
81
+ // signal that parent is live
82
+ parentAlive: 'i-dont-know-more-lines',
81
83
  };
82
84
  class App {
83
85
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
@@ -94,7 +96,7 @@ class App {
94
96
  this.stopCallbacks = [];
95
97
  this.commitCallbacks = [];
96
98
  this.activityState = ActivityState.NotActive;
97
- this.version = '14.0.8'; // TODO: version compatability check inside each plugin.
99
+ this.version = '14.0.9'; // TODO: version compatability check inside each plugin.
98
100
  this.socketMode = false;
99
101
  this.compressionThreshold = 24 * 1000;
100
102
  this.bc = null;
@@ -109,6 +111,128 @@ class App {
109
111
  'feature-flags': true,
110
112
  'usability-test': true,
111
113
  };
114
+ /** used by child iframes for crossdomain only */
115
+ this.parentActive = false;
116
+ this.checkStatus = () => {
117
+ return this.parentActive;
118
+ };
119
+ /** used by child iframes for crossdomain only */
120
+ /** track app instances in crossdomain child iframes */
121
+ this.crossdomainIframesModule = () => {
122
+ if (!this.insideIframe) {
123
+ /**
124
+ * if we get a signal from child iframes, we check for their node_id and send it back,
125
+ * so they can act as if it was just a same-domain iframe
126
+ * */
127
+ let crossdomainFrameCount = 0;
128
+ const catchIframeMessage = (event) => {
129
+ if (!this.active())
130
+ return;
131
+ const { data } = event;
132
+ if (!data)
133
+ return;
134
+ if (data.line === proto.iframeSignal) {
135
+ // @ts-ignore
136
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
137
+ const childIframeDomain = data.domain;
138
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
139
+ this.pageFrames = pageIframes;
140
+ const signalId = async () => {
141
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
142
+ if (id) {
143
+ try {
144
+ await this.waitStarted();
145
+ crossdomainFrameCount++;
146
+ const token = this.session.getSessionToken();
147
+ const iframeData = {
148
+ line: proto.iframeId,
149
+ context: this.contextId,
150
+ domain: childIframeDomain,
151
+ id,
152
+ token,
153
+ frameOrderNumber: crossdomainFrameCount,
154
+ };
155
+ this.debug.log('iframe_data', iframeData);
156
+ // @ts-ignore
157
+ event.source?.postMessage(iframeData, '*');
158
+ }
159
+ catch (e) {
160
+ console.error(e);
161
+ }
162
+ }
163
+ };
164
+ void signalId();
165
+ }
166
+ /**
167
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
168
+ * plus we rewrite some of the messages to be relative to the main context/window
169
+ * */
170
+ if (data.line === proto.iframeBatch) {
171
+ const msgBatch = data.messages;
172
+ const mappedMessages = msgBatch.map((msg) => {
173
+ if (msg[0] === 20 /* MType.MouseMove */) {
174
+ let fixedMessage = msg;
175
+ this.pageFrames.forEach((frame) => {
176
+ if (frame.dataset.domain === event.data.domain) {
177
+ const [type, x, y] = msg;
178
+ const { left, top } = frame.getBoundingClientRect();
179
+ fixedMessage = [type, x + left, y + top];
180
+ }
181
+ });
182
+ return fixedMessage;
183
+ }
184
+ if (msg[0] === 68 /* MType.MouseClick */) {
185
+ let fixedMessage = msg;
186
+ this.pageFrames.forEach((frame) => {
187
+ if (frame.dataset.domain === event.data.domain) {
188
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
189
+ const { left, top, width, height } = frame.getBoundingClientRect();
190
+ const contentWidth = document.documentElement.scrollWidth;
191
+ const contentHeight = document.documentElement.scrollHeight;
192
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
193
+ const fullX = (normX / 100) * width + left;
194
+ const fullY = (normY / 100) * height + top;
195
+ const fixedX = fullX / contentWidth;
196
+ const fixedY = fullY / contentHeight;
197
+ fixedMessage = [
198
+ type,
199
+ id,
200
+ hesitationTime,
201
+ label,
202
+ selector,
203
+ Math.round(fixedX * 1e3) / 1e1,
204
+ Math.round(fixedY * 1e3) / 1e1,
205
+ ];
206
+ }
207
+ });
208
+ return fixedMessage;
209
+ }
210
+ return msg;
211
+ });
212
+ this.messages.push(...mappedMessages);
213
+ }
214
+ };
215
+ window.addEventListener('message', catchIframeMessage);
216
+ }
217
+ };
218
+ this.signalIframeTracker = () => {
219
+ const domain = this.initialHostName;
220
+ const thisTab = this.session.getTabId();
221
+ const signalToParent = (n) => {
222
+ window.parent.postMessage({
223
+ line: proto.iframeSignal,
224
+ source: thisTab,
225
+ context: this.contextId,
226
+ domain,
227
+ }, this.options.crossdomain?.parentDomain ?? '*');
228
+ setTimeout(() => {
229
+ if (!this.checkStatus() && n < 100) {
230
+ void signalToParent(n + 1);
231
+ }
232
+ }, 250);
233
+ };
234
+ void signalToParent(1);
235
+ };
112
236
  this.startTimeout = null;
113
237
  this.coldStartCommitN = 0;
114
238
  this.delay = 0;
@@ -227,133 +351,25 @@ class App {
227
351
  }
228
352
  this.initWorker();
229
353
  const thisTab = this.session.getTabId();
230
- if (!this.insideIframe) {
231
- /**
232
- * if we get a signal from child iframes, we check for their node_id and send it back,
233
- * so they can act as if it was just a same-domain iframe
234
- * */
235
- let crossdomainFrameCount = 0;
236
- const catchIframeMessage = (event) => {
237
- const { data } = event;
238
- if (!data)
239
- return;
240
- if (data.line === proto.iframeSignal) {
241
- const childIframeDomain = data.domain;
242
- const pageIframes = Array.from(document.querySelectorAll('iframe'));
243
- this.pageFrames = pageIframes;
244
- const signalId = async () => {
245
- let tries = 0;
246
- while (tries < 10) {
247
- const id = this.checkNodeId(pageIframes, childIframeDomain);
248
- if (id) {
249
- this.waitStarted()
250
- .then(() => {
251
- crossdomainFrameCount++;
252
- const token = this.session.getSessionToken();
253
- const iframeData = {
254
- line: proto.iframeId,
255
- context: this.contextId,
256
- domain: childIframeDomain,
257
- id,
258
- token,
259
- frameOrderNumber: crossdomainFrameCount,
260
- };
261
- this.debug.log('iframe_data', iframeData);
262
- // @ts-ignore
263
- event.source?.postMessage(iframeData, '*');
264
- })
265
- .catch(console.error);
266
- tries = 10;
267
- break;
268
- }
269
- tries++;
270
- await delay(100);
271
- }
272
- };
273
- void signalId();
274
- }
275
- /**
276
- * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
277
- * plus we rewrite some of the messages to be relative to the main context/window
278
- * */
279
- if (data.line === proto.iframeBatch) {
280
- const msgBatch = data.messages;
281
- const mappedMessages = msgBatch.map((msg) => {
282
- if (msg[0] === 20 /* MType.MouseMove */) {
283
- let fixedMessage = msg;
284
- this.pageFrames.forEach((frame) => {
285
- if (frame.dataset.domain === event.data.domain) {
286
- const [type, x, y] = msg;
287
- const { left, top } = frame.getBoundingClientRect();
288
- fixedMessage = [type, x + left, y + top];
289
- }
290
- });
291
- return fixedMessage;
292
- }
293
- if (msg[0] === 68 /* MType.MouseClick */) {
294
- let fixedMessage = msg;
295
- this.pageFrames.forEach((frame) => {
296
- if (frame.dataset.domain === event.data.domain) {
297
- const [type, id, hesitationTime, label, selector, normX, normY] = msg;
298
- const { left, top, width, height } = frame.getBoundingClientRect();
299
- const contentWidth = document.documentElement.scrollWidth;
300
- const contentHeight = document.documentElement.scrollHeight;
301
- // (normalizedX * frameWidth + frameLeftOffset)/docSize
302
- const fullX = (normX / 100) * width + left;
303
- const fullY = (normY / 100) * height + top;
304
- const fixedX = fullX / contentWidth;
305
- const fixedY = fullY / contentHeight;
306
- fixedMessage = [
307
- type,
308
- id,
309
- hesitationTime,
310
- label,
311
- selector,
312
- Math.round(fixedX * 1e3) / 1e1,
313
- Math.round(fixedY * 1e3) / 1e1,
314
- ];
315
- }
316
- });
317
- return fixedMessage;
318
- }
319
- return msg;
320
- });
321
- this.messages.push(...mappedMessages);
322
- }
323
- };
324
- window.addEventListener('message', catchIframeMessage);
325
- this.attachStopCallback(() => {
326
- window.removeEventListener('message', catchIframeMessage);
327
- });
328
- }
329
- else {
330
- const catchParentMessage = (event) => {
331
- const { data } = event;
332
- if (!data)
333
- return;
334
- if (data.line !== proto.iframeId) {
335
- return;
336
- }
354
+ const catchParentMessage = (event) => {
355
+ if (!this.active())
356
+ return;
357
+ const { data } = event;
358
+ if (!data)
359
+ return;
360
+ if (data.line === proto.parentAlive) {
361
+ this.parentActive = true;
362
+ }
363
+ if (data.line === proto.iframeId) {
364
+ this.parentActive = true;
337
365
  this.rootId = data.id;
338
366
  this.session.setSessionToken(data.token);
339
367
  this.frameOderNumber = data.frameOrderNumber;
340
368
  this.debug.log('starting iframe tracking', data);
341
369
  this.allowAppStart();
342
- };
343
- window.addEventListener('message', catchParentMessage);
344
- this.attachStopCallback(() => {
345
- window.removeEventListener('message', catchParentMessage);
346
- });
347
- // communicating with parent window,
348
- // even if its crossdomain is possible via postMessage api
349
- const domain = this.initialHostName;
350
- window.parent.postMessage({
351
- line: proto.iframeSignal,
352
- source: thisTab,
353
- context: this.contextId,
354
- domain,
355
- }, '*');
356
- }
370
+ }
371
+ };
372
+ window.addEventListener('message', catchParentMessage);
357
373
  if (this.bc !== null) {
358
374
  this.bc.postMessage({
359
375
  line: proto.ask,
@@ -399,11 +415,29 @@ class App {
399
415
  this.startTimeout = null;
400
416
  }
401
417
  }
402
- checkNodeId(iframes, domain) {
418
+ async checkNodeId(iframes, domain) {
403
419
  for (const iframe of iframes) {
404
420
  if (iframe.dataset.domain === domain) {
405
- // @ts-ignore
406
- return iframe[this.options.node_id];
421
+ /**
422
+ * Here we're trying to get node id from the iframe (which is kept in observer)
423
+ * because of async nature of dom initialization, we give 100 retries with 100ms delay each
424
+ * which equals to 10 seconds. This way we have a period where we give app some time to load
425
+ * and tracker some time to parse the initial DOM tree even on slower devices
426
+ * */
427
+ let tries = 0;
428
+ while (tries < 100) {
429
+ // @ts-ignore
430
+ const potentialId = iframe[this.options.node_id];
431
+ if (potentialId !== undefined) {
432
+ tries = 100;
433
+ return potentialId;
434
+ }
435
+ else {
436
+ tries++;
437
+ await delay(100);
438
+ }
439
+ }
440
+ return null;
407
441
  }
408
442
  }
409
443
  return null;
@@ -528,7 +562,7 @@ class App {
528
562
  line: proto.iframeBatch,
529
563
  messages: this.messages,
530
564
  domain: this.initialHostName,
531
- }, '*');
565
+ }, this.options.crossdomain?.parentDomain ?? '*');
532
566
  this.commitCallbacks.forEach((cb) => cb(this.messages));
533
567
  this.messages.length = 0;
534
568
  return;
@@ -537,7 +571,6 @@ class App {
537
571
  (0, utils_js_1.requestIdleCb)(() => {
538
572
  this.messages.unshift((0, messages_gen_js_1.TabData)(this.session.getTabId()));
539
573
  this.messages.unshift((0, messages_gen_js_1.Timestamp)(this.timestamp()));
540
- // why I need to add opt chaining?
541
574
  this.worker?.postMessage(this.messages);
542
575
  this.commitCallbacks.forEach((cb) => cb(this.messages));
543
576
  this.messages.length = 0;
@@ -613,7 +646,6 @@ class App {
613
646
  }
614
647
  this.stopCallbacks.push(cb);
615
648
  }
616
- // Use app.nodes.attachNodeListener for registered nodes instead
617
649
  attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
618
650
  if (useSafe) {
619
651
  listener = this.safe(listener);
@@ -1080,6 +1112,9 @@ class App {
1080
1112
  }
1081
1113
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1082
1114
  this.activityState = ActivityState.Active;
1115
+ if (this.options.crossdomain?.enabled || this.insideIframe) {
1116
+ this.crossdomainIframesModule();
1117
+ }
1083
1118
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1084
1119
  this.canvasRecorder =
1085
1120
  this.canvasRecorder ??
@@ -1090,7 +1125,6 @@ class App {
1090
1125
  fixedScaling: this.options.canvas.fixedCanvasScaling,
1091
1126
  useAnimationFrame: this.options.canvas.useAnimationFrame,
1092
1127
  });
1093
- this.canvasRecorder.startTracking();
1094
1128
  }
1095
1129
  /** --------------- COLD START BUFFER ------------------*/
1096
1130
  if (isColdStart) {
@@ -1113,8 +1147,11 @@ class App {
1113
1147
  }
1114
1148
  this.ticker.start();
1115
1149
  }
1150
+ this.canvasRecorder?.startTracking();
1116
1151
  if (this.features['usability-test']) {
1117
- this.uxtManager = this.uxtManager ? this.uxtManager : new index_js_1.default(this, uxtStorageKey);
1152
+ this.uxtManager = this.uxtManager
1153
+ ? this.uxtManager
1154
+ : new index_js_1.default(this, uxtStorageKey);
1118
1155
  let uxtId;
1119
1156
  const savedUxtTag = this.localStorage.getItem(uxtStorageKey);
1120
1157
  if (savedUxtTag) {
@@ -1147,6 +1184,11 @@ class App {
1147
1184
  catch (reason) {
1148
1185
  this.stop();
1149
1186
  this.session.reset();
1187
+ if (!reason) {
1188
+ console.error('Unknown error during start');
1189
+ this.signalError('Unknown error', []);
1190
+ return UnsuccessfulStart('Unknown error');
1191
+ }
1150
1192
  if (reason === CANCELED) {
1151
1193
  this.signalError(CANCELED, []);
1152
1194
  return UnsuccessfulStart(CANCELED);
@@ -1195,6 +1237,9 @@ class App {
1195
1237
  * and here we just apply 10ms delay just in case
1196
1238
  * */
1197
1239
  async start(...args) {
1240
+ if (this.insideIframe) {
1241
+ this.signalIframeTracker();
1242
+ }
1198
1243
  if (this.activityState === ActivityState.Active ||
1199
1244
  this.activityState === ActivityState.Starting) {
1200
1245
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
@@ -1261,6 +1306,7 @@ class App {
1261
1306
  this.worker.postMessage('stop');
1262
1307
  }
1263
1308
  this.canvasRecorder?.clear();
1309
+ this.messages.length = 0;
1264
1310
  }
1265
1311
  finally {
1266
1312
  this.activityState = ActivityState.NotActive;
package/cjs/index.js CHANGED
@@ -98,7 +98,7 @@ class API {
98
98
  const orig = this.options.ingestPoint || index_js_1.DEFAULT_INGEST_POINT;
99
99
  req.open('POST', orig + '/v1/web/not-started');
100
100
  req.send(JSON.stringify({
101
- trackerVersion: '14.0.8',
101
+ trackerVersion: '14.0.9',
102
102
  projectKey: this.options.projectKey,
103
103
  doNotTrack,
104
104
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
package/lib/app/canvas.js CHANGED
@@ -104,10 +104,8 @@ class CanvasRecorder {
104
104
  startTracking() {
105
105
  setTimeout(() => {
106
106
  this.app.nodes.scanTree(this.captureCanvas);
107
- this.app.nodes.attachNodeCallback((node) => {
108
- this.captureCanvas(node);
109
- });
110
- }, 500);
107
+ this.app.nodes.attachNodeCallback(this.captureCanvas);
108
+ }, 250);
111
109
  }
112
110
  sendSnaps(images, canvasId, createdAt) {
113
111
  if (Object.keys(this.snapshots).length === 0) {
@@ -143,6 +143,13 @@ export default class App {
143
143
  private readonly initialHostName;
144
144
  private features;
145
145
  constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>, signalError: (error: string, apis: string[]) => void, insideIframe: boolean);
146
+ /** used by child iframes for crossdomain only */
147
+ parentActive: boolean;
148
+ checkStatus: () => boolean;
149
+ /** used by child iframes for crossdomain only */
150
+ /** track app instances in crossdomain child iframes */
151
+ crossdomainIframesModule: () => void;
152
+ signalIframeTracker: () => void;
146
153
  startTimeout: ReturnType<typeof setTimeout> | null;
147
154
  private allowAppStart;
148
155
  private checkNodeId;
package/lib/app/index.js CHANGED
@@ -49,6 +49,8 @@ const proto = {
49
49
  iframeId: 'never-gonna-say-goodbye',
50
50
  // batch of messages from an iframe window
51
51
  iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you',
52
+ // signal that parent is live
53
+ parentAlive: 'i-dont-know-more-lines',
52
54
  };
53
55
  export default class App {
54
56
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
@@ -65,7 +67,7 @@ export default class App {
65
67
  this.stopCallbacks = [];
66
68
  this.commitCallbacks = [];
67
69
  this.activityState = ActivityState.NotActive;
68
- this.version = '14.0.8'; // TODO: version compatability check inside each plugin.
70
+ this.version = '14.0.9'; // TODO: version compatability check inside each plugin.
69
71
  this.socketMode = false;
70
72
  this.compressionThreshold = 24 * 1000;
71
73
  this.bc = null;
@@ -80,6 +82,128 @@ export default class App {
80
82
  'feature-flags': true,
81
83
  'usability-test': true,
82
84
  };
85
+ /** used by child iframes for crossdomain only */
86
+ this.parentActive = false;
87
+ this.checkStatus = () => {
88
+ return this.parentActive;
89
+ };
90
+ /** used by child iframes for crossdomain only */
91
+ /** track app instances in crossdomain child iframes */
92
+ this.crossdomainIframesModule = () => {
93
+ if (!this.insideIframe) {
94
+ /**
95
+ * if we get a signal from child iframes, we check for their node_id and send it back,
96
+ * so they can act as if it was just a same-domain iframe
97
+ * */
98
+ let crossdomainFrameCount = 0;
99
+ const catchIframeMessage = (event) => {
100
+ if (!this.active())
101
+ return;
102
+ const { data } = event;
103
+ if (!data)
104
+ return;
105
+ if (data.line === proto.iframeSignal) {
106
+ // @ts-ignore
107
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
108
+ const childIframeDomain = data.domain;
109
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
110
+ this.pageFrames = pageIframes;
111
+ const signalId = async () => {
112
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
113
+ if (id) {
114
+ try {
115
+ await this.waitStarted();
116
+ crossdomainFrameCount++;
117
+ const token = this.session.getSessionToken();
118
+ const iframeData = {
119
+ line: proto.iframeId,
120
+ context: this.contextId,
121
+ domain: childIframeDomain,
122
+ id,
123
+ token,
124
+ frameOrderNumber: crossdomainFrameCount,
125
+ };
126
+ this.debug.log('iframe_data', iframeData);
127
+ // @ts-ignore
128
+ event.source?.postMessage(iframeData, '*');
129
+ }
130
+ catch (e) {
131
+ console.error(e);
132
+ }
133
+ }
134
+ };
135
+ void signalId();
136
+ }
137
+ /**
138
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
139
+ * plus we rewrite some of the messages to be relative to the main context/window
140
+ * */
141
+ if (data.line === proto.iframeBatch) {
142
+ const msgBatch = data.messages;
143
+ const mappedMessages = msgBatch.map((msg) => {
144
+ if (msg[0] === 20 /* MType.MouseMove */) {
145
+ let fixedMessage = msg;
146
+ this.pageFrames.forEach((frame) => {
147
+ if (frame.dataset.domain === event.data.domain) {
148
+ const [type, x, y] = msg;
149
+ const { left, top } = frame.getBoundingClientRect();
150
+ fixedMessage = [type, x + left, y + top];
151
+ }
152
+ });
153
+ return fixedMessage;
154
+ }
155
+ if (msg[0] === 68 /* MType.MouseClick */) {
156
+ let fixedMessage = msg;
157
+ this.pageFrames.forEach((frame) => {
158
+ if (frame.dataset.domain === event.data.domain) {
159
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
160
+ const { left, top, width, height } = frame.getBoundingClientRect();
161
+ const contentWidth = document.documentElement.scrollWidth;
162
+ const contentHeight = document.documentElement.scrollHeight;
163
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
164
+ const fullX = (normX / 100) * width + left;
165
+ const fullY = (normY / 100) * height + top;
166
+ const fixedX = fullX / contentWidth;
167
+ const fixedY = fullY / contentHeight;
168
+ fixedMessage = [
169
+ type,
170
+ id,
171
+ hesitationTime,
172
+ label,
173
+ selector,
174
+ Math.round(fixedX * 1e3) / 1e1,
175
+ Math.round(fixedY * 1e3) / 1e1,
176
+ ];
177
+ }
178
+ });
179
+ return fixedMessage;
180
+ }
181
+ return msg;
182
+ });
183
+ this.messages.push(...mappedMessages);
184
+ }
185
+ };
186
+ window.addEventListener('message', catchIframeMessage);
187
+ }
188
+ };
189
+ this.signalIframeTracker = () => {
190
+ const domain = this.initialHostName;
191
+ const thisTab = this.session.getTabId();
192
+ const signalToParent = (n) => {
193
+ window.parent.postMessage({
194
+ line: proto.iframeSignal,
195
+ source: thisTab,
196
+ context: this.contextId,
197
+ domain,
198
+ }, this.options.crossdomain?.parentDomain ?? '*');
199
+ setTimeout(() => {
200
+ if (!this.checkStatus() && n < 100) {
201
+ void signalToParent(n + 1);
202
+ }
203
+ }, 250);
204
+ };
205
+ void signalToParent(1);
206
+ };
83
207
  this.startTimeout = null;
84
208
  this.coldStartCommitN = 0;
85
209
  this.delay = 0;
@@ -198,133 +322,25 @@ export default class App {
198
322
  }
199
323
  this.initWorker();
200
324
  const thisTab = this.session.getTabId();
201
- if (!this.insideIframe) {
202
- /**
203
- * if we get a signal from child iframes, we check for their node_id and send it back,
204
- * so they can act as if it was just a same-domain iframe
205
- * */
206
- let crossdomainFrameCount = 0;
207
- const catchIframeMessage = (event) => {
208
- const { data } = event;
209
- if (!data)
210
- return;
211
- if (data.line === proto.iframeSignal) {
212
- const childIframeDomain = data.domain;
213
- const pageIframes = Array.from(document.querySelectorAll('iframe'));
214
- this.pageFrames = pageIframes;
215
- const signalId = async () => {
216
- let tries = 0;
217
- while (tries < 10) {
218
- const id = this.checkNodeId(pageIframes, childIframeDomain);
219
- if (id) {
220
- this.waitStarted()
221
- .then(() => {
222
- crossdomainFrameCount++;
223
- const token = this.session.getSessionToken();
224
- const iframeData = {
225
- line: proto.iframeId,
226
- context: this.contextId,
227
- domain: childIframeDomain,
228
- id,
229
- token,
230
- frameOrderNumber: crossdomainFrameCount,
231
- };
232
- this.debug.log('iframe_data', iframeData);
233
- // @ts-ignore
234
- event.source?.postMessage(iframeData, '*');
235
- })
236
- .catch(console.error);
237
- tries = 10;
238
- break;
239
- }
240
- tries++;
241
- await delay(100);
242
- }
243
- };
244
- void signalId();
245
- }
246
- /**
247
- * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
248
- * plus we rewrite some of the messages to be relative to the main context/window
249
- * */
250
- if (data.line === proto.iframeBatch) {
251
- const msgBatch = data.messages;
252
- const mappedMessages = msgBatch.map((msg) => {
253
- if (msg[0] === 20 /* MType.MouseMove */) {
254
- let fixedMessage = msg;
255
- this.pageFrames.forEach((frame) => {
256
- if (frame.dataset.domain === event.data.domain) {
257
- const [type, x, y] = msg;
258
- const { left, top } = frame.getBoundingClientRect();
259
- fixedMessage = [type, x + left, y + top];
260
- }
261
- });
262
- return fixedMessage;
263
- }
264
- if (msg[0] === 68 /* MType.MouseClick */) {
265
- let fixedMessage = msg;
266
- this.pageFrames.forEach((frame) => {
267
- if (frame.dataset.domain === event.data.domain) {
268
- const [type, id, hesitationTime, label, selector, normX, normY] = msg;
269
- const { left, top, width, height } = frame.getBoundingClientRect();
270
- const contentWidth = document.documentElement.scrollWidth;
271
- const contentHeight = document.documentElement.scrollHeight;
272
- // (normalizedX * frameWidth + frameLeftOffset)/docSize
273
- const fullX = (normX / 100) * width + left;
274
- const fullY = (normY / 100) * height + top;
275
- const fixedX = fullX / contentWidth;
276
- const fixedY = fullY / contentHeight;
277
- fixedMessage = [
278
- type,
279
- id,
280
- hesitationTime,
281
- label,
282
- selector,
283
- Math.round(fixedX * 1e3) / 1e1,
284
- Math.round(fixedY * 1e3) / 1e1,
285
- ];
286
- }
287
- });
288
- return fixedMessage;
289
- }
290
- return msg;
291
- });
292
- this.messages.push(...mappedMessages);
293
- }
294
- };
295
- window.addEventListener('message', catchIframeMessage);
296
- this.attachStopCallback(() => {
297
- window.removeEventListener('message', catchIframeMessage);
298
- });
299
- }
300
- else {
301
- const catchParentMessage = (event) => {
302
- const { data } = event;
303
- if (!data)
304
- return;
305
- if (data.line !== proto.iframeId) {
306
- return;
307
- }
325
+ const catchParentMessage = (event) => {
326
+ if (!this.active())
327
+ return;
328
+ const { data } = event;
329
+ if (!data)
330
+ return;
331
+ if (data.line === proto.parentAlive) {
332
+ this.parentActive = true;
333
+ }
334
+ if (data.line === proto.iframeId) {
335
+ this.parentActive = true;
308
336
  this.rootId = data.id;
309
337
  this.session.setSessionToken(data.token);
310
338
  this.frameOderNumber = data.frameOrderNumber;
311
339
  this.debug.log('starting iframe tracking', data);
312
340
  this.allowAppStart();
313
- };
314
- window.addEventListener('message', catchParentMessage);
315
- this.attachStopCallback(() => {
316
- window.removeEventListener('message', catchParentMessage);
317
- });
318
- // communicating with parent window,
319
- // even if its crossdomain is possible via postMessage api
320
- const domain = this.initialHostName;
321
- window.parent.postMessage({
322
- line: proto.iframeSignal,
323
- source: thisTab,
324
- context: this.contextId,
325
- domain,
326
- }, '*');
327
- }
341
+ }
342
+ };
343
+ window.addEventListener('message', catchParentMessage);
328
344
  if (this.bc !== null) {
329
345
  this.bc.postMessage({
330
346
  line: proto.ask,
@@ -370,11 +386,29 @@ export default class App {
370
386
  this.startTimeout = null;
371
387
  }
372
388
  }
373
- checkNodeId(iframes, domain) {
389
+ async checkNodeId(iframes, domain) {
374
390
  for (const iframe of iframes) {
375
391
  if (iframe.dataset.domain === domain) {
376
- // @ts-ignore
377
- return iframe[this.options.node_id];
392
+ /**
393
+ * Here we're trying to get node id from the iframe (which is kept in observer)
394
+ * because of async nature of dom initialization, we give 100 retries with 100ms delay each
395
+ * which equals to 10 seconds. This way we have a period where we give app some time to load
396
+ * and tracker some time to parse the initial DOM tree even on slower devices
397
+ * */
398
+ let tries = 0;
399
+ while (tries < 100) {
400
+ // @ts-ignore
401
+ const potentialId = iframe[this.options.node_id];
402
+ if (potentialId !== undefined) {
403
+ tries = 100;
404
+ return potentialId;
405
+ }
406
+ else {
407
+ tries++;
408
+ await delay(100);
409
+ }
410
+ }
411
+ return null;
378
412
  }
379
413
  }
380
414
  return null;
@@ -499,7 +533,7 @@ export default class App {
499
533
  line: proto.iframeBatch,
500
534
  messages: this.messages,
501
535
  domain: this.initialHostName,
502
- }, '*');
536
+ }, this.options.crossdomain?.parentDomain ?? '*');
503
537
  this.commitCallbacks.forEach((cb) => cb(this.messages));
504
538
  this.messages.length = 0;
505
539
  return;
@@ -508,7 +542,6 @@ export default class App {
508
542
  requestIdleCb(() => {
509
543
  this.messages.unshift(TabData(this.session.getTabId()));
510
544
  this.messages.unshift(Timestamp(this.timestamp()));
511
- // why I need to add opt chaining?
512
545
  this.worker?.postMessage(this.messages);
513
546
  this.commitCallbacks.forEach((cb) => cb(this.messages));
514
547
  this.messages.length = 0;
@@ -584,7 +617,6 @@ export default class App {
584
617
  }
585
618
  this.stopCallbacks.push(cb);
586
619
  }
587
- // Use app.nodes.attachNodeListener for registered nodes instead
588
620
  attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
589
621
  if (useSafe) {
590
622
  listener = this.safe(listener);
@@ -1051,6 +1083,9 @@ export default class App {
1051
1083
  }
1052
1084
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1053
1085
  this.activityState = ActivityState.Active;
1086
+ if (this.options.crossdomain?.enabled || this.insideIframe) {
1087
+ this.crossdomainIframesModule();
1088
+ }
1054
1089
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1055
1090
  this.canvasRecorder =
1056
1091
  this.canvasRecorder ??
@@ -1061,7 +1096,6 @@ export default class App {
1061
1096
  fixedScaling: this.options.canvas.fixedCanvasScaling,
1062
1097
  useAnimationFrame: this.options.canvas.useAnimationFrame,
1063
1098
  });
1064
- this.canvasRecorder.startTracking();
1065
1099
  }
1066
1100
  /** --------------- COLD START BUFFER ------------------*/
1067
1101
  if (isColdStart) {
@@ -1084,8 +1118,11 @@ export default class App {
1084
1118
  }
1085
1119
  this.ticker.start();
1086
1120
  }
1121
+ this.canvasRecorder?.startTracking();
1087
1122
  if (this.features['usability-test']) {
1088
- this.uxtManager = this.uxtManager ? this.uxtManager : new UserTestManager(this, uxtStorageKey);
1123
+ this.uxtManager = this.uxtManager
1124
+ ? this.uxtManager
1125
+ : new UserTestManager(this, uxtStorageKey);
1089
1126
  let uxtId;
1090
1127
  const savedUxtTag = this.localStorage.getItem(uxtStorageKey);
1091
1128
  if (savedUxtTag) {
@@ -1118,6 +1155,11 @@ export default class App {
1118
1155
  catch (reason) {
1119
1156
  this.stop();
1120
1157
  this.session.reset();
1158
+ if (!reason) {
1159
+ console.error('Unknown error during start');
1160
+ this.signalError('Unknown error', []);
1161
+ return UnsuccessfulStart('Unknown error');
1162
+ }
1121
1163
  if (reason === CANCELED) {
1122
1164
  this.signalError(CANCELED, []);
1123
1165
  return UnsuccessfulStart(CANCELED);
@@ -1166,6 +1208,9 @@ export default class App {
1166
1208
  * and here we just apply 10ms delay just in case
1167
1209
  * */
1168
1210
  async start(...args) {
1211
+ if (this.insideIframe) {
1212
+ this.signalIframeTracker();
1213
+ }
1169
1214
  if (this.activityState === ActivityState.Active ||
1170
1215
  this.activityState === ActivityState.Starting) {
1171
1216
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
@@ -1232,6 +1277,7 @@ export default class App {
1232
1277
  this.worker.postMessage('stop');
1233
1278
  }
1234
1279
  this.canvasRecorder?.clear();
1280
+ this.messages.length = 0;
1235
1281
  }
1236
1282
  finally {
1237
1283
  this.activityState = ActivityState.NotActive;
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.8',
70
+ trackerVersion: '14.0.9',
71
71
  projectKey: this.options.projectKey,
72
72
  doNotTrack,
73
73
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openreplay/tracker",
3
3
  "description": "The OpenReplay tracker main package",
4
- "version": "14.0.8",
4
+ "version": "14.0.9",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"