@openreplay/tracker 18.0.1 → 18.0.3

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.
@@ -172,11 +172,10 @@ export default class App {
172
172
  parentActive: boolean;
173
173
  checkStatus: () => boolean;
174
174
  parentCrossDomainFrameListener: (event: MessageEvent) => void;
175
- /**
176
- * context ids for iframes,
177
- * order is not so important as long as its consistent
178
- * */
179
175
  trackedFrames: string[];
176
+ private frameLastSeen;
177
+ private readonly FRAME_STALE_MS;
178
+ private pruneStaleFrames;
180
179
  crossDomainIframeListener: (event: MessageEvent) => void;
181
180
  /**
182
181
  * { command : [remaining iframes] }
@@ -291,6 +290,7 @@ export default class App {
291
290
  * */
292
291
  uploadOfflineRecording(): Promise<void>;
293
292
  prevOpts: StartOptions;
293
+ private userStartCallback?;
294
294
  private _start;
295
295
  restartCanvasTracking: () => void;
296
296
  flushBuffer: (buffer: Message[]) => Promise<unknown>;
@@ -28,6 +28,12 @@ export default abstract class Observer {
28
28
  private readonly inlineRemoteCss;
29
29
  private readonly inlinerOptions;
30
30
  private readonly domParser;
31
+ /**
32
+ * Bumped on every disconnect(). Stale async CSS-inlining callbacks from a
33
+ * previous session (e.g. agent reload → tracker restart) compare against
34
+ * this and bail instead of sending messages that reference vanished node ids.
35
+ */
36
+ private generation;
31
37
  constructor(app: App, isTopContext?: boolean, options?: Options);
32
38
  private clear;
33
39
  /**
package/dist/lib/entry.js CHANGED
@@ -2386,8 +2386,44 @@ function inlineRemoteCss(node, id, baseHref, getNextID, insertRule, addOwner, fo
2386
2386
  })
2387
2387
  .catch(error => {
2388
2388
  console.error(`OpenReplay: Failed to fetch CSS from ${node.href}:`, error);
2389
+ // Fetch failed (likely CORS) — retry via load event + sheet access
2390
+ retryViaLoadEvent();
2389
2391
  });
2390
2392
  }
2393
+ else {
2394
+ retryViaLoadEvent();
2395
+ }
2396
+ function retryViaLoadEvent() {
2397
+ const trySheet = () => {
2398
+ const loadedSheet = node.sheet;
2399
+ if (loadedSheet) {
2400
+ try {
2401
+ const cssText = stringifyStylesheet(loadedSheet);
2402
+ if (cssText) {
2403
+ if (sendPlain && onPlain) {
2404
+ onPlain(cssText, fakeIdHolder++);
2405
+ }
2406
+ else {
2407
+ processCssText(cssText);
2408
+ }
2409
+ return;
2410
+ }
2411
+ }
2412
+ catch (e) {
2413
+ // cross-origin sheet — nothing more we can do
2414
+ }
2415
+ }
2416
+ };
2417
+ if (node.sheet) {
2418
+ trySheet();
2419
+ }
2420
+ else {
2421
+ node.addEventListener('load', function onLoad() {
2422
+ node.removeEventListener('load', onLoad);
2423
+ trySheet();
2424
+ });
2425
+ }
2426
+ }
2391
2427
  function processCssText(cssText) {
2392
2428
  // Remove comments
2393
2429
  cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
@@ -2609,7 +2645,6 @@ function ConstructedStyleSheets (app) {
2609
2645
  if (!hasAdoptedSS(document)) {
2610
2646
  return;
2611
2647
  }
2612
- const styleSheetIDMap = new Map();
2613
2648
  const adoptedStyleSheetsOwnings = new Map();
2614
2649
  const sendAdoptedStyleSheetsUpdate = (root) => setTimeout(() => {
2615
2650
  let nodeID = app.nodes.getID(root);
@@ -2874,6 +2909,12 @@ class Observer {
2874
2909
  this.inlineRemoteCss = false;
2875
2910
  this.inlinerOptions = undefined;
2876
2911
  this.domParser = new DOMParser();
2912
+ /**
2913
+ * Bumped on every disconnect(). Stale async CSS-inlining callbacks from a
2914
+ * previous session (e.g. agent reload → tracker restart) compare against
2915
+ * this and bail instead of sending messages that reference vanished node ids.
2916
+ */
2917
+ this.generation = 0;
2877
2918
  this.throttling = true;
2878
2919
  this.throttledSetNodeData = throttleWithTrailing((id, parentElement, data) => this.sendNodeData(id, parentElement, data), 30);
2879
2920
  this.throttling = !Boolean(options.disableThrottling);
@@ -3020,18 +3061,23 @@ class Observer {
3020
3061
  }
3021
3062
  if (name === 'style' || (name === 'href' && hasTag(node, 'link'))) {
3022
3063
  if ('rel' in node && node.rel === 'stylesheet' && this.inlineRemoteCss) {
3064
+ const gen = this.generation;
3023
3065
  setTimeout(() => {
3024
3066
  inlineRemoteCss(
3025
3067
  // @ts-ignore
3026
3068
  node, id, this.app.getBaseHref(), nextID, (id, cssText, index, baseHref) => {
3069
+ if (this.generation !== gen)
3070
+ return;
3027
3071
  this.app.send(AdoptedSSInsertRuleURLBased(id, cssText, index, baseHref));
3028
3072
  }, (sheetId, ownerId) => {
3073
+ if (this.generation !== gen)
3074
+ return;
3029
3075
  this.app.send(AdoptedSSAddOwner(sheetId, ownerId));
3030
3076
  }, this.inlinerOptions?.forceFetch, this.inlinerOptions?.forcePlain, (cssText, fakeTextId) => {
3077
+ if (this.generation !== gen)
3078
+ return;
3031
3079
  this.app.send(CreateTextNode(fakeTextId, id, 0));
3032
- setTimeout(() => {
3033
- this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref()));
3034
- }, 10);
3080
+ this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref()));
3035
3081
  });
3036
3082
  }, 0);
