@openreplay/tracker 18.0.10 → 18.0.12-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/dist/cjs/entry.js CHANGED
@@ -1473,6 +1473,7 @@ const perf = IN_BROWSER && 'performance' in window && 'memory' in performance //
1473
1473
  : { memory: {} };
1474
1474
  const deviceMemory = IN_BROWSER ? (navigator.deviceMemory || 0) * 1024 : 0;
1475
1475
  const jsHeapSizeLimit = perf.memory.jsHeapSizeLimit || 0;
1476
+ const PAUSED = -1;
1476
1477
  function Performance (app, opts) {
1477
1478
  const options = Object.assign({
1478
1479
  capturePerformance: true,
@@ -1480,35 +1481,73 @@ function Performance (app, opts) {
1480
1481
  if (!options.capturePerformance) {
1481
1482
  return;
1482
1483
  }
1483
- let frames;
1484
- let ticks;
1485
- const nextFrame = () => {
1486
- if (frames === undefined || frames === -1) {
1484
+ // Capture references up front so a third party that later replaces the global
1485
+ // (Sentry's browserApiErrors, polyfills, zones, ...) can't change which
1486
+ // implementation we drive the loop with mid-session.
1487
+ const raf = IN_BROWSER && typeof window.requestAnimationFrame === 'function'
1488
+ ? window.requestAnimationFrame.bind(window)
1489
+ : null;
1490
+ const caf = IN_BROWSER && typeof window.cancelAnimationFrame === 'function'
1491
+ ? window.cancelAnimationFrame.bind(window)
1492
+ : null;
1493
+ let running = false;
1494
+ let frames = 0;
1495
+ let ticks = 0;
1496
+ let rafHandle = null;
1497
+ let inFrame = false;
1498
+ const onFrame = () => {
1499
+ rafHandle = null;
1500
+ if (!running || frames === PAUSED) {
1487
1501
  return;
1488
1502
  }
1489
1503
  frames++;
1490
- requestAnimationFrame(nextFrame);
1504
+ if (inFrame) {
1505
+ return;
1506
+ }
1507
+ scheduleFrame();
1508
+ };
1509
+ const scheduleFrame = () => {
1510
+ if (!raf || rafHandle !== null) {
1511
+ return;
1512
+ }
1513
+ inFrame = true;
1514
+ rafHandle = raf(onFrame);
1515
+ inFrame = false;
1516
+ };
1517
+ const stopLoop = () => {
1518
+ if (rafHandle !== null && caf) {
1519
+ caf(rafHandle);
1520
+ }
1521
+ rafHandle = null;
1491
1522
  };
1492
1523
  app.ticker.attach(() => {
1493
- if (ticks === undefined || ticks === -1) {
1524
+ if (!running || ticks === PAUSED) {
1494
1525
  return;
1495
1526
  }
1496
1527
  ticks++;
1497
1528
  }, 0, false);
1498
1529
  const sendPerformanceTrack = () => {
1499
- if (frames === undefined || ticks === undefined) {
1530
+ if (!running) {
1500
1531
  return;
1501
1532
  }
1502
1533
  app.send(PerformanceTrack(frames, ticks, perf.memory.totalJSHeapSize || 0, perf.memory.usedJSHeapSize || 0));
1503
- ticks = frames = document.hidden ? -1 : 0;
1534
+ if (document.hidden) {
1535
+ frames = ticks = PAUSED;
1536
+ }
1537
+ else {
1538
+ frames = ticks = 0;
1539
+ scheduleFrame();
1540
+ }
1504
1541
  };
1505
1542
  app.attachStartCallback(() => {
1506
- ticks = frames = -1;
1543
+ running = true;
1544
+ frames = ticks = PAUSED;
1507
1545
  sendPerformanceTrack();
1508
- nextFrame();
1509
1546
  });
1510
1547
  app.attachStopCallback(() => {
1511
- ticks = frames = undefined;
1548
+ running = false;
1549
+ frames = ticks = 0;
1550
+ stopLoop();
1512
1551
  });
1513
1552
  app.ticker.attach(sendPerformanceTrack, 165, false);
1514
1553
  if (document.hidden !== undefined) {
@@ -2676,9 +2715,15 @@ function ConstructedStyleSheets (app) {
2676
2715
  app.send(AdoptedSSAddOwner(sheetID, nodeID));
2677
2716
  }
2678
2717
  if (init) {
2679
- const rules = s.cssRules;
2680
- for (let i = 0; i < rules.length; i++) {
2681
- app.send(AdoptedSSInsertRuleURLBased(sheetID, rules[i].cssText, i, app.getBaseHref()));
2718
+ try {
2719
+ const rules = s.cssRules;
2720
+ for (let i = 0; i < rules.length; i++) {
2721
+ app.send(AdoptedSSInsertRuleURLBased(sheetID, rules[i].cssText, i, app.getBaseHref()));
2722
+ }
2723
+ }
2724
+ catch (e) {
2725
+ app.debug.log('Couldnt access adopted stylesheet', e);
2726
+ // Skip inaccessible (cross-origin) stylesheet
2682
2727
  }
2683
2728
  }
2684
2729
  nowOwning.push(sheetID);
@@ -3989,12 +4034,16 @@ const proto = {
3989
4034
  parentAlive: 'signal that parent is live',
3990
4035
  killIframe: 'stop tracker inside frame',
3991
4036
  startIframe: 'start tracker inside frame',
4037
+ // child -> parent: once-per-minute encoded debug snapshot from inside an iframe
4038
+ iframeDebug: 'iframe debug snapshot',
3992
4039
  // checking updates
3993
4040
  polling: 'hello-how-are-you-im-under-the-water-please-help-me',
3994
4041
  // happens if tab is old and has outdated token but
3995
4042
  // not communicating with backend to update it (for whatever reason)
3996
4043
  reset: 'reset-your-session-please',
3997
4044
  };
4045
+ /** reverse map proto value -> short readable key, for the crossdomain debug log */
4046
+ const protoLabel = Object.fromEntries(Object.entries(proto).map(([k, v]) => [v, k]));
3998
4047
  class App {
3999
4048
  get tagMatcher() {
4000
4049
  return this.tagWatcher.matcher;
@@ -4013,7 +4062,7 @@ class App {
4013
4062
  this.stopCallbacks = [];
4014
4063
  this.commitCallbacks = [];
4015
4064
  this.activityState = ActivityState.NotActive;
4016
- this.version = '18.0.10'; // TODO: version compatability check inside each plugin.
4065
+ this.version = '18.0.12-beta.1'; // TODO: version compatability check inside each plugin.
4017
4066
  this.socketMode = false;
4018
4067
  this.compressionThreshold = 24 * 1000;
4019
4068
  this.bc = null;
@@ -4030,10 +4079,64 @@ class App {
4030
4079
  this.checkStatus = () => {
4031
4080
  return this.parentActive;
4032
4081
  };
4082
+ /** child-side crossdomain debug state (only meaningful when insideIframe) */
4083
+ this.lastTokenReceived = null;
4084
+ this.lastParentMsgAt = 0;
4085
+ this.lastSentToParentAt = 0;
4086
+ this.iframeDebugInterval = null;
4087
+ /**
4088
+ * Child-side counterpart of emitCrossdomainDebug: once per minute an iframe posts an
4089
+ * encoded snapshot of its own tracking state up to the parent, which records it as a
4090
+ * console log. Posted directly (not via this.send) so it is reported even when the
4091
+ * child is NOT active — an inactive/orphaned child is exactly what we want to catch.
4092
+ */
4093
+ this.emitIframeDebug = () => {
4094
+ if (!this.insideIframe || !this.options.crossdomain?.enabled)
4095
+ return;
4096
+ const now = Date.now();
4097
+ const rel = (t) => (t ? now - t : null);
4098
+ const payload = {
4099
+ ctx: this.contextId,
4100
+ active: this.active(),
4101
+ state: ActivityState[this.activityState],
4102
+ parentActive: this.parentActive,
4103
+ rootId: this.rootId,
4104
+ frameOrder: this.frameOderNumber,
4105
+ // when and what token we last received from the parent (token truncated)
4106
+ token: this.lastTokenReceived
4107
+ ? { tok: this.lastTokenReceived.tok, agoMs: now - this.lastTokenReceived.at }
4108
+ : null,
4109
+ // last two-way communication with the parent
4110
+ lastParentMsgAgoMs: rel(this.lastParentMsgAt),
4111
+ lastSentToParentAgoMs: rel(this.lastSentToParentAt),
4112
+ };
4113
+ const json = JSON.stringify(payload);
4114
+ let encoded;
4115
+ try {
4116
+ encoded = btoa(json);
4117
+ }
4118
+ catch {
4119
+ encoded = json;
4120
+ }
4121
+ try {
4122
+ window.parent.postMessage({ line: proto.iframeDebug, context: this.contextId, debug: encoded }, this.options.crossdomain?.parentDomain ?? '*');
4123
+ this.markSentToParent();
4124
+ }
4125
+ catch (e) {
4126
+ this.debug.error('iframe debug post failed', e);
4127
+ }
4128
+ };
4033
4129
  this.parentCrossDomainFrameListener = (event) => {
4034
4130
  const { data } = event;
4035
4131
  if (!data || event.source === window)
4036
4132
  return;
4133
+ // Debug: remember the last time the parent talked to us.
4134
+ if (data.line === proto.startIframe ||
4135
+ data.line === proto.parentAlive ||
4136
+ data.line === proto.iframeId ||
4137
+ data.line === proto.killIframe) {
4138
+ this.lastParentMsgAt = Date.now();
4139
+ }
4037
4140
  if (data.line === proto.startIframe) {
4038
4141
  // Avoid corrupting an in-flight start; let it complete.
4039
4142
  if (this.activityState === ActivityState.Starting)
@@ -4044,6 +4147,7 @@ class App {
4044
4147
  }
4045
4148
  if (data.token) {
4046
4149
  this.session.setSessionToken(data.token, this.projectKey);
4150
+ this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
4047
4151
  }
4048
4152
  if (data.id !== undefined) {
4049
4153
  this.rootId = data.id;
@@ -4062,6 +4166,7 @@ class App {
4062
4166
  this.parentActive = true;
4063
4167
  this.rootId = data.id;
4064
4168
  this.session.setSessionToken(data.token, this.projectKey);
4169
+ this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
4065
4170
  this.frameOderNumber = data.frameOrderNumber;
4066
4171
  this.frameLevel = data.frameLevel;
4067
4172
  this.debug.log('starting iframe tracking', data);
@@ -4074,14 +4179,110 @@ class App {
4074
4179
  }
4075
4180
  };
4076
4181
  this.trackedFrames = [];
4182
+ /** every context that has been enrolled at least once, to tell an orphan (re-adopt) apart
4183
+ * from a brand-new child still mid-enrollment (leave alone). */
4184
+ this.everTrackedFrames = new Set();
4077
4185
  this.frameLastSeen = new Map();
4186
+ /** crossdomain debug diagnostics, reported once per minute as an encoded console log */
4187
+ this.frameOrigin = new Map();
4188
+ this.frameAnyLastSeen = new Map();
4189
+ this.frameBatchLastSeen = new Map();
4190
+ this.frameLastSent = new Map();
4191
+ this.xdomainDebugInterval = null;
4192
+ /** last time we re-adopted a given orphaned context, to avoid restart spam */
4193
+ this.reAdoptCooldown = new Map();
4194
+ this.RE_ADOPT_COOLDOWN_MS = 2000;
4195
+ /**
4196
+ * Stable, collision-free frame-order allocation. Node ids are partitioned by
4197
+ * (frameLevel, frameOrder) via pack() — every (level, order) owns its own id block, so
4198
+ * two simultaneously-live frames sharing an order at the same level corrupt each other's
4199
+ * node trees and one stops rendering. The previous `trackedFrames.findIndex+1` derived
4200
+ * order from a mutable array index, and pruneStaleFrames()'s .filter() shifts those
4201
+ * indices, so a newly enrolled frame could be handed an order still in use by a live
4202
+ * (but pruned) frame. We instead assign each context a persistent order, unique among all
4203
+ * non-recycled contexts at its level, freed only when the context is GC'd (truly gone).
4204
+ */
4205
+ this.frameAlloc = new Map();
4206
+ this.usedOrdersByLevel = new Map();
4078
4207
  this.FRAME_STALE_MS = 1500;
4208
+ /**
4209
+ * Once per minute: emit an encoded console log from the parent tracker describing every
4210
+ * tracked child iframe and the freshness of our two-way communication with it. Lets us
4211
+ * see in replay which crossdomain iframe went silent and on which leg of the handshake.
4212
+ */
4213
+ /** drop debug entries for contexts we have neither heard from nor messaged in this long */
4214
+ this.XDOMAIN_DEBUG_RETENTION_MS = 10 * 60000;
4215
+ this.emitCrossdomainDebug = () => {
4216
+ if (this.insideIframe || !this.options.crossdomain?.enabled || !this.active())
4217
+ return;
4218
+ const now = Date.now();
4219
+ const rel = (t) => (t === undefined ? null : now - t);
4220
+ // Report the union of currently-tracked frames and every context we have any debug
4221
+ // record for: a frame that broke and stopped polling gets pruned from trackedFrames,
4222
+ // but it is exactly the one we want to surface (with a large lastAnyMsgAgoMs).
4223
+ const tracked = new Set(this.trackedFrames);
4224
+ const contexts = new Set([
4225
+ ...this.trackedFrames,
4226
+ ...this.frameAnyLastSeen.keys(),
4227
+ ...this.frameLastSent.keys(),
4228
+ ]);
4229
+ const frames = Array.from(contexts).map((ctx, i) => {
4230
+ const sent = this.frameLastSent.get(ctx);
4231
+ const alloc = this.frameAlloc.get(ctx);
4232
+ return {
4233
+ // the actual allocated (level, order) node-id partition, else an enumeration index
4234
+ n: alloc ? alloc.order : i + 1,
4235
+ level: alloc ? alloc.level : null,
4236
+ // identify by domain if we have it, otherwise the context id, otherwise the number
4237
+ id: this.frameOrigin.get(ctx) || ctx || `#${i + 1}`,
4238
+ tracked: tracked.has(ctx),
4239
+ lastAnyMsgAgoMs: rel(this.frameAnyLastSeen.get(ctx)),
4240
+ lastBatchAgoMs: rel(this.frameBatchLastSeen.get(ctx)),
4241
+ lastSent: sent ? { line: sent.line, agoMs: now - sent.t } : null,
4242
+ };
4243
+ });
4244
+ // GC: forget contexts that have been silent and un-messaged past the retention window.
4245
+ const cutoff = now - this.XDOMAIN_DEBUG_RETENTION_MS;
4246
+ for (const ctx of contexts) {
4247
+ if (tracked.has(ctx))
4248
+ continue;
4249
+ const seen = this.frameAnyLastSeen.get(ctx) ?? 0;
4250
+ const sentT = this.frameLastSent.get(ctx)?.t ?? 0;
4251
+ if (Math.max(seen, sentT) < cutoff) {
4252
+ this.frameOrigin.delete(ctx);
4253
+ this.frameAnyLastSeen.delete(ctx);
4254
+ this.frameBatchLastSeen.delete(ctx);
4255
+ this.frameLastSent.delete(ctx);
4256
+ this.reAdoptCooldown.delete(ctx);
4257
+ this.everTrackedFrames.delete(ctx);
4258
+ this.freeFrameOrder(ctx);
4259
+ }
4260
+ }
4261
+ const payload = { t: now, count: frames.length, frames };
4262
+ const json = JSON.stringify(payload);
4263
+ let encoded;
4264
+ try {
4265
+ // payload is ASCII (base36 contexts, URL origins, numbers), so plain base64 is safe
4266
+ encoded = btoa(json);
4267
+ }
4268
+ catch {
4269
+ encoded = json;
4270
+ }
4271
+ this.send(ConsoleLog('info', `[OR_XDOMAIN_DEBUG] ${encoded}`));
4272
+ };
4079
4273
  this.crossDomainIframeListener = (event) => {
4080
4274
  if (event.source === window)
4081
4275
  return;
4082
4276
  const { data } = event;
4083
4277
  if (!data)
4084
4278
  return;
4279
+ // Debug: remember when we last heard *anything* from this context, and its domain.
4280
+ if (data.context) {
4281
+ this.frameAnyLastSeen.set(data.context, Date.now());
4282
+ if (event.origin && !this.frameOrigin.has(data.context)) {
4283
+ this.frameOrigin.set(data.context, event.origin);
4284
+ }
4285
+ }
4085
4286
  // Record liveness regardless of our own active state so the queue can prune
4086
4287
  // stale contexts reliably once we resume dispatching commands after a cycle.
4087
4288
  if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
@@ -4089,9 +4290,15 @@ class App {
4089
4290
  }
4090
4291
  if (!this.active())
4091
4292
  return;
4293
+ if (data.line === proto.iframeDebug) {
4294
+ // A child posted its once-per-minute snapshot; surface it in our recorded console.
4295
+ this.send(ConsoleLog('info', `[OR_XDOMAIN_IFRAME_DEBUG] ${data.debug}`));
4296
+ return;
4297
+ }
4092
4298
  if (data.line === proto.iframeSignal) {
4093
4299
  // @ts-ignore
4094
4300
  event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
4301
+ this.recordSentToFrame(data.context, proto.parentAlive);
4095
4302
  const signalId = async () => {
4096
4303
  if (event.source === null) {
4097
4304
  return console.error('Couldnt connect to event.source for child iframe tracking');
@@ -4108,22 +4315,25 @@ class App {
4108
4315
  else {
4109
4316
  this.trackedFrames.push(data.context);
4110
4317
  }
4318
+ this.everTrackedFrames.add(data.context);
4111
4319
  await this.waitStarted();
4112
4320
  const token = this.session.getSessionToken(this.projectKey);
4113
- const order = this.trackedFrames.findIndex((f) => f === data.context) + 1;
4114
- if (order === 0) {
4115
- this.debug.error('Couldnt get order number for iframe', data.context, this.trackedFrames);
4116
- }
4321
+ // Persistent, collision-free order (NOT the shifting array index). A restart of the
4322
+ // same context keeps its order/id-block for continuity; distinct live frames at the
4323
+ // same level never share one.
4324
+ const frameLevel = this.frameLevel + 1;
4325
+ const order = this.allocateFrameOrder(data.context, frameLevel);
4117
4326
  const iframeData = {
4118
4327
  line: proto.iframeId,
4119
4328
  id,
4120
4329
  token,
4121
4330
  frameOrderNumber: order,
4122
- frameLevel: this.frameLevel + 1,
4331
+ frameLevel,
4123
4332
  };
4124
4333
  this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
4125
4334
  // @ts-ignore
4126
4335
  event.source?.postMessage(iframeData, '*');
4336
+ this.recordSentToFrame(data.context, proto.iframeId);
4127
4337
  }
4128
4338
  catch (e) {
4129
4339
  console.error(e);
@@ -4136,6 +4346,9 @@ class App {
4136
4346
  * plus we rewrite some of the messages to be relative to the main context/window
4137
4347
  * */
4138
4348
  if (data.line === proto.iframeBatch) {
4349
+ if (data.context) {
4350
+ this.frameBatchLastSeen.set(data.context, Date.now());
4351
+ }
4139
4352
  const msgBatch = data.messages;
4140
4353
  const mappedMessages = [];
4141
4354
  msgBatch.forEach((msg) => {
@@ -4183,6 +4396,16 @@ class App {
4183
4396
  this.messages.push(...mappedMessages);
4184
4397
  }
4185
4398
  if (data.line === proto.polling) {
4399
+ // Self-heal: a live child that was enrolled before but fell out of trackedFrames
4400
+ // (pruned during a stop/start gap) keeps polling yet never re-signals. Re-adopt it
4401
+ // so it restarts and re-enrolls. We require everTrackedFrames so a brand-new child
4402
+ // still mid-enrollment (iframeSignal/checkNodeId in flight) is left alone.
4403
+ if (data.context &&
4404
+ this.everTrackedFrames.has(data.context) &&
4405
+ !this.trackedFrames.includes(data.context)) {
4406
+ this.reAdoptOrphanFrame(event, data.context);
4407
+ return;
4408
+ }
4186
4409
  if (!this.pollingQueue.order.length) {
4187
4410
  return;
4188
4411
  }
@@ -4219,6 +4442,7 @@ class App {
4219
4442
  }
4220
4443
  // @ts-ignore
4221
4444
  event.source?.postMessage(message, '*');
4445
+ this.recordSentToFrame(data.context, nextCommand);
4222
4446
  if (this.pollingQueue[nextCommand].length === 0) {
4223
4447
  delete this.pollingQueue[nextCommand];
4224
4448
  this.pollingQueue.order.shift();
@@ -4256,6 +4480,7 @@ class App {
4256
4480
  source: thisTab,
4257
4481
  context: this.contextId,
4258
4482
  }, this.options.crossdomain?.parentDomain ?? '*');
4483
+ this.markSentToParent();
4259
4484
  /**
4260
4485
  * since we need to wait uncertain amount of time
4261
4486
  * and I don't want to have recursion going on,
@@ -4276,6 +4501,7 @@ class App {
4276
4501
  source: thisTab,
4277
4502
  context: this.contextId,
4278
4503
  }, this.options.crossdomain?.parentDomain ?? '*');
4504
+ this.markSentToParent();
4279
4505
  this.debug.info('Trying to signal to parent, attempt:', retries + 1);
4280
4506
  retries++;
4281
4507
  };
@@ -4493,7 +4719,13 @@ class App {
4493
4719
  line: proto.polling,
4494
4720
  context: this.contextId,
4495
4721
  }, options.crossdomain?.parentDomain ?? '*');
4722
+ this.markSentToParent();
4496
4723
  }, 250);
4724
+ // Child-only: once per minute, post an encoded snapshot of our own tracking state
4725
+ // (active?, token received, last comms) up to the parent so it lands in the replay.
4726
+ if (this.iframeDebugInterval)
4727
+ clearInterval(this.iframeDebugInterval);
4728
+ this.iframeDebugInterval = setInterval(this.emitIframeDebug, 60000);
4497
4729
  }
4498
4730
  else {
4499
4731
  this.initWorker();
@@ -4502,6 +4734,13 @@ class App {
4502
4734
  * so they can act as if it was just a same-domain iframe
4503
4735
  * */
4504
4736
  window.addEventListener('message', this.crossDomainIframeListener);
4737
+ // Parent-only: once per minute, log an encoded snapshot of every tracked child
4738
+ // iframe and the freshness of our two-way comms, to debug iframes that go silent.
4739
+ if (this.options.crossdomain?.enabled) {
4740
+ if (this.xdomainDebugInterval)
4741
+ clearInterval(this.xdomainDebugInterval);
4742
+ this.xdomainDebugInterval = setInterval(this.emitCrossdomainDebug, 60000);
4743
+ }
4505
4744
  }
4506
4745
  if (this.bc !== null) {
4507
4746
  this.bc.postMessage({
@@ -4551,6 +4790,62 @@ class App {
4551
4790
  };
4552
4791
  }
4553
4792
  }
4793
+ /** stamp every outbound post to the parent window, for the child debug snapshot */
4794
+ markSentToParent() {
4795
+ this.lastSentToParentAt = Date.now();
4796
+ }
4797
+ allocateFrameOrder(ctx, level) {
4798
+ const existing = this.frameAlloc.get(ctx);
4799
+ if (existing !== undefined)
4800
+ return existing.order;
4801
+ let used = this.usedOrdersByLevel.get(level);
4802
+ if (!used) {
4803
+ used = new Set();
4804
+ this.usedOrdersByLevel.set(level, used);
4805
+ }
4806
+ let order = -1;
4807
+ for (let n = 1; n <= MASK_ORDER; n++) {
4808
+ if (!used.has(n)) {
4809
+ order = n;
4810
+ break;
4811
+ }
4812
+ }
4813
+ if (order === -1) {
4814
+ // Overflow (>127 live frames at one level): evict the least-recently-seen context at
4815
+ // this level that is not currently tracked, and reuse its slot rather than failing.
4816
+ let lru = null;
4817
+ let lruSeen = Infinity;
4818
+ const trackedSet = new Set(this.trackedFrames);
4819
+ this.frameAlloc.forEach((alloc, c) => {
4820
+ if (alloc.level !== level || trackedSet.has(c))
4821
+ return;
4822
+ const seen = this.frameAnyLastSeen.get(c) ?? 0;
4823
+ if (seen < lruSeen) {
4824
+ lruSeen = seen;
4825
+ lru = c;
4826
+ }
4827
+ });
4828
+ if (lru !== null) {
4829
+ order = this.frameAlloc.get(lru).order;
4830
+ this.frameAlloc.delete(lru);
4831
+ this.debug.error('OR: frame order space exhausted, evicting', lru, 'for', ctx);
4832
+ }
4833
+ else {
4834
+ order = MASK_ORDER;
4835
+ this.debug.error('OR: frame order overflow, reusing max order for', ctx);
4836
+ }
4837
+ }
4838
+ used.add(order);
4839
+ this.frameAlloc.set(ctx, { order, level });
4840
+ return order;
4841
+ }
4842
+ freeFrameOrder(ctx) {
4843
+ const alloc = this.frameAlloc.get(ctx);
4844
+ if (!alloc)
4845
+ return;
4846
+ this.frameAlloc.delete(ctx);
4847
+ this.usedOrdersByLevel.get(alloc.level)?.delete(alloc.order);
4848
+ }
4554
4849
  pruneStaleFrames() {
4555
4850
  const staleAfter = Date.now() - this.FRAME_STALE_MS;
4556
4851
  this.trackedFrames = this.trackedFrames.filter((ctx) => {
@@ -4561,6 +4856,45 @@ class App {
4561
4856
  return false;
4562
4857
  });
4563
4858
  }
4859
+ /** records the last command/signal we posted to a given child iframe context (debug) */
4860
+ recordSentToFrame(ctx, line) {
4861
+ if (!ctx)
4862
+ return;
4863
+ this.frameLastSent.set(ctx, { line: protoLabel[line] ?? line, t: Date.now() });
4864
+ }
4865
+ /**
4866
+ * Self-heal for the "kill-then-prune orphan" race: a live child can fall out of
4867
+ * `trackedFrames` (its 250ms poll was delayed past FRAME_STALE_MS during the parent's
4868
+ * stop/start NotActive gap, so pruneStaleFrames evicted it). It keeps polling but the
4869
+ * only re-enrollment path is an `iframeSignal`, which a stopped/active-but-orphaned
4870
+ * child never re-emits — so it would record nothing forever. When we (the parent) are
4871
+ * active and see a poll from an un-tracked context, push a `startIframe` so the child
4872
+ * restarts, re-runs the full handshake and re-observes with a fresh rootId. Cooldowned
4873
+ * so we don't spam restarts during the child's start window.
4874
+ */
4875
+ reAdoptOrphanFrame(event, ctx) {
4876
+ const now = Date.now();
4877
+ const last = this.reAdoptCooldown.get(ctx) ?? 0;
4878
+ if (now - last < this.RE_ADOPT_COOLDOWN_MS)
4879
+ return;
4880
+ this.reAdoptCooldown.set(ctx, now);
4881
+ const message = {
4882
+ line: proto.startIframe,
4883
+ token: this.session.getSessionToken(this.projectKey),
4884
+ };
4885
+ const targetFrame = this.pageFrames.find((f) => f.contentWindow === event.source) ||
4886
+ Array.from(document.querySelectorAll('iframe')).find((f) => f.contentWindow === event.source);
4887
+ if (targetFrame) {
4888
+ const nodeId = targetFrame[this.options.node_id];
4889
+ if (nodeId !== undefined) {
4890
+ message.id = nodeId;
4891
+ }
4892
+ }
4893
+ // @ts-ignore
4894
+ event.source?.postMessage(message, '*');
4895
+ this.recordSentToFrame(ctx, proto.startIframe);
4896
+ this.debug.log('Re-adopting orphaned crossdomain iframe', ctx);
4897
+ }
4564
4898
  allowAppStart() {
4565
4899
  this.canStart = true;
4566
4900
  if (this.startTimeout) {
@@ -4726,7 +5060,9 @@ class App {
4726
5060
  window.parent.postMessage({
4727
5061
  line: proto.iframeBatch,
4728
5062
  messages: this.messages,
5063
+ context: this.contextId,
4729
5064
  }, this.options.crossdomain?.parentDomain ?? '*');
5065
+ this.markSentToParent();
4730
5066
  this.commitCallbacks.forEach((cb) => cb(this.messages));
4731
5067
  this.messages.length = 0;
4732
5068
  return;
@@ -6982,15 +7318,28 @@ function CSSRules (app, opts) {
6982
7318
  if (!nodeID)
6983
7319
  return;
6984
7320
  const sheet = node.sheet;
7321
+ // Accessing cssRules on a cross-origin stylesheet (e.g. injected by a
7322
+ // browser extension) throws a SecurityError. Probe it before registering
7323
+ // the sheet so an inaccessible sheet is skipped instead of aborting start.
7324
+ let rules;
7325
+ try {
7326
+ rules = sheet.cssRules;
7327
+ }
7328
+ catch (e) {
7329
+ // Skip inaccessible (cross-origin) stylesheet
7330
+ app.debug.log('Couldnt access stylesheet during initial scan', e);
7331
+ return;
7332
+ }
6985
7333
  const sheetID = nextID();
6986
7334
  styleSheetIDMap.set(sheet, sheetID);
6987
7335
  app.send(AdoptedSSAddOwner(sheetID, nodeID));
6988
- for (let i = 0; i < sheet.cssRules.length; i++) {
7336
+ for (let i = 0; i < rules.length; i++) {
6989
7337
  try {
6990
- sendInsertDeleteRule(sheet, i, sheet.cssRules[i].cssText);
7338
+ sendInsertDeleteRule(sheet, i, rules[i].cssText);
6991
7339
  }
6992
7340
  catch (e) {
6993
7341
  // Skip inaccessible rules
7342
+ app.debug.log('Couldnt access stylesheet rule during initial scan', e);
6994
7343
  }
6995
7344
  }
6996
7345
  });
@@ -7419,7 +7768,7 @@ class NetworkMessage {
7419
7768
  return null;
7420
7769
  const gqlHeader = "application/graphql-response";
7421
7770
  const isGraphql = messageInfo.url.includes("/graphql")
7422
- || Object.values(messageInfo.request.headers).some(v => v && typeof v === 'string' && v.includes(gqlHeader));
7771
+ || Object.values(messageInfo.request.headers).some(v => v.includes(gqlHeader));
7423
7772
  if (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
7424
7773
  const isError = messageInfo.response.body.includes("errors");
7425
7774
  messageInfo.status = isError ? 400 : 200;
@@ -7523,7 +7872,6 @@ const genStringBody = (body) => {
7523
7872
  }
7524
7873
  else if (body instanceof Blob ||
7525
7874
  body instanceof ReadableStream ||
7526
- ArrayBuffer.isView(body) ||
7527
7875
  body instanceof ArrayBuffer) {
7528
7876
  result = 'byte data';
7529
7877
  }
@@ -9012,7 +9360,7 @@ class ConstantProperties {
9012
9360
  user_id: this.user_id,
9013
9361
  distinct_id: this.deviceId,
9014
9362
  sdk_edition: 'web',
9015
- sdk_version: '18.0.10',
9363
+ sdk_version: '18.0.12-beta.1',
9016
9364
  timezone: getUTCOffsetString(),
9017
9365
  search_engine: this.searchEngine,
9018
9366
  };
@@ -9714,7 +10062,7 @@ class API {
9714
10062
  this.signalStartIssue = (reason, missingApi) => {
9715
10063
  const doNotTrack = this.checkDoNotTrack();
9716
10064
  console.log("Tracker couldn't start due to:", JSON.stringify({
9717
- trackerVersion: '18.0.10',
10065
+ trackerVersion: '18.0.12-beta.1',
9718
10066
  projectKey: this.options.projectKey,
9719
10067
  doNotTrack,
9720
10068
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,