@openreplay/tracker 14.0.8 → 14.0.9-beta.1

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/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-beta.1'; // 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,129 @@ 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
+ const { data } = event;
130
+ if (!data)
131
+ return;
132
+ if (data.line === proto.iframeSignal) {
133
+ // @ts-ignore
134
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
135
+ const childIframeDomain = data.domain;
136
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
137
+ this.pageFrames = pageIframes;
138
+ const signalId = async () => {
139
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
140
+ if (id) {
141
+ try {
142
+ await this.waitStarted();
143
+ crossdomainFrameCount++;
144
+ const token = this.session.getSessionToken();
145
+ const iframeData = {
146
+ line: proto.iframeId,
147
+ context: this.contextId,
148
+ domain: childIframeDomain,
149
+ id,
150
+ token,
151
+ frameOrderNumber: crossdomainFrameCount,
152
+ };
153
+ this.debug.log('iframe_data', iframeData);
154
+ // @ts-ignore
155
+ event.source?.postMessage(iframeData, '*');
156
+ }
157
+ catch (e) {
158
+ console.error(e);
159
+ }
160
+ }
161
+ };
162
+ void signalId();
163
+ }
164
+ /**
165
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
166
+ * plus we rewrite some of the messages to be relative to the main context/window
167
+ * */
168
+ if (data.line === proto.iframeBatch) {
169
+ const msgBatch = data.messages;
170
+ const mappedMessages = msgBatch.map((msg) => {
171
+ if (msg[0] === 20 /* MType.MouseMove */) {
172
+ let fixedMessage = msg;
173
+ this.pageFrames.forEach((frame) => {
174
+ if (frame.dataset.domain === event.data.domain) {
175
+ const [type, x, y] = msg;
176
+ const { left, top } = frame.getBoundingClientRect();
177
+ fixedMessage = [type, x + left, y + top];
178
+ }
179
+ });
180
+ return fixedMessage;
181
+ }
182
+ if (msg[0] === 68 /* MType.MouseClick */) {
183
+ let fixedMessage = msg;
184
+ this.pageFrames.forEach((frame) => {
185
+ if (frame.dataset.domain === event.data.domain) {
186
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
187
+ const { left, top, width, height } = frame.getBoundingClientRect();
188
+ const contentWidth = document.documentElement.scrollWidth;
189
+ const contentHeight = document.documentElement.scrollHeight;
190
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
191
+ const fullX = (normX / 100) * width + left;
192
+ const fullY = (normY / 100) * height + top;
193
+ const fixedX = fullX / contentWidth;
194
+ const fixedY = fullY / contentHeight;
195
+ fixedMessage = [
196
+ type,
197
+ id,
198
+ hesitationTime,
199
+ label,
200
+ selector,
201
+ Math.round(fixedX * 1e3) / 1e1,
202
+ Math.round(fixedY * 1e3) / 1e1,
203
+ ];
204
+ }
205
+ });
206
+ return fixedMessage;
207
+ }
208
+ return msg;
209
+ });
210
+ this.messages.push(...mappedMessages);
211
+ }
212
+ };
213
+ window.addEventListener('message', catchIframeMessage);
214
+ this.attachStopCallback(() => {
215
+ window.removeEventListener('message', catchIframeMessage);
216
+ });
217
+ }
218
+ };
219
+ this.signalIframeTracker = () => {
220
+ const domain = this.initialHostName;
221
+ const thisTab = this.session.getTabId();
222
+ const signalToParent = (n) => {
223
+ window.parent.postMessage({
224
+ line: proto.iframeSignal,
225
+ source: thisTab,
226
+ context: this.contextId,
227
+ domain,
228
+ }, this.options.crossdomain?.parentDomain ?? '*');
229
+ setTimeout(() => {
230
+ if (!this.checkStatus() && n < 100) {
231
+ void signalToParent(n + 1);
232
+ }
233
+ }, 250);
234
+ };
235
+ void signalToParent(1);
236
+ };
112
237
  this.startTimeout = null;
113
238
  this.coldStartCommitN = 0;
114
239
  this.delay = 0;
@@ -227,133 +352,26 @@ class App {
227
352
  }
228
353
  this.initWorker();
229
354
  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
