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