3037
3083
  return;
@@ -3295,6 +3341,7 @@ class Observer {
3295
3341
  this.observer.disconnect();
3296
3342
  this.clear();
3297
3343
  this.throttledSetNodeData.clear();
3344
+ this.generation++;
3298
3345
  }
3299
3346
  }
3300
3347
 
@@ -3958,7 +4005,7 @@ class App {
3958
4005
  this.stopCallbacks = [];
3959
4006
  this.commitCallbacks = [];
3960
4007
  this.activityState = ActivityState.NotActive;
3961
- this.version = '18.0.1'; // TODO: version compatability check inside each plugin.
4008
+ this.version = '18.0.3'; // TODO: version compatability check inside each plugin.
3962
4009
  this.socketMode = false;
3963
4010
  this.compressionThreshold = 24 * 1000;
3964
4011
  this.bc = null;
@@ -3980,12 +4027,19 @@ class App {
3980
4027
  if (!data || event.source === window)
3981
4028
  return;
3982
4029
  if (data.line === proto.startIframe) {
3983
- if (this.active())
4030
+ // Avoid corrupting an in-flight start; let it complete.
4031
+ if (this.activityState === ActivityState.Starting)
3984
4032
  return;
3985
4033
  try {
4034
+ if (this.active()) {
4035
+ this.stop();
4036
+ }
3986
4037
  if (data.token) {
3987
4038
  this.session.setSessionToken(data.token, this.projectKey);
3988
4039
  }
4040
+ if (data.id !== undefined) {
4041
+ this.rootId = data.id;
4042
+ }
3989
4043
  this.allowAppStart();
3990
4044
  void this.start();
3991
4045
  }
@@ -4011,17 +4065,22 @@ class App {
4011
4065
  }
4012
4066
  }
4013
4067
  };
4014
- /**
4015
- * context ids for iframes,
4016
- * order is not so important as long as its consistent
4017
- * */
4018
4068
  this.trackedFrames = [];
4069
+ this.frameLastSeen = new Map();
4070
+ this.FRAME_STALE_MS = 1500;
4019
4071
  this.crossDomainIframeListener = (event) => {
4020
- if (!this.active() || event.source === window)
4072
+ if (event.source === window)
4021
4073
  return;
4022
4074
  const { data } = event;
4023
4075
  if (!data)
4024
4076
  return;
4077
+ // Record liveness regardless of our own active state so the queue can prune
4078
+ // stale contexts reliably once we resume dispatching commands after a cycle.
4079
+ if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
4080
+ this.frameLastSeen.set(data.context, Date.now());
4081
+ }
4082
+ if (!this.active())
4083
+ return;
4025
4084
  if (data.line === proto.iframeSignal) {
4026
4085
  // @ts-ignore
4027
4086
  event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
@@ -4119,20 +4178,41 @@ class App {
4119
4178
  if (!this.pollingQueue.order.length) {
4120
4179
  return;
4121
4180
  }
4122
- const nextCommand = this.pollingQueue.order[0];
4123
- if (nextCommand && this.pollingQueue[nextCommand].length === 0) {
4124
- this.pollingQueue.order = this.pollingQueue.order.filter((c) => c !== nextCommand);
4181
+ this.pruneStaleFrames();
4182
+ const liveSet = new Set(this.trackedFrames);
4183
+ while (this.pollingQueue.order.length > 0) {
4184
+ const head = this.pollingQueue.order[0];
4185
+ this.pollingQueue[head] = this.pollingQueue[head].filter((ctx) => liveSet.has(ctx));
4186
+ if (this.pollingQueue[head].length === 0) {
4187
+ delete this.pollingQueue[head];
4188
+ this.pollingQueue.order.shift();
4189
+ }
4190
+ else {
4191
+ break;
4192
+ }
4193
+ }
4194
+ if (!this.pollingQueue.order.length) {
4125
4195
  return;
4126
4196
  }
4197
+ const nextCommand = this.pollingQueue.order[0];
4127
4198
  if (this.pollingQueue[nextCommand].includes(data.context)) {
4128
4199
  this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter((c) => c !== data.context);
4129
4200
  const message = { line: nextCommand };
4130
4201
  if (nextCommand === proto.startIframe) {
4131
4202
  message.token = this.session.getSessionToken(this.projectKey);
4203
+ const targetFrame = this.pageFrames.find((f) => f.contentWindow === event.source)
4204
+ || Array.from(document.querySelectorAll('iframe')).find((f) => f.contentWindow === event.source);
4205
+ if (targetFrame) {
4206
+ const nodeId = targetFrame[this.options.node_id];
4207
+ if (nodeId !== undefined) {
4208
+ message.id = nodeId;
4209
+ }
4210
+ }
4132
4211
  }
4133
4212
  // @ts-ignore
4134
4213
  event.source?.postMessage(message, '*');
4135
4214
  if (this.pollingQueue[nextCommand].length === 0) {
4215
+ delete this.pollingQueue[nextCommand];
4136
4216
  this.pollingQueue.order.shift();
4137
4217
  }
4138
4218
  }
@@ -4146,6 +4226,11 @@ class App {
4146
4226
  order: [],
4147
4227
  };
4148
4228
  this.addCommand = (cmd) => {
4229
+ this.pruneStaleFrames();
4230
+ if (this.pollingQueue[cmd]) {
4231
+ this.pollingQueue[cmd] = Array.from(new Set([...this.pollingQueue[cmd], ...this.trackedFrames]));
4232
+ return;
4233
+ }
4149
4234
  this.pollingQueue.order.push(cmd);
4150
4235
  this.pollingQueue[cmd] = [...this.trackedFrames];
4151
4236
  };
@@ -4458,6 +4543,16 @@ class App {
4458
4543
  };
4459
4544
  }
4460
4545
  }
4546
+ pruneStaleFrames() {
4547
+ const staleAfter = Date.now() - this.FRAME_STALE_MS;
4548
+ this.trackedFrames = this.trackedFrames.filter((ctx) => {
4549
+ const last = this.frameLastSeen.get(ctx);
4550
+ if (last !== undefined && last >= staleAfter)
4551
+ return true;
4552
+ this.frameLastSeen.delete(ctx);
4553
+ return false;
4554
+ });
4555
+ }
4461
4556
  allowAppStart() {
4462
4557
  this.canStart = true;
4463
4558
  if (this.startTimeout) {
@@ -5039,6 +5134,9 @@ class App {
5039
5134
  if (Object.keys(startOpts).length !== 0) {
5040
5135
  this.prevOpts = startOpts;
5041
5136
  }
5137
+ if (startOpts.startCallback) {
5138
+ this.userStartCallback = startOpts.startCallback;
5139
+ }
5042
5140
  const isColdStart = this.activityState === ActivityState.ColdStart;
5043
5141
  if (isColdStart && this.coldInterval) {
5044
5142
  clearInterval(this.coldInterval);
@@ -5180,8 +5278,8 @@ class App {
5180
5278
  // TODO: start as early as possible (before receiving the token)
5181
5279
  /** after start */
5182
5280
  this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
5183
- if (startOpts.startCallback) {
5184
- startOpts.startCallback(SuccessfulStart(onStartInfo));
5281
+ if (this.userStartCallback) {
5282
+ this.userStartCallback(SuccessfulStart(onStartInfo));
5185
5283
  }
5186
5284
  this.activityState = ActivityState.Active;
5187
5285
  if (this.options.crossdomain?.enabled) {
@@ -5344,6 +5442,7 @@ class App {
5344
5442
  this.canvasRecorder?.clear();
5345
5443
  this.messages.length = 0;
5346
5444
  this.parentActive = false;
5445
+ this.pageFrames = [];
5347
5446
  }
5348
5447
  finally {
5349
5448
  this.activityState = ActivityState.NotActive;
@@ -8873,7 +8972,7 @@ class ConstantProperties {
8873
8972
  user_id: this.user_id,
8874
8973
  distinct_id: this.deviceId,
8875
8974
  sdk_edition: 'web',
8876
- sdk_version: '18.0.1',
8975
+ sdk_version: '18.0.3',
8877
8976
  timezone: getUTCOffsetString(),
8878
8977
  search_engine: this.searchEngine,
8879
8978
  };
@@ -9575,7 +9674,7 @@ class API {
9575
9674
  this.signalStartIssue = (reason, missingApi) => {
9576
9675
  const doNotTrack = this.checkDoNotTrack();
9577
9676
  console.log("Tracker couldn't start due to:", JSON.stringify({
9578
- trackerVersion: '18.0.1',
9677
+ trackerVersion: '18.0.3',
9579
9678
  projectKey: this.options.projectKey,
9580
9679
  doNotTrack,
9581
9680
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,