- }
355
+ const catchParentMessage = (event) => {
356
+ const { data } = event;
357
+ if (!data)
358
+ return;
359
+ if (data.line === proto.parentAlive) {
360
+ this.parentActive = true;
361
+ }
362
+ if (data.line === proto.iframeId) {
363
+ this.parentActive = true;
337
364
  this.rootId = data.id;
338
365
  this.session.setSessionToken(data.token);
339
366
  this.frameOderNumber = data.frameOrderNumber;
340
367
  this.debug.log('starting iframe tracking', data);
341
368
  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
- }
369
+ }
370
+ };
371
+ window.addEventListener('message', catchParentMessage);
372
+ this.attachStopCallback(() => {
373
+ window.removeEventListener('message', catchParentMessage);
374
+ });
357
375
  if (this.bc !== null) {
358
376
  this.bc.postMessage({
359
377
  line: proto.ask,
@@ -399,11 +417,29 @@ class App {
399
417
  this.startTimeout = null;
400
418
  }
401
419
  }
402
- checkNodeId(iframes, domain) {
420
+ async checkNodeId(iframes, domain) {
403
421
  for (const iframe of iframes) {
404
422
  if (iframe.dataset.domain === domain) {
405
- // @ts-ignore
406
- return iframe[this.options.node_id];
423
+ /**
424
+ * Here we're trying to get node id from the iframe (which is kept in observer)
425
+ * because of async nature of dom initialization, we give 100 retries with 100ms delay each
426
+ * which equals to 10 seconds. This way we have a period where we give app some time to load
427
+ * and tracker some time to parse the initial DOM tree even on slower devices
428
+ * */
429
+ let tries = 0;
430
+ while (tries < 100) {
431
+ // @ts-ignore
432
+ const potentialId = iframe[this.options.node_id];
433
+ if (potentialId !== undefined) {
434
+ tries = 100;
435
+ return potentialId;
436
+ }
437
+ else {
438
+ tries++;
439
+ await delay(100);
440
+ }
441
+ }
442
+ return null;
407
443
  }
408
444
  }
409
445
  return null;
@@ -528,7 +564,7 @@ class App {
528
564
  line: proto.iframeBatch,
529
565
  messages: this.messages,
530
566
  domain: this.initialHostName,
531
- }, '*');
567
+ }, this.options.crossdomain?.parentDomain ?? '*');
532
568
  this.commitCallbacks.forEach((cb) => cb(this.messages));
533
569
  this.messages.length = 0;
534
570
  return;
@@ -613,7 +649,6 @@ class App {
613
649
  }
614
650
  this.stopCallbacks.push(cb);
615
651
  }
616
- // Use app.nodes.attachNodeListener for registered nodes instead
617
652
  attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
618
653
  if (useSafe) {
619
654
  listener = this.safe(listener);
@@ -1080,6 +1115,9 @@ class App {
1080
1115
  }
1081
1116
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1082
1117
  this.activityState = ActivityState.Active;
1118
+ if (this.options.crossdomain?.enabled || this.insideIframe) {
1119
+ this.crossdomainIframesModule();
1120
+ }
1083
1121
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1084
1122
  this.canvasRecorder =
1085
1123
  this.canvasRecorder ??
@@ -1090,7 +1128,6 @@ class App {
1090
1128
  fixedScaling: this.options.canvas.fixedCanvasScaling,
1091
1129
  useAnimationFrame: this.options.canvas.useAnimationFrame,
1092
1130
  });
1093
- this.canvasRecorder.startTracking();
1094
1131
  }
1095
1132
  /** --------------- COLD START BUFFER ------------------*/
1096
1133
  if (isColdStart) {
@@ -1113,8 +1150,11 @@ class App {
1113
1150
  }
1114
1151
  this.ticker.start();
1115
1152
  }
