@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 +374 -26
- package/dist/cjs/entry.js.map +1 -1
- package/dist/cjs/index.js +374 -26
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/main/app/index.d.ts +61 -0
- package/dist/lib/entry.js +374 -26
- package/dist/lib/entry.js.map +1 -1
- package/dist/lib/index.js +374 -26
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/main/app/index.d.ts +61 -0
- package/dist/types/main/app/index.d.ts +61 -0
- package/package.json +1 -1
package/dist/lib/entry.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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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 (
|
|
1520
|
+
if (!running || ticks === PAUSED) {
|
|
1490
1521
|
return;
|
|
1491
1522
|
}
|
|
1492
1523
|
ticks++;
|
|
1493
1524
|
}, 0, false);
|
|
1494
1525
|
const sendPerformanceTrack = () => {
|
|
1495
|
-
if (
|
|
1526
|
+
if (!running) {
|
|
1496
1527
|
return;
|
|
1497
1528
|
}
|
|
1498
1529
|
app.send(PerformanceTrack(frames, ticks, perf.memory.totalJSHeapSize || 0, perf.memory.usedJSHeapSize || 0));
|
|
1499
|
-
|
|
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
|
-
|
|
1539
|
+
running = true;
|
|
1540
|
+
frames = ticks = PAUSED;
|
|
1503
1541
|
sendPerformanceTrack();
|
|
1504
|
-
nextFrame();
|
|
1505
1542
|
});
|
|
1506
1543
|
app.attachStopCallback(() => {
|
|
1507
|
-
|
|
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
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
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.
|
|
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
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
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
|
|
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 <
|
|
7332
|
+
for (let i = 0; i < rules.length; i++) {
|
|
6985
7333
|
try {
|
|
6986
|
-
sendInsertDeleteRule(sheet, i,
|
|
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
|
|
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.
|
|
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.
|
|
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,
|