@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 +2 -4
- package/cjs/app/index.d.ts +7 -0
- package/cjs/app/index.js +173 -130
- package/cjs/index.js +1 -1
- package/lib/app/canvas.js +2 -4
- package/lib/app/index.d.ts +7 -0
- package/lib/app/index.js +173 -130
- package/lib/index.js +1 -1
- package/package.json +1 -1
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(
|
|
110
|
-
|
|
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) {
|
package/cjs/app/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
|
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.
|
|
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(
|
|
108
|
-
|
|
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) {
|
package/lib/app/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
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.
|
|
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,
|