1153
+ this.canvasRecorder?.startTracking();
1116
1154
  if (this.features['usability-test']) {
1117
- this.uxtManager = this.uxtManager ? this.uxtManager : new index_js_1.default(this, uxtStorageKey);
1155
+ this.uxtManager = this.uxtManager
1156
+ ? this.uxtManager
1157
+ : new index_js_1.default(this, uxtStorageKey);
1118
1158
  let uxtId;
1119
1159
  const savedUxtTag = this.localStorage.getItem(uxtStorageKey);
1120
1160
  if (savedUxtTag) {
@@ -1195,6 +1235,9 @@ class App {
1195
1235
  * and here we just apply 10ms delay just in case
1196
1236
  * */
1197
1237
  async start(...args) {
1238
+ if (this.insideIframe) {
1239
+ this.signalIframeTracker();
1240
+ }
1198
1241
  if (this.activityState === ActivityState.Active ||
1199
1242
  this.activityState === ActivityState.Starting) {
1200
1243
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
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-beta.1',
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-beta.1'; // 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,129 @@ 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
+ const { data } = event;
101
+ if (!data)
102
+ return;
103
+ if (data.line === proto.iframeSignal) {
104
+ // @ts-ignore
105
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
106
+ const childIframeDomain = data.domain;
107
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
108
+ this.pageFrames = pageIframes;
109
+ const signalId = async () => {
110
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
111
+ if (id) {
112
+ try {
113
+ await this.waitStarted();
114
+ crossdomainFrameCount++;
115
+ const token = this.session.getSessionToken();
116
+ const iframeData = {
117
+ line: proto.iframeId,
118
+ context: this.contextId,
119
+ domain: childIframeDomain,
120
+ id,
121
+ token,
122
+ frameOrderNumber: crossdomainFrameCount,
123
+ };
124
+ this.debug.log('iframe_data', iframeData);
125
+ // @ts-ignore
126
+ event.source?.postMessage(iframeData, '*');
127
+ }
128
+ catch (e) {
129
+ console.error(e);
130
+ }
131
+ }
132
+ };
133
+ void signalId();
134
+ }
135
+ /**
136
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
137
+ * plus we rewrite some of the messages to be relative to the main context/window
138
+ * */
139
+ if (data.line === proto.iframeBatch) {
140
+ const msgBatch = data.messages;
141
+ const mappedMessages = msgBatch.map((msg) => {
142
+ if (msg[0] === 20 /* MType.MouseMove */) {
143
+ let fixedMessage = msg;
144
+ this.pageFrames.forEach((frame) => {
145
+ if (frame.dataset.domain === event.data.domain) {
146
+ const [type, x, y] = msg;
147
+ const { left, top } = frame.getBoundingClientRect();
148
+ fixedMessage = [type, x + left, y + top];
149
+ }
150
+ });
151
+ return fixedMessage;
152
+ }
153
+ if (msg[0] === 68 /* MType.MouseClick */) {
154
+ let fixedMessage = msg;
155
+ this.pageFrames.forEach((frame) => {
156
+ if (frame.dataset.domain === event.data.domain) {
157
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
158
+ const { left, top, width, height } = frame.getBoundingClientRect();
159
+ const contentWidth = document.documentElement.scrollWidth;
160
+ const contentHeight = document.documentElement.scrollHeight;
161
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
162
+ const fullX = (normX / 100) * width + left;
163
+ const fullY = (normY / 100) * height + top;
164
+ const fixedX = fullX / contentWidth;
165
+ const fixedY = fullY / contentHeight;
166
+ fixedMessage = [
167
+ type,
168
+ id,
169
+ hesitationTime,
170
+ label,
171
+ selector,
172
+ Math.round(fixedX * 1e3) / 1e1,
173
+ Math.round(fixedY * 1e3) / 1e1,
174
+ ];
175
+ }
176
+ });
177
+ return fixedMessage;
178
+ }
179
+ return msg;
180
+ });
181
+ this.messages.push(...mappedMessages);
182
+ }
183
+ };
184
+ window.addEventListener('message', catchIframeMessage);
185
+ this.attachStopCallback(() => {
186
+ window.removeEventListener('message', catchIframeMessage);
187
+ });
188
+ }
189
+ };
190
+ this.signalIframeTracker = () => {
191
+ const domain = this.initialHostName;
192
+ const thisTab = this.session.getTabId();
193
+ const signalToParent = (n) => {
194
+ window.parent.postMessage({
195
+ line: proto.iframeSignal,
196
+ source: thisTab,
197
+ context: this.contextId,
198
+ domain,
199
+ }, this.options.crossdomain?.parentDomain ?? '*');
200
+ setTimeout(() => {
201
+ if (!this.checkStatus() && n < 100) {
202
+ void signalToParent(n + 1);
203
+ }
204
+ }, 250);
205
+ };
206
+ void signalToParent(1);
207
+ };
83
208
  this.startTimeout = null;
84
209
  this.coldStartCommitN = 0;
85
210
  this.delay = 0;
@@ -198,133 +323,26 @@ export default class App {
198
323
  }
199
324
  this.initWorker();
200
325
  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
- }
326
+ const catchParentMessage = (event) => {
327
+ const { data } = event;
328
+ if (!data)
329
+ return;
330
+ if (data.line === proto.parentAlive) {
331
+ this.parentActive = true;
332
+ }
333
+ if (data.line === proto.iframeId) {
334
+ this.parentActive = true;
308
335
  this.rootId = data.id;
309
336
  this.session.setSessionToken(data.token);
310
337
  this.frameOderNumber = data.frameOrderNumber;
311
338
  this.debug.log('starting iframe tracking', data);
312
339
  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
- }
340
+ }
341
+ };
342
+ window.addEventListener('message', catchParentMessage);
343
+ this.attachStopCallback(() => {
344
+ window.removeEventListener('message', catchParentMessage);
345
+ });
328
346
  if (this.bc !== null) {
329
347
  this.bc.postMessage({
330
348
  line: proto.ask,
@@ -370,11 +388,29 @@ export default class App {
370
388
  this.startTimeout = null;
371
389
  }
372
390
  }
373
- checkNodeId(iframes, domain) {
391
+ async checkNodeId(iframes, domain) {
374
392
  for (const iframe of iframes) {
375
393
  if (iframe.dataset.domain === domain) {
376
- // @ts-ignore
377
- return iframe[this.options.node_id];
394
+ /**
395
+ * Here we're trying to get node id from the iframe (which is kept in observer)
396
+ * because of async nature of dom initialization, we give 100 retries with 100ms delay each
397
+ * which equals to 10 seconds. This way we have a period where we give app some time to load
398
+ * and tracker some time to parse the initial DOM tree even on slower devices
399
+ * */
400
+ let tries = 0;
401
+ while (tries < 100) {
402
+ // @ts-ignore
403
+ const potentialId = iframe[this.options.node_id];
404
+ if (potentialId !== undefined) {
405
+ tries = 100;
406
+ return potentialId;
407
+ }
408
+ else {
409
+ tries++;
410
+ await delay(100);
411
+ }
412
+ }
413
+ return null;
378
414
  }
379
415
  }
380
416
  return null;
@@ -499,7 +535,7 @@ export default class App {
499
535
  line: proto.iframeBatch,
500
536
  messages: this.messages,
501
537
  domain: this.initialHostName,
502
- }, '*');
538
+ }, this.options.crossdomain?.parentDomain ?? '*');
503
539
  this.commitCallbacks.forEach((cb) => cb(this.messages));
504
540
  this.messages.length = 0;
505
541
  return;
@@ -584,7 +620,6 @@ export default class App {
584
620
  }
585
621
  this.stopCallbacks.push(cb);
586
622
  }
