@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/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
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
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 (
|
|
1524
|
+
if (!running || ticks === PAUSED) {
|
|
1494
1525
|
return;
|
|
1495
1526
|
}
|
|
1496
1527
|
ticks++;
|
|
1497
1528
|
}, 0, false);
|
|
1498
1529
|
const sendPerformanceTrack = () => {
|
|
1499
|
-
if (
|
|
1530
|
+
if (!running) {
|
|
1500
1531
|
return;
|
|
1501
1532
|
}
|
|
1502
1533
|
app.send(PerformanceTrack(frames, ticks, perf.memory.totalJSHeapSize || 0, perf.memory.usedJSHeapSize || 0));
|
|
1503
|
-
|
|
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
|
-
|
|
1543
|
+
running = true;
|
|
1544
|
+
frames = ticks = PAUSED;
|
|
1507
1545
|
sendPerformanceTrack();
|
|
1508
|
-
nextFrame();
|
|
1509
1546
|
});
|
|
1510
1547
|
app.attachStopCallback(() => {
|
|
1511
|
-
|
|
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
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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.
|
|
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
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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
|
|
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 <
|
|
7336
|
+
for (let i = 0; i < rules.length; i++) {
|
|
6989
7337
|
try {
|
|
6990
|
-
sendInsertDeleteRule(sheet, i,
|
|
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
|
|
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.
|
|
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.
|
|
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,
|