@openreplay/tracker 14.0.9 → 14.0.10-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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 14.0.10
2
+
3
+ - adjust timestamps for messages from tracker instances inside child iframes (if they were loaded later)
4
+ - restart child trackers if parent tracker is restarted
5
+ - fixes for general stability of crossdomain iframe tracking
6
+
1
7
  # 14.0.9
2
8
 
3
9
  - more stable crossdomain iframe tracking (refactored child/parent process discovery)
@@ -37,6 +37,12 @@ declare const SuccessfulStart: (body: OnStartInfo) => SuccessfulStart;
37
37
  export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart;
38
38
  type StartCallback = (i: OnStartInfo) => void;
39
39
  type CommitCallback = (messages: Array<Message>) => void;
40
+ declare enum ActivityState {
41
+ NotActive = 0,
42
+ Starting = 1,
43
+ Active = 2,
44
+ ColdStart = 3
45
+ }
40
46
  type AppOptions = {
41
47
  revID: string;
42
48
  node_id: string;
@@ -99,7 +105,7 @@ export type Options = AppOptions & ObserverOptions & SanitizerOptions;
99
105
  export declare const DEFAULT_INGEST_POINT = "https://api.openreplay.com/ingest";
100
106
  export default class App {
101
107
  private readonly signalError;
102
- private readonly insideIframe;
108
+ readonly insideIframe: boolean;
103
109
  readonly nodes: Nodes;
104
110
  readonly ticker: Ticker;
105
111
  readonly projectKey: string;
@@ -146,12 +152,15 @@ export default class App {
146
152
  /** used by child iframes for crossdomain only */
147
153
  parentActive: boolean;
148
154
  checkStatus: () => boolean;
149
- /** used by child iframes for crossdomain only */
150
- /** track app instances in crossdomain child iframes */
151
- crossdomainIframesModule: () => void;
155
+ parentCrossDomainFrameListener: (event: MessageEvent) => void;
156
+ trackedFrames: number[];
157
+ crossDomainIframeListener: (event: MessageEvent) => void;
158
+ pollingQueue: string[];
159
+ bootChildrenFrames: () => Promise<void>;
160
+ killChildrenFrames: () => void;
152
161
  signalIframeTracker: () => void;
153
162
  startTimeout: ReturnType<typeof setTimeout> | null;
154
- private allowAppStart;
163
+ allowAppStart(): void;
155
164
  private checkNodeId;
156
165
  private initWorker;
157
166
  private handleWorkerMsg;
@@ -176,9 +185,9 @@ export default class App {
176
185
  timestamp(): number;
177
186
  safe<T extends (this: any, ...args: any[]) => void>(fn: T): T;
178
187
  attachCommitCallback(cb: CommitCallback): void;
179
- attachStartCallback(cb: StartCallback, useSafe?: boolean): void;
180
- attachStopCallback(cb: () => any, useSafe?: boolean): void;
181
- attachEventListener(target: EventTarget, type: string, listener: EventListener, useSafe?: boolean, useCapture?: boolean): void;
188
+ attachStartCallback: (cb: StartCallback, useSafe?: boolean) => void;
189
+ attachStopCallback: (cb: () => any, useSafe?: boolean) => void;
190
+ attachEventListener: (target: EventTarget, type: string, listener: EventListener, useSafe?: boolean, useCapture?: boolean) => void;
182
191
  checkRequiredVersion(version: string): boolean;
183
192
  private getTrackerInfo;
184
193
  getSessionInfo(): {
@@ -260,6 +269,7 @@ export default class App {
260
269
  getUxtId(): number | null;
261
270
  waitStart(): Promise<unknown>;
262
271
  waitStarted(): Promise<unknown>;
272
+ waitStatus(status: ActivityState): Promise<unknown>;
263
273
  /**
264
274
  * basically we ask other tabs during constructor
265
275
  * and here we just apply 10ms delay just in case
package/cjs/app/index.js CHANGED
@@ -72,14 +72,14 @@ const proto = {
72
72
  resp: 'never-gonna-let-you-down',
73
73
  // regenerating id (copied other tab)
74
74
  reg: 'never-gonna-run-around-and-desert-you',
75
- // tracker inside a child iframe
76
- iframeSignal: 'never-gonna-make-you-cry',
77
- // getting node id for child iframe
78
- iframeId: 'never-gonna-say-goodbye',
79
- // batch of messages from an iframe window
80
- iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you',
81
- // signal that parent is live
82
- parentAlive: 'i-dont-know-more-lines',
75
+ iframeSignal: 'tracker inside a child iframe',
76
+ iframeId: 'getting node id for child iframe',
77
+ iframeBatch: 'batch of messages from an iframe window',
78
+ parentAlive: 'signal that parent is live',
79
+ killIframe: 'stop tracker inside frame',
80
+ startIframe: 'start tracker inside frame',
81
+ // checking updates
82
+ polling: 'hello-how-are-you-im-under-the-water-please-help-me',
83
83
  };
84
84
  class App {
85
85
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
@@ -96,7 +96,7 @@ class App {
96
96
  this.stopCallbacks = [];
97
97
  this.commitCallbacks = [];
98
98
  this.activityState = ActivityState.NotActive;
99
- this.version = '14.0.9'; // TODO: version compatability check inside each plugin.
99
+ this.version = '14.0.10-beta.1'; // TODO: version compatability check inside each plugin.
100
100
  this.socketMode = false;
101
101
  this.compressionThreshold = 24 * 1000;
102
102
  this.bc = null;
@@ -116,104 +116,144 @@ class App {
116
116
  this.checkStatus = () => {
117
117
  return this.parentActive;
118
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();
119
+ this.parentCrossDomainFrameListener = (event) => {
120
+ const { data } = event;
121
+ if (!data || event.source === window)
122
+ return;
123
+ if (data.line === proto.startIframe) {
124
+ if (this.active())
125
+ return;
126
+ try {
127
+ this.allowAppStart();
128
+ void this.start();
129
+ }
130
+ catch (e) {
131
+ console.error('children frame restart failed:', e);
132
+ }
133
+ }
134
+ if (data.line === proto.parentAlive) {
135
+ this.parentActive = true;
136
+ }
137
+ if (data.line === proto.iframeId) {
138
+ this.parentActive = true;
139
+ this.rootId = data.id;
140
+ this.session.setSessionToken(data.token);
141
+ this.frameOderNumber = data.frameOrderNumber;
142
+ this.debug.log('starting iframe tracking', data);
143
+ this.allowAppStart();
144
+ this.delay = data.frameTimeOffset;
145
+ }
146
+ if (data.line === proto.killIframe) {
147
+ if (this.active()) {
148
+ this.stop();
149
+ }
150
+ }
151
+ };
152
+ this.trackedFrames = [];
153
+ this.crossDomainIframeListener = (event) => {
154
+ if (!this.active() || event.source === window)
155
+ return;
156
+ const { data } = event;
157
+ if (!data)
158
+ return;
159
+ if (data.line === proto.iframeSignal) {
160
+ // @ts-ignore
161
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
162
+ const childIframeDomain = data.domain;
163
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
164
+ this.pageFrames = pageIframes;
165
+ const signalId = async () => {
166
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
167
+ if (id && !this.trackedFrames.includes(id)) {
168
+ try {
169
+ this.trackedFrames.push(id);
170
+ await this.waitStarted();
171
+ const token = this.session.getSessionToken();
172
+ const iframeData = {
173
+ line: proto.iframeId,
174
+ context: this.contextId,
175
+ domain: childIframeDomain,
176
+ id,
177
+ token,
178
+ frameOrderNumber: this.trackedFrames.length,
179
+ frameTimeOffset: this.timestamp(),
180
+ };
181
+ // @ts-ignore
182
+ event.source?.postMessage(iframeData, '*');
183
+ }
184
+ catch (e) {
185
+ console.error(e);
186
+ }
165
187
  }
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;
188
+ };
189
+ void signalId();
190
+ }
191
+ /**
192
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
193
+ * plus we rewrite some of the messages to be relative to the main context/window
194
+ * */
195
+ if (data.line === proto.iframeBatch) {
196
+ const msgBatch = data.messages;
197
+ const mappedMessages = msgBatch.map((msg) => {
198
+ if (msg[0] === 20 /* MType.MouseMove */) {
199
+ let fixedMessage = msg;
200
+ this.pageFrames.forEach((frame) => {
201
+ if (frame.dataset.domain === event.data.domain) {
202
+ const [type, x, y] = msg;
203
+ const { left, top } = frame.getBoundingClientRect();
204
+ fixedMessage = [type, x + left, y + top];
183
205
  }
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;
206
+ });
207
+ return fixedMessage;
208
+ }
209
+ if (msg[0] === 68 /* MType.MouseClick */) {
210
+ let fixedMessage = msg;
211
+ this.pageFrames.forEach((frame) => {
212
+ if (frame.dataset.domain === event.data.domain) {
213
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
214
+ const { left, top, width, height } = frame.getBoundingClientRect();
215
+ const contentWidth = document.documentElement.scrollWidth;
216
+ const contentHeight = document.documentElement.scrollHeight;
217
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
218
+ const fullX = (normX / 100) * width + left;
219
+ const fullY = (normY / 100) * height + top;
220
+ const fixedX = fullX / contentWidth;
221
+ const fixedY = fullY / contentHeight;
222
+ fixedMessage = [
223
+ type,
224
+ id,
225
+ hesitationTime,
226
+ label,
227
+ selector,
228
+ Math.round(fixedX * 1e3) / 1e1,
229
+ Math.round(fixedY * 1e3) / 1e1,
230
+ ];
209
231
  }
210
- return msg;
211
232
  });
212
- this.messages.push(...mappedMessages);
233
+ return fixedMessage;
213
234
  }
214
- };
215
- window.addEventListener('message', catchIframeMessage);
235
+ return msg;
236
+ });
237
+ this.messages.push(...mappedMessages);
216
238
  }
239
+ if (data.line === proto.polling) {
240
+ if (!this.pollingQueue.length) {
241
+ return;
242
+ }
243
+ while (this.pollingQueue.length) {
244
+ const msg = this.pollingQueue.shift();
245
+ // @ts-ignore
246
+ event.source?.postMessage({ line: msg }, '*');
247
+ }
248
+ }
249
+ };
250
+ this.pollingQueue = [];
251
+ this.bootChildrenFrames = async () => {
252
+ await this.waitStarted();
253
+ this.pollingQueue.push(proto.startIframe);
254
+ };
255
+ this.killChildrenFrames = () => {
256
+ this.pollingQueue.push(proto.killIframe);
217
257
  };
218
258
  this.signalIframeTracker = () => {
219
259
  const domain = this.initialHostName;
@@ -236,6 +276,27 @@ class App {
236
276
  this.startTimeout = null;
237
277
  this.coldStartCommitN = 0;
238
278
  this.delay = 0;
279
+ this.attachStartCallback = (cb, useSafe = false) => {
280
+ if (useSafe) {
281
+ cb = this.safe(cb);
282
+ }
283
+ this.startCallbacks.push(cb);
284
+ };
285
+ this.attachStopCallback = (cb, useSafe = false) => {
286
+ if (useSafe) {
287
+ cb = this.safe(cb);
288
+ }
289
+ this.stopCallbacks.push(cb);
290
+ };
291
+ this.attachEventListener = (target, type, listener, useSafe = true, useCapture = true) => {
292
+ if (useSafe) {
293
+ listener = this.safe(listener);
294
+ }
295
+ const createListener = () => target ? (0, utils_js_1.createEventListener)(target, type, listener, useCapture) : null;
296
+ const deleteListener = () => target ? (0, utils_js_1.deleteEventListener)(target, type, listener, useCapture) : null;
297
+ this.attachStartCallback(createListener, useSafe);
298
+ this.attachStopCallback(deleteListener, useSafe);
299
+ };
239
300
  this.coldInterval = null;
240
301
  this.orderNumber = 0;
241
302
  this.coldStartTs = 0;
@@ -351,25 +412,24 @@ class App {
351
412
  }
352
413
  this.initWorker();
353
414
  const thisTab = this.session.getTabId();
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;
365
- this.rootId = data.id;
366
- this.session.setSessionToken(data.token);
367
- this.frameOderNumber = data.frameOrderNumber;
368
- this.debug.log('starting iframe tracking', data);
369
- this.allowAppStart();
370
- }
371
- };
372
- window.addEventListener('message', catchParentMessage);
415
+ /**
416
+ * listen for messages from parent window, so we can signal that we're alive
417
+ * */
418
+ if (this.insideIframe) {
419
+ window.addEventListener('message', this.parentCrossDomainFrameListener);
420
+ setInterval(() => {
421
+ window.parent.postMessage({
422
+ line: proto.polling,
423
+ }, '*');
424
+ }, 250);
425
+ }
426
+ /**
427
+ * if we get a signal from child iframes, we check for their node_id and send it back,
428
+ * so they can act as if it was just a same-domain iframe
429
+ * */
430
+ if (!this.insideIframe) {
431
+ window.addEventListener('message', this.crossDomainIframeListener);
432
+ }
373
433
  if (this.bc !== null) {
374
434
  this.bc.postMessage({
375
435
  line: proto.ask,
@@ -378,7 +438,7 @@ class App {
378
438
  });
379
439
  this.startTimeout = setTimeout(() => {
380
440
  this.allowAppStart();
381
- }, 500);
441
+ }, 250);
382
442
  this.bc.onmessage = (ev) => {
383
443
  if (ev.data.context === this.contextId) {
384
444
  return;
@@ -634,27 +694,6 @@ class App {
634
694
  attachCommitCallback(cb) {
635
695
  this.commitCallbacks.push(cb);
636
696
  }
637
- attachStartCallback(cb, useSafe = false) {
638
- if (useSafe) {
639
- cb = this.safe(cb);
640
- }
641
- this.startCallbacks.push(cb);
642
- }
643
- attachStopCallback(cb, useSafe = false) {
644
- if (useSafe) {
645
- cb = this.safe(cb);
646
- }
647
- this.stopCallbacks.push(cb);
648
- }
649
- attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
650
- if (useSafe) {
651
- listener = this.safe(listener);
652
- }
653
- const createListener = () => target ? (0, utils_js_1.createEventListener)(target, type, listener, useCapture) : null;
654
- const deleteListener = () => target ? (0, utils_js_1.deleteEventListener)(target, type, listener, useCapture) : null;
655
- this.attachStartCallback(createListener, useSafe);
656
- this.attachStopCallback(deleteListener, useSafe);
657
- }
658
697
  // TODO: full correct semantic
659
698
  checkRequiredVersion(version) {
660
699
  const reqVer = version.split(/[.-]/);
@@ -1112,8 +1151,8 @@ class App {
1112
1151
  }
1113
1152
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1114
1153
  this.activityState = ActivityState.Active;
1115
- if (this.options.crossdomain?.enabled || this.insideIframe) {
1116
- this.crossdomainIframesModule();
1154
+ if (this.options.crossdomain?.enabled && !this.insideIframe) {
1155
+ void this.bootChildrenFrames();
1117
1156
  }
1118
1157
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1119
1158
  this.canvasRecorder =
@@ -1220,9 +1259,12 @@ class App {
1220
1259
  });
1221
1260
  }
1222
1261
  async waitStarted() {
1262
+ return this.waitStatus(ActivityState.Active);
1263
+ }
1264
+ async waitStatus(status) {
1223
1265
  return new Promise((resolve) => {
1224
1266
  const check = () => {
1225
- if (this.activityState === ActivityState.Active) {
1267
+ if (this.activityState === status) {
1226
1268
  resolve(true);
1227
1269
  }
1228
1270
  else {
@@ -1237,14 +1279,14 @@ class App {
1237
1279
  * and here we just apply 10ms delay just in case
1238
1280
  * */
1239
1281
  async start(...args) {
1240
- if (this.insideIframe) {
1241
- this.signalIframeTracker();
1242
- }
1243
1282
  if (this.activityState === ActivityState.Active ||
1244
1283
  this.activityState === ActivityState.Starting) {
1245
1284
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
1246
1285
  return Promise.resolve(UnsuccessfulStart(reason));
1247
1286
  }
1287
+ if (this.insideIframe) {
1288
+ this.signalIframeTracker();
1289
+ }
1248
1290
  if (!document.hidden) {
1249
1291
  await this.waitStart();
1250
1292
  return this._start(...args);
@@ -1293,23 +1335,30 @@ class App {
1293
1335
  }
1294
1336
  stop(stopWorker = true) {
1295
1337
  if (this.activityState !== ActivityState.NotActive) {
1338
+ console.trace('stopped');
1296
1339
  try {
1340
+ if (!this.insideIframe && this.options.crossdomain?.enabled) {
1341
+ this.killChildrenFrames();
1342
+ }
1297
1343
  this.attributeSender.clear();
1298
1344
  this.sanitizer.clear();
1299
1345
  this.observer.disconnect();
1300
1346
  this.nodes.clear();
1301
1347
  this.ticker.stop();
1302
1348
  this.stopCallbacks.forEach((cb) => cb());
1303
- this.debug.log('OpenReplay tracking stopped.');
1304
1349
  this.tagWatcher.clear();
1305
1350
  if (this.worker && stopWorker) {
1306
1351
  this.worker.postMessage('stop');
1307
1352
  }
1308
1353
  this.canvasRecorder?.clear();
1309
1354
  this.messages.length = 0;
1355
+ this.trackedFrames = [];
1356
+ this.parentActive = false;
1357
+ this.canStart = false;
1310
1358
  }
1311
1359
  finally {
1312
1360
  this.activityState = ActivityState.NotActive;
1361
+ this.debug.log('OpenReplay tracking stopped.');
1313
1362
  }
1314
1363
  }
1315
1364
  }
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.9',
101
+ trackerVersion: '14.0.10-beta.1',
102
102
  projectKey: this.options.projectKey,
103
103
  doNotTrack,
104
104
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -69,8 +69,13 @@ function default_1(app, opts) {
69
69
  app.send(msg);
70
70
  }
71
71
  }
72
- app.attachEventListener(context, 'unhandledrejection', handler);
73
- app.attachEventListener(context, 'error', handler);
72
+ try {
73
+ app.attachEventListener(context, 'unhandledrejection', handler);
74
+ app.attachEventListener(context, 'error', handler);
75
+ }
76
+ catch (e) {
77
+ console.error('Error while attaching to error proto contexts', e);
78
+ }
74
79
  }
75
80
  if (options.captureExceptions) {
76
81
  app.observer.attachContextCallback(patchContext); // TODO: attach once-per-iframe (?)
package/cjs/utils.js CHANGED
@@ -131,9 +131,9 @@ function createEventListener(target, event, cb, capture) {
131
131
  }
132
132
  catch (e) {
133
133
  const msg = e.message;
134
- console.debug(
134
+ console.error(
135
135
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
136
- `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`);
136
+ `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
137
137
  }
138
138
  }
139
139
  exports.createEventListener = createEventListener;
@@ -144,9 +144,9 @@ function deleteEventListener(target, event, cb, capture) {
144
144
  }
145
145
  catch (e) {
146
146
  const msg = e.message;
147
- console.debug(
147
+ console.error(
148
148
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
149
- `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`);
149
+ `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
150
150
  }
151
151
  }
152
152
  exports.deleteEventListener = deleteEventListener;
@@ -37,6 +37,12 @@ declare const SuccessfulStart: (body: OnStartInfo) => SuccessfulStart;
37
37
  export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart;
38
38
  type StartCallback = (i: OnStartInfo) => void;
39
39
  type CommitCallback = (messages: Array<Message>) => void;
40
+ declare enum ActivityState {
41
+ NotActive = 0,
42
+ Starting = 1,
43
+ Active = 2,
44
+ ColdStart = 3
45
+ }
40
46
  type AppOptions = {
41
47
  revID: string;
42
48
  node_id: string;
@@ -99,7 +105,7 @@ export type Options = AppOptions & ObserverOptions & SanitizerOptions;
99
105
  export declare const DEFAULT_INGEST_POINT = "https://api.openreplay.com/ingest";
100
106
  export default class App {
101
107
  private readonly signalError;
102
- private readonly insideIframe;
108
+ readonly insideIframe: boolean;
103
109
  readonly nodes: Nodes;
104
110
  readonly ticker: Ticker;
105
111
  readonly projectKey: string;
@@ -146,12 +152,15 @@ export default class App {
146
152
  /** used by child iframes for crossdomain only */
147
153
  parentActive: boolean;
148
154
  checkStatus: () => boolean;
149
- /** used by child iframes for crossdomain only */
150
- /** track app instances in crossdomain child iframes */
151
- crossdomainIframesModule: () => void;
155
+ parentCrossDomainFrameListener: (event: MessageEvent) => void;
156
+ trackedFrames: number[];
157
+ crossDomainIframeListener: (event: MessageEvent) => void;
158
+ pollingQueue: string[];
159
+ bootChildrenFrames: () => Promise<void>;
160
+ killChildrenFrames: () => void;
152
161
  signalIframeTracker: () => void;
153
162
  startTimeout: ReturnType<typeof setTimeout> | null;
154
- private allowAppStart;
163
+ allowAppStart(): void;
155
164
  private checkNodeId;
156
165
  private initWorker;
157
166
  private handleWorkerMsg;
@@ -176,9 +185,9 @@ export default class App {
176
185
  timestamp(): number;
177
186
  safe<T extends (this: any, ...args: any[]) => void>(fn: T): T;
178
187
  attachCommitCallback(cb: CommitCallback): void;
179
- attachStartCallback(cb: StartCallback, useSafe?: boolean): void;
180
- attachStopCallback(cb: () => any, useSafe?: boolean): void;
181
- attachEventListener(target: EventTarget, type: string, listener: EventListener, useSafe?: boolean, useCapture?: boolean): void;
188
+ attachStartCallback: (cb: StartCallback, useSafe?: boolean) => void;
189
+ attachStopCallback: (cb: () => any, useSafe?: boolean) => void;
190
+ attachEventListener: (target: EventTarget, type: string, listener: EventListener, useSafe?: boolean, useCapture?: boolean) => void;
182
191
  checkRequiredVersion(version: string): boolean;
183
192
  private getTrackerInfo;
184
193
  getSessionInfo(): {
@@ -260,6 +269,7 @@ export default class App {
260
269
  getUxtId(): number | null;
261
270
  waitStart(): Promise<unknown>;
262
271
  waitStarted(): Promise<unknown>;
272
+ waitStatus(status: ActivityState): Promise<unknown>;
263
273
  /**
264
274
  * basically we ask other tabs during constructor
265
275
  * and here we just apply 10ms delay just in case
package/lib/app/index.js CHANGED
@@ -43,14 +43,14 @@ const proto = {
43
43
  resp: 'never-gonna-let-you-down',
44
44
  // regenerating id (copied other tab)
45
45
  reg: 'never-gonna-run-around-and-desert-you',
46
- // tracker inside a child iframe
47
- iframeSignal: 'never-gonna-make-you-cry',
48
- // getting node id for child iframe
49
- iframeId: 'never-gonna-say-goodbye',
50
- // batch of messages from an iframe window
51
- iframeBatch: 'never-gonna-tell-a-lie-and-hurt-you',
52
- // signal that parent is live
53
- parentAlive: 'i-dont-know-more-lines',
46
+ iframeSignal: 'tracker inside a child iframe',
47
+ iframeId: 'getting node id for child iframe',
48
+ iframeBatch: 'batch of messages from an iframe window',
49
+ parentAlive: 'signal that parent is live',
50
+ killIframe: 'stop tracker inside frame',
51
+ startIframe: 'start tracker inside frame',
52
+ // checking updates
53
+ polling: 'hello-how-are-you-im-under-the-water-please-help-me',
54
54
  };
55
55
  export default class App {
56
56
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
@@ -67,7 +67,7 @@ export default class App {
67
67
  this.stopCallbacks = [];
68
68
  this.commitCallbacks = [];
69
69
  this.activityState = ActivityState.NotActive;
70
- this.version = '14.0.9'; // TODO: version compatability check inside each plugin.
70
+ this.version = '14.0.10-beta.1'; // TODO: version compatability check inside each plugin.
71
71
  this.socketMode = false;
72
72
  this.compressionThreshold = 24 * 1000;
73
73
  this.bc = null;
@@ -87,104 +87,144 @@ export default class App {
87
87
  this.checkStatus = () => {
88
88
  return this.parentActive;
89
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();
90
+ this.parentCrossDomainFrameListener = (event) => {
91
+ const { data } = event;
92
+ if (!data || event.source === window)
93
+ return;
94
+ if (data.line === proto.startIframe) {
95
+ if (this.active())
96
+ return;
97
+ try {
98
+ this.allowAppStart();
99
+ void this.start();
100
+ }
101
+ catch (e) {
102
+ console.error('children frame restart failed:', e);
103
+ }
104
+ }
105
+ if (data.line === proto.parentAlive) {
106
+ this.parentActive = true;
107
+ }
108
+ if (data.line === proto.iframeId) {
109
+ this.parentActive = true;
110
+ this.rootId = data.id;
111
+ this.session.setSessionToken(data.token);
112
+ this.frameOderNumber = data.frameOrderNumber;
113
+ this.debug.log('starting iframe tracking', data);
114
+ this.allowAppStart();
115
+ this.delay = data.frameTimeOffset;
116
+ }
117
+ if (data.line === proto.killIframe) {
118
+ if (this.active()) {
119
+ this.stop();
120
+ }
121
+ }
122
+ };
123
+ this.trackedFrames = [];
124
+ this.crossDomainIframeListener = (event) => {
125
+ if (!this.active() || event.source === window)
126
+ return;
127
+ const { data } = event;
128
+ if (!data)
129
+ return;
130
+ if (data.line === proto.iframeSignal) {
131
+ // @ts-ignore
132
+ event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
133
+ const childIframeDomain = data.domain;
134
+ const pageIframes = Array.from(document.querySelectorAll('iframe'));
135
+ this.pageFrames = pageIframes;
136
+ const signalId = async () => {
137
+ const id = await this.checkNodeId(pageIframes, childIframeDomain);
138
+ if (id && !this.trackedFrames.includes(id)) {
139
+ try {
140
+ this.trackedFrames.push(id);
141
+ await this.waitStarted();
142
+ const token = this.session.getSessionToken();
143
+ const iframeData = {
144
+ line: proto.iframeId,
145
+ context: this.contextId,
146
+ domain: childIframeDomain,
147
+ id,
148
+ token,
149
+ frameOrderNumber: this.trackedFrames.length,
150
+ frameTimeOffset: this.timestamp(),
151
+ };
152
+ // @ts-ignore
153
+ event.source?.postMessage(iframeData, '*');
154
+ }
155
+ catch (e) {
156
+ console.error(e);
157
+ }
136
158
  }
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;
159
+ };
160
+ void signalId();
161
+ }
162
+ /**
163
+ * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
164
+ * plus we rewrite some of the messages to be relative to the main context/window
165
+ * */
166
+ if (data.line === proto.iframeBatch) {
167
+ const msgBatch = data.messages;
168
+ const mappedMessages = msgBatch.map((msg) => {
169
+ if (msg[0] === 20 /* MType.MouseMove */) {
170
+ let fixedMessage = msg;
171
+ this.pageFrames.forEach((frame) => {
172
+ if (frame.dataset.domain === event.data.domain) {
173
+ const [type, x, y] = msg;
174
+ const { left, top } = frame.getBoundingClientRect();
175
+ fixedMessage = [type, x + left, y + top];
154
176
  }
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;
177
+ });
178
+ return fixedMessage;
179
+ }
180
+ if (msg[0] === 68 /* MType.MouseClick */) {
181
+ let fixedMessage = msg;
182
+ this.pageFrames.forEach((frame) => {
183
+ if (frame.dataset.domain === event.data.domain) {
184
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
185
+ const { left, top, width, height } = frame.getBoundingClientRect();
186
+ const contentWidth = document.documentElement.scrollWidth;
187
+ const contentHeight = document.documentElement.scrollHeight;
188
+ // (normalizedX * frameWidth + frameLeftOffset)/docSize
189
+ const fullX = (normX / 100) * width + left;
190
+ const fullY = (normY / 100) * height + top;
191
+ const fixedX = fullX / contentWidth;
192
+ const fixedY = fullY / contentHeight;
193
+ fixedMessage = [
194
+ type,
195
+ id,
196
+ hesitationTime,
197
+ label,
198
+ selector,
199
+ Math.round(fixedX * 1e3) / 1e1,
200
+ Math.round(fixedY * 1e3) / 1e1,
201
+ ];
180
202
  }
181
- return msg;
182
203
  });
183
- this.messages.push(...mappedMessages);
204
+ return fixedMessage;
184
205
  }
185
- };
186
- window.addEventListener('message', catchIframeMessage);
206
+ return msg;
207
+ });
208
+ this.messages.push(...mappedMessages);
187
209
  }
210
+ if (data.line === proto.polling) {
211
+ if (!this.pollingQueue.length) {
212
+ return;
213
+ }
214
+ while (this.pollingQueue.length) {
215
+ const msg = this.pollingQueue.shift();
216
+ // @ts-ignore
217
+ event.source?.postMessage({ line: msg }, '*');
218
+ }
219
+ }
220
+ };
221
+ this.pollingQueue = [];
222
+ this.bootChildrenFrames = async () => {
223
+ await this.waitStarted();
224
+ this.pollingQueue.push(proto.startIframe);
225
+ };
226
+ this.killChildrenFrames = () => {
227
+ this.pollingQueue.push(proto.killIframe);
188
228
  };
189
229
  this.signalIframeTracker = () => {
190
230
  const domain = this.initialHostName;
@@ -207,6 +247,27 @@ export default class App {
207
247
  this.startTimeout = null;
208
248
  this.coldStartCommitN = 0;
209
249
  this.delay = 0;
250
+ this.attachStartCallback = (cb, useSafe = false) => {
251
+ if (useSafe) {
252
+ cb = this.safe(cb);
253
+ }
254
+ this.startCallbacks.push(cb);
255
+ };
256
+ this.attachStopCallback = (cb, useSafe = false) => {
257
+ if (useSafe) {
258
+ cb = this.safe(cb);
259
+ }
260
+ this.stopCallbacks.push(cb);
261
+ };
262
+ this.attachEventListener = (target, type, listener, useSafe = true, useCapture = true) => {
263
+ if (useSafe) {
264
+ listener = this.safe(listener);
265
+ }
266
+ const createListener = () => target ? createEventListener(target, type, listener, useCapture) : null;
267
+ const deleteListener = () => target ? deleteEventListener(target, type, listener, useCapture) : null;
268
+ this.attachStartCallback(createListener, useSafe);
269
+ this.attachStopCallback(deleteListener, useSafe);
270
+ };
210
271
  this.coldInterval = null;
211
272
  this.orderNumber = 0;
212
273
  this.coldStartTs = 0;
@@ -322,25 +383,24 @@ export default class App {
322
383
  }
323
384
  this.initWorker();
324
385
  const thisTab = this.session.getTabId();
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;
336
- this.rootId = data.id;
337
- this.session.setSessionToken(data.token);
338
- this.frameOderNumber = data.frameOrderNumber;
339
- this.debug.log('starting iframe tracking', data);
340
- this.allowAppStart();
341
- }
342
- };
343
- window.addEventListener('message', catchParentMessage);
386
+ /**
387
+ * listen for messages from parent window, so we can signal that we're alive
388
+ * */
389
+ if (this.insideIframe) {
390
+ window.addEventListener('message', this.parentCrossDomainFrameListener);
391
+ setInterval(() => {
392
+ window.parent.postMessage({
393
+ line: proto.polling,
394
+ }, '*');
395
+ }, 250);
396
+ }
397
+ /**
398
+ * if we get a signal from child iframes, we check for their node_id and send it back,
399
+ * so they can act as if it was just a same-domain iframe
400
+ * */
401
+ if (!this.insideIframe) {
402
+ window.addEventListener('message', this.crossDomainIframeListener);
403
+ }
344
404
  if (this.bc !== null) {
345
405
  this.bc.postMessage({
346
406
  line: proto.ask,
@@ -349,7 +409,7 @@ export default class App {
349
409
  });
350
410
  this.startTimeout = setTimeout(() => {
351
411
  this.allowAppStart();
352
- }, 500);
412
+ }, 250);
353
413
  this.bc.onmessage = (ev) => {
354
414
  if (ev.data.context === this.contextId) {
355
415
  return;
@@ -605,27 +665,6 @@ export default class App {
605
665
  attachCommitCallback(cb) {
606
666
  this.commitCallbacks.push(cb);
607
667
  }
608
- attachStartCallback(cb, useSafe = false) {
609
- if (useSafe) {
610
- cb = this.safe(cb);
611
- }
612
- this.startCallbacks.push(cb);
613
- }
614
- attachStopCallback(cb, useSafe = false) {
615
- if (useSafe) {
616
- cb = this.safe(cb);
617
- }
618
- this.stopCallbacks.push(cb);
619
- }
620
- attachEventListener(target, type, listener, useSafe = true, useCapture = true) {
621
- if (useSafe) {
622
- listener = this.safe(listener);
623
- }
624
- const createListener = () => target ? createEventListener(target, type, listener, useCapture) : null;
625
- const deleteListener = () => target ? deleteEventListener(target, type, listener, useCapture) : null;
626
- this.attachStartCallback(createListener, useSafe);
627
- this.attachStopCallback(deleteListener, useSafe);
628
- }
629
668
  // TODO: full correct semantic
630
669
  checkRequiredVersion(version) {
631
670
  const reqVer = version.split(/[.-]/);
@@ -1083,8 +1122,8 @@ export default class App {
1083
1122
  }
1084
1123
  await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
1085
1124
  this.activityState = ActivityState.Active;
1086
- if (this.options.crossdomain?.enabled || this.insideIframe) {
1087
- this.crossdomainIframesModule();
1125
+ if (this.options.crossdomain?.enabled && !this.insideIframe) {
1126
+ void this.bootChildrenFrames();
1088
1127
  }
1089
1128
  if (canvasEnabled && !this.options.canvas.disableCanvas) {
1090
1129
  this.canvasRecorder =
@@ -1191,9 +1230,12 @@ export default class App {
1191
1230
  });
1192
1231
  }
1193
1232
  async waitStarted() {
1233
+ return this.waitStatus(ActivityState.Active);
1234
+ }
1235
+ async waitStatus(status) {
1194
1236
  return new Promise((resolve) => {
1195
1237
  const check = () => {
1196
- if (this.activityState === ActivityState.Active) {
1238
+ if (this.activityState === status) {
1197
1239
  resolve(true);
1198
1240
  }
1199
1241
  else {
@@ -1208,14 +1250,14 @@ export default class App {
1208
1250
  * and here we just apply 10ms delay just in case
1209
1251
  * */
1210
1252
  async start(...args) {
1211
- if (this.insideIframe) {
1212
- this.signalIframeTracker();
1213
- }
1214
1253
  if (this.activityState === ActivityState.Active ||
1215
1254
  this.activityState === ActivityState.Starting) {
1216
1255
  const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
1217
1256
  return Promise.resolve(UnsuccessfulStart(reason));
1218
1257
  }
1258
+ if (this.insideIframe) {
1259
+ this.signalIframeTracker();
1260
+ }
1219
1261
  if (!document.hidden) {
1220
1262
  await this.waitStart();
1221
1263
  return this._start(...args);
@@ -1264,23 +1306,30 @@ export default class App {
1264
1306
  }
1265
1307
  stop(stopWorker = true) {
1266
1308
  if (this.activityState !== ActivityState.NotActive) {
1309
+ console.trace('stopped');
1267
1310
  try {
1311
+ if (!this.insideIframe && this.options.crossdomain?.enabled) {
1312
+ this.killChildrenFrames();
1313
+ }
1268
1314
  this.attributeSender.clear();
1269
1315
  this.sanitizer.clear();
1270
1316
  this.observer.disconnect();
1271
1317
  this.nodes.clear();
1272
1318
  this.ticker.stop();
1273
1319
  this.stopCallbacks.forEach((cb) => cb());
1274
- this.debug.log('OpenReplay tracking stopped.');
1275
1320
  this.tagWatcher.clear();
1276
1321
  if (this.worker && stopWorker) {
1277
1322
  this.worker.postMessage('stop');
1278
1323
  }
1279
1324
  this.canvasRecorder?.clear();
1280
1325
  this.messages.length = 0;
1326
+ this.trackedFrames = [];
1327
+ this.parentActive = false;
1328
+ this.canStart = false;
1281
1329
  }
1282
1330
  finally {
1283
1331
  this.activityState = ActivityState.NotActive;
1332
+ this.debug.log('OpenReplay tracking stopped.');
1284
1333
  }
1285
1334
  }
1286
1335
  }
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.9',
70
+ trackerVersion: '14.0.10-beta.1',
71
71
  projectKey: this.options.projectKey,
72
72
  doNotTrack,
73
73
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -61,8 +61,13 @@ export default function (app, opts) {
61
61
  app.send(msg);
62
62
  }
63
63
  }
64
- app.attachEventListener(context, 'unhandledrejection', handler);
65
- app.attachEventListener(context, 'error', handler);
64
+ try {
65
+ app.attachEventListener(context, 'unhandledrejection', handler);
66
+ app.attachEventListener(context, 'error', handler);
67
+ }
68
+ catch (e) {
69
+ console.error('Error while attaching to error proto contexts', e);
70
+ }
66
71
  }
67
72
  if (options.captureExceptions) {
68
73
  app.observer.attachContextCallback(patchContext); // TODO: attach once-per-iframe (?)
package/lib/utils.js CHANGED
@@ -116,9 +116,9 @@ export function createEventListener(target, event, cb, capture) {
116
116
  }
117
117
  catch (e) {
118
118
  const msg = e.message;
119
- console.debug(
119
+ console.error(
120
120
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
121
- `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`);
121
+ `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
122
122
  }
123
123
  }
124
124
  export function deleteEventListener(target, event, cb, capture) {
@@ -128,9 +128,9 @@ export function deleteEventListener(target, event, cb, capture) {
128
128
  }
129
129
  catch (e) {
130
130
  const msg = e.message;
131
- console.debug(
131
+ console.error(
132
132
  // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
133
- `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`);
133
+ `Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`, event);
134
134
  }
135
135
  }
136
136
  class FIFOTaskScheduler {
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.9",
4
+ "version": "14.0.10-beta.1",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"