587
- // Use app.nodes.attachNodeListener for registered nodes instead
588
623
  attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
589
624
  if (useSafe) {
590
625
  listener = this.safe(listener);
@@ -1051,6 +1086,9 @@ export default class App {
1051
1086
  }
1052
1087
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1053
1088
  this.activityState = ActivityState.Active;
1089
+ if (this.options.crossdomain?.enabled || this.insideIframe) {
1090
+ this.crossdomainIframesModule();
1091
+ }
1054
1092
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1055
1093
  this.canvasRecorder =
1056
1094
  this.canvasRecorder ??
@@ -1061,7 +1099,6 @@ export default class App {
1061
1099
  fixedScaling: this.options.canvas.fixedCanvasScaling,
1062
1100
  useAnimationFrame: this.options.canvas.useAnimationFrame,
1063
1101
  });
1064
- this.canvasRecorder.startTracking();
1065
1102
  }
1066
1103
  /** --------------- COLD START BUFFER ------------------*/
1067
1104
  if (isColdStart) {
@@ -1084,8 +1121,11 @@ export default class App {
1084
1121
  }
1085
1122
  this.ticker.start();
1086
1123
  }
1124
+ this.canvasRecorder?.startTracking();
1087
1125
  if (this.features['usability-test']) {
1088
- this.uxtManager = this.uxtManager ? this.uxtManager : new UserTestManager(this, uxtStorageKey);
1126
+ this.uxtManager = this.uxtManager
1127
+ ? this.uxtManager
1128
+ : new UserTestManager(this, uxtStorageKey);
1089
1129
  let uxtId;
1090
1130
  const savedUxtTag = this.localStorage.getItem(uxtStorageKey);
1091
1131
  if (savedUxtTag) {
@@ -1166,6 +1206,9 @@ export default class App {
1166
1206
  * and here we just apply 10ms delay just in case
1167
1207
  * */
1168
1208
  async start(...args) {
1209
+ if (this.insideIframe) {
1210
+ this.signalIframeTracker();
1211
+ }
1169
1212
  if (this.activityState === ActivityState.Active ||
1170
1213
  this.activityState === ActivityState.Starting) {
1171
1214
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
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-beta.1',
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-beta.1",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"