@openreplay/tracker 18.0.11 → 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 +324 -15
- package/dist/cjs/entry.js.map +1 -1
- package/dist/cjs/index.js +324 -15
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/main/app/index.d.ts +61 -0
- package/dist/lib/entry.js +324 -15
- package/dist/lib/entry.js.map +1 -1
- package/dist/lib/index.js +324 -15
- 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/index.js
CHANGED
|
@@ -2715,9 +2715,15 @@ function ConstructedStyleSheets (app) {
|
|
|
2715
2715
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
2716
2716
|
}
|
|
2717
2717
|
if (init) {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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
|
|
2721
2727
|
}
|
|
2722
2728
|
}
|
|
2723
2729
|
nowOwning.push(sheetID);
|
|
@@ -4028,12 +4034,16 @@ const proto = {
|
|
|
4028
4034
|
parentAlive: 'signal that parent is live',
|
|
4029
4035
|
killIframe: 'stop tracker inside frame',
|
|
4030
4036
|
startIframe: 'start tracker inside frame',
|
|
4037
|
+
// child -> parent: once-per-minute encoded debug snapshot from inside an iframe
|
|
4038
|
+
iframeDebug: 'iframe debug snapshot',
|
|
4031
4039
|
// checking updates
|
|
4032
4040
|
polling: 'hello-how-are-you-im-under-the-water-please-help-me',
|
|
4033
4041
|
// happens if tab is old and has outdated token but
|
|
4034
4042
|
// not communicating with backend to update it (for whatever reason)
|
|
4035
4043
|
reset: 'reset-your-session-please',
|
|
4036
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]));
|
|
4037
4047
|
class App {
|
|
4038
4048
|
get tagMatcher() {
|
|
4039
4049
|
return this.tagWatcher.matcher;
|
|
@@ -4052,7 +4062,7 @@ class App {
|
|
|
4052
4062
|
this.stopCallbacks = [];
|
|
4053
4063
|
this.commitCallbacks = [];
|
|
4054
4064
|
this.activityState = ActivityState.NotActive;
|
|
4055
|
-
this.version = '18.0.
|
|
4065
|
+
this.version = '18.0.12-beta.1'; // TODO: version compatability check inside each plugin.
|
|
4056
4066
|
this.socketMode = false;
|
|
4057
4067
|
this.compressionThreshold = 24 * 1000;
|
|
4058
4068
|
this.bc = null;
|
|
@@ -4069,10 +4079,64 @@ class App {
|
|
|
4069
4079
|
this.checkStatus = () => {
|
|
4070
4080
|
return this.parentActive;
|
|
4071
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
|
+
};
|
|
4072
4129
|
this.parentCrossDomainFrameListener = (event) => {
|
|
4073
4130
|
const { data } = event;
|
|
4074
4131
|
if (!data || event.source === window)
|
|
4075
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
|
+
}
|
|
4076
4140
|
if (data.line === proto.startIframe) {
|
|
4077
4141
|
// Avoid corrupting an in-flight start; let it complete.
|
|
4078
4142
|
if (this.activityState === ActivityState.Starting)
|
|
@@ -4083,6 +4147,7 @@ class App {
|
|
|
4083
4147
|
}
|
|
4084
4148
|
if (data.token) {
|
|
4085
4149
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4150
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4086
4151
|
}
|
|
4087
4152
|
if (data.id !== undefined) {
|
|
4088
4153
|
this.rootId = data.id;
|
|
@@ -4101,6 +4166,7 @@ class App {
|
|
|
4101
4166
|
this.parentActive = true;
|
|
4102
4167
|
this.rootId = data.id;
|
|
4103
4168
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4169
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4104
4170
|
this.frameOderNumber = data.frameOrderNumber;
|
|
4105
4171
|
this.frameLevel = data.frameLevel;
|
|
4106
4172
|
this.debug.log('starting iframe tracking', data);
|
|
@@ -4113,14 +4179,110 @@ class App {
|
|
|
4113
4179
|
}
|
|
4114
4180
|
};
|
|
4115
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();
|
|
4116
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();
|
|
4117
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
|
+
};
|
|
4118
4273
|
this.crossDomainIframeListener = (event) => {
|
|
4119
4274
|
if (event.source === window)
|
|
4120
4275
|
return;
|
|
4121
4276
|
const { data } = event;
|
|
4122
4277
|
if (!data)
|
|
4123
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
|
+
}
|
|
4124
4286
|
// Record liveness regardless of our own active state so the queue can prune
|
|
4125
4287
|
// stale contexts reliably once we resume dispatching commands after a cycle.
|
|
4126
4288
|
if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
|
|
@@ -4128,9 +4290,15 @@ class App {
|
|
|
4128
4290
|
}
|
|
4129
4291
|
if (!this.active())
|
|
4130
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
|
+
}
|
|
4131
4298
|
if (data.line === proto.iframeSignal) {
|
|
4132
4299
|
// @ts-ignore
|
|
4133
4300
|
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
|
|
4301
|
+
this.recordSentToFrame(data.context, proto.parentAlive);
|
|
4134
4302
|
const signalId = async () => {
|
|
4135
4303
|
if (event.source === null) {
|
|
4136
4304
|
return console.error('Couldnt connect to event.source for child iframe tracking');
|
|
@@ -4147,22 +4315,25 @@ class App {
|
|
|
4147
4315
|
else {
|
|
4148
4316
|
this.trackedFrames.push(data.context);
|
|
4149
4317
|
}
|
|
4318
|
+
this.everTrackedFrames.add(data.context);
|
|
4150
4319
|
await this.waitStarted();
|
|
4151
4320
|
const token = this.session.getSessionToken(this.projectKey);
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
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);
|
|
4156
4326
|
const iframeData = {
|
|
4157
4327
|
line: proto.iframeId,
|
|
4158
4328
|
id,
|
|
4159
4329
|
token,
|
|
4160
4330
|
frameOrderNumber: order,
|
|
4161
|
-
frameLevel
|
|
4331
|
+
frameLevel,
|
|
4162
4332
|
};
|
|
4163
4333
|
this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
|
|
4164
4334
|
// @ts-ignore
|
|
4165
4335
|
event.source?.postMessage(iframeData, '*');
|
|
4336
|
+
this.recordSentToFrame(data.context, proto.iframeId);
|
|
4166
4337
|
}
|
|
4167
4338
|
catch (e) {
|
|
4168
4339
|
console.error(e);
|
|
@@ -4175,6 +4346,9 @@ class App {
|
|
|
4175
4346
|
* plus we rewrite some of the messages to be relative to the main context/window
|
|
4176
4347
|
* */
|
|
4177
4348
|
if (data.line === proto.iframeBatch) {
|
|
4349
|
+
if (data.context) {
|
|
4350
|
+
this.frameBatchLastSeen.set(data.context, Date.now());
|
|
4351
|
+
}
|
|
4178
4352
|
const msgBatch = data.messages;
|
|
4179
4353
|
const mappedMessages = [];
|
|
4180
4354
|
msgBatch.forEach((msg) => {
|
|
@@ -4222,6 +4396,16 @@ class App {
|
|
|
4222
4396
|
this.messages.push(...mappedMessages);
|
|
4223
4397
|
}
|
|
4224
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
|
+
}
|
|
4225
4409
|
if (!this.pollingQueue.order.length) {
|
|
4226
4410
|
return;
|
|
4227
4411
|
}
|
|
@@ -4258,6 +4442,7 @@ class App {
|
|
|
4258
4442
|
}
|
|
4259
4443
|
// @ts-ignore
|
|
4260
4444
|
event.source?.postMessage(message, '*');
|
|
4445
|
+
this.recordSentToFrame(data.context, nextCommand);
|
|
4261
4446
|
if (this.pollingQueue[nextCommand].length === 0) {
|
|
4262
4447
|
delete this.pollingQueue[nextCommand];
|
|
4263
4448
|
this.pollingQueue.order.shift();
|
|
@@ -4295,6 +4480,7 @@ class App {
|
|
|
4295
4480
|
source: thisTab,
|
|
4296
4481
|
context: this.contextId,
|
|
4297
4482
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4483
|
+
this.markSentToParent();
|
|
4298
4484
|
/**
|
|
4299
4485
|
* since we need to wait uncertain amount of time
|
|
4300
4486
|
* and I don't want to have recursion going on,
|
|
@@ -4315,6 +4501,7 @@ class App {
|
|
|
4315
4501
|
source: thisTab,
|
|
4316
4502
|
context: this.contextId,
|
|
4317
4503
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4504
|
+
this.markSentToParent();
|
|
4318
4505
|
this.debug.info('Trying to signal to parent, attempt:', retries + 1);
|
|
4319
4506
|
retries++;
|
|
4320
4507
|
};
|
|
@@ -4532,7 +4719,13 @@ class App {
|
|
|
4532
4719
|
line: proto.polling,
|
|
4533
4720
|
context: this.contextId,
|
|
4534
4721
|
}, options.crossdomain?.parentDomain ?? '*');
|
|
4722
|
+
this.markSentToParent();
|
|
4535
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);
|
|
4536
4729
|
}
|
|
4537
4730
|
else {
|
|
4538
4731
|
this.initWorker();
|
|
@@ -4541,6 +4734,13 @@ class App {
|
|
|
4541
4734
|
* so they can act as if it was just a same-domain iframe
|
|
4542
4735
|
* */
|
|
4543
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
|
+
}
|
|
4544
4744
|
}
|
|
4545
4745
|
if (this.bc !== null) {
|
|
4546
4746
|
this.bc.postMessage({
|
|
@@ -4590,6 +4790,62 @@ class App {
|
|
|
4590
4790
|
};
|
|
4591
4791
|
}
|
|
4592
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
|
+
}
|
|
4593
4849
|
pruneStaleFrames() {
|
|
4594
4850
|
const staleAfter = Date.now() - this.FRAME_STALE_MS;
|
|
4595
4851
|
this.trackedFrames = this.trackedFrames.filter((ctx) => {
|
|
@@ -4600,6 +4856,45 @@ class App {
|
|
|
4600
4856
|
return false;
|
|
4601
4857
|
});
|
|
4602
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
|
+
}
|
|
4603
4898
|
allowAppStart() {
|
|
4604
4899
|
this.canStart = true;
|
|
4605
4900
|
if (this.startTimeout) {
|
|
@@ -4765,7 +5060,9 @@ class App {
|
|
|
4765
5060
|
window.parent.postMessage({
|
|
4766
5061
|
line: proto.iframeBatch,
|
|
4767
5062
|
messages: this.messages,
|
|
5063
|
+
context: this.contextId,
|
|
4768
5064
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
5065
|
+
this.markSentToParent();
|
|
4769
5066
|
this.commitCallbacks.forEach((cb) => cb(this.messages));
|
|
4770
5067
|
this.messages.length = 0;
|
|
4771
5068
|
return;
|
|
@@ -7021,15 +7318,28 @@ function CSSRules (app, opts) {
|
|
|
7021
7318
|
if (!nodeID)
|
|
7022
7319
|
return;
|
|
7023
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
|
+
}
|
|
7024
7333
|
const sheetID = nextID();
|
|
7025
7334
|
styleSheetIDMap.set(sheet, sheetID);
|
|
7026
7335
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
7027
|
-
for (let i = 0; i <
|
|
7336
|
+
for (let i = 0; i < rules.length; i++) {
|
|
7028
7337
|
try {
|
|
7029
|
-
sendInsertDeleteRule(sheet, i,
|
|
7338
|
+
sendInsertDeleteRule(sheet, i, rules[i].cssText);
|
|
7030
7339
|
}
|
|
7031
7340
|
catch (e) {
|
|
7032
7341
|
// Skip inaccessible rules
|
|
7342
|
+
app.debug.log('Couldnt access stylesheet rule during initial scan', e);
|
|
7033
7343
|
}
|
|
7034
7344
|
}
|
|
7035
7345
|
});
|
|
@@ -7458,7 +7768,7 @@ class NetworkMessage {
|
|
|
7458
7768
|
return null;
|
|
7459
7769
|
const gqlHeader = "application/graphql-response";
|
|
7460
7770
|
const isGraphql = messageInfo.url.includes("/graphql")
|
|
7461
|
-
|| Object.values(messageInfo.request.headers).some(v => v
|
|
7771
|
+
|| Object.values(messageInfo.request.headers).some(v => v.includes(gqlHeader));
|
|
7462
7772
|
if (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
|
|
7463
7773
|
const isError = messageInfo.response.body.includes("errors");
|
|
7464
7774
|
messageInfo.status = isError ? 400 : 200;
|
|
@@ -7562,7 +7872,6 @@ const genStringBody = (body) => {
|
|
|
7562
7872
|
}
|
|
7563
7873
|
else if (body instanceof Blob ||
|
|
7564
7874
|
body instanceof ReadableStream ||
|
|
7565
|
-
ArrayBuffer.isView(body) ||
|
|
7566
7875
|
body instanceof ArrayBuffer) {
|
|
7567
7876
|
result = 'byte data';
|
|
7568
7877
|
}
|
|
@@ -9051,7 +9360,7 @@ class ConstantProperties {
|
|
|
9051
9360
|
user_id: this.user_id,
|
|
9052
9361
|
distinct_id: this.deviceId,
|
|
9053
9362
|
sdk_edition: 'web',
|
|
9054
|
-
sdk_version: '18.0.
|
|
9363
|
+
sdk_version: '18.0.12-beta.1',
|
|
9055
9364
|
timezone: getUTCOffsetString(),
|
|
9056
9365
|
search_engine: this.searchEngine,
|
|
9057
9366
|
};
|
|
@@ -9753,7 +10062,7 @@ class API {
|
|
|
9753
10062
|
this.signalStartIssue = (reason, missingApi) => {
|
|
9754
10063
|
const doNotTrack = this.checkDoNotTrack();
|
|
9755
10064
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
9756
|
-
trackerVersion: '18.0.
|
|
10065
|
+
trackerVersion: '18.0.12-beta.1',
|
|
9757
10066
|
projectKey: this.options.projectKey,
|
|
9758
10067
|
doNotTrack,
|
|
9759
10068
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|