@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
|
@@ -171,11 +171,72 @@ export default class App {
|
|
|
171
171
|
/** used by child iframes for crossdomain only */
|
|
172
172
|
parentActive: boolean;
|
|
173
173
|
checkStatus: () => boolean;
|
|
174
|
+
/** child-side crossdomain debug state (only meaningful when insideIframe) */
|
|
175
|
+
private lastTokenReceived;
|
|
176
|
+
private lastParentMsgAt;
|
|
177
|
+
private lastSentToParentAt;
|
|
178
|
+
private iframeDebugInterval;
|
|
179
|
+
/** stamp every outbound post to the parent window, for the child debug snapshot */
|
|
180
|
+
private markSentToParent;
|
|
181
|
+
/**
|
|
182
|
+
* Child-side counterpart of emitCrossdomainDebug: once per minute an iframe posts an
|
|
183
|
+
* encoded snapshot of its own tracking state up to the parent, which records it as a
|
|
184
|
+
* console log. Posted directly (not via this.send) so it is reported even when the
|
|
185
|
+
* child is NOT active — an inactive/orphaned child is exactly what we want to catch.
|
|
186
|
+
*/
|
|
187
|
+
private emitIframeDebug;
|
|
174
188
|
parentCrossDomainFrameListener: (event: MessageEvent) => void;
|
|
175
189
|
trackedFrames: string[];
|
|
190
|
+
/** every context that has been enrolled at least once, to tell an orphan (re-adopt) apart
|
|
191
|
+
* from a brand-new child still mid-enrollment (leave alone). */
|
|
192
|
+
private everTrackedFrames;
|
|
176
193
|
private frameLastSeen;
|
|
194
|
+
/** crossdomain debug diagnostics, reported once per minute as an encoded console log */
|
|
195
|
+
private frameOrigin;
|
|
196
|
+
private frameAnyLastSeen;
|
|
197
|
+
private frameBatchLastSeen;
|
|
198
|
+
private frameLastSent;
|
|
199
|
+
private xdomainDebugInterval;
|
|
200
|
+
/** last time we re-adopted a given orphaned context, to avoid restart spam */
|
|
201
|
+
private reAdoptCooldown;
|
|
202
|
+
private readonly RE_ADOPT_COOLDOWN_MS;
|
|
203
|
+
/**
|
|
204
|
+
* Stable, collision-free frame-order allocation. Node ids are partitioned by
|
|
205
|
+
* (frameLevel, frameOrder) via pack() — every (level, order) owns its own id block, so
|
|
206
|
+
* two simultaneously-live frames sharing an order at the same level corrupt each other's
|
|
207
|
+
* node trees and one stops rendering. The previous `trackedFrames.findIndex+1` derived
|
|
208
|
+
* order from a mutable array index, and pruneStaleFrames()'s .filter() shifts those
|
|
209
|
+
* indices, so a newly enrolled frame could be handed an order still in use by a live
|
|
210
|
+
* (but pruned) frame. We instead assign each context a persistent order, unique among all
|
|
211
|
+
* non-recycled contexts at its level, freed only when the context is GC'd (truly gone).
|
|
212
|
+
*/
|
|
213
|
+
private frameAlloc;
|
|
214
|
+
private usedOrdersByLevel;
|
|
215
|
+
private allocateFrameOrder;
|
|
216
|
+
private freeFrameOrder;
|
|
177
217
|
private readonly FRAME_STALE_MS;
|
|
178
218
|
private pruneStaleFrames;
|
|
219
|
+
/** records the last command/signal we posted to a given child iframe context (debug) */
|
|
220
|
+
private recordSentToFrame;
|
|
221
|
+
/**
|
|
222
|
+
* Self-heal for the "kill-then-prune orphan" race: a live child can fall out of
|
|
223
|
+
* `trackedFrames` (its 250ms poll was delayed past FRAME_STALE_MS during the parent's
|
|
224
|
+
* stop/start NotActive gap, so pruneStaleFrames evicted it). It keeps polling but the
|
|
225
|
+
* only re-enrollment path is an `iframeSignal`, which a stopped/active-but-orphaned
|
|
226
|
+
* child never re-emits — so it would record nothing forever. When we (the parent) are
|
|
227
|
+
* active and see a poll from an un-tracked context, push a `startIframe` so the child
|
|
228
|
+
* restarts, re-runs the full handshake and re-observes with a fresh rootId. Cooldowned
|
|
229
|
+
* so we don't spam restarts during the child's start window.
|
|
230
|
+
*/
|
|
231
|
+
private reAdoptOrphanFrame;
|
|
232
|
+
/**
|
|
233
|
+
* Once per minute: emit an encoded console log from the parent tracker describing every
|
|
234
|
+
* tracked child iframe and the freshness of our two-way communication with it. Lets us
|
|
235
|
+
* see in replay which crossdomain iframe went silent and on which leg of the handshake.
|
|
236
|
+
*/
|
|
237
|
+
/** drop debug entries for contexts we have neither heard from nor messaged in this long */
|
|
238
|
+
private readonly XDOMAIN_DEBUG_RETENTION_MS;
|
|
239
|
+
private emitCrossdomainDebug;
|
|
179
240
|
crossDomainIframeListener: (event: MessageEvent) => void;
|
|
180
241
|
/**
|
|
181
242
|
* { command : [remaining iframes] }
|
package/dist/lib/entry.js
CHANGED
|
@@ -2711,9 +2711,15 @@ function ConstructedStyleSheets (app) {
|
|
|
2711
2711
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
2712
2712
|
}
|
|
2713
2713
|
if (init) {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
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
|
|
2717
2723
|
}
|
|
2718
2724
|
}
|
|
2719
2725
|
nowOwning.push(sheetID);
|
|
@@ -4024,12 +4030,16 @@ const proto = {
|
|
|
4024
4030
|
parentAlive: 'signal that parent is live',
|
|
4025
4031
|
killIframe: 'stop tracker inside frame',
|
|
4026
4032
|
startIframe: 'start tracker inside frame',
|
|
4033
|
+
// child -> parent: once-per-minute encoded debug snapshot from inside an iframe
|
|
4034
|
+
iframeDebug: 'iframe debug snapshot',
|
|
4027
4035
|
// checking updates
|
|
4028
4036
|
polling: 'hello-how-are-you-im-under-the-water-please-help-me',
|
|
4029
4037
|
// happens if tab is old and has outdated token but
|
|
4030
4038
|
// not communicating with backend to update it (for whatever reason)
|
|
4031
4039
|
reset: 'reset-your-session-please',
|
|
4032
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]));
|
|
4033
4043
|
class App {
|
|
4034
4044
|
get tagMatcher() {
|
|
4035
4045
|
return this.tagWatcher.matcher;
|
|
@@ -4048,7 +4058,7 @@ class App {
|
|
|
4048
4058
|
this.stopCallbacks = [];
|
|
4049
4059
|
this.commitCallbacks = [];
|
|
4050
4060
|
this.activityState = ActivityState.NotActive;
|
|
4051
|
-
this.version = '18.0.
|
|
4061
|
+
this.version = '18.0.12-beta.1'; // TODO: version compatability check inside each plugin.
|
|
4052
4062
|
this.socketMode = false;
|
|
4053
4063
|
this.compressionThreshold = 24 * 1000;
|
|
4054
4064
|
this.bc = null;
|
|
@@ -4065,10 +4075,64 @@ class App {
|
|
|
4065
4075
|
this.checkStatus = () => {
|
|
4066
4076
|
return this.parentActive;
|
|
4067
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
|
+
};
|
|
4068
4125
|
this.parentCrossDomainFrameListener = (event) => {
|
|
4069
4126
|
const { data } = event;
|
|
4070
4127
|
if (!data || event.source === window)
|
|
4071
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
|
+
}
|
|
4072
4136
|
if (data.line === proto.startIframe) {
|
|
4073
4137
|
// Avoid corrupting an in-flight start; let it complete.
|
|
4074
4138
|
if (this.activityState === ActivityState.Starting)
|
|
@@ -4079,6 +4143,7 @@ class App {
|
|
|
4079
4143
|
}
|
|
4080
4144
|
if (data.token) {
|
|
4081
4145
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4146
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4082
4147
|
}
|
|
4083
4148
|
if (data.id !== undefined) {
|
|
4084
4149
|
this.rootId = data.id;
|
|
@@ -4097,6 +4162,7 @@ class App {
|
|
|
4097
4162
|
this.parentActive = true;
|
|
4098
4163
|
this.rootId = data.id;
|
|
4099
4164
|
this.session.setSessionToken(data.token, this.projectKey);
|
|
4165
|
+
this.lastTokenReceived = { tok: String(data.token).slice(-8), at: Date.now() };
|
|
4100
4166
|
this.frameOderNumber = data.frameOrderNumber;
|
|
4101
4167
|
this.frameLevel = data.frameLevel;
|
|
4102
4168
|
this.debug.log('starting iframe tracking', data);
|
|
@@ -4109,14 +4175,110 @@ class App {
|
|
|
4109
4175
|
}
|
|
4110
4176
|
};
|
|
4111
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();
|
|
4112
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();
|
|
4113
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
|
+
};
|
|
4114
4269
|
this.crossDomainIframeListener = (event) => {
|
|
4115
4270
|
if (event.source === window)
|
|
4116
4271
|
return;
|
|
4117
4272
|
const { data } = event;
|
|
4118
4273
|
if (!data)
|
|
4119
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
|
+
}
|
|
4120
4282
|
// Record liveness regardless of our own active state so the queue can prune
|
|
4121
4283
|
// stale contexts reliably once we resume dispatching commands after a cycle.
|
|
4122
4284
|
if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
|
|
@@ -4124,9 +4286,15 @@ class App {
|
|
|
4124
4286
|
}
|
|
4125
4287
|
if (!this.active())
|
|
4126
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
|
+
}
|
|
4127
4294
|
if (data.line === proto.iframeSignal) {
|
|
4128
4295
|
// @ts-ignore
|
|
4129
4296
|
event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
|
|
4297
|
+
this.recordSentToFrame(data.context, proto.parentAlive);
|
|
4130
4298
|
const signalId = async () => {
|
|
4131
4299
|
if (event.source === null) {
|
|
4132
4300
|
return console.error('Couldnt connect to event.source for child iframe tracking');
|
|
@@ -4143,22 +4311,25 @@ class App {
|
|
|
4143
4311
|
else {
|
|
4144
4312
|
this.trackedFrames.push(data.context);
|
|
4145
4313
|
}
|
|
4314
|
+
this.everTrackedFrames.add(data.context);
|
|
4146
4315
|
await this.waitStarted();
|
|
4147
4316
|
const token = this.session.getSessionToken(this.projectKey);
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
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);
|
|
4152
4322
|
const iframeData = {
|
|
4153
4323
|
line: proto.iframeId,
|
|
4154
4324
|
id,
|
|
4155
4325
|
token,
|
|
4156
4326
|
frameOrderNumber: order,
|
|
4157
|
-
frameLevel
|
|
4327
|
+
frameLevel,
|
|
4158
4328
|
};
|
|
4159
4329
|
this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
|
|
4160
4330
|
// @ts-ignore
|
|
4161
4331
|
event.source?.postMessage(iframeData, '*');
|
|
4332
|
+
this.recordSentToFrame(data.context, proto.iframeId);
|
|
4162
4333
|
}
|
|
4163
4334
|
catch (e) {
|
|
4164
4335
|
console.error(e);
|
|
@@ -4171,6 +4342,9 @@ class App {
|
|
|
4171
4342
|
* plus we rewrite some of the messages to be relative to the main context/window
|
|
4172
4343
|
* */
|
|
4173
4344
|
if (data.line === proto.iframeBatch) {
|
|
4345
|
+
if (data.context) {
|
|
4346
|
+
this.frameBatchLastSeen.set(data.context, Date.now());
|
|
4347
|
+
}
|
|
4174
4348
|
const msgBatch = data.messages;
|
|
4175
4349
|
const mappedMessages = [];
|
|
4176
4350
|
msgBatch.forEach((msg) => {
|
|
@@ -4218,6 +4392,16 @@ class App {
|
|
|
4218
4392
|
this.messages.push(...mappedMessages);
|
|
4219
4393
|
}
|
|
4220
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
|
+
}
|
|
4221
4405
|
if (!this.pollingQueue.order.length) {
|
|
4222
4406
|
return;
|
|
4223
4407
|
}
|
|
@@ -4254,6 +4438,7 @@ class App {
|
|
|
4254
4438
|
}
|
|
4255
4439
|
// @ts-ignore
|
|
4256
4440
|
event.source?.postMessage(message, '*');
|
|
4441
|
+
this.recordSentToFrame(data.context, nextCommand);
|
|
4257
4442
|
if (this.pollingQueue[nextCommand].length === 0) {
|
|
4258
4443
|
delete this.pollingQueue[nextCommand];
|
|
4259
4444
|
this.pollingQueue.order.shift();
|
|
@@ -4291,6 +4476,7 @@ class App {
|
|
|
4291
4476
|
source: thisTab,
|
|
4292
4477
|
context: this.contextId,
|
|
4293
4478
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4479
|
+
this.markSentToParent();
|
|
4294
4480
|
/**
|
|
4295
4481
|
* since we need to wait uncertain amount of time
|
|
4296
4482
|
* and I don't want to have recursion going on,
|
|
@@ -4311,6 +4497,7 @@ class App {
|
|
|
4311
4497
|
source: thisTab,
|
|
4312
4498
|
context: this.contextId,
|
|
4313
4499
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
4500
|
+
this.markSentToParent();
|
|
4314
4501
|
this.debug.info('Trying to signal to parent, attempt:', retries + 1);
|
|
4315
4502
|
retries++;
|
|
4316
4503
|
};
|
|
@@ -4528,7 +4715,13 @@ class App {
|
|
|
4528
4715
|
line: proto.polling,
|
|
4529
4716
|
context: this.contextId,
|
|
4530
4717
|
}, options.crossdomain?.parentDomain ?? '*');
|
|
4718
|
+
this.markSentToParent();
|
|
4531
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);
|
|
4532
4725
|
}
|
|
4533
4726
|
else {
|
|
4534
4727
|
this.initWorker();
|
|
@@ -4537,6 +4730,13 @@ class App {
|
|
|
4537
4730
|
* so they can act as if it was just a same-domain iframe
|
|
4538
4731
|
* */
|
|
4539
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
|
+
}
|
|
4540
4740
|
}
|
|
4541
4741
|
if (this.bc !== null) {
|
|
4542
4742
|
this.bc.postMessage({
|
|
@@ -4586,6 +4786,62 @@ class App {
|
|
|
4586
4786
|
};
|
|
4587
4787
|
}
|
|
4588
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
|
+
}
|
|
4589
4845
|
pruneStaleFrames() {
|
|
4590
4846
|
const staleAfter = Date.now() - this.FRAME_STALE_MS;
|
|
4591
4847
|
this.trackedFrames = this.trackedFrames.filter((ctx) => {
|
|
@@ -4596,6 +4852,45 @@ class App {
|
|
|
4596
4852
|
return false;
|
|
4597
4853
|
});
|
|
4598
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
|
+
}
|
|
4599
4894
|
allowAppStart() {
|
|
4600
4895
|
this.canStart = true;
|
|
4601
4896
|
if (this.startTimeout) {
|
|
@@ -4761,7 +5056,9 @@ class App {
|
|
|
4761
5056
|
window.parent.postMessage({
|
|
4762
5057
|
line: proto.iframeBatch,
|
|
4763
5058
|
messages: this.messages,
|
|
5059
|
+
context: this.contextId,
|
|
4764
5060
|
}, this.options.crossdomain?.parentDomain ?? '*');
|
|
5061
|
+
this.markSentToParent();
|
|
4765
5062
|
this.commitCallbacks.forEach((cb) => cb(this.messages));
|
|
4766
5063
|
this.messages.length = 0;
|
|
4767
5064
|
return;
|
|
@@ -7017,15 +7314,28 @@ function CSSRules (app, opts) {
|
|
|
7017
7314
|
if (!nodeID)
|
|
7018
7315
|
return;
|
|
7019
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
|
+
}
|
|
7020
7329
|
const sheetID = nextID();
|
|
7021
7330
|
styleSheetIDMap.set(sheet, sheetID);
|
|
7022
7331
|
app.send(AdoptedSSAddOwner(sheetID, nodeID));
|
|
7023
|
-
for (let i = 0; i <
|
|
7332
|
+
for (let i = 0; i < rules.length; i++) {
|
|
7024
7333
|
try {
|
|
7025
|
-
sendInsertDeleteRule(sheet, i,
|
|
7334
|
+
sendInsertDeleteRule(sheet, i, rules[i].cssText);
|
|
7026
7335
|
}
|
|
7027
7336
|
catch (e) {
|
|
7028
7337
|
// Skip inaccessible rules
|
|
7338
|
+
app.debug.log('Couldnt access stylesheet rule during initial scan', e);
|
|
7029
7339
|
}
|
|
7030
7340
|
}
|
|
7031
7341
|
});
|
|
@@ -7454,7 +7764,7 @@ class NetworkMessage {
|
|
|
7454
7764
|
return null;
|
|
7455
7765
|
const gqlHeader = "application/graphql-response";
|
|
7456
7766
|
const isGraphql = messageInfo.url.includes("/graphql")
|
|
7457
|
-
|| Object.values(messageInfo.request.headers).some(v => v
|
|
7767
|
+
|| Object.values(messageInfo.request.headers).some(v => v.includes(gqlHeader));
|
|
7458
7768
|
if (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
|
|
7459
7769
|
const isError = messageInfo.response.body.includes("errors");
|
|
7460
7770
|
messageInfo.status = isError ? 400 : 200;
|
|
@@ -7558,7 +7868,6 @@ const genStringBody = (body) => {
|
|
|
7558
7868
|
}
|
|
7559
7869
|
else if (body instanceof Blob ||
|
|
7560
7870
|
body instanceof ReadableStream ||
|
|
7561
|
-
ArrayBuffer.isView(body) ||
|
|
7562
7871
|
body instanceof ArrayBuffer) {
|
|
7563
7872
|
result = 'byte data';
|
|
7564
7873
|
}
|
|
@@ -9047,7 +9356,7 @@ class ConstantProperties {
|
|
|
9047
9356
|
user_id: this.user_id,
|
|
9048
9357
|
distinct_id: this.deviceId,
|
|
9049
9358
|
sdk_edition: 'web',
|
|
9050
|
-
sdk_version: '18.0.
|
|
9359
|
+
sdk_version: '18.0.12-beta.1',
|
|
9051
9360
|
timezone: getUTCOffsetString(),
|
|
9052
9361
|
search_engine: this.searchEngine,
|
|
9053
9362
|
};
|
|
@@ -9749,7 +10058,7 @@ class API {
|
|
|
9749
10058
|
this.signalStartIssue = (reason, missingApi) => {
|
|
9750
10059
|
const doNotTrack = this.checkDoNotTrack();
|
|
9751
10060
|
console.log("Tracker couldn't start due to:", JSON.stringify({
|
|
9752
|
-
trackerVersion: '18.0.
|
|
10061
|
+
trackerVersion: '18.0.12-beta.1',
|
|
9753
10062
|
projectKey: this.options.projectKey,
|
|
9754
10063
|
doNotTrack,
|
|
9755
10064
|
reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
|