@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 +5 -0
- package/cjs/app/canvas.js +2 -4
- package/cjs/app/index.d.ts +7 -0
- package/cjs/app/index.js +177 -131
- 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 +177 -131
- package/lib/index.js +1 -1
- package/package.json +1 -1
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(
|
|
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'; // 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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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'; // 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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
|
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.
|
|
70
|
+
trackerVersion: '14.0.9',
|
|
71
71
|
projectKey: this.options.projectKey,
|
|
72
72
|
doNotTrack,
|
|
73
73
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|