@oxvo/browser 7.1.0 → 7.3.4

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.
Files changed (68) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/common/interaction.d.ts +12 -1
  3. package/dist/cjs/common/messages.gen.d.ts +4 -7
  4. package/dist/cjs/entry.js +2382 -1191
  5. package/dist/cjs/entry.js.map +1 -1
  6. package/dist/cjs/index.js +2329 -1179
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/main/app/canvas.d.ts +13 -0
  9. package/dist/cjs/main/app/cross_domain_iframe.d.ts +44 -0
  10. package/dist/cjs/main/app/index.d.ts +63 -15
  11. package/dist/cjs/main/app/messages.gen.d.ts +2 -1
  12. package/dist/cjs/main/app/observer/observer.d.ts +16 -0
  13. package/dist/cjs/main/app/sanitizer.d.ts +5 -3
  14. package/dist/cjs/main/app/session.d.ts +1 -0
  15. package/dist/cjs/main/entry.d.ts +5 -5
  16. package/dist/cjs/main/index.d.ts +17 -1
  17. package/dist/cjs/main/modules/analytics/batcher.d.ts +6 -1
  18. package/dist/cjs/main/modules/analytics/index.d.ts +10 -0
  19. package/dist/cjs/main/modules/input.d.ts +2 -1
  20. package/dist/cjs/main/modules/markerWatcher.d.ts +4 -8
  21. package/dist/cjs/main/modules/mouse.d.ts +1 -0
  22. package/dist/cjs/main/modules/tagMatcher.d.ts +28 -0
  23. package/dist/cjs/main/singleton.d.ts +24 -0
  24. package/dist/cjs/main/utils.d.ts +2 -0
  25. package/dist/cjs/package.json +1 -0
  26. package/dist/lib/common/interaction.d.ts +12 -1
  27. package/dist/lib/common/messages.gen.d.ts +4 -7
  28. package/dist/lib/entry.js +2382 -1191
  29. package/dist/lib/entry.js.map +1 -1
  30. package/dist/lib/index.js +2329 -1179
  31. package/dist/lib/index.js.map +1 -1
  32. package/dist/lib/main/app/canvas.d.ts +13 -0
  33. package/dist/lib/main/app/cross_domain_iframe.d.ts +44 -0
  34. package/dist/lib/main/app/index.d.ts +63 -15
  35. package/dist/lib/main/app/messages.gen.d.ts +2 -1
  36. package/dist/lib/main/app/observer/observer.d.ts +16 -0
  37. package/dist/lib/main/app/sanitizer.d.ts +5 -3
  38. package/dist/lib/main/app/session.d.ts +1 -0
  39. package/dist/lib/main/entry.d.ts +5 -5
  40. package/dist/lib/main/index.d.ts +17 -1
  41. package/dist/lib/main/modules/analytics/batcher.d.ts +6 -1
  42. package/dist/lib/main/modules/analytics/index.d.ts +10 -0
  43. package/dist/lib/main/modules/input.d.ts +2 -1
  44. package/dist/lib/main/modules/markerWatcher.d.ts +4 -8
  45. package/dist/lib/main/modules/mouse.d.ts +1 -0
  46. package/dist/lib/main/modules/tagMatcher.d.ts +28 -0
  47. package/dist/lib/main/singleton.d.ts +24 -0
  48. package/dist/lib/main/utils.d.ts +2 -0
  49. package/dist/types/common/interaction.d.ts +12 -1
  50. package/dist/types/common/messages.gen.d.ts +4 -7
  51. package/dist/types/main/app/canvas.d.ts +13 -0
  52. package/dist/types/main/app/cross_domain_iframe.d.ts +44 -0
  53. package/dist/types/main/app/index.d.ts +63 -15
  54. package/dist/types/main/app/messages.gen.d.ts +2 -1
  55. package/dist/types/main/app/observer/observer.d.ts +16 -0
  56. package/dist/types/main/app/sanitizer.d.ts +5 -3
  57. package/dist/types/main/app/session.d.ts +1 -0
  58. package/dist/types/main/entry.d.ts +5 -5
  59. package/dist/types/main/index.d.ts +17 -1
  60. package/dist/types/main/modules/analytics/batcher.d.ts +6 -1
  61. package/dist/types/main/modules/analytics/index.d.ts +10 -0
  62. package/dist/types/main/modules/input.d.ts +2 -1
  63. package/dist/types/main/modules/markerWatcher.d.ts +4 -8
  64. package/dist/types/main/modules/mouse.d.ts +1 -0
  65. package/dist/types/main/modules/tagMatcher.d.ts +28 -0
  66. package/dist/types/main/singleton.d.ts +24 -0
  67. package/dist/types/main/utils.d.ts +2 -0
  68. package/package.json +14 -14
package/dist/lib/index.js CHANGED
@@ -1,716 +1,3 @@
1
- // DEFLATE is a complex format; to read this code, you should probably check the RFC first:
2
- // https://tools.ietf.org/html/rfc1951
3
- // You may also wish to take a look at the guide I made about this program:
4
- // https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
5
- // Some of the following code is similar to that of UZIP.js:
6
- // https://github.com/photopea/UZIP.js
7
- // However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
8
- // Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
9
- // is better for memory in most engines (I *think*).
10
- var ch2 = {};
11
- var wk = (function (c, id, msg, transfer, cb) {
12
- var w = new Worker(ch2[id] || (ch2[id] = URL.createObjectURL(new Blob([
13
- c + ';addEventListener("error",function(e){e=e.error;postMessage({$e$:[e.message,e.code,e.stack]})})'
14
- ], { type: 'text/javascript' }))));
15
- w.onmessage = function (e) {
16
- var d = e.data, ed = d.$e$;
17
- if (ed) {
18
- var err = new Error(ed[0]);
19
- err['code'] = ed[1];
20
- err.stack = ed[2];
21
- cb(err, null);
22
- }
23
- else
24
- cb(null, d);
25
- };
26
- w.postMessage(msg, transfer);
27
- return w;
28
- });
29
-
30
- // aliases for shorter compressed code (most minifers don't do this)
31
- var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
32
- // fixed length extra bits
33
- var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
34
- // fixed distance extra bits
35
- var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
36
- // code length index map
37
- var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
38
- // get base, reverse index map from extra bits
39
- var freb = function (eb, start) {
40
- var b = new u16(31);
41
- for (var i = 0; i < 31; ++i) {
42
- b[i] = start += 1 << eb[i - 1];
43
- }
44
- // numbers here are at max 18 bits
45
- var r = new i32(b[30]);
46
- for (var i = 1; i < 30; ++i) {
47
- for (var j = b[i]; j < b[i + 1]; ++j) {
48
- r[j] = ((j - b[i]) << 5) | i;
49
- }
50
- }
51
- return { b: b, r: r };
52
- };
53
- var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
54
- // we can ignore the fact that the other numbers are wrong; they never happen anyway
55
- fl[28] = 258, revfl[258] = 28;
56
- var _b = freb(fdeb, 0), revfd = _b.r;
57
- // map of value to reverse (assuming 16 bits)
58
- var rev = new u16(32768);
59
- for (var i$1 = 0; i$1 < 32768; ++i$1) {
60
- // reverse table algorithm from SO
61
- var x$1 = ((i$1 & 0xAAAA) >> 1) | ((i$1 & 0x5555) << 1);
62
- x$1 = ((x$1 & 0xCCCC) >> 2) | ((x$1 & 0x3333) << 2);
63
- x$1 = ((x$1 & 0xF0F0) >> 4) | ((x$1 & 0x0F0F) << 4);
64
- rev[i$1] = (((x$1 & 0xFF00) >> 8) | ((x$1 & 0x00FF) << 8)) >> 1;
65
- }
66
- // create huffman tree from u8 "map": index -> code length for code index
67
- // mb (max bits) must be at most 15
68
- // TODO: optimize/split up?
69
- var hMap = (function (cd, mb, r) {
70
- var s = cd.length;
71
- // index
72
- var i = 0;
73
- // u16 "map": index -> # of codes with bit length = index
74
- var l = new u16(mb);
75
- // length of cd must be 288 (total # of codes)
76
- for (; i < s; ++i) {
77
- if (cd[i])
78
- ++l[cd[i] - 1];
79
- }
80
- // u16 "map": index -> minimum code for bit length = index
81
- var le = new u16(mb);
82
- for (i = 1; i < mb; ++i) {
83
- le[i] = (le[i - 1] + l[i - 1]) << 1;
84
- }
85
- var co;
86
- if (r) {
87
- // u16 "map": index -> number of actual bits, symbol for code
88
- co = new u16(1 << mb);
89
- // bits to remove for reverser
90
- var rvb = 15 - mb;
91
- for (i = 0; i < s; ++i) {
92
- // ignore 0 lengths
93
- if (cd[i]) {
94
- // num encoding both symbol and bits read
95
- var sv = (i << 4) | cd[i];
96
- // free bits
97
- var r_1 = mb - cd[i];
98
- // start value
99
- var v = le[cd[i] - 1]++ << r_1;
100
- // m is end value
101
- for (var m = v | ((1 << r_1) - 1); v <= m; ++v) {
102
- // every 16 bit value starting with the code yields the same result
103
- co[rev[v] >> rvb] = sv;
104
- }
105
- }
106
- }
107
- }
108
- else {
109
- co = new u16(s);
110
- for (i = 0; i < s; ++i) {
111
- if (cd[i]) {
112
- co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]);
113
- }
114
- }
115
- }
116
- return co;
117
- });
118
- // fixed length tree
119
- var flt = new u8(288);
120
- for (var i$1 = 0; i$1 < 144; ++i$1)
121
- flt[i$1] = 8;
122
- for (var i$1 = 144; i$1 < 256; ++i$1)
123
- flt[i$1] = 9;
124
- for (var i$1 = 256; i$1 < 280; ++i$1)
125
- flt[i$1] = 7;
126
- for (var i$1 = 280; i$1 < 288; ++i$1)
127
- flt[i$1] = 8;
128
- // fixed distance tree
129
- var fdt = new u8(32);
130
- for (var i$1 = 0; i$1 < 32; ++i$1)
131
- fdt[i$1] = 5;
132
- // fixed length map
133
- var flm = /*#__PURE__*/ hMap(flt, 9, 0);
134
- // fixed distance map
135
- var fdm = /*#__PURE__*/ hMap(fdt, 5, 0);
136
- // get end of byte
137
- var shft = function (p) { return ((p + 7) / 8) | 0; };
138
- // typed array slice - allows garbage collector to free original reference,
139
- // while being more compatible than .slice
140
- var slc = function (v, s, e) {
141
- if (s == null || s < 0)
142
- s = 0;
143
- if (e == null || e > v.length)
144
- e = v.length;
145
- // can't use .constructor in case user-supplied
146
- return new u8(v.subarray(s, e));
147
- };
148
- // error codes
149
- var ec = [
150
- 'unexpected EOF',
151
- 'invalid block type',
152
- 'invalid length/literal',
153
- 'invalid distance',
154
- 'stream finished',
155
- 'no stream handler',
156
- ,
157
- 'no callback',
158
- 'invalid UTF-8 data',
159
- 'extra field too long',
160
- 'date not in range 1980-2099',
161
- 'filename too long',
162
- 'stream finishing',
163
- 'invalid zip data'
164
- // determined by unknown compression method
165
- ];
166
- var err = function (ind, msg, nt) {
167
- var e = new Error(msg || ec[ind]);
168
- e.code = ind;
169
- if (Error.captureStackTrace)
170
- Error.captureStackTrace(e, err);
171
- if (!nt)
172
- throw e;
173
- return e;
174
- };
175
- // starting at p, write the minimum number of bits that can hold v to d
176
- var wbits = function (d, p, v) {
177
- v <<= p & 7;
178
- var o = (p / 8) | 0;
179
- d[o] |= v;
180
- d[o + 1] |= v >> 8;
181
- };
182
- // starting at p, write the minimum number of bits (>8) that can hold v to d
183
- var wbits16 = function (d, p, v) {
184
- v <<= p & 7;
185
- var o = (p / 8) | 0;
186
- d[o] |= v;
187
- d[o + 1] |= v >> 8;
188
- d[o + 2] |= v >> 16;
189
- };
190
- // creates code lengths from a frequency table
191
- var hTree = function (d, mb) {
192
- // Need extra info to make a tree
193
- var t = [];
194
- for (var i = 0; i < d.length; ++i) {
195
- if (d[i])
196
- t.push({ s: i, f: d[i] });
197
- }
198
- var s = t.length;
199
- var t2 = t.slice();
200
- if (!s)
201
- return { t: et, l: 0 };
202
- if (s == 1) {
203
- var v = new u8(t[0].s + 1);
204
- v[t[0].s] = 1;
205
- return { t: v, l: 1 };
206
- }
207
- t.sort(function (a, b) { return a.f - b.f; });
208
- // after i2 reaches last ind, will be stopped
209
- // freq must be greater than largest possible number of symbols
210
- t.push({ s: -1, f: 25001 });
211
- var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2;
212
- t[0] = { s: -1, f: l.f + r.f, l: l, r: r };
213
- // efficient algorithm from UZIP.js
214
- // i0 is lookbehind, i2 is lookahead - after processing two low-freq
215
- // symbols that combined have high freq, will start processing i2 (high-freq,
216
- // non-composite) symbols instead
217
- // see https://reddit.com/r/photopea/comments/ikekht/uzipjs_questions/
218
- while (i1 != s - 1) {
219
- l = t[t[i0].f < t[i2].f ? i0++ : i2++];
220
- r = t[i0 != i1 && t[i0].f < t[i2].f ? i0++ : i2++];
221
- t[i1++] = { s: -1, f: l.f + r.f, l: l, r: r };
222
- }
223
- var maxSym = t2[0].s;
224
- for (var i = 1; i < s; ++i) {
225
- if (t2[i].s > maxSym)
226
- maxSym = t2[i].s;
227
- }
228
- // code lengths
229
- var tr = new u16(maxSym + 1);
230
- // max bits in tree
231
- var mbt = ln(t[i1 - 1], tr, 0);
232
- if (mbt > mb) {
233
- // more algorithms from UZIP.js
234
- // TODO: find out how this code works (debt)
235
- // ind debt
236
- var i = 0, dt = 0;
237
- // left cost
238
- var lft = mbt - mb, cst = 1 << lft;
239
- t2.sort(function (a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; });
240
- for (; i < s; ++i) {
241
- var i2_1 = t2[i].s;
242
- if (tr[i2_1] > mb) {
243
- dt += cst - (1 << (mbt - tr[i2_1]));
244
- tr[i2_1] = mb;
245
- }
246
- else
247
- break;
248
- }
249
- dt >>= lft;
250
- while (dt > 0) {
251
- var i2_2 = t2[i].s;
252
- if (tr[i2_2] < mb)
253
- dt -= 1 << (mb - tr[i2_2]++ - 1);
254
- else
255
- ++i;
256
- }
257
- for (; i >= 0 && dt; --i) {
258
- var i2_3 = t2[i].s;
259
- if (tr[i2_3] == mb) {
260
- --tr[i2_3];
261
- ++dt;
262
- }
263
- }
264
- mbt = mb;
265
- }
266
- return { t: new u8(tr), l: mbt };
267
- };
268
- // get the max length and assign length codes
269
- var ln = function (n, l, d) {
270
- return n.s == -1
271
- ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1))
272
- : (l[n.s] = d);
273
- };
274
- // length codes generation
275
- var lc = function (c) {
276
- var s = c.length;
277
- // Note that the semicolon was intentional
278
- while (s && !c[--s])
279
- ;
280
- var cl = new u16(++s);
281
- // ind num streak
282
- var cli = 0, cln = c[0], cls = 1;
283
- var w = function (v) { cl[cli++] = v; };
284
- for (var i = 1; i <= s; ++i) {
285
- if (c[i] == cln && i != s)
286
- ++cls;
287
- else {
288
- if (!cln && cls > 2) {
289
- for (; cls > 138; cls -= 138)
290
- w(32754);
291
- if (cls > 2) {
292
- w(cls > 10 ? ((cls - 11) << 5) | 28690 : ((cls - 3) << 5) | 12305);
293
- cls = 0;
294
- }
295
- }
296
- else if (cls > 3) {
297
- w(cln), --cls;
298
- for (; cls > 6; cls -= 6)
299
- w(8304);
300
- if (cls > 2)
301
- w(((cls - 3) << 5) | 8208), cls = 0;
302
- }
303
- while (cls--)
304
- w(cln);
305
- cls = 1;
306
- cln = c[i];
307
- }
308
- }
309
- return { c: cl.subarray(0, cli), n: s };
310
- };
311
- // calculate the length of output from tree, code lengths
312
- var clen = function (cf, cl) {
313
- var l = 0;
314
- for (var i = 0; i < cl.length; ++i)
315
- l += cf[i] * cl[i];
316
- return l;
317
- };
318
- // writes a fixed block
319
- // returns the new bit pos
320
- var wfblk = function (out, pos, dat) {
321
- // no need to write 00 as type: TypedArray defaults to 0
322
- var s = dat.length;
323
- var o = shft(pos + 2);
324
- out[o] = s & 255;
325
- out[o + 1] = s >> 8;
326
- out[o + 2] = out[o] ^ 255;
327
- out[o + 3] = out[o + 1] ^ 255;
328
- for (var i = 0; i < s; ++i)
329
- out[o + i + 4] = dat[i];
330
- return (o + 4 + s) * 8;
331
- };
332
- // writes a block
333
- var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) {
334
- wbits(out, p++, final);
335
- ++lf[256];
336
- var _a = hTree(lf, 15), dlt = _a.t, mlb = _a.l;
337
- var _b = hTree(df, 15), ddt = _b.t, mdb = _b.l;
338
- var _c = lc(dlt), lclt = _c.c, nlc = _c.n;
339
- var _d = lc(ddt), lcdt = _d.c, ndc = _d.n;
340
- var lcfreq = new u16(19);
341
- for (var i = 0; i < lclt.length; ++i)
342
- ++lcfreq[lclt[i] & 31];
343
- for (var i = 0; i < lcdt.length; ++i)
344
- ++lcfreq[lcdt[i] & 31];
345
- var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l;
346
- var nlcc = 19;
347
- for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc)
348
- ;
349
- var flen = (bl + 5) << 3;
350
- var ftlen = clen(lf, flt) + clen(df, fdt) + eb;
351
- var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18];
352
- if (bs >= 0 && flen <= ftlen && flen <= dtlen)
353
- return wfblk(out, p, dat.subarray(bs, bs + bl));
354
- var lm, ll, dm, dl;
355
- wbits(out, p, 1 + (dtlen < ftlen)), p += 2;
356
- if (dtlen < ftlen) {
357
- lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt;
358
- var llm = hMap(lct, mlcb, 0);
359
- wbits(out, p, nlc - 257);
360
- wbits(out, p + 5, ndc - 1);
361
- wbits(out, p + 10, nlcc - 4);
362
- p += 14;
363
- for (var i = 0; i < nlcc; ++i)
364
- wbits(out, p + 3 * i, lct[clim[i]]);
365
- p += 3 * nlcc;
366
- var lcts = [lclt, lcdt];
367
- for (var it = 0; it < 2; ++it) {
368
- var clct = lcts[it];
369
- for (var i = 0; i < clct.length; ++i) {
370
- var len = clct[i] & 31;
371
- wbits(out, p, llm[len]), p += lct[len];
372
- if (len > 15)
373
- wbits(out, p, (clct[i] >> 5) & 127), p += clct[i] >> 12;
374
- }
375
- }
376
- }
377
- else {
378
- lm = flm, ll = flt, dm = fdm, dl = fdt;
379
- }
380
- for (var i = 0; i < li; ++i) {
381
- var sym = syms[i];
382
- if (sym > 255) {
383
- var len = (sym >> 18) & 31;
384
- wbits16(out, p, lm[len + 257]), p += ll[len + 257];
385
- if (len > 7)
386
- wbits(out, p, (sym >> 23) & 31), p += fleb[len];
387
- var dst = sym & 31;
388
- wbits16(out, p, dm[dst]), p += dl[dst];
389
- if (dst > 3)
390
- wbits16(out, p, (sym >> 5) & 8191), p += fdeb[dst];
391
- }
392
- else {
393
- wbits16(out, p, lm[sym]), p += ll[sym];
394
- }
395
- }
396
- wbits16(out, p, lm[256]);
397
- return p + ll[256];
398
- };
399
- // deflate options (nice << 13) | chain
400
- var deo = /*#__PURE__*/ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]);
401
- // empty
402
- var et = /*#__PURE__*/ new u8(0);
403
- // compresses data into a raw DEFLATE buffer
404
- var dflt = function (dat, lvl, plvl, pre, post, st) {
405
- var s = st.z || dat.length;
406
- var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post);
407
- // writing to this writes to the output buffer
408
- var w = o.subarray(pre, o.length - post);
409
- var lst = st.l;
410
- var pos = (st.r || 0) & 7;
411
- if (lvl) {
412
- if (pos)
413
- w[0] = st.r >> 3;
414
- var opt = deo[lvl - 1];
415
- var n = opt >> 13, c = opt & 8191;
416
- var msk_1 = (1 << plvl) - 1;
417
- // prev 2-byte val map curr 2-byte val map
418
- var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1);
419
- var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1;
420
- var hsh = function (i) { return (dat[i] ^ (dat[i + 1] << bs1_1) ^ (dat[i + 2] << bs2_1)) & msk_1; };
421
- // 24576 is an arbitrary number of maximum symbols per block
422
- // 424 buffer for last block
423
- var syms = new i32(25000);
424
- // length/literal freq distance freq
425
- var lf = new u16(288), df = new u16(32);
426
- // l/lcnt exbits index l/lind waitdx blkpos
427
- var lc_1 = 0, eb = 0, i = st.i || 0, li = 0, wi = st.w || 0, bs = 0;
428
- for (; i + 2 < s; ++i) {
429
- // hash value
430
- var hv = hsh(i);
431
- // index mod 32768 previous index mod
432
- var imod = i & 32767, pimod = head[hv];
433
- prev[imod] = pimod;
434
- head[hv] = imod;
435
- // We always should modify head and prev, but only add symbols if
436
- // this data is not yet processed ("wait" for wait index)
437
- if (wi <= i) {
438
- // bytes remaining
439
- var rem = s - i;
440
- if ((lc_1 > 7000 || li > 24576) && (rem > 423 || !lst)) {
441
- pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos);
442
- li = lc_1 = eb = 0, bs = i;
443
- for (var j = 0; j < 286; ++j)
444
- lf[j] = 0;
445
- for (var j = 0; j < 30; ++j)
446
- df[j] = 0;
447
- }
448
- // len dist chain
449
- var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767;
450
- if (rem > 2 && hv == hsh(i - dif)) {
451
- var maxn = Math.min(n, rem) - 1;
452
- var maxd = Math.min(32767, i);
453
- // max possible length
454
- // not capped at dif because decompressors implement "rolling" index population
455
- var ml = Math.min(258, rem);
456
- while (dif <= maxd && --ch_1 && imod != pimod) {
457
- if (dat[i + l] == dat[i + l - dif]) {
458
- var nl = 0;
459
- for (; nl < ml && dat[i + nl] == dat[i + nl - dif]; ++nl)
460
- ;
461
- if (nl > l) {
462
- l = nl, d = dif;
463
- // break out early when we reach "nice" (we are satisfied enough)
464
- if (nl > maxn)
465
- break;
466
- // now, find the rarest 2-byte sequence within this
467
- // length of literals and search for that instead.
468
- // Much faster than just using the start
469
- var mmd = Math.min(dif, nl - 2);
470
- var md = 0;
471
- for (var j = 0; j < mmd; ++j) {
472
- var ti = i - dif + j & 32767;
473
- var pti = prev[ti];
474
- var cd = ti - pti & 32767;
475
- if (cd > md)
476
- md = cd, pimod = ti;
477
- }
478
- }
479
- }
480
- // check the previous match
481
- imod = pimod, pimod = prev[imod];
482
- dif += imod - pimod & 32767;
483
- }
484
- }
485
- // d will be nonzero only when a match was found
486
- if (d) {
487
- // store both dist and len data in one int32
488
- // Make sure this is recognized as a len/dist with 28th bit (2^28)
489
- syms[li++] = 268435456 | (revfl[l] << 18) | revfd[d];
490
- var lin = revfl[l] & 31, din = revfd[d] & 31;
491
- eb += fleb[lin] + fdeb[din];
492
- ++lf[257 + lin];
493
- ++df[din];
494
- wi = i + l;
495
- ++lc_1;
496
- }
497
- else {
498
- syms[li++] = dat[i];
499
- ++lf[dat[i]];
500
- }
501
- }
502
- }
503
- for (i = Math.max(i, wi); i < s; ++i) {
504
- syms[li++] = dat[i];
505
- ++lf[dat[i]];
506
- }
507
- pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos);
508
- if (!lst) {
509
- st.r = (pos & 7) | w[(pos / 8) | 0] << 3;
510
- // shft(pos) now 1 less if pos & 7 != 0
511
- pos -= 7;
512
- st.h = head, st.p = prev, st.i = i, st.w = wi;
513
- }
514
- }
515
- else {
516
- for (var i = st.w || 0; i < s + lst; i += 65535) {
517
- // end
518
- var e = i + 65535;
519
- if (e >= s) {
520
- // write final block
521
- w[(pos / 8) | 0] = lst;
522
- e = s;
523
- }
524
- pos = wfblk(w, pos + 1, dat.subarray(i, e));
525
- }
526
- st.i = s;
527
- }
528
- return slc(o, 0, pre + shft(pos) + post);
529
- };
530
- // CRC32 table
531
- var crct = /*#__PURE__*/ (function () {
532
- var t = new Int32Array(256);
533
- for (var i = 0; i < 256; ++i) {
534
- var c = i, k = 9;
535
- while (--k)
536
- c = ((c & 1) && -306674912) ^ (c >>> 1);
537
- t[i] = c;
538
- }
539
- return t;
540
- })();
541
- // CRC32
542
- var crc = function () {
543
- var c = -1;
544
- return {
545
- p: function (d) {
546
- // closures have awful performance
547
- var cr = c;
548
- for (var i = 0; i < d.length; ++i)
549
- cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8);
550
- c = cr;
551
- },
552
- d: function () { return ~c; }
553
- };
554
- };
555
- // deflate with opts
556
- var dopt = function (dat, opt, pre, post, st) {
557
- if (!st) {
558
- st = { l: 1 };
559
- if (opt.dictionary) {
560
- var dict = opt.dictionary.subarray(-32768);
561
- var newDat = new u8(dict.length + dat.length);
562
- newDat.set(dict);
563
- newDat.set(dat, dict.length);
564
- dat = newDat;
565
- st.w = dict.length;
566
- }
567
- }
568
- return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? (st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20) : (12 + opt.mem), pre, post, st);
569
- };
570
- // Walmart object spread
571
- var mrg = function (a, b) {
572
- var o = {};
573
- for (var k in a)
574
- o[k] = a[k];
575
- for (var k in b)
576
- o[k] = b[k];
577
- return o;
578
- };
579
- // worker clone
580
- // This is possibly the craziest part of the entire codebase, despite how simple it may seem.
581
- // The only parameter to this function is a closure that returns an array of variables outside of the function scope.
582
- // We're going to try to figure out the variable names used in the closure as strings because that is crucial for workerization.
583
- // We will return an object mapping of true variable name to value (basically, the current scope as a JS object).
584
- // The reason we can't just use the original variable names is minifiers mangling the toplevel scope.
585
- // This took me three weeks to figure out how to do.
586
- var wcln = function (fn, fnStr, td) {
587
- var dt = fn();
588
- var st = fn.toString();
589
- var ks = st.slice(st.indexOf('[') + 1, st.lastIndexOf(']')).replace(/\s+/g, '').split(',');
590
- for (var i = 0; i < dt.length; ++i) {
591
- var v = dt[i], k = ks[i];
592
- if (typeof v == 'function') {
593
- fnStr += ';' + k + '=';
594
- var st_1 = v.toString();
595
- if (v.prototype) {
596
- // for global objects
597
- if (st_1.indexOf('[native code]') != -1) {
598
- var spInd = st_1.indexOf(' ', 8) + 1;
599
- fnStr += st_1.slice(spInd, st_1.indexOf('(', spInd));
600
- }
601
- else {
602
- fnStr += st_1;
603
- for (var t in v.prototype)
604
- fnStr += ';' + k + '.prototype.' + t + '=' + v.prototype[t].toString();
605
- }
606
- }
607
- else
608
- fnStr += st_1;
609
- }
610
- else
611
- td[k] = v;
612
- }
613
- return fnStr;
614
- };
615
- var ch = [];
616
- // clone bufs
617
- var cbfs = function (v) {
618
- var tl = [];
619
- for (var k in v) {
620
- if (v[k].buffer) {
621
- tl.push((v[k] = new v[k].constructor(v[k])).buffer);
622
- }
623
- }
624
- return tl;
625
- };
626
- // use a worker to execute code
627
- var wrkr = function (fns, init, id, cb) {
628
- if (!ch[id]) {
629
- var fnStr = '', td_1 = {}, m = fns.length - 1;
630
- for (var i = 0; i < m; ++i)
631
- fnStr = wcln(fns[i], fnStr, td_1);
632
- ch[id] = { c: wcln(fns[m], fnStr, td_1), e: td_1 };
633
- }
634
- var td = mrg({}, ch[id].e);
635
- return wk(ch[id].c + ';onmessage=function(e){for(var k in e.data)self[k]=e.data[k];onmessage=' + init.toString() + '}', id, td, cbfs(td), cb);
636
- };
637
- var bDflt = function () { return [u8, u16, i32, fleb, fdeb, clim, revfl, revfd, flm, flt, fdm, fdt, rev, deo, et, hMap, wbits, wbits16, hTree, ln, lc, clen, wfblk, wblk, shft, slc, dflt, dopt, deflateSync, pbf]; };
638
- // gzip extra
639
- var gze = function () { return [gzh, gzhl, wbytes, crc, crct]; };
640
- // post buf
641
- var pbf = function (msg) { return postMessage(msg, [msg.buffer]); };
642
- // async helper
643
- var cbify = function (dat, opts, fns, init, id, cb) {
644
- var w = wrkr(fns, init, id, function (err, dat) {
645
- w.terminate();
646
- cb(err, dat);
647
- });
648
- w.postMessage([dat, opts], opts.consume ? [dat.buffer] : []);
649
- return function () { w.terminate(); };
650
- };
651
- // write bytes
652
- var wbytes = function (d, b, v) {
653
- for (; v; ++b)
654
- d[b] = v, v >>>= 8;
655
- };
656
- // gzip header
657
- var gzh = function (c, o) {
658
- var fn = o.filename;
659
- c[0] = 31, c[1] = 139, c[2] = 8, c[8] = o.level < 2 ? 4 : o.level == 9 ? 2 : 0, c[9] = 3; // assume Unix
660
- if (o.mtime != 0)
661
- wbytes(c, 4, Math.floor(new Date(o.mtime || Date.now()) / 1000));
662
- if (fn) {
663
- c[3] = 8;
664
- for (var i = 0; i <= fn.length; ++i)
665
- c[i + 10] = fn.charCodeAt(i);
666
- }
667
- };
668
- // gzip header length
669
- var gzhl = function (o) { return 10 + (o.filename ? o.filename.length + 1 : 0); };
670
- /**
671
- * Compresses data with DEFLATE without any wrapper
672
- * @param data The data to compress
673
- * @param opts The compression options
674
- * @returns The deflated version of the data
675
- */
676
- function deflateSync(data, opts) {
677
- return dopt(data, opts || {}, 0, 0);
678
- }
679
- function gzip(data, opts, cb) {
680
- if (!cb)
681
- cb = opts, opts = {};
682
- if (typeof cb != 'function')
683
- err(7);
684
- return cbify(data, opts, [
685
- bDflt,
686
- gze,
687
- function () { return [gzipSync]; }
688
- ], function (ev) { return pbf(gzipSync(ev.data[0], ev.data[1])); }, 2, cb);
689
- }
690
- /**
691
- * Compresses data with GZIP
692
- * @param data The data to compress
693
- * @param opts The compression options
694
- * @returns The gzipped version of the data
695
- */
696
- function gzipSync(data, opts) {
697
- if (!opts)
698
- opts = {};
699
- var c = crc(), l = data.length;
700
- c.p(data);
701
- var d = dopt(data, opts, gzhl(opts), 8), s = d.length;
702
- return gzh(d, opts), wbytes(d, s - 8, c.d()), wbytes(d, s - 4, l), d;
703
- }
704
- // text decoder
705
- var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
706
- // text decoder stream
707
- var tds = 0;
708
- try {
709
- td.decode(et, { stream: true });
710
- tds = 1;
711
- }
712
- catch (e) { }
713
-
714
1
  class StringDictionary {
715
2
  constructor() {
716
3
  this.lastTs = 0;
@@ -1161,6 +448,23 @@ function deprecationWarn(nameOfFeature, useInstead, docsPath = '/') {
1161
448
  console.warn(`OxvoSessions: ${nameOfFeature} is deprecated. ${useInstead ? `Please, use ${useInstead} instead.` : ''} Visit ${DOCS_HOST}${docsPath} for more information.`);
1162
449
  warnedFeatures[nameOfFeature] = true;
1163
450
  }
451
+ function getCustomAttributeLabel(e, customAttributes) {
452
+ if (!customAttributes || customAttributes.length === 0)
453
+ return '';
454
+ const parts = [];
455
+ for (const attr of customAttributes) {
456
+ const value = e.getAttribute(attr);
457
+ if (value !== null) {
458
+ parts.push(`[${attr}=${value}]`);
459
+ }
460
+ }
461
+ return parts.join('');
462
+ }
463
+ function getClassSelector(e) {
464
+ if (!e.classList || e.classList.length === 0)
465
+ return '';
466
+ return '.' + Array.from(e.classList).join('.');
467
+ }
1164
468
  function getLabelAttribute(e) {
1165
469
  let value = e.getAttribute('data-oxvosessions-label');
1166
470
  if (value !== null) {
@@ -1226,7 +530,7 @@ function dec2hex(dec) {
1226
530
  return dec.toString(16).padStart(2, '0');
1227
531
  }
1228
532
  function generateRandomId(len) {
1229
- const arr = new Uint8Array((len) / 2);
533
+ const arr = new Uint8Array((len || 40) / 2);
1230
534
  // msCrypto = IE11
1231
535
  // @ts-ignore
1232
536
  const safeCrypto = window.crypto || window.msCrypto;
@@ -1235,7 +539,7 @@ function generateRandomId(len) {
1235
539
  return Array.from(arr, dec2hex).join('');
1236
540
  }
1237
541
  else {
1238
- return Array.from({ length: len }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
542
+ return Array.from({ length: len || 40 }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
1239
543
  }
1240
544
  }
1241
545
  function inIframe() {
@@ -1432,6 +736,88 @@ function throttleWithTrailing(fn, interval) {
1432
736
 
1433
737
  // Auto-generated, do not edit
1434
738
  /* eslint-disable */
739
+ const MESSAGE_FIELD_TYPES = {
740
+ 0: ['number'],
741
+ 4: ['string', 'string', 'number'],
742
+ 5: ['number', 'number'],
743
+ 6: ['number', 'number'],
744
+ 7: [],
745
+ 8: ['number', 'number', 'number', 'string', 'boolean'],
746
+ 9: ['number', 'number', 'number'],
747
+ 10: ['number', 'number', 'number'],
748
+ 11: ['number'],
749
+ 12: ['number', 'string', 'string'],
750
+ 13: ['number', 'string'],
751
+ 14: ['number', 'string'],
752
+ 16: ['number', 'number', 'number'],
753
+ 17: ['number', 'string'],
754
+ 18: ['number', 'string', 'number'],
755
+ 19: ['number', 'boolean'],
756
+ 20: ['number', 'number'],
757
+ 21: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number'],
758
+ 22: ['string', 'string'],
759
+ 23: ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
760
+ 24: ['number', 'number', 'number'],
761
+ 27: ['string', 'string'],
762
+ 28: ['string'],
763
+ 29: ['string'],
764
+ 30: ['string', 'string'],
765
+ 34: ['number', 'string'],
766
+ 35: ['number', 'number', 'number'],
767
+ 36: ['number', 'string'],
768
+ 40: ['string', 'number', 'string', 'string'],
769
+ 41: ['string', 'string'],
770
+ 42: ['string'],
771
+ 44: ['string', 'string', 'number'],
772
+ 45: ['string', 'string'],
773
+ 46: ['string', 'string'],
774
+ 47: ['string', 'string', 'number'],
775
+ 48: ['string', 'string', 'string', 'string', 'number'],
776
+ 49: ['number', 'number', 'number', 'number'],
777
+ 50: ['number', 'string'],
778
+ 51: ['number', 'number', 'number'],
779
+ 43: ['string', 'string'],
780
+ 52: ['number', 'string', 'string'],
781
+ 53: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string'],
782
+ 54: ['number', 'string'],
783
+ 55: ['boolean'],
784
+ 57: ['number', 'string', 'string', 'string'],
785
+ 58: ['number'],
786
+ 60: ['number', 'string', 'string', 'string'],
787
+ 61: ['number', 'string', 'string'],
788
+ 63: ['string', 'string'],
789
+ 64: ['string', 'string'],
790
+ 65: ['number', 'number'],
791
+ 68: ['number', 'number', 'string', 'string', 'number', 'number'],
792
+ 69: ['number', 'number', 'string', 'string'],
793
+ 70: ['number', 'number'],
794
+ 71: ['number', 'string', 'string'],
795
+ 73: ['number', 'string', 'number', 'string'],
796
+ 75: ['number', 'number'],
797
+ 76: ['number', 'number'],
798
+ 77: ['number', 'number'],
799
+ 78: ['string', 'string', 'string', 'string'],
800
+ 79: ['string', 'string'],
801
+ 81: ['number', 'number', 'number', 'number', 'string'],
802
+ 83: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number', 'number'],
803
+ 84: ['string', 'string', 'string', 'number', 'string', 'string'],
804
+ 85: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
805
+ 87: ['string', 'number', 'number'],
806
+ 89: ['string', 'number', 'number', 'number', 'number', 'string'],
807
+ 112: ['number', 'string', 'boolean', 'string', 'number', 'number'],
808
+ 113: ['number', 'number', 'string'],
809
+ 114: ['number'],
810
+ 115: ['number'],
811
+ 116: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean'],
812
+ 117: ['string'],
813
+ 118: ['string'],
814
+ 119: ['string', 'number'],
815
+ 120: ['number'],
816
+ 121: ['string', 'string', 'number', 'number'],
817
+ 122: ['string', 'string', 'number', 'string'],
818
+ 123: ['string', 'string', 'string', 'string', 'number'],
819
+ 124: ['string', 'string'],
820
+ };
1435
821
  function Timestamp(timestamp) {
1436
822
  return [
1437
823
  0 /* Messages.Type.Timestamp */,
@@ -1917,13 +1303,6 @@ function BatchMetadata(version, pageNo, firstIndex, timestamp, location) {
1917
1303
  location,
1918
1304
  ];
1919
1305
  }
1920
- function PartitionedMessage(partNo, partTotal) {
1921
- return [
1922
- 82 /* Messages.Type.PartitionedMessage */,
1923
- partNo,
1924
- partTotal,
1925
- ];
1926
- }
1927
1306
  function NetworkRequest(type, method, url, request, response, status, timestamp, duration, transferredBodySize) {
1928
1307
  return [
1929
1308
  83 /* Messages.Type.NetworkRequest */,
@@ -2121,6 +1500,7 @@ var _Messages = /*#__PURE__*/Object.freeze({
2121
1500
  JSException: JSException,
2122
1501
  LoadFontFace: LoadFontFace,
2123
1502
  LongAnimationTask: LongAnimationTask$1,
1503
+ MESSAGE_FIELD_TYPES: MESSAGE_FIELD_TYPES,
2124
1504
  Metadata: Metadata,
2125
1505
  MobX: MobX,
2126
1506
  MouseClick: MouseClick,
@@ -2135,7 +1515,6 @@ var _Messages = /*#__PURE__*/Object.freeze({
2135
1515
  OTable: OTable,
2136
1516
  PageLoadTiming: PageLoadTiming,
2137
1517
  PageRenderTiming: PageRenderTiming,
2138
- PartitionedMessage: PartitionedMessage,
2139
1518
  PerformanceTrack: PerformanceTrack,
2140
1519
  Profiler: Profiler,
2141
1520
  Redux: Redux,
@@ -2230,11 +1609,157 @@ function Performance (app, opts) {
2230
1609
  }
2231
1610
  }
2232
1611
 
1612
+ /**
1613
+ * Two-tier tag matching:
1614
+ * 1. Fast fingerprint lookup by id, data-attr,
1615
+ * or class from the selector's last segment
1616
+ * 2. Fallback iteration using native element.matches()
1617
+ */
1618
+ class TagMatcher {
1619
+ constructor() {
1620
+ this.tags = [];
1621
+ this.byId = new Map();
1622
+ this.byDataAttr = new Map();
1623
+ this.byClass = new Map();
1624
+ this.fallback = [];
1625
+ }
1626
+ setTags(tags) {
1627
+ this.tags = tags;
1628
+ this.byId.clear();
1629
+ this.byDataAttr.clear();
1630
+ this.byClass.clear();
1631
+ this.fallback = [];
1632
+ for (const tag of tags) {
1633
+ const last = lastSegment(tag.selector);
1634
+ if (!last) {
1635
+ this.fallback.push(tag);
1636
+ continue;
1637
+ }
1638
+ if (last.startsWith('#')) {
1639
+ this.byId.set(last.slice(1), tag);
1640
+ }
1641
+ else if (last.startsWith('[data-')) {
1642
+ this.byDataAttr.set(last, tag);
1643
+ }
1644
+ else {
1645
+ const cls = extractClass(last);
1646
+ if (cls) {
1647
+ this.byClass.set(cls, tag);
1648
+ }
1649
+ else {
1650
+ this.fallback.push(tag);
1651
+ }
1652
+ }
1653
+ }
1654
+ }
1655
+ getTags() {
1656
+ return this.tags;
1657
+ }
1658
+ /** Match element, its parent, or direct children against known tag selectors */
1659
+ match(el) {
1660
+ const direct = this.matchExact(el);
1661
+ if (direct)
1662
+ return direct;
1663
+ if (el.parentElement) {
1664
+ const parent = this.matchExact(el.parentElement);
1665
+ if (parent)
1666
+ return parent;
1667
+ }
1668
+ const children = el.children;
1669
+ for (let i = 0; i < children.length; i++) {
1670
+ const child = this.matchExact(children[i]);
1671
+ if (child)
1672
+ return child;
1673
+ }
1674
+ return null;
1675
+ }
1676
+ matchExact(el) {
1677
+ if (el.id && this.byId.has(el.id)) {
1678
+ const tag = this.byId.get(el.id);
1679
+ if (safeMatches(el, tag.selector) && matchesLocation(tag))
1680
+ return tag;
1681
+ }
1682
+ if (this.byDataAttr.size > 0) {
1683
+ const attrs = el.attributes;
1684
+ for (let i = 0; i < attrs.length; i++) {
1685
+ const attr = attrs[i];
1686
+ if (attr.name.startsWith('data-')) {
1687
+ const key = `[${attr.name}="${attr.value}"]`;
1688
+ if (this.byDataAttr.has(key)) {
1689
+ const tag = this.byDataAttr.get(key);
1690
+ if (safeMatches(el, tag.selector) && matchesLocation(tag))
1691
+ return tag;
1692
+ }
1693
+ }
1694
+ }
1695
+ }
1696
+ if (this.byClass.size > 0 && el.classList) {
1697
+ for (let i = 0; i < el.classList.length; i++) {
1698
+ const cls = el.classList[i];
1699
+ if (this.byClass.has(cls)) {
1700
+ const tag = this.byClass.get(cls);
1701
+ if (safeMatches(el, tag.selector) && matchesLocation(tag))
1702
+ return tag;
1703
+ }
1704
+ }
1705
+ }
1706
+ for (const tag of this.fallback) {
1707
+ if (safeMatches(el, tag.selector) && matchesLocation(tag))
1708
+ return tag;
1709
+ }
1710
+ return null;
1711
+ }
1712
+ clear() {
1713
+ this.tags = [];
1714
+ this.byId.clear();
1715
+ this.byDataAttr.clear();
1716
+ this.byClass.clear();
1717
+ this.fallback = [];
1718
+ }
1719
+ }
1720
+ /** Last combinator-separated segment of a CSS selector */
1721
+ function lastSegment(selector) {
1722
+ const trimmed = selector.trim();
1723
+ if (!trimmed)
1724
+ return null;
1725
+ const parts = trimmed.split(/\s*[>+~ ]\s*/);
1726
+ const last = parts[parts.length - 1]?.trim();
1727
+ return last || null;
1728
+ }
1729
+ /** First class name from a selector segment, e.g. "div.my-class" -> "my-class" */
1730
+ function extractClass(segment) {
1731
+ const match = segment.match(/\.([a-zA-Z_-][\w-]*)/);
1732
+ return match ? match[1] : null;
1733
+ }
1734
+ function safeMatches(el, selector) {
1735
+ try {
1736
+ return el.matches(selector);
1737
+ }
1738
+ catch {
1739
+ return false;
1740
+ }
1741
+ }
1742
+ function matchesLocation(tag) {
1743
+ if (!tag.location)
1744
+ return true;
1745
+ try {
1746
+ const loc = tag.location;
1747
+ if (loc.startsWith('/')) {
1748
+ return window.location.pathname === loc;
1749
+ }
1750
+ return window.location.href === loc;
1751
+ }
1752
+ catch {
1753
+ return true;
1754
+ }
1755
+ }
1756
+
2233
1757
  const WATCHED_MARKERS_KEY = '__or__watched_markers__';
2234
1758
  class MarkerWatcher {
2235
1759
  constructor(params) {
2236
1760
  this.interval = null;
2237
1761
  this.tags = [];
1762
+ this.matcher = new TagMatcher();
2238
1763
  this.sessionStorage = params.sessionStorage;
2239
1764
  this.errLog = params.errLog;
2240
1765
  this.onMarkerHit = params.onMarkerHit;
@@ -2277,12 +1802,15 @@ class MarkerWatcher {
2277
1802
  }
2278
1803
  setTags(tags) {
2279
1804
  this.tags = tags;
1805
+ this.matcher.setTags(tags);
2280
1806
  if (this.interval) {
2281
1807
  clearInterval(this.interval);
2282
1808
  this.interval = null;
2283
1809
  }
2284
1810
  this.interval = setInterval(() => {
2285
1811
  this.tags.forEach((tag) => {
1812
+ if (!matchesLocation(tag))
1813
+ return;
2286
1814
  const possibleEls = document.querySelectorAll(tag.selector);
2287
1815
  if (possibleEls.length > 0) {
2288
1816
  const el = possibleEls[0];
@@ -2294,13 +1822,17 @@ class MarkerWatcher {
2294
1822
  }, 500);
2295
1823
  }
2296
1824
  onMarkerRendered(markerId) {
2297
- if (this.tags.findIndex(t => t.id === markerId)) {
2298
- this.tags = this.tags.filter((tag) => tag.id !== markerId);
1825
+ const tag = this.tags.find((t) => t.id === markerId);
1826
+ if (tag) {
1827
+ this.tags = this.tags.filter((t) => t.id !== markerId);
1828
+ if (!matchesLocation(tag))
1829
+ return;
2299
1830
  }
2300
1831
  this.onMarkerHit(markerId);
2301
1832
  }
2302
1833
  clear() {
2303
1834
  this.tags = [];
1835
+ this.matcher.clear();
2304
1836
  if (this.interval) {
2305
1837
  clearInterval(this.interval);
2306
1838
  this.interval = null;
@@ -2349,8 +1881,34 @@ class CanvasRecorder {
2349
1881
  this.uploadQueue = 0;
2350
1882
  this.MAX_CONCURRENT_UPLOADS = 2;
2351
1883
  this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
1884
+ this.MAX_UPLOAD_ATTEMPTS = 5;
1885
+ this.MAX_RETRY_DELAY_MS = 10000;
2352
1886
  this.pendingBatches = [];
2353
1887
  this.isProcessingQueue = false;
1888
+ /**
1889
+ * Reacts to a runtime sanitization change on a canvas: stop capturing if it
1890
+ * just became masked, start if it just became visible. Already-sent frames
1891
+ * cannot be retracted, so escalation only stops future capture.
1892
+ */
1893
+ this.resanitizeCanvas = (node, id) => {
1894
+ if (!hasTag(node, 'canvas')) {
1895
+ return;
1896
+ }
1897
+ const isIgnored = this.app.sanitizer.isObscured(id) || this.app.sanitizer.isHidden(id);
1898
+ if (isIgnored) {
1899
+ if (this.snapshots[id] || this.observers.has(id)) {
1900
+ const observer = this.observers.get(id);
1901
+ if (observer) {
1902
+ observer.disconnect();
1903
+ this.observers.delete(id);
1904
+ }
1905
+ this.cleanupCanvas(id);
1906
+ }
1907
+ }
1908
+ else if (!this.snapshots[id] && !this.observers.has(id)) {
1909
+ this.captureCanvas(node);
1910
+ }
1911
+ };
2354
1912
  this.restartTracking = () => {
2355
1913
  this.clear();
2356
1914
  this.app.nodes.scanTree(this.captureCanvas);
@@ -2447,14 +2005,15 @@ class CanvasRecorder {
2447
2005
  }, this.interval);
2448
2006
  this.intervals.set(id, int);
2449
2007
  };
2450
- this.fileExt = options.fileExt ?? 'webp';
2008
+ this.fileExt = 'webp';
2451
2009
  this.interval = 1000 / options.fps;
2452
2010
  }
2453
2011
  startTracking() {
2454
2012
  setTimeout(() => {
2455
2013
  this.app.nodes.scanTree(this.captureCanvas);
2456
2014
  this.app.nodes.attachNodeCallback(this.captureCanvas);
2457
- }, 250);
2015
+ this.app.attachResanitizeCallback(this.resanitizeCanvas);
2016
+ }, 125);
2458
2017
  }
2459
2018
  sendSnaps(images, canvasId, createdAt) {
2460
2019
  if (Object.keys(this.snapshots).length === 0) {
@@ -2464,37 +2023,111 @@ class CanvasRecorder {
2464
2023
  this.app.debug.warn('Upload queue full, dropping canvas batch');
2465
2024
  return;
2466
2025
  }
2467
- this.pendingBatches.push({ images, canvasId, createdAt });
2026
+ const token = this.app.session.getSessionSecret();
2027
+ if (!token) {
2028
+ this.app.debug.warn('Canvas batch has no session token');
2029
+ return;
2030
+ }
2031
+ this.pendingBatches.push({ images, canvasId, createdAt, token });
2468
2032
  if (!this.isProcessingQueue) {
2469
- this.processUploadQueue();
2033
+ void this.processUploadQueue();
2470
2034
  }
2471
2035
  }
2472
2036
  async processUploadQueue() {
2037
+ if (this.isProcessingQueue)
2038
+ return;
2473
2039
  this.isProcessingQueue = true;
2474
- while (this.pendingBatches.length > 0) {
2475
- if (this.uploadQueue >= this.MAX_CONCURRENT_UPLOADS) {
2476
- await new Promise((resolve) => setTimeout(resolve, 100));
2477
- continue;
2040
+ const worker = async () => {
2041
+ while (true) {
2042
+ const batch = this.pendingBatches.shift();
2043
+ if (!batch)
2044
+ return;
2045
+ await this.uploadBatch(batch.images, batch.canvasId, batch.createdAt, batch.token);
2478
2046
  }
2479
- const batch = this.pendingBatches.shift();
2480
- if (!batch)
2481
- break;
2482
- this.uploadBatch(batch.images, batch.canvasId, batch.createdAt);
2047
+ };
2048
+ try {
2049
+ await Promise.all(Array.from({ length: this.MAX_CONCURRENT_UPLOADS }, () => worker()));
2050
+ }
2051
+ finally {
2052
+ this.isProcessingQueue = false;
2053
+ // A batch can be appended between the final worker check and the flag
2054
+ // reset. Restart once so that race cannot strand queued frames.
2055
+ if (this.pendingBatches.length > 0) {
2056
+ void this.processUploadQueue();
2057
+ }
2058
+ }
2059
+ }
2060
+ async uploadBatch(images, canvasId, createdAt, token) {
2061
+ if (this.options.isDebug) {
2062
+ const packed = await packFrames(images);
2063
+ if (packed) {
2064
+ const fileName = `${createdAt}_${canvasId}.${this.fileExt}.frames`;
2065
+ const url = URL.createObjectURL(new Blob([packed]));
2066
+ const link = document.createElement('a');
2067
+ link.href = url;
2068
+ link.download = fileName;
2069
+ link.style.display = 'none';
2070
+ document.body.appendChild(link);
2071
+ link.click();
2072
+ document.body.removeChild(link);
2073
+ URL.revokeObjectURL(url);
2074
+ }
2075
+ // fall through to also send to backend
2076
+ }
2077
+ let formData;
2078
+ if (this.options.framesSupport) {
2079
+ // Pack frames into binary format: [uint64 LE timestamp][uint32 LE size][data] per frame
2080
+ const buffers = [];
2081
+ let totalSize = 0;
2082
+ for (const snapshot of images) {
2083
+ if (!snapshot.data)
2084
+ continue;
2085
+ const ab = await snapshot.data.arrayBuffer();
2086
+ buffers.push(ab);
2087
+ totalSize += 8 + 4 + ab.byteLength; // uint64 ts + uint32 size + data
2088
+ }
2089
+ if (totalSize === 0)
2090
+ return;
2091
+ const packed = new ArrayBuffer(totalSize);
2092
+ const view = new DataView(packed);
2093
+ const bytes = new Uint8Array(packed);
2094
+ let offset = 0;
2095
+ for (let i = 0; i < images.length; i++) {
2096
+ if (!images[i].data)
2097
+ continue;
2098
+ const ab = buffers.shift();
2099
+ const ts = images[i].id;
2100
+ // uint64 LE as two uint32 LE writes -- timestamp
2101
+ view.setUint32(offset, ts % 0x100000000, true);
2102
+ view.setUint32(offset + 4, Math.floor(ts / 0x100000000), true);
2103
+ offset += 8;
2104
+ // uint32 LE -- size
2105
+ view.setUint32(offset, ab.byteLength, true);
2106
+ offset += 4;
2107
+ // image data
2108
+ bytes.set(new Uint8Array(ab), offset);
2109
+ offset += ab.byteLength;
2110
+ }
2111
+ formData = new FormData();
2112
+ formData.append('type', 'frames');
2113
+ const fileName = `${createdAt}_${canvasId}.${this.fileExt}.frames`;
2114
+ formData.append('frames', new Blob([packed]), fileName);
2483
2115
  }
2484
- this.isProcessingQueue = false;
2485
- }
2486
- uploadBatch(images, canvasId, createdAt) {
2487
- const formData = new FormData();
2488
- images.forEach((snapshot) => {
2489
- const blob = snapshot.data;
2490
- if (!blob)
2116
+ else {
2117
+ // Legacy: send individual image files
2118
+ formData = new FormData();
2119
+ images.forEach((snapshot) => {
2120
+ const blob = snapshot.data;
2121
+ if (!blob)
2122
+ return;
2123
+ const name = `${createdAt}_${canvasId}_${snapshot.id}.${this.fileExt}`;
2124
+ formData.append('snapshot', blob, name);
2125
+ });
2126
+ }
2127
+ const initRestart = () => {
2128
+ if (this.app.session.getSessionSecret() !== token) {
2491
2129
  return;
2492
- formData.append('snapshot', blob, `${createdAt}_${canvasId}_${snapshot.id}.${this.fileExt}`);
2493
- if (this.options.isDebug) {
2494
- saveImageData(blob, `${createdAt}_${canvasId}_${snapshot.id}.${this.fileExt}`);
2495
2130
  }
2496
- });
2497
- const initRestart = () => {
2498
2131
  this.app.debug.log('Restarting tracker; token expired');
2499
2132
  this.app.stop(false);
2500
2133
  setTimeout(() => {
@@ -2504,25 +2137,72 @@ class CanvasRecorder {
2504
2137
  this.uploadQueue++;
2505
2138
  const base = this.app.options.endpoint.replace(/\/$/, '');
2506
2139
  const ingestPoint = base.endsWith('/oxvo') ? base : `${base}/oxvo`;
2507
- fetch(ingestPoint + '/v1/assets', {
2508
- method: 'POST',
2509
- headers: {
2510
- Authorization: `Bearer ${this.app.session.getSessionSecret() ?? ''}`,
2511
- },
2512
- body: formData,
2513
- })
2514
- .then((r) => {
2515
- if (r.status === 401) {
2516
- return initRestart();
2140
+ try {
2141
+ for (let attempt = 1; attempt <= this.MAX_UPLOAD_ATTEMPTS; attempt++) {
2142
+ try {
2143
+ const response = await fetch(ingestPoint + '/v1/assets', {
2144
+ method: 'POST',
2145
+ headers: {
2146
+ Authorization: `Bearer ${token}`,
2147
+ },
2148
+ body: formData,
2149
+ });
2150
+ if (response.ok)
2151
+ return;
2152
+ if (response.status === 401) {
2153
+ initRestart();
2154
+ return;
2155
+ }
2156
+ if (response.status === 413) {
2157
+ this.app.debug.warn('Canvas batch exceeds the upload size limit');
2158
+ return;
2159
+ }
2160
+ if (!this.isRetryableUploadStatus(response.status)) {
2161
+ this.app.debug.error(`Canvas upload failed with status ${response.status}`);
2162
+ return;
2163
+ }
2164
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2165
+ this.app.debug.error(`Canvas upload failed after ${attempt} attempts (status ${response.status})`);
2166
+ return;
2167
+ }
2168
+ await this.waitBeforeUploadRetry(response, attempt);
2169
+ }
2170
+ catch (error) {
2171
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2172
+ this.app.debug.error('Canvas upload failed after retrying', error);
2173
+ return;
2174
+ }
2175
+ await this.waitBeforeUploadRetry(undefined, attempt);
2176
+ }
2517
2177
  }
2518
- return true;
2519
- })
2520
- .catch((e) => {
2521
- this.app.debug.error('error saving canvas', e);
2522
- })
2523
- .finally(() => {
2178
+ }
2179
+ finally {
2524
2180
  this.uploadQueue--;
2525
- });
2181
+ }
2182
+ }
2183
+ isRetryableUploadStatus(status) {
2184
+ return status === 429 || status === 408 || status >= 500;
2185
+ }
2186
+ async waitBeforeUploadRetry(response, attempt) {
2187
+ const retryAfter = response?.headers.get('Retry-After');
2188
+ let retryDelayMs = 0;
2189
+ if (retryAfter) {
2190
+ const seconds = Number(retryAfter);
2191
+ if (Number.isFinite(seconds) && seconds >= 0) {
2192
+ retryDelayMs = seconds * 1000;
2193
+ }
2194
+ else {
2195
+ const retryAt = Date.parse(retryAfter);
2196
+ if (Number.isFinite(retryAt)) {
2197
+ retryDelayMs = Math.max(0, retryAt - Date.now());
2198
+ }
2199
+ }
2200
+ }
2201
+ if (retryDelayMs === 0) {
2202
+ retryDelayMs = 250 * 2 ** (attempt - 1);
2203
+ }
2204
+ retryDelayMs = Math.min(retryDelayMs, this.MAX_RETRY_DELAY_MS);
2205
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
2526
2206
  }
2527
2207
  cleanupCanvas(id) {
2528
2208
  if (this.snapshots[id]) {
@@ -2545,8 +2225,7 @@ class CanvasRecorder {
2545
2225
  }
2546
2226
  delete this.snapshots[id];
2547
2227
  }
2548
- clear() {
2549
- // Flush remaining images before cleanup
2228
+ flushPendingSnapshots() {
2550
2229
  Object.keys(this.snapshots).forEach((idStr) => {
2551
2230
  const id = parseInt(idStr, 10);
2552
2231
  const snapshot = this.snapshots[id];
@@ -2555,6 +2234,9 @@ class CanvasRecorder {
2555
2234
  snapshot.images = [];
2556
2235
  }
2557
2236
  });
2237
+ }
2238
+ clear() {
2239
+ this.flushPendingSnapshots();
2558
2240
  Object.keys(this.snapshots).forEach((idStr) => {
2559
2241
  const id = parseInt(idStr, 10);
2560
2242
  this.cleanupCanvas(id);
@@ -2589,40 +2271,346 @@ function captureSnapshot(canvas, quality = 'medium', dummy, fixedScaling = false
2589
2271
  canvas.toBlob(onBlob, imageFormat, qualityInt[quality]);
2590
2272
  }
2591
2273
  }
2592
- function saveImageData(imageDataBlob, name) {
2593
- const imageDataUrl = URL.createObjectURL(imageDataBlob);
2594
- const link = document.createElement('a');
2595
- link.href = imageDataUrl;
2596
- link.download = name;
2597
- link.style.display = 'none';
2598
- document.body.appendChild(link);
2599
- link.click();
2600
- document.body.removeChild(link);
2274
+ async function packFrames(images) {
2275
+ const buffers = [];
2276
+ let totalSize = 0;
2277
+ for (const snapshot of images) {
2278
+ if (!snapshot.data)
2279
+ continue;
2280
+ const ab = await snapshot.data.arrayBuffer();
2281
+ buffers.push(ab);
2282
+ totalSize += 8 + 4 + ab.byteLength;
2283
+ }
2284
+ if (totalSize === 0)
2285
+ return null;
2286
+ const packed = new ArrayBuffer(totalSize);
2287
+ const view = new DataView(packed);
2288
+ const bytes = new Uint8Array(packed);
2289
+ let offset = 0;
2290
+ for (let i = 0; i < images.length; i++) {
2291
+ if (!images[i].data)
2292
+ continue;
2293
+ const ab = buffers.shift();
2294
+ const ts = images[i].id;
2295
+ view.setUint32(offset, ts % 0x100000000, true);
2296
+ view.setUint32(offset + 4, Math.floor(ts / 0x100000000), true);
2297
+ offset += 8;
2298
+ view.setUint32(offset, ab.byteLength, true);
2299
+ offset += 4;
2300
+ bytes.set(new Uint8Array(ab), offset);
2301
+ offset += ab.byteLength;
2302
+ }
2303
+ return packed;
2601
2304
  }
2602
2305
 
2603
- const LogLevel = {
2604
- Verbose: 5,
2605
- Log: 4,
2606
- Warnings: 3,
2607
- Errors: 2,
2608
- Silent: 0,
2609
- };
2610
- class Logger {
2611
- constructor(debugLevel = LogLevel.Silent) {
2612
- this.shouldLog = (level) => {
2613
- return this.level >= level;
2614
- };
2615
- this.info = (...args) => {
2616
- if (this.shouldLog(LogLevel.Verbose)) {
2617
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2618
- console.info(...args);
2619
- }
2620
- };
2621
- this.log = (...args) => {
2622
- if (this.shouldLog(LogLevel.Log)) {
2623
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2624
- console.log(...args);
2625
- }
2306
+ const CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION = 1;
2307
+ const SECRET_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
2308
+ const CONTEXT_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
2309
+ const MAX_MESSAGE_FIELDS = 24;
2310
+ const MAX_MESSAGE_STRING_LENGTH = 100000;
2311
+ const MAX_BATCH_MESSAGES = 25000;
2312
+ const MAX_BATCH_BYTES = 2 * 1024 * 1024;
2313
+ // Cross-frame batches may contain only recording events. Identity, dictionary,
2314
+ // batch-header, and tab/session control opcodes remain owned by the top-level
2315
+ // tracker.
2316
+ const CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES = new Set([
2317
+ 4 /* MType.SetPageLocationDeprecated */,
2318
+ 5 /* MType.SetViewportSize */,
2319
+ 6 /* MType.SetViewportScroll */,
2320
+ 7 /* MType.CreateDocument */,
2321
+ 8 /* MType.CreateElementNode */,
2322
+ 9 /* MType.CreateTextNode */,
2323
+ 10 /* MType.MoveNode */,
2324
+ 11 /* MType.RemoveNode */,
2325
+ 12 /* MType.SetNodeAttribute */,
2326
+ 13 /* MType.RemoveNodeAttribute */,
2327
+ 14 /* MType.SetNodeData */,
2328
+ 16 /* MType.SetNodeScroll */,
2329
+ 17 /* MType.SetInputTarget */,
2330
+ 18 /* MType.SetInputValue */,
2331
+ 19 /* MType.SetInputChecked */,
2332
+ 20 /* MType.MouseMove */,
2333
+ 21 /* MType.NetworkRequestDeprecated */,
2334
+ 22 /* MType.ConsoleLog */,
2335
+ 23 /* MType.PageLoadTiming */,
2336
+ 24 /* MType.PageRenderTiming */,
2337
+ 27 /* MType.CustomEvent */,
2338
+ 36 /* MType.NodeAnimationResult */,
2339
+ 40 /* MType.Profiler */,
2340
+ 41 /* MType.OTable */,
2341
+ 42 /* MType.StateAction */,
2342
+ 44 /* MType.ReduxDeprecated */,
2343
+ 45 /* MType.Vuex */,
2344
+ 46 /* MType.MobX */,
2345
+ 47 /* MType.NgRx */,
2346
+ 48 /* MType.GraphQLDeprecated */,
2347
+ 49 /* MType.PerformanceTrack */,
2348
+ 53 /* MType.ResourceTimingDeprecatedDeprecated */,
2349
+ 54 /* MType.ConnectionInformation */,
2350
+ 55 /* MType.SetPageVisibility */,
2351
+ 57 /* MType.LoadFontFace */,
2352
+ 58 /* MType.SetNodeFocus */,
2353
+ 60 /* MType.SetNodeAttributeURLBased */,
2354
+ 61 /* MType.SetCSSDataURLBased */,
2355
+ 63 /* MType.TechnicalInfo */,
2356
+ 64 /* MType.CustomIssue */,
2357
+ 65 /* MType.SetNodeSlot */,
2358
+ 68 /* MType.MouseClick */,
2359
+ 69 /* MType.MouseClickDeprecated */,
2360
+ 70 /* MType.CreateIFrameDocument */,
2361
+ 71 /* MType.AdoptedSSReplaceURLBased */,
2362
+ 73 /* MType.AdoptedSSInsertRuleURLBased */,
2363
+ 75 /* MType.AdoptedSSDeleteRule */,
2364
+ 76 /* MType.AdoptedSSAddOwner */,
2365
+ 77 /* MType.AdoptedSSRemoveOwner */,
2366
+ 78 /* MType.JSException */,
2367
+ 79 /* MType.Zustand */,
2368
+ 83 /* MType.NetworkRequest */,
2369
+ 84 /* MType.WSChannel */,
2370
+ 85 /* MType.ResourceTiming */,
2371
+ 87 /* MType.Incident */,
2372
+ 89 /* MType.LongAnimationTask */,
2373
+ 112 /* MType.InputChange */,
2374
+ 113 /* MType.SelectionChange */,
2375
+ 114 /* MType.MouseThrashing */,
2376
+ 115 /* MType.UnbindNodes */,
2377
+ 116 /* MType.ResourceTimingDeprecated */,
2378
+ 119 /* MType.CanvasNode */,
2379
+ 120 /* MType.TagTrigger */,
2380
+ 121 /* MType.Redux */,
2381
+ 122 /* MType.SetPageLocation */,
2382
+ 123 /* MType.GraphQL */,
2383
+ 124 /* MType.WebVitals */,
2384
+ ]);
2385
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2386
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
2387
+ const isSafeIntegerAtLeast = (value, minimum) => typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
2388
+ const isBoundedString = (value, maxLength) => typeof value === 'string' && value.length > 0 && value.length <= maxLength;
2389
+ const isCanvasQuality = (value) => value === 'low' || value === 'medium' || value === 'high';
2390
+ function normalizeCrossDomainOrigin(value) {
2391
+ if (typeof value !== 'string' || value.trim() === '' || value.trim() === '*') {
2392
+ return null;
2393
+ }
2394
+ try {
2395
+ const url = new URL(value.trim());
2396
+ if (!['http:', 'https:'].includes(url.protocol) ||
2397
+ url.origin === 'null' ||
2398
+ url.username !== '' ||
2399
+ url.password !== '' ||
2400
+ (url.pathname !== '' && url.pathname !== '/') ||
2401
+ url.search !== '' ||
2402
+ url.hash !== '') {
2403
+ return null;
2404
+ }
2405
+ return url.origin;
2406
+ }
2407
+ catch {
2408
+ return null;
2409
+ }
2410
+ }
2411
+ function normalizeCrossDomainOrigins(values) {
2412
+ if (!Array.isArray(values)) {
2413
+ return new Set();
2414
+ }
2415
+ return new Set(values.map(normalizeCrossDomainOrigin).filter((origin) => origin !== null));
2416
+ }
2417
+ function createCrossDomainSecret() {
2418
+ if (!globalThis.crypto?.getRandomValues) {
2419
+ return null;
2420
+ }
2421
+ const bytes = new Uint8Array(24);
2422
+ globalThis.crypto.getRandomValues(bytes);
2423
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
2424
+ }
2425
+ function parseCrossDomainHandshake(data, expectedLine, expectedProjectKey) {
2426
+ if (!isRecord(data) ||
2427
+ data.line !== expectedLine ||
2428
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2429
+ data.projectKey !== expectedProjectKey ||
2430
+ !isBoundedString(data.context, 128) ||
2431
+ !CONTEXT_PATTERN.test(data.context) ||
2432
+ !isBoundedString(data.nonce, 128) ||
2433
+ !SECRET_PATTERN.test(data.nonce)) {
2434
+ return null;
2435
+ }
2436
+ return {
2437
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2438
+ context: data.context,
2439
+ nonce: data.nonce,
2440
+ projectKey: expectedProjectKey,
2441
+ };
2442
+ }
2443
+ function matchesCrossDomainEnvelope(data, expectedLine, expected) {
2444
+ return (isRecord(data) &&
2445
+ data.line === expectedLine &&
2446
+ data.protocolVersion === CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION &&
2447
+ data.context === expected.context &&
2448
+ data.nonce === expected.nonce &&
2449
+ data.capability === expected.capability);
2450
+ }
2451
+ function parseCrossDomainFrameInit(data, expectedLine, expected) {
2452
+ if (!isRecord(data) ||
2453
+ data.line !== expectedLine ||
2454
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2455
+ data.context !== expected.context ||
2456
+ data.nonce !== expected.nonce ||
2457
+ !isBoundedString(data.capability, 128) ||
2458
+ !SECRET_PATTERN.test(data.capability) ||
2459
+ !isSafeIntegerAtLeast(data.id, 0) ||
2460
+ !isSafeIntegerAtLeast(data.frameOrderNumber, 1) ||
2461
+ !isSafeIntegerAtLeast(data.frameLevel, 0) ||
2462
+ !isSafeIntegerAtLeast(data.capabilityExpiresAt, 1) ||
2463
+ data.capabilityExpiresAt <= Date.now() ||
2464
+ !isRecord(data.session)) {
2465
+ return null;
2466
+ }
2467
+ const session = data.session;
2468
+ if (!isBoundedString(session.sessionID, 256) ||
2469
+ !isFiniteNumber(session.startedAt) ||
2470
+ !isFiniteNumber(session.delayMs) ||
2471
+ typeof session.userUUID !== 'string' ||
2472
+ session.userUUID.length > 256) {
2473
+ return null;
2474
+ }
2475
+ let projectID;
2476
+ if (session.projectID !== undefined) {
2477
+ if (!isBoundedString(session.projectID, 256)) {
2478
+ return null;
2479
+ }
2480
+ projectID = session.projectID;
2481
+ }
2482
+ const canvas = parseCanvasConfig(data.canvas);
2483
+ if (data.canvas !== undefined && canvas === null) {
2484
+ return null;
2485
+ }
2486
+ return {
2487
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2488
+ context: data.context,
2489
+ nonce: data.nonce,
2490
+ capability: data.capability,
2491
+ id: data.id,
2492
+ frameOrderNumber: data.frameOrderNumber,
2493
+ frameLevel: data.frameLevel,
2494
+ capabilityExpiresAt: data.capabilityExpiresAt,
2495
+ session: {
2496
+ sessionID: session.sessionID,
2497
+ projectID,
2498
+ startedAt: session.startedAt,
2499
+ delayMs: session.delayMs,
2500
+ userUUID: session.userUUID,
2501
+ },
2502
+ ...(canvas ? { canvas } : {}),
2503
+ };
2504
+ }
2505
+ function parseCanvasConfig(value) {
2506
+ if (value === undefined) {
2507
+ return null;
2508
+ }
2509
+ if (!isRecord(value) || typeof value.enabled !== 'boolean') {
2510
+ return null;
2511
+ }
2512
+ let quality;
2513
+ if (value.quality !== undefined) {
2514
+ if (!isCanvasQuality(value.quality)) {
2515
+ return null;
2516
+ }
2517
+ quality = value.quality;
2518
+ }
2519
+ let fps;
2520
+ if (value.fps !== undefined) {
2521
+ if (!isFiniteNumber(value.fps)) {
2522
+ return null;
2523
+ }
2524
+ fps = value.fps;
2525
+ }
2526
+ let framesSupport;
2527
+ if (value.framesSupport !== undefined) {
2528
+ if (typeof value.framesSupport !== 'boolean') {
2529
+ return null;
2530
+ }
2531
+ framesSupport = value.framesSupport;
2532
+ }
2533
+ return {
2534
+ enabled: value.enabled,
2535
+ quality,
2536
+ fps,
2537
+ framesSupport,
2538
+ };
2539
+ }
2540
+ function parseCrossDomainIframeBatch(messages) {
2541
+ if (!Array.isArray(messages) || messages.length > MAX_BATCH_MESSAGES) {
2542
+ return null;
2543
+ }
2544
+ let bytes = 0;
2545
+ for (const message of messages) {
2546
+ if (!Array.isArray(message) || message.length < 1 || message.length > MAX_MESSAGE_FIELDS) {
2547
+ return null;
2548
+ }
2549
+ const messageType = message[0];
2550
+ if (typeof messageType !== 'number' ||
2551
+ !Number.isInteger(messageType) ||
2552
+ !CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES.has(messageType)) {
2553
+ return null;
2554
+ }
2555
+ const expectedFieldTypes = MESSAGE_FIELD_TYPES[messageType];
2556
+ if (expectedFieldTypes === undefined || message.length !== expectedFieldTypes.length + 1) {
2557
+ return null;
2558
+ }
2559
+ bytes += 8;
2560
+ for (let fieldIndex = 0; fieldIndex < expectedFieldTypes.length; fieldIndex++) {
2561
+ const field = message[fieldIndex + 1];
2562
+ if (typeof field !== expectedFieldTypes[fieldIndex]) {
2563
+ return null;
2564
+ }
2565
+ if (typeof field === 'string') {
2566
+ if (field.length > MAX_MESSAGE_STRING_LENGTH) {
2567
+ return null;
2568
+ }
2569
+ bytes += field.length * 3;
2570
+ }
2571
+ else if (typeof field === 'number') {
2572
+ if (!Number.isFinite(field)) {
2573
+ return null;
2574
+ }
2575
+ bytes += 8;
2576
+ }
2577
+ else if (typeof field === 'boolean') {
2578
+ bytes += 1;
2579
+ }
2580
+ else {
2581
+ return null;
2582
+ }
2583
+ if (bytes > MAX_BATCH_BYTES) {
2584
+ return null;
2585
+ }
2586
+ }
2587
+ }
2588
+ return { messages: messages, bytes };
2589
+ }
2590
+
2591
+ const LogLevel = {
2592
+ Verbose: 5,
2593
+ Log: 4,
2594
+ Warnings: 3,
2595
+ Errors: 2,
2596
+ Silent: 0,
2597
+ };
2598
+ class Logger {
2599
+ constructor(debugLevel = LogLevel.Silent) {
2600
+ this.shouldLog = (level) => {
2601
+ return this.level >= level;
2602
+ };
2603
+ this.info = (...args) => {
2604
+ if (this.shouldLog(LogLevel.Verbose)) {
2605
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2606
+ console.info(...args);
2607
+ }
2608
+ };
2609
+ this.log = (...args) => {
2610
+ if (this.shouldLog(LogLevel.Log)) {
2611
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2612
+ console.log(...args);
2613
+ }
2626
2614
  };
2627
2615
  this.warn = (...args) => {
2628
2616
  if (this.shouldLog(LogLevel.Warnings)) {
@@ -2878,8 +2866,44 @@ function inlineRemoteCss(node, id, baseHref, getNextID, insertRule, addOwner, fo
2878
2866
  })
2879
2867
  .catch(error => {
2880
2868
  console.error(`OxvoSessions: Failed to fetch CSS from ${node.href}:`, error);
2869
+ // Fetch failed, likely due to CORS. Retry via load event and sheet access.
2870
+ retryViaLoadEvent();
2881
2871
  });
2882
2872
  }
2873
+ else {
2874
+ retryViaLoadEvent();
2875
+ }
2876
+ function retryViaLoadEvent() {
2877
+ const trySheet = () => {
2878
+ const loadedSheet = node.sheet;
2879
+ if (loadedSheet) {
2880
+ try {
2881
+ const cssText = stringifyStylesheet(loadedSheet);
2882
+ if (cssText) {
2883
+ if (sendPlain && onPlain) {
2884
+ onPlain(cssText, fakeIdHolder++);
2885
+ }
2886
+ else {
2887
+ processCssText(cssText);
2888
+ }
2889
+ return;
2890
+ }
2891
+ }
2892
+ catch (e) {
2893
+ // Cross-origin sheet. Nothing more can be read safely here.
2894
+ }
2895
+ }
2896
+ };
2897
+ if (node.sheet) {
2898
+ trySheet();
2899
+ }
2900
+ else {
2901
+ node.addEventListener('load', function onLoad() {
2902
+ node.removeEventListener('load', onLoad);
2903
+ trySheet();
2904
+ });
2905
+ }
2906
+ }
2883
2907
  function processCssText(cssText) {
2884
2908
  // Remove comments
2885
2909
  cssText = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
@@ -3101,7 +3125,6 @@ function ConstructedStyleSheets (app) {
3101
3125
  if (!hasAdoptedSS(document)) {
3102
3126
  return;
3103
3127
  }
3104
- const styleSheetIDMap = new Map();
3105
3128
  const adoptedStyleSheetsOwnings = new Map();
3106
3129
  const sendAdoptedStyleSheetsUpdate = (root) => setTimeout(() => {
3107
3130
  let nodeID = app.nodes.getID(root);
@@ -3215,6 +3238,119 @@ function ConstructedStyleSheets (app) {
3215
3238
  });
3216
3239
  }
3217
3240
 
3241
+ var SanitizeLevel;
3242
+ (function (SanitizeLevel) {
3243
+ SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
3244
+ SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
3245
+ SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
3246
+ })(SanitizeLevel || (SanitizeLevel = {}));
3247
+ const stringWiper = (input) => input
3248
+ .trim()
3249
+ .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
3250
+ class Sanitizer {
3251
+ constructor(params) {
3252
+ // Node id -> level. Plain (0) is never stored; a missing entry means Plain.
3253
+ // A map (not the old grow-only Sets) so levels can be raised and lowered.
3254
+ this.levels = new Map();
3255
+ this.app = params.app;
3256
+ const defaultOptions = {
3257
+ maskTextEmails: true,
3258
+ maskTextNumbers: false,
3259
+ maskAllByDefault: false,
3260
+ domSanitizer: undefined,
3261
+ };
3262
+ this.maskAllByDefault = params.options?.maskAllByDefault ?? false;
3263
+ this.options = Object.assign(defaultOptions, params.options);
3264
+ }
3265
+ // Pure recomputation of a node's level from the live DOM + parent level.
3266
+ // Reading current state on every call lets resanitize() pick up runtime changes.
3267
+ computeLevel(node, parentLevel) {
3268
+ if (this.options.maskAllByDefault) {
3269
+ if (isElementNode(node) && !hasOxvoSessionsAttribute(node, 'unmask')) {
3270
+ return SanitizeLevel.Obscured;
3271
+ }
3272
+ if (isTextNode(node) && !hasOxvoSessionsAttribute(node.parentNode, 'unmask')) {
3273
+ return SanitizeLevel.Obscured;
3274
+ }
3275
+ }
3276
+ let level = SanitizeLevel.Plain;
3277
+ if (parentLevel >= SanitizeLevel.Obscured ||
3278
+ (isElementNode(node) &&
3279
+ (hasOxvoSessionsAttribute(node, 'masked') || hasOxvoSessionsAttribute(node, 'obscured')))) {
3280
+ level = SanitizeLevel.Obscured;
3281
+ }
3282
+ if (parentLevel === SanitizeLevel.Hidden ||
3283
+ (isElementNode(node) &&
3284
+ (hasOxvoSessionsAttribute(node, 'htmlmasked') || hasOxvoSessionsAttribute(node, 'hidden')))) {
3285
+ level = SanitizeLevel.Hidden;
3286
+ }
3287
+ if (this.options.domSanitizer !== undefined && isElementNode(node)) {
3288
+ const sanitizeLevel = this.options.domSanitizer(node);
3289
+ if (sanitizeLevel === SanitizeLevel.Obscured && level < SanitizeLevel.Obscured) {
3290
+ level = SanitizeLevel.Obscured;
3291
+ }
3292
+ if (sanitizeLevel === SanitizeLevel.Hidden) {
3293
+ level = SanitizeLevel.Hidden;
3294
+ }
3295
+ }
3296
+ return level;
3297
+ }
3298
+ getLevel(id) {
3299
+ return this.levels.get(id) ?? SanitizeLevel.Plain;
3300
+ }
3301
+ // Sets a node's level in either direction and returns the previous level.
3302
+ setLevel(id, level) {
3303
+ const prev = this.getLevel(id);
3304
+ if (level === SanitizeLevel.Plain) {
3305
+ this.levels.delete(id);
3306
+ }
3307
+ else {
3308
+ this.levels.set(id, level);
3309
+ }
3310
+ return prev;
3311
+ }
3312
+ handleNode(id, parentID, node) {
3313
+ const level = this.computeLevel(node, this.getLevel(parentID));
3314
+ // Escalate-only: commits never lower a level, only resanitize/setLevel do.
3315
+ if (level > this.getLevel(id)) {
3316
+ this.setLevel(id, level);
3317
+ }
3318
+ }
3319
+ sanitize(id, data) {
3320
+ if (this.getLevel(id) >= SanitizeLevel.Obscured) {
3321
+ // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
3322
+ return stringWiper(data);
3323
+ }
3324
+ if (this.options.maskTextNumbers) {
3325
+ data = data.replace(/\d/g, '0');
3326
+ }
3327
+ if (this.options.maskTextEmails) {
3328
+ data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
3329
+ const [name, domain] = email.split('@');
3330
+ const [domainName, host] = domain.split('.');
3331
+ return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
3332
+ });
3333
+ }
3334
+ return data;
3335
+ }
3336
+ isObscured(id) {
3337
+ return this.getLevel(id) >= SanitizeLevel.Obscured;
3338
+ }
3339
+ isHidden(id) {
3340
+ return this.getLevel(id) === SanitizeLevel.Hidden;
3341
+ }
3342
+ getInnerTextSecure(el) {
3343
+ const id = this.app.nodes.getID(el);
3344
+ if (!id) {
3345
+ return '';
3346
+ }
3347
+ return this.sanitize(id, el.innerText);
3348
+ }
3349
+ clear() {
3350
+ this.levels.clear();
3351
+ }
3352
+ }
3353
+
3218
3354
  const iconCache = {};
3219
3355
  const svgUrlCache = {};
3220
3356
  async function parseUseEl(useElement, mode, domParser) {
@@ -3366,6 +3502,12 @@ class Observer {
3366
3502
  this.inlineRemoteCss = false;
3367
3503
  this.inlinerOptions = undefined;
3368
3504
  this.domParser = new DOMParser();
3505
+ /**
3506
+ * Bumped on every disconnect(). Stale async CSS-inlining callbacks from a
3507
+ * previous session (e.g. agent reload → tracker restart) compare against
3508
+ * this and bail instead of sending messages that reference vanished node ids.
3509
+ */
3510
+ this.generation = 0;
3369
3511
  this.throttling = true;
3370
3512
  this.throttledSetNodeData = throttleWithTrailing((id, parentElement, data) => this.sendNodeData(id, parentElement, data), 30);
3371
3513
  this.throttling = !Boolean(options.disableThrottling);
@@ -3512,18 +3654,23 @@ class Observer {
3512
3654
  }
3513
3655
  if (name === 'style' || (name === 'href' && hasTag(node, 'link'))) {
3514
3656
  if ('rel' in node && node.rel === 'stylesheet' && this.inlineRemoteCss) {
3657
+ const gen = this.generation;
3515
3658
  setTimeout(() => {
3516
3659
  inlineRemoteCss(
3517
3660
  // @ts-ignore
3518
3661
  node, id, this.app.getBaseHref(), nextID, (id, cssText, index, baseHref) => {
3662
+ if (this.generation !== gen)
3663
+ return;
3519
3664
  this.app.send(AdoptedSSInsertRuleURLBased(id, cssText, index, baseHref));
3520
3665
  }, (sheetId, ownerId) => {
3666
+ if (this.generation !== gen)
3667
+ return;
3521
3668
  this.app.send(AdoptedSSAddOwner(sheetId, ownerId));
3522
3669
  }, this.inlinerOptions?.forceFetch, this.inlinerOptions?.forcePlain, (cssText, fakeTextId) => {
3670
+ if (this.generation !== gen)
3671
+ return;
3523
3672
  this.app.send(CreateTextNode(fakeTextId, id, 0));
3524
- setTimeout(() => {
3525
- this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref()));
3526
- }, 10);
3673
+ this.app.send(SetCSSDataURLBased(fakeTextId, cssText, this.app.getBaseHref()));
3527
3674
  });
3528
3675
  }, 0);
3529
3676
  return;
@@ -3783,10 +3930,105 @@ class Observer {
3783
3930
  beforeCommit(this.app.nodes.getID(node));
3784
3931
  this.commitNodes(true);
3785
3932
  }
3933
+ /**
3934
+ * Re-evaluates sanitization for every tracked node in `root`'s subtree against
3935
+ * the current DOM and re-emits whatever changed. Pass the highest node you
3936
+ * changed (or the document root) so inherited levels propagate correctly.
3937
+ */
3938
+ resanitizeSubtree(root) {
3939
+ if (!isObservable(root)) {
3940
+ return;
3941
+ }
3942
+ const parent = root.parentNode;
3943
+ const parentId = parent !== null ? this.app.nodes.getID(parent) : undefined;
3944
+ const parentLevel = parentId !== undefined ? this.app.sanitizer.getLevel(parentId) : SanitizeLevel.Plain;
3945
+ this.resanitizeNode(root, parentLevel);
3946
+ }
3947
+ resanitizeNode(node, parentLevel) {
3948
+ if (isIgnored(node)) {
3949
+ return;
3950
+ }
3951
+ const id = this.app.nodes.getID(node);
3952
+ if (id === undefined) {
3953
+ // Untracked (new, or under a hidden ancestor): the live observer handles it.
3954
+ return;
3955
+ }
3956
+ const newLevel = this.app.sanitizer.computeLevel(node, parentLevel);
3957
+ const prevLevel = this.app.sanitizer.getLevel(id);
3958
+ const wasHidden = prevLevel === SanitizeLevel.Hidden;
3959
+ const willHidden = newLevel === SanitizeLevel.Hidden;
3960
+ // Crossing the hidden boundary changes the rendered structure (placeholder vs
3961
+ // real subtree), so rebuild rather than re-emit.
3962
+ if (wasHidden !== willHidden) {
3963
+ this.recreateSubtree(node);
3964
+ return;
3965
+ }
3966
+ if (willHidden) {
3967
+ return;
3968
+ }
3969
+ // Plain <-> Obscured: same structure, only leaf content changes.
3970
+ if (prevLevel !== newLevel) {
3971
+ this.app.sanitizer.setLevel(id, newLevel);
3972
+ this.reemitNode(id, node);
3973
+ }
3974
+ for (let child = node.firstChild; child !== null; child = child.nextSibling) {
3975
+ this.resanitizeNode(child, newLevel);
3976
+ }
3977
+ }
3978
+ // Destroys the node player-side and re-emits its subtree from scratch (new ids)
3979
+ // so it materializes at the freshly-computed level.
3980
+ recreateSubtree(node) {
3981
+ const id = this.app.nodes.getID(node);
3982
+ if (id === undefined) {
3983
+ return;
3984
+ }
3985
+ this.app.send(RemoveNode(id));
3986
+ this.clearSubtreeRegistration(node);
3987
+ this.bindTree(node);
3988
+ this.commitNodes();
3989
+ }
3990
+ clearSubtreeRegistration(node) {
3991
+ const clearOne = (n) => {
3992
+ const oldId = this.app.nodes.getID(n);
3993
+ if (oldId !== undefined) {
3994
+ this.app.sanitizer.setLevel(oldId, SanitizeLevel.Plain);
3995
+ }
3996
+ this.app.nodes.unregisterNode(n);
3997
+ };
3998
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
3999
+ acceptNode: (n) => isIgnored(n) || this.app.nodes.getID(n) === undefined
4000
+ ? NodeFilter.FILTER_REJECT
4001
+ : NodeFilter.FILTER_ACCEPT,
4002
+ },
4003
+ // @ts-ignore
4004
+ false);
4005
+ // Collect first, then clear: unregistering mutates the ids the walker reads.
4006
+ const subtree = [];
4007
+ while (walker.nextNode()) {
4008
+ subtree.push(walker.currentNode);
4009
+ }
4010
+ clearOne(node);
4011
+ subtree.forEach(clearOne);
4012
+ }
4013
+ reemitNode(id, node) {
4014
+ if (isTextNode(node)) {
4015
+ const parent = node.parentNode;
4016
+ if (parent !== null && isElementNode(parent)) {
4017
+ // re-runs sanitize() at the level we just set
4018
+ this.sendNodeData(id, parent, node.data);
4019
+ }
4020
+ return;
4021
+ }
4022
+ if (isElementNode(node)) {
4023
+ // inputs/images/canvas re-emit their own payload via registered callbacks
4024
+ this.app.callResanitizeCallbacks(node, id);
4025
+ }
4026
+ }
3786
4027
  disconnect() {
3787
4028
  this.observer.disconnect();
3788
4029
  this.clear();
3789
4030
  this.throttledSetNodeData.clear();
4031
+ this.generation++;
3790
4032
  }
3791
4033
  }
3792
4034
 
@@ -4021,7 +4263,10 @@ class TopObserver extends Observer {
4021
4263
  this.app.debug.info('doc already observed for', id);
4022
4264
  return;
4023
4265
  }
4024
- const observer = new IFrameObserver(this.app, false, {});
4266
+ const observer = new IFrameObserver(this.app, false, {
4267
+ disableSprites: this.options.disableSprites,
4268
+ ...getInlineOptions(this.options.inlineCss, console.warn),
4269
+ });
4025
4270
  this.iframeObservers.set(iframe, observer);
4026
4271
  this.docObservers.set(currentDoc, observer);
4027
4272
  this.iframeObserversArr.push(observer);
@@ -4043,7 +4288,10 @@ class TopObserver extends Observer {
4043
4288
  handle();
4044
4289
  }
4045
4290
  handleShadowRoot(shRoot) {
4046
- const observer = new ShadowRootObserver(this.app);
4291
+ const observer = new ShadowRootObserver(this.app, false, {
4292
+ disableSprites: this.options.disableSprites,
4293
+ ...getInlineOptions(this.options.inlineCss, console.warn),
4294
+ });
4047
4295
  this.shadowRootObservers.set(shRoot, observer);
4048
4296
  observer.observe(shRoot.host);
4049
4297
  }
@@ -4079,7 +4327,10 @@ class TopObserver extends Observer {
4079
4327
  };
4080
4328
  this.app.nodes.clear();
4081
4329
  this.app.nodes.crossdomainMode(frameLevel, frameOder);
4082
- const iframeObserver = new IFrameObserver(this.app);
4330
+ const iframeObserver = new IFrameObserver(this.app, false, {
4331
+ disableSprites: this.options.disableSprites,
4332
+ ...getInlineOptions(this.options.inlineCss, console.warn),
4333
+ });
4083
4334
  this.iframeObservers.set(window.document, iframeObserver);
4084
4335
  iframeObserver.syntheticObserve(rootNodeId, window.document);
4085
4336
  }
@@ -4095,94 +4346,6 @@ class TopObserver extends Observer {
4095
4346
  }
4096
4347
  }
4097
4348
 
4098
- var SanitizeLevel;
4099
- (function (SanitizeLevel) {
4100
- SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
4101
- SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
4102
- SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
4103
- })(SanitizeLevel || (SanitizeLevel = {}));
4104
- const stringWiper = (input) => input
4105
- .trim()
4106
- .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*');
4107
- class Sanitizer {
4108
- constructor(params) {
4109
- this.obscured = new Set();
4110
- this.hidden = new Set();
4111
- this.app = params.app;
4112
- const defaultOptions = {
4113
- maskTextEmails: true,
4114
- maskTextNumbers: false,
4115
- maskAllByDefault: false,
4116
- domSanitizer: undefined,
4117
- };
4118
- this.maskAllByDefault = params.options?.maskAllByDefault ?? false;
4119
- this.options = Object.assign(defaultOptions, params.options);
4120
- }
4121
- handleNode(id, parentID, node) {
4122
- if (this.options.maskAllByDefault) {
4123
- if (isElementNode(node) && !hasOxvoSessionsAttribute(node, 'unmask')) {
4124
- return this.obscured.add(id);
4125
- }
4126
- if (isTextNode(node) && !hasOxvoSessionsAttribute(node.parentNode, 'unmask')) {
4127
- return this.obscured.add(id);
4128
- }
4129
- }
4130
- if (this.obscured.has(parentID) ||
4131
- (isElementNode(node) &&
4132
- (hasOxvoSessionsAttribute(node, 'masked') || hasOxvoSessionsAttribute(node, 'obscured')))) {
4133
- this.obscured.add(id);
4134
- }
4135
- if (this.hidden.has(parentID) ||
4136
- (isElementNode(node) &&
4137
- (hasOxvoSessionsAttribute(node, 'htmlmasked') || hasOxvoSessionsAttribute(node, 'hidden')))) {
4138
- this.hidden.add(id);
4139
- }
4140
- if (this.options.domSanitizer !== undefined && isElementNode(node)) {
4141
- const sanitizeLevel = this.options.domSanitizer(node);
4142
- if (sanitizeLevel === SanitizeLevel.Obscured) {
4143
- this.obscured.add(id);
4144
- }
4145
- if (sanitizeLevel === SanitizeLevel.Hidden) {
4146
- this.hidden.add(id);
4147
- }
4148
- }
4149
- }
4150
- sanitize(id, data) {
4151
- if (this.obscured.has(id)) {
4152
- // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
4153
- return stringWiper(data);
4154
- }
4155
- if (this.options.maskTextNumbers) {
4156
- data = data.replace(/\d/g, '0');
4157
- }
4158
- if (this.options.maskTextEmails) {
4159
- data = data.replace(/^\w+([+.-]\w+)*@\w+([.-]\w+)*\.\w{2,3}$/g, (email) => {
4160
- const [name, domain] = email.split('@');
4161
- const [domainName, host] = domain.split('.');
4162
- return `${stars(name)}@${stars(domainName)}.${stars(host)}`;
4163
- });
4164
- }
4165
- return data;
4166
- }
4167
- isObscured(id) {
4168
- return this.obscured.has(id);
4169
- }
4170
- isHidden(id) {
4171
- return this.hidden.has(id);
4172
- }
4173
- getInnerTextSecure(el) {
4174
- const id = this.app.nodes.getID(el);
4175
- if (!id) {
4176
- return '';
4177
- }
4178
- return this.sanitize(id, el.innerText);
4179
- }
4180
- clear() {
4181
- this.obscured.clear();
4182
- this.hidden.clear();
4183
- }
4184
- }
4185
-
4186
4349
  const tokenSeparator = '_$_';
4187
4350
  class Session {
4188
4351
  constructor(params) {
@@ -4228,6 +4391,10 @@ class Session {
4228
4391
  this.token = `${token}${tokenSeparator}${projectKey}`;
4229
4392
  this.app.sessionStorage.setItem(this.options.session_token_key, `${token}${tokenSeparator}${projectKey}`);
4230
4393
  };
4394
+ this.clearSessionSecret = () => {
4395
+ this.token = undefined;
4396
+ this.app.sessionStorage.removeItem(this.options.session_token_key);
4397
+ };
4231
4398
  this.app = params.app;
4232
4399
  this.options = params.options;
4233
4400
  this.createTabId();
@@ -4324,7 +4491,7 @@ class Session {
4324
4491
  };
4325
4492
  }
4326
4493
  reset() {
4327
- this.app.sessionStorage.removeItem(this.options.session_token_key);
4494
+ this.clearSessionSecret();
4328
4495
  this.metadata = {};
4329
4496
  this.userID = null;
4330
4497
  this.sessionID = undefined;
@@ -4382,9 +4549,12 @@ class Ticker {
4382
4549
  * this value is injected during build time via rollup
4383
4550
  * */
4384
4551
  // @ts-ignore
4385
- const workerBodyFn = "!function(){\"use strict\";class t{constructor(t,s,i,e=10,n=250,h,r){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.pageNo=r,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.lastBatchNum=0,this.ingestURL=t+\"/v1/events\",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t){if(this.busy||!this.token)this.queue.push(t);else if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t);else{const s=++this.lastBatchNum;this.sendBatch(t,!1,s)}}sendNext(){const t=this.queue.shift();if(t)if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t);else{const s=++this.lastBatchNum;this.sendBatch(t,!1,s)}else this.busy=!1}retry(t,s,i){this.attemptsCount>=this.MAX_ATTEMPTS_COUNT?this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`):(this.attemptsCount++,setTimeout((()=>this.sendBatch(t,s,i)),this.ATTEMPT_TIMEOUT*this.attemptsCount))}sendBatch(t,s,i){var e;const n=null==i?void 0:i.toString().replace(/^([^_]+)_([^_]+).*/,\"$1_$2_$3\");this.busy=!0;const h={Authorization:`Bearer ${this.token}`};if(s&&(h[\"Content-Encoding\"]=\"gzip\"),null===this.token)return void setTimeout((()=>{this.sendBatch(t,s,`${null!=i?i:\"noBatchNum\"}_newToken`)}),500);const r=`${null!==(e=this.pageNo)&&void 0!==e?e:\"noPageNum\"}_${null!=n?n:\"noBatchNum\"}`;fetch(`${this.ingestURL}?seq=${r}`,{body:t,method:\"POST\",headers:h,keepalive:t.length<65536}).then((e=>{if(401===e.status)return this.busy=!1,void this.onUnauthorised();e.status>=400?this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_network:${e.status}`):(this.attemptsCount=0,this.sendNext())})).catch((e=>{console.warn(\"OxvoSessions:\",e),this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_reject:${e.message}`)}))}sendCompressed(t){const s=++this.lastBatchNum;this.sendBatch(t,!0,s)}sendUncompressed(t){const s=++this.lastBatchNum;this.sendBatch(t,!1,s)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s=\"function\"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class i{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const i=s.encode(t),e=i.byteLength;return!(!this.uint(e)||this.offset+e>this.size)&&(this.data.set(i,this.offset),this.offset+=e,!0)}reset(){this.offset=0,this.checkpointOffset=0}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class e extends i{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 65:case 70:case 75:case 76:case 77:case 82:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 35:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 52:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17:case 34:case 36:case 50:case 54:return this.uint(t[1])&&this.string(t[2]);case 16:return this.uint(t[1])&&this.int(t[2])&&this.int(t[3]);case 18:return this.uint(t[1])&&this.string(t[2])&&this.int(t[3]);case 19:return this.uint(t[1])&&this.boolean(t[2]);case 21:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8]);case 22:case 27:case 30:case 41:case 45:case 46:case 43:case 63:case 64:case 79:case 124:return this.string(t[1])&&this.string(t[2]);case 23:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 28:case 29:case 42:case 117:case 118:return this.string(t[1]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.int(t[5]);case 49:return this.int(t[1])&&this.int(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 53:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8]);case 55:return this.boolean(t[1]);case 57:case 60:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 58:case 120:return this.int(t[1]);case 68:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6]);case 69:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 81:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.int(t[4])&&this.string(t[5]);case 83:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 84:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6]);case 85:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10])&&this.uint(t[11])&&this.uint(t[12])&&this.uint(t[13])&&this.uint(t[14])&&this.uint(t[15])&&this.uint(t[16])&&this.uint(t[17]);case 87:return this.string(t[1])&&this.int(t[2])&&this.int(t[3]);case 89:return this.string(t[1])&&this.int(t[2])&&this.int(t[3])&&this.int(t[4])&&this.int(t[5])&&this.string(t[6]);case 112:return this.uint(t[1])&&this.string(t[2])&&this.boolean(t[3])&&this.string(t[4])&&this.int(t[5])&&this.int(t[6]);case 113:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3]);case 116:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10]);case 119:return this.string(t[1])&&this.uint(t[2]);case 121:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 122:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 123:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])}}}class n{constructor(t,s,i,n,h,r){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=n,this.tabId=h,this.onOfflineEnd=r,this.nextIndex=0,this.beaconSize=2e5,this.encoder=new e(this.beaconSize),this.sizeBuffer=new Uint8Array(3),this.isEmpty=!0,this.checkpoints=[],this.beaconSizeLimit=1e6,this.prepare()}writeType(t){return this.encoder.uint(t[0])}writeFields(t){return this.encoder.encode(t)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}prepare(){if(!this.encoder.isEmpty)return;this.checkpoints.length=0;const t=[81,1,this.pageNo,this.nextIndex,this.timestamp,this.url],s=[0,this.timestamp],i=[118,this.tabId];this.writeType(t),this.writeFields(t),this.writeWithSize(s),this.writeWithSize(i),this.isEmpty=!0}writeWithSize(t){const s=this.encoder;if(!this.writeType(t)||!s.skip(3))return!1;const i=s.getCurrentOffset(),e=this.writeFields(t);if(e){const e=s.getCurrentOffset()-i;if(e>16777215)return console.warn(\"OxvoSessions: max message size overflow.\"),!1;this.writeSizeAt(e,i-3),s.checkpoint(),this.checkpoints.push(s.getCurrentOffset()),this.isEmpty=this.isEmpty&&0===t[0],this.nextIndex++}return e}setBeaconSizeLimit(t){this.beaconSizeLimit=t}writeMessage(t){if(-1===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),122===t[0]&&(this.url=t[1]),this.writeWithSize(t)||(this.finaliseBatch(),this.writeWithSize(t)||(this.encoder=new e(this.beaconSizeLimit),this.prepare(),this.writeWithSize(t)?this.finaliseBatch():console.warn(\"OxvoSessions: beacon size overflow. Skipping large message.\",t,this),this.encoder=new e(this.beaconSize),this.prepare()))}finaliseBatch(t=!1){if(this.isEmpty)return;const s=this.encoder.flush();this.onBatch(s,t),this.prepare()}clean(){this.encoder.reset(),this.checkpoints.length=0}}var h;!function(t){t[t.NotActive=0]=\"NotActive\",t[t.Starting=1]=\"Starting\",t[t.Stopping=2]=\"Stopping\",t[t.Active=3]=\"Active\",t[t.Stopped=4]=\"Stopped\"}(h||(h={}));let r=null,u=null,a=h.NotActive;function o(t){u&&u.finaliseBatch(t)}function c(){return new Promise((t=>{a=h.Stopping,null!==p&&(clearInterval(p),p=null),u&&(u.clean(),u=null),r&&(r.clean(),setTimeout((()=>{r=null}),20)),setTimeout((()=>{a=h.NotActive,t(null)}),100)}))}function g(){[h.Stopped,h.Stopping].includes(a)||(postMessage(\"a_stop\"),c().then((()=>{postMessage(\"a_start\")})))}let l,p=null;self.onmessage=({data:s})=>{if(\"stop\"===s)return o(),void c().then((()=>{a=h.Stopped}));if(\"forceFlushBatch\"!==s)if(\"closing\"!==s){if(!Array.isArray(s)){if(\"compressed\"===s.type){if(!r)return console.debug(\"OR WebWorker: sender not initialised. Compressed batch.\"),void g();s.batch&&r.sendCompressed(s.batch)}if(\"uncompressed\"===s.type){if(!r)return console.debug(\"OR WebWorker: sender not initialised. Uncompressed batch.\"),void g();s.batch&&r.sendUncompressed(s.batch)}return\"start\"===s.type?(a=h.Starting,r=new t(s.ingestPoint,(()=>{g()}),(t=>{!function(t){postMessage({type:\"failure\",reason:t}),c()}(t)}),s.connAttemptCount,s.connAttemptGap,(t=>{postMessage({type:\"compress\",batch:t},[t.buffer])}),s.pageNo),u=new n(s.pageNo,s.timestamp,s.url,((t,s)=>{r&&(s?r.sendUncompressed(t):r.push(t))}),s.tabId,(()=>postMessage({type:\"queue_empty\"}))),null===p&&(p=setInterval(o,3e4)),a=h.Active):\"auth\"===s.type?r?u?(r.authorise(s.token),void(s.beaconSizeLimit&&u.setBeaconSizeLimit(s.beaconSizeLimit))):(console.debug(\"OR WebWorker: writer not initialised. Received auth.\"),void g()):(console.debug(\"OR WebWorker: sender not initialised. Received auth.\"),void g()):void 0}if(u){const t=u;s.forEach((s=>{55===s[0]&&(s[1]?l=setTimeout((()=>g()),18e5):clearTimeout(l)),t.writeMessage(s)}))}else postMessage(\"not_init\"),g()}else o(!0);else o()}}();\n";
4552
+ const workerBodyFn = "!function(){\"use strict\";class t{constructor(t,s,i,e=10,n=250,h,r){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.pageNo=r,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.lastBatchNum=0,this.inflightKeepaliveBytes=0,this.ingestURL=t+\"/v1/events\",this.analyticsIngestURL=t+\"/v1/analytics\",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t,s=\"player\"){if(this.busy||!this.token)this.queue.push({batch:t,dataType:s});else if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t,s);else{const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}}sendNext(){const t=this.queue.shift();if(t)if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t.batch,t.dataType);else{const s=++this.lastBatchNum;this.sendBatch(t.batch,!1,s,t.dataType)}else this.busy=!1}retry(t,s,i,e=\"player\"){if(this.attemptsCount>=this.MAX_ATTEMPTS_COUNT)return void this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`);this.attemptsCount++;const n=new Uint8Array(t);setTimeout((()=>this.sendBatch(n,s,i,e)),this.ATTEMPT_TIMEOUT*this.attemptsCount)}sendBatch(t,s,i,e=\"player\"){var n;if(0===t.length)return console.error(\"OxvoSessions: refusing to send 0-byte batch.\",{batchNum:i,dataType:e,isCompressed:s,batch:t}),this.attemptsCount=0,void this.sendNext();const h=null==i?void 0:i.toString().replace(/^([^_]+)_([^_]+).*/,\"$1_$2_$3\");this.busy=!0;const r={Authorization:`Bearer ${this.token}`,DataType:e};if(s&&(r[\"Content-Encoding\"]=\"gzip\"),null===this.token)return void setTimeout((()=>{this.sendBatch(t,s,`${null!=i?i:\"noBatchNum\"}_newToken`,e)}),500);const a=t.length<65536&&this.inflightKeepaliveBytes+t.length<=65536;a&&(this.inflightKeepaliveBytes+=t.length);const o=()=>{a&&(this.inflightKeepaliveBytes-=t.length)},u=`${null!==(n=this.pageNo)&&void 0!==n?n:\"noPageNum\"}_${null!=h?h:\"noBatchNum\"}`,c=\"analytics\"===e?this.analyticsIngestURL:this.ingestURL;fetch(`${c}?seq=${u}&keepalive=${a?\"yes\":\"no\"}`,{body:t,method:\"POST\",headers:r,keepalive:a}).then((n=>{var h;if(o(),null===(h=n.body)||void 0===h||h.cancel().catch((()=>{})),401===n.status)return this.busy=!1,void this.onUnauthorised();n.status>=400?this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_network:${n.status}`,e):(this.attemptsCount=0,this.sendNext())})).catch((n=>{o(),console.warn(\"OxvoSessions:\",n),this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_reject:${n.message}`,e)}))}sendCompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!0,i,s)}sendUncompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s=new Set([60,61,71,73]),i=new Set([21,22,40,41,44,45,46,47,48,79,83,84,85,87,89,116,120,121,123]),e=new Set([17,23,24,27,28,29,30,42,63,64,78,112,115,124]),n=\"function\"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class h{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}getCurrentCheckpoint(){return this.checkpointOffset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const s=n.encode(t),i=s.byteLength;return!(!this.uint(i)||this.offset+i>this.size)&&(this.data.set(s,this.offset),this.offset+=i,!0)}reset(){this.offset=0,this.checkpointOffset=0}rewind(t,s){t>this.offset||s>this.checkpointOffset||(this.offset=t,this.checkpointOffset=s)}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class r extends h{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 65:case 70:case 75:case 76:case 77:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 35:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 52:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17:case 34:case 36:case 50:case 54:return this.uint(t[1])&&this.string(t[2]);case 16:return this.uint(t[1])&&this.int(t[2])&&this.int(t[3]);case 18:return this.uint(t[1])&&this.string(t[2])&&this.int(t[3]);case 19:return this.uint(t[1])&&this.boolean(t[2]);case 21:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8]);case 22:case 27:case 30:case 41:case 45:case 46:case 43:case 63:case 64:case 79:case 124:return this.string(t[1])&&this.string(t[2]);case 23:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 28:case 29:case 42:case 117:case 118:return this.string(t[1]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.int(t[5]);case 49:return this.int(t[1])&&this.int(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 53:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8]);case 55:return this.boolean(t[1]);case 57:case 60:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 58:case 120:return this.int(t[1]);case 68:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6]);case 69:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 81:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.int(t[4])&&this.string(t[5]);case 83:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 84:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6]);case 85:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10])&&this.uint(t[11])&&this.uint(t[12])&&this.uint(t[13])&&this.uint(t[14])&&this.uint(t[15])&&this.uint(t[16])&&this.uint(t[17]);case 87:return this.string(t[1])&&this.int(t[2])&&this.int(t[3]);case 89:return this.string(t[1])&&this.int(t[2])&&this.int(t[3])&&this.int(t[4])&&this.int(t[5])&&this.string(t[6]);case 112:return this.uint(t[1])&&this.string(t[2])&&this.boolean(t[3])&&this.string(t[4])&&this.int(t[5])&&this.int(t[6]);case 113:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3]);case 116:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10]);case 119:return this.string(t[1])&&this.uint(t[2]);case 121:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 122:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 123:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])}}}class a{constructor(t,s,i){this.bufferSize=t,this.version=s,this.dataType=i,this.sizeBuffer=new Uint8Array(3),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,this.encoder=new r(t)}push(t,s){const i=this.encoder,e=null===this.snap,n=i.getCurrentOffset(),h=i.getCurrentCheckpoint();if(e){const t={pageNo:s.pageNo,firstIndex:s.index,timestamp:s.timestamp,url:s.url,tabId:s.tabId};if(!this.writeHeader(t))return i.rewind(n,h),!1;this.snap=t,this.lastPushedTs=s.timestamp}return 0===t[0]||s.timestamp===this.lastPushedTs||this.writeMessageWithSize([0,s.timestamp])?this.writeMessageWithSize(t)?(this.lastPushedTs=s.timestamp,0!==t[0]&&(this.hasNonTimestamp=!0),!0):(i.rewind(n,h),e&&(this.snap=null),!1):(i.rewind(n,h),!1)}hasContent(){return null!==this.snap&&this.hasNonTimestamp}flush(){if(!this.hasContent())return this.reset(),null;const t=this.encoder.flush();return this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,t}reset(){this.encoder.reset(),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0}writeHeader(t){const s=this.encoder,i=[81,this.version,t.pageNo,t.firstIndex,t.timestamp,t.url];return!!s.uint(i[0])&&(!!s.encode(i)&&(!(s.getCurrentOffset()>this.bufferSize)&&(s.checkpoint(),!!this.writeMessageWithSize([0,t.timestamp])&&!!this.writeMessageWithSize([118,t.tabId]))))}writeMessageWithSize(t){const s=this.encoder;if(!s.uint(t[0])||!s.skip(3))return!1;const i=s.getCurrentOffset();if(!s.encode(t))return!1;const e=s.getCurrentOffset(),n=e-i;return n>16777215?(console.warn(\"OxvoSessions: max message size overflow.\"),!1):!(e>this.bufferSize)&&(this.writeSizeAt(n,i-3),s.checkpoint(),!0)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}}class o{constructor(t,s,i,e,n,h,r=!1,o){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=e,this.tabId=n,this.onOfflineEnd=h,this.localDebug=r,this.onLocalSave=o,this.nextIndex=0,this.beaconSize=2e5,this.beaconSizeLimit=1e6,this.protocolVersion=1,this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"),this.assetBuilder=new a(this.beaconSize,3,\"assets\"),this.devtoolsBuilder=new a(this.beaconSize,4,\"devtools\"),this.analyticsBuilder=new a(this.beaconSize,5,\"analytics\")}playerVersion(){return 2===this.protocolVersion?2:1}currentCtx(){return{pageNo:this.pageNo,index:this.nextIndex,timestamp:this.timestamp,url:this.url,tabId:this.tabId}}setBeaconSizeLimit(t){this.beaconSizeLimit=t}setProtocolVersion(t){this.protocolVersion!==t&&(this.protocolVersion=t,this.playerBuilder.reset(),this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"))}writeMessage(t){if(-1===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),122===t[0]&&(this.url=t[1]);const s=this.routeMessage(t);this.pushTo(s,t)}routeMessage(t){if(2===this.protocolVersion){const n=t[0];if(s.has(n))return this.assetBuilder;if(i.has(n))return this.devtoolsBuilder;if(e.has(n))return this.analyticsBuilder}return this.playerBuilder}pushTo(t,s){const i=this.currentCtx();if(t.push(s,i))return void this.nextIndex++;if(this.flushBuilder(t),t.push(s,i))return void this.nextIndex++;const e=new a(this.beaconSizeLimit,t.version,t.dataType);if(!e.push(s,i))return void console.warn(\"OxvoSessions: beacon size overflow. Skipping large message.\",s);this.nextIndex++;const n=e.flush();n&&this.emitBatch(n,t.dataType,!1)}flushBuilder(t,s=!1){const i=t.flush();return!!i&&(this.emitBatch(i,t.dataType,s),!0)}emitBatch(t,s,i){this.localDebug&&this.onLocalSave&&this.onLocalSave(`${s}-${Date.now()}`,t.slice()),this.onBatch(t,i,s)}finaliseBatch(t=!1){this.flushBuilder(this.playerBuilder,t),this.flushBuilder(this.assetBuilder,t),this.flushBuilder(this.devtoolsBuilder,t),this.flushBuilder(this.analyticsBuilder,t)}clean(){this.playerBuilder.reset(),this.assetBuilder.reset(),this.devtoolsBuilder.reset(),this.analyticsBuilder.reset()}}var u;!function(t){t[t.NotActive=0]=\"NotActive\",t[t.Starting=1]=\"Starting\",t[t.Stopping=2]=\"Stopping\",t[t.Active=3]=\"Active\",t[t.Stopped=4]=\"Stopped\"}(u||(u={}));let c=null,l=null,d=u.NotActive;function p(t){l&&l.finaliseBatch(t)}function f(){return new Promise((t=>{d=u.Stopping,null!==y&&(clearInterval(y),y=null),l&&(l.clean(),l=null),c&&(c.clean(),setTimeout((()=>{c=null}),20)),setTimeout((()=>{d=u.NotActive,t(null)}),100)}))}function g(){[u.Stopped,u.Stopping].includes(d)||(postMessage(\"a_stop\"),f().then((()=>{postMessage(\"a_start\")})))}let m,y=null;self.onmessage=({data:s})=>{var i;if(\"stop\"===s)return p(),void f().then((()=>{d=u.Stopped}));if(\"forceFlushBatch\"!==s)if(\"closing\"!==s){if(!Array.isArray(s)){if(\"compressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Compressed batch.\"),void g();s.batch&&c.sendCompressed(s.batch,s.dataType)}if(\"uncompressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Uncompressed batch.\"),void g();s.batch&&c.sendUncompressed(s.batch,s.dataType)}return\"start\"===s.type?(d=u.Starting,c=new t(s.ingestPoint,(()=>{g()}),(t=>{!function(t){postMessage({type:\"failure\",reason:t}),f()}(t)}),s.connAttemptCount,s.connAttemptGap,((t,s)=>{postMessage({type:\"compress\",batch:t,dataType:s},[t.buffer])}),s.pageNo),l=new o(s.pageNo,s.timestamp,s.url,((t,s,i=\"player\")=>{c&&(s?c.sendUncompressed(t,i):c.push(t,i))}),s.tabId,(()=>postMessage({type:\"queue_empty\"})),null!==(i=s.localDebug)&&void 0!==i&&i,((t,s)=>{postMessage({type:\"local_save\",name:t,batch:s},[s.buffer])})),null===y&&(y=setInterval(p,3e4)),d=u.Active):\"auth\"===s.type?c?l?(c.authorise(s.token),s.beaconSizeLimit&&l.setBeaconSizeLimit(s.beaconSizeLimit),void(s.protocolVersion&&l.setProtocolVersion(s.protocolVersion))):(console.debug(\"OR WebWorker: writer not initialised. Received auth.\"),void g()):(console.debug(\"OR WebWorker: sender not initialised. Received auth.\"),void g()):void 0}if(l){const t=l;s.forEach((s=>{55===s[0]&&(s[1]?m=setTimeout((()=>g()),18e5):clearTimeout(m)),t.writeMessage(s)}))}else postMessage(\"not_init\"),g()}else p(!0);else p()}}();\n";
4386
4553
  const CANCELED = 'canceled';
4387
4554
  const bufferStorageKey = 'or_buffer_1';
4555
+ const PROTO_VERSION = '2';
4556
+ const BOOTSTRAP_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
4557
+ const MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH = 1024 * 1024;
4388
4558
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
4389
4559
  const SuccessfulStart = (body) => ({ ...body, success: true });
4390
4560
  var ActivityState;
@@ -4415,22 +4585,34 @@ const proto = {
4415
4585
  resp: 'never-gonna-let-you-down',
4416
4586
  // regenerating id (copied other tab)
4417
4587
  reg: 'never-gonna-run-around-and-desert-you',
4418
- iframeSignal: 'tracker inside a child iframe',
4419
- iframeId: 'getting node id for child iframe',
4420
- iframeBatch: 'batch of messages from an iframe window',
4421
- parentAlive: 'signal that parent is live',
4422
- killIframe: 'stop tracker inside frame',
4423
- startIframe: 'start tracker inside frame',
4588
+ iframeSignal: 'oxvo:cross-frame:v1:handshake',
4589
+ iframeId: 'oxvo:cross-frame:v1:ready',
4590
+ iframeBatch: 'oxvo:cross-frame:v1:batch',
4591
+ parentAlive: 'oxvo:cross-frame:v1:pong',
4592
+ killIframe: 'oxvo:cross-frame:v1:stop',
4593
+ startIframe: 'oxvo:cross-frame:v1:start',
4424
4594
  // checking updates
4425
- polling: 'hello-how-are-you-im-under-the-water-please-help-me',
4595
+ polling: 'oxvo:cross-frame:v1:ping',
4426
4596
  // happens if tab is old and has outdated token but
4427
4597
  // not communicating with backend to update it (for whatever reason)
4428
4598
  reset: 'reset-your-session-please',
4429
4599
  };
4600
+ const FRAME_RATE_WINDOW_MS = 1000;
4601
+ const MAX_FRAME_ENVELOPES_PER_WINDOW = 120;
4602
+ const MAX_FRAME_MESSAGES_PER_WINDOW = 50000;
4603
+ const MAX_FRAME_BYTES_PER_WINDOW = 4 * 1024 * 1024;
4604
+ const FRAME_CAPABILITY_TTL_MS = 5 * 60 * 1000;
4605
+ const FRAME_CAPABILITY_REFRESH_LEAD_MS = 60 * 1000;
4606
+ const FRAME_PREVIOUS_CAPABILITY_GRACE_MS = 5 * 1000;
4430
4607
  class App {
4608
+ get tagMatcher() {
4609
+ return this.markerWatcher.matcher;
4610
+ }
4431
4611
  constructor(projectKey, sessionToken, options, signalError, insideIframe) {
4432
4612
  this.signalError = signalError;
4433
4613
  this.insideIframe = insideIframe;
4614
+ // Registered by input/img/canvas to re-emit a node when its level changes.
4615
+ this.resanitizeCallbacks = [];
4434
4616
  this.messages = [];
4435
4617
  /**
4436
4618
  * we need 2 buffers, so we don't lose anything
@@ -4442,18 +4624,31 @@ class App {
4442
4624
  this.stopCallbacks = [];
4443
4625
  this.commitCallbacks = [];
4444
4626
  this.activityState = ActivityState.NotActive;
4445
- this.version = '7.1.0'; // TODO: version compatability check inside each plugin.
4627
+ this.version = '7.3.4'; // TODO: version compatability check inside each plugin.
4446
4628
  this.socketMode = false;
4447
4629
  this.compressionThreshold = 24 * 1000;
4448
4630
  this.bc = null;
4449
4631
  this.canvasRecorder = null;
4450
4632
  this.conditionsManager = null;
4633
+ this.bootstrapAttempts = {};
4451
4634
  this.canStart = false;
4452
4635
  this.rootId = null;
4453
4636
  this.pageFrames = [];
4454
4637
  this.frameOderNumber = 0;
4455
4638
  this.frameLevel = 0;
4456
4639
  this.emptyBatchCounter = 0;
4640
+ this.crossDomainFrameCapability = null;
4641
+ this.crossDomainFrameCapabilityExpiresAt = 0;
4642
+ this.crossDomainFrameSession = null;
4643
+ this.crossDomainFrameCanvas = null;
4644
+ this.crossDomainResumeAfterHandshake = false;
4645
+ this.crossDomainConnectionRequested = false;
4646
+ this.crossDomainFrames = new Map();
4647
+ this.crossDomainEnvelopeBudgets = new WeakMap();
4648
+ this.pendingCrossDomainFrames = new Map();
4649
+ this.crossDomainAuthorizationGeneration = 0;
4650
+ this.nextCrossDomainFrameOrder = 1;
4651
+ this.crossDomainCanvasConfig = null;
4457
4652
  /** used by child iframes for crossdomain only */
4458
4653
  this.parentActive = false;
4459
4654
  this.checkStatus = () => {
@@ -4461,185 +4656,201 @@ class App {
4461
4656
  };
4462
4657
  this.parentCrossDomainFrameListener = (event) => {
4463
4658
  const { data } = event;
4464
- if (!data || event.source === window)
4659
+ if (!data ||
4660
+ this.crossDomainParentOrigin === null ||
4661
+ this.crossDomainFrameNonce === null ||
4662
+ event.source !== window.parent ||
4663
+ event.origin !== this.crossDomainParentOrigin) {
4465
4664
  return;
4466
- if (data.line === proto.startIframe) {
4467
- if (this.active())
4665
+ }
4666
+ if (data.line === proto.iframeId) {
4667
+ const init = parseCrossDomainFrameInit(data, proto.iframeId, {
4668
+ context: this.contextId,
4669
+ nonce: this.crossDomainFrameNonce,
4670
+ });
4671
+ if (init === null)
4672
+ return;
4673
+ const sessionChanged = this.crossDomainFrameSession !== null &&
4674
+ this.crossDomainFrameSession.sessionID !== init.session.sessionID;
4675
+ if (sessionChanged && this.activityState === ActivityState.Starting) {
4676
+ this.postHandshakeToParent();
4468
4677
  return;
4469
- try {
4470
- this.allowAppStart();
4471
- void this.start();
4472
4678
  }
4473
- catch (e) {
4474
- console.error('children frame restart failed:', e);
4679
+ const shouldResume = this.crossDomainResumeAfterHandshake || (sessionChanged && this.active());
4680
+ if (sessionChanged && this.active()) {
4681
+ this.stop();
4682
+ }
4683
+ this.applyCrossDomainFrameInit(init);
4684
+ if (shouldResume &&
4685
+ this.activityState !== ActivityState.Active &&
4686
+ this.activityState !== ActivityState.Starting) {
4687
+ this.crossDomainResumeAfterHandshake = false;
4688
+ this.startCrossDomainFrame(this.prevOpts);
4475
4689
  }
4690
+ this.debug.log('Starting secure cross-domain iframe tracking');
4691
+ return;
4692
+ }
4693
+ const expectedLine = data.line === proto.parentAlive
4694
+ ? proto.parentAlive
4695
+ : data.line === proto.startIframe
4696
+ ? proto.startIframe
4697
+ : data.line === proto.killIframe
4698
+ ? proto.killIframe
4699
+ : null;
4700
+ if (this.crossDomainFrameCapability === null ||
4701
+ expectedLine === null ||
4702
+ (expectedLine !== proto.killIframe &&
4703
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) ||
4704
+ !matchesCrossDomainEnvelope(data, expectedLine, {
4705
+ context: this.contextId,
4706
+ nonce: this.crossDomainFrameNonce,
4707
+ capability: this.crossDomainFrameCapability,
4708
+ })) {
4709
+ return;
4476
4710
  }
4477
4711
  if (data.line === proto.parentAlive) {
4478
4712
  this.parentActive = true;
4713
+ return;
4479
4714
  }
4480
- if (data.line === proto.iframeId) {
4481
- this.parentActive = true;
4482
- this.rootId = data.id;
4483
- this.session.setSessionSecret(data.token, this.projectKey);
4484
- this.frameOderNumber = data.frameOrderNumber;
4485
- this.frameLevel = data.frameLevel;
4486
- this.debug.log('starting iframe tracking', data);
4487
- this.allowAppStart();
4715
+ if (data.line === proto.startIframe) {
4716
+ // Avoid corrupting an in-flight start; let it complete.
4717
+ if (this.activityState === ActivityState.Starting)
4718
+ return;
4719
+ const init = parseCrossDomainFrameInit(data, proto.startIframe, {
4720
+ context: this.contextId,
4721
+ nonce: this.crossDomainFrameNonce,
4722
+ });
4723
+ if (init === null)
4724
+ return;
4725
+ try {
4726
+ if (this.active()) {
4727
+ this.stop();
4728
+ }
4729
+ this.applyCrossDomainFrameInit(init);
4730
+ this.startCrossDomainFrame(this.prevOpts);
4731
+ }
4732
+ catch (e) {
4733
+ console.error('children frame restart failed:', e);
4734
+ }
4735
+ return;
4488
4736
  }
4489
4737
  if (data.line === proto.killIframe) {
4490
- if (this.active()) {
4738
+ const shouldResume = this.activityState === ActivityState.Active || this.activityState === ActivityState.Starting;
4739
+ if (this.activityState !== ActivityState.NotActive) {
4491
4740
  this.stop();
4492
4741
  }
4742
+ this.clearCrossDomainFrameAuthorization();
4743
+ this.crossDomainResumeAfterHandshake = shouldResume;
4744
+ this.crossDomainConnectionRequested = shouldResume;
4493
4745
  }
4494
4746
  };
4495
- /**
4496
- * context ids for iframes,
4497
- * order is not so important as long as its consistent
4498
- * */
4499
- this.trackedFrames = [];
4500
4747
  this.crossDomainIframeListener = (event) => {
4501
- if (!this.active() || event.source === window)
4502
- return;
4503
4748
  const { data } = event;
4504
- if (!data)
4749
+ if (!data || event.source === window)
4505
4750
  return;
4506
4751
  if (data.line === proto.iframeSignal) {
4507
- // @ts-ignore
4508
- event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
4509
- const signalId = async () => {
4510
- if (event.source === null) {
4511
- return console.error('Couldnt connect to event.source for child iframe tracking');
4512
- }
4513
- const id = await this.checkNodeId(event.source);
4514
- if (!id) {
4515
- this.debug.log('Couldnt get node id for iframe', event.source);
4516
- return;
4517
- }
4518
- try {
4519
- if (this.trackedFrames.includes(data.context)) {
4520
- this.debug.log('Trying to observe already added iframe; ignore if its a restart');
4521
- }
4522
- else {
4523
- this.trackedFrames.push(data.context);
4524
- }
4525
- await this.waitStarted();
4526
- const token = this.session.getSessionSecret(this.projectKey);
4527
- const order = this.trackedFrames.findIndex((f) => f === data.context) + 1;
4528
- if (order === 0) {
4529
- this.debug.error('Couldnt get order number for iframe', data.context, this.trackedFrames);
4530
- }
4531
- const iframeData = {
4532
- line: proto.iframeId,
4533
- id,
4534
- token,
4535
- frameOrderNumber: order,
4536
- frameLevel: this.frameLevel + 1,
4537
- };
4538
- this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
4539
- // @ts-ignore
4540
- event.source?.postMessage(iframeData, '*');
4541
- }
4542
- catch (e) {
4543
- console.error(e);
4544
- }
4545
- };
4546
- void signalId();
4752
+ const handshake = parseCrossDomainHandshake(data, proto.iframeSignal, this.projectKey);
4753
+ if (handshake !== null) {
4754
+ void this.establishCrossDomainFrame(event, handshake);
4755
+ }
4756
+ return;
4757
+ }
4758
+ if (!this.active())
4759
+ return;
4760
+ if (data.line === proto.polling) {
4761
+ const state = this.validatedCrossDomainFrame(event, proto.polling);
4762
+ if (state !== null && this.consumeCrossDomainEnvelopeBudget(state.source)) {
4763
+ this.postToCrossDomainFrame(state, proto.parentAlive);
4764
+ }
4765
+ return;
4547
4766
  }
4548
4767
  /**
4549
4768
  * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
4550
4769
  * plus we rewrite some of the messages to be relative to the main context/window
4551
4770
  * */
4552
4771
  if (data.line === proto.iframeBatch) {
4553
- const msgBatch = data.messages;
4772
+ const state = this.validatedCrossDomainFrame(event, proto.iframeBatch);
4773
+ if (state === null || !this.consumeCrossDomainEnvelopeBudget(state.source)) {
4774
+ return;
4775
+ }
4776
+ const batch = parseCrossDomainIframeBatch(data.messages);
4777
+ if (batch === null ||
4778
+ !this.consumeCrossDomainFrameBudget(state, batch.messages.length, batch.bytes)) {
4779
+ return;
4780
+ }
4554
4781
  const mappedMessages = [];
4555
- msgBatch.forEach((msg) => {
4782
+ batch.messages.forEach((msg) => {
4783
+ let fixedMessage = msg;
4556
4784
  if (msg[0] === 20 /* MType.MouseMove */) {
4557
- let fixedMessage = msg;
4558
- this.pageFrames.forEach((frame) => {
4559
- if (frame.contentWindow === event.source) {
4560
- const [type, x, y] = msg;
4561
- const { left, top } = frame.getBoundingClientRect();
4562
- fixedMessage = [type, x + left, y + top];
4563
- }
4564
- });
4565
- mappedMessages.push(fixedMessage);
4785
+ const [type, x, y] = msg;
4786
+ const { left, top } = state.frame.getBoundingClientRect();
4787
+ fixedMessage = [type, x + left, y + top];
4566
4788
  }
4567
4789
  if (msg[0] === 68 /* MType.MouseClick */) {
4568
- let fixedMessage = msg;
4569
- this.pageFrames.forEach((frame) => {
4570
- if (frame.contentWindow === event.source) {
4571
- const [type, id, hesitationTime, label, selector, normX, normY] = msg;
4572
- const { left, top, width, height } = frame.getBoundingClientRect();
4573
- const contentWidth = document.documentElement.scrollWidth;
4574
- const contentHeight = document.documentElement.scrollHeight;
4575
- // (normalizedX * frameWidth + frameLeftOffset)/docSize
4576
- const fullX = (normX / 100) * width + left;
4577
- const fullY = (normY / 100) * height + top;
4578
- const fixedX = fullX / contentWidth;
4579
- const fixedY = fullY / contentHeight;
4580
- fixedMessage = [
4581
- type,
4582
- id,
4583
- hesitationTime,
4584
- label,
4585
- selector,
4586
- Math.round(fixedX * 1e3) / 1e1,
4587
- Math.round(fixedY * 1e3) / 1e1,
4588
- ];
4589
- }
4590
- });
4591
- mappedMessages.push(fixedMessage);
4592
- }
4593
- if (![28 /* MType.UserID */, 29 /* MType.UserAnonymousID */, 30 /* MType.Metadata */].includes(msg[0])) {
4594
- mappedMessages.push(msg);
4790
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
4791
+ const { left, top, width, height } = state.frame.getBoundingClientRect();
4792
+ const contentWidth = document.documentElement.scrollWidth;
4793
+ const contentHeight = document.documentElement.scrollHeight;
4794
+ const fullX = (normX / 100) * width + left;
4795
+ const fullY = (normY / 100) * height + top;
4796
+ const fixedX = fullX / contentWidth;
4797
+ const fixedY = fullY / contentHeight;
4798
+ fixedMessage = [
4799
+ type,
4800
+ id,
4801
+ hesitationTime,
4802
+ label,
4803
+ selector,
4804
+ Math.round(fixedX * 1e3) / 1e1,
4805
+ Math.round(fixedY * 1e3) / 1e1,
4806
+ ];
4595
4807
  }
4808
+ mappedMessages.push(fixedMessage);
4596
4809
  });
4597
4810
  this.messages.push(...mappedMessages);
4598
4811
  }
4599
- if (data.line === proto.polling) {
4600
- if (!this.pollingQueue.order.length) {
4601
- return;
4812
+ };
4813
+ this.bootChildrenFrames = async () => {
4814
+ await this.waitStarted();
4815
+ this.pruneDetachedCrossDomainFrames();
4816
+ for (const state of this.crossDomainFrames.values()) {
4817
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
4818
+ const id = await this.checkNodeId(state.source);
4819
+ const session = this.currentCrossDomainSession();
4820
+ if (id === null ||
4821
+ session === null ||
4822
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
4823
+ this.crossDomainFrames.get(state.source) !== state ||
4824
+ !this.active()) {
4825
+ continue;
4602
4826
  }
4603
- const nextCommand = this.pollingQueue.order[0];
4604
- if (nextCommand && this.pollingQueue[nextCommand].length === 0) {
4605
- this.pollingQueue.order = this.pollingQueue.order.filter((c) => c !== nextCommand);
4606
- return;
4827
+ const rotated = this.crossDomainFrameNeedsRotation(state, session.sessionID);
4828
+ if (rotated && !this.rotateCrossDomainFrameCapability(state, session.sessionID)) {
4829
+ continue;
4607
4830
  }
4608
- if (this.pollingQueue[nextCommand].includes(data.context)) {
4609
- this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter((c) => c !== data.context);
4610
- // @ts-ignore
4611
- event.source?.postMessage({ line: nextCommand }, '*');
4612
- if (this.pollingQueue[nextCommand].length === 0) {
4613
- this.pollingQueue.order.shift();
4614
- }
4831
+ const payload = id === null ? null : this.frameInitPayload(state, id);
4832
+ if (payload) {
4833
+ this.postToCrossDomainFrame(state, rotated ? proto.iframeId : proto.startIframe, payload);
4615
4834
  }
4616
4835
  }
4617
4836
  };
4618
- /**
4619
- * { command : [remaining iframes] }
4620
- * + order of commands
4621
- **/
4622
- this.pollingQueue = {
4623
- order: [],
4624
- };
4625
- this.addCommand = (cmd) => {
4626
- this.pollingQueue.order.push(cmd);
4627
- this.pollingQueue[cmd] = [...this.trackedFrames];
4628
- };
4629
- this.bootChildrenFrames = async () => {
4630
- await this.waitStarted();
4631
- this.addCommand(proto.startIframe);
4632
- };
4633
4837
  this.killChildrenFrames = () => {
4634
- this.addCommand(proto.killIframe);
4838
+ this.pruneDetachedCrossDomainFrames();
4839
+ const states = Array.from(this.crossDomainFrames.values());
4840
+ this.crossDomainAuthorizationGeneration += 1;
4841
+ this.crossDomainFrames.clear();
4842
+ this.pendingCrossDomainFrames.clear();
4843
+ states.forEach((state) => {
4844
+ this.postToCrossDomainFrame(state, proto.killIframe);
4845
+ });
4635
4846
  };
4636
4847
  this.signalIframeTracker = () => {
4637
- const thisTab = this.session.getTabId();
4638
- window.parent.postMessage({
4639
- line: proto.iframeSignal,
4640
- source: thisTab,
4641
- context: this.contextId,
4642
- }, this.options.crossdomain?.parentDomain ?? '*');
4848
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
4849
+ this.debug.error('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
4850
+ return;
4851
+ }
4852
+ this.crossDomainConnectionRequested = true;
4853
+ this.postHandshakeToParent();
4643
4854
  /**
4644
4855
  * since we need to wait uncertain amount of time
4645
4856
  * and I don't want to have recursion going on,
@@ -4651,20 +4862,16 @@ class App {
4651
4862
  let cumulativeDelay = 0;
4652
4863
  let stopAttempts = false;
4653
4864
  const checkAndSendMessage = () => {
4654
- if (stopAttempts || this.checkStatus()) {
4865
+ if (stopAttempts || !this.crossDomainConnectionRequested || this.checkStatus()) {
4655
4866
  stopAttempts = true;
4656
4867
  return;
4657
4868
  }
4658
- window.parent.postMessage({
4659
- line: proto.iframeSignal,
4660
- source: thisTab,
4661
- context: this.contextId,
4662
- }, this.options.crossdomain?.parentDomain ?? '*');
4869
+ this.postHandshakeToParent();
4663
4870
  this.debug.info('Trying to signal to parent, attempt:', retries + 1);
4664
4871
  retries++;
4665
4872
  };
4666
4873
  for (let i = 0; i < maxRetries; i++) {
4667
- if (this.checkStatus()) {
4874
+ if (!this.crossDomainConnectionRequested || this.checkStatus()) {
4668
4875
  stopAttempts = true;
4669
4876
  break;
4670
4877
  }
@@ -4751,6 +4958,26 @@ class App {
4751
4958
  this.restartCanvasTracking = () => {
4752
4959
  this.canvasRecorder?.restartTracking();
4753
4960
  };
4961
+ this.attachResanitizeCallback = (cb) => {
4962
+ this.resanitizeCallbacks.push(cb);
4963
+ };
4964
+ this.callResanitizeCallbacks = (node, id) => {
4965
+ this.resanitizeCallbacks.forEach((cb) => cb(node, id));
4966
+ };
4967
+ this.resanitize = (el) => {
4968
+ const root = el ?? (IN_BROWSER ? document.documentElement : undefined);
4969
+ if (!root) {
4970
+ return;
4971
+ }
4972
+ this.observer.resanitizeSubtree(root);
4973
+ };
4974
+ this.checkSanitization = (el) => {
4975
+ const id = this.nodes.getID(el);
4976
+ if (id === undefined) {
4977
+ return undefined;
4978
+ }
4979
+ return this.sanitizer.getLevel(id);
4980
+ };
4754
4981
  this.flushBuffer = async (buffer) => {
4755
4982
  return new Promise((res, reject) => {
4756
4983
  if (buffer.length === 0) {
@@ -4792,6 +5019,7 @@ class App {
4792
5019
  __is_snippet: false,
4793
5020
  __debug_report_edp: null,
4794
5021
  __debug__: LogLevel.Silent,
5022
+ __local_debug: false,
4795
5023
  localStorage: null,
4796
5024
  sessionStorage: null,
4797
5025
  forceSingleTab: false,
@@ -4800,9 +5028,7 @@ class App {
4800
5028
  maskTextEmails: false,
4801
5029
  maskTextNumbers: false,
4802
5030
  disableStringDict: false,
4803
- crossdomain: {
4804
- parentDomain: '*',
4805
- },
5031
+ crossdomain: {},
4806
5032
  canvas: {
4807
5033
  disableCanvas: false,
4808
5034
  fixedCanvasScaling: false,
@@ -4815,6 +5041,9 @@ class App {
4815
5041
  disableThrottling: false,
4816
5042
  };
4817
5043
  this.options = simpleMerge(defaultOptions, options);
5044
+ this.crossDomainChildOrigins = normalizeCrossDomainOrigins(this.options.crossdomain?.childOrigins);
5045
+ this.crossDomainParentOrigin = normalizeCrossDomainOrigin(this.options.crossdomain?.parentDomain);
5046
+ this.crossDomainFrameNonce = this.insideIframe ? createCrossDomainSecret() : null;
4818
5047
  if (!this.insideIframe &&
4819
5048
  !this.options.forceSingleTab &&
4820
5049
  globalThis &&
@@ -4850,9 +5079,7 @@ class App {
4850
5079
  });
4851
5080
  this.session.attachUpdateCallback(({ userID, metadata }) => {
4852
5081
  if (userID != null) {
4853
- if (!userID ||
4854
- typeof userID !== 'string' ||
4855
- userID.trim().length === 0) {
5082
+ if (!userID || typeof userID !== 'string' || userID.trim().length === 0) {
4856
5083
  this.debug.warn('Invalid userID (must be type string), ignoring.');
4857
5084
  return;
4858
5085
  }
@@ -4863,25 +5090,33 @@ class App {
4863
5090
  }
4864
5091
  });
4865
5092
  // @deprecated (use sessionHash on start instead)
4866
- if (sessionToken != null) {
5093
+ if (sessionToken != null && !this.insideIframe) {
4867
5094
  this.session.applySessionHash(sessionToken);
4868
5095
  }
5096
+ if (this.insideIframe) {
5097
+ this.session.clearSessionSecret();
5098
+ }
4869
5099
  const thisTab = this.session.getTabId();
4870
5100
  if (this.insideIframe) {
4871
- /**
4872
- * listen for messages from parent window, so we can signal that we're alive
4873
- * */
4874
- window.addEventListener('message', this.parentCrossDomainFrameListener);
4875
- window.addEventListener('message', this.crossDomainIframeListener);
4876
- setInterval(() => {
4877
- if (document.hidden) {
4878
- return;
4879
- }
4880
- window.parent.postMessage({
4881
- line: proto.polling,
4882
- context: this.contextId,
4883
- }, options.crossdomain?.parentDomain ?? '*');
4884
- }, 250);
5101
+ if (this.crossDomainParentOrigin !== null && this.crossDomainFrameNonce !== null) {
5102
+ /**
5103
+ * listen for messages from parent window, so we can signal that we're alive
5104
+ * */
5105
+ window.addEventListener('message', this.parentCrossDomainFrameListener);
5106
+ setInterval(() => {
5107
+ if (!this.crossDomainConnectionRequested) {
5108
+ return;
5109
+ }
5110
+ if (this.crossDomainFrameCapability === null ||
5111
+ Date.now() >=
5112
+ this.crossDomainFrameCapabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS) {
5113
+ this.postHandshakeToParent();
5114
+ }
5115
+ else {
5116
+ this.postToParent(proto.polling);
5117
+ }
5118
+ }, 1000);
5119
+ }
4885
5120
  }
4886
5121
  else {
4887
5122
  this.initWorker();
@@ -4889,7 +5124,9 @@ class App {
4889
5124
  * if we get a signal from child iframes, we check for their node_id and send it back,
4890
5125
  * so they can act as if it was just a same-domain iframe
4891
5126
  * */
4892
- window.addEventListener('message', this.crossDomainIframeListener);
5127
+ if (this.options.crossdomain?.enabled && this.crossDomainChildOrigins.size > 0) {
5128
+ window.addEventListener('message', this.crossDomainIframeListener);
5129
+ }
4893
5130
  }
4894
5131
  if (this.bc !== null) {
4895
5132
  this.bc.postMessage({
@@ -4939,6 +5176,319 @@ class App {
4939
5176
  };
4940
5177
  }
4941
5178
  }
5179
+ postToParent(line, extra = {}) {
5180
+ if (this.crossDomainParentOrigin === null ||
5181
+ this.crossDomainFrameNonce === null ||
5182
+ this.crossDomainFrameCapability === null ||
5183
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) {
5184
+ return;
5185
+ }
5186
+ window.parent.postMessage({
5187
+ line,
5188
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5189
+ context: this.contextId,
5190
+ nonce: this.crossDomainFrameNonce,
5191
+ capability: this.crossDomainFrameCapability,
5192
+ ...extra,
5193
+ }, this.crossDomainParentOrigin);
5194
+ }
5195
+ postHandshakeToParent() {
5196
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
5197
+ return;
5198
+ }
5199
+ window.parent.postMessage({
5200
+ line: proto.iframeSignal,
5201
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5202
+ context: this.contextId,
5203
+ nonce: this.crossDomainFrameNonce,
5204
+ projectKey: this.projectKey,
5205
+ }, this.crossDomainParentOrigin);
5206
+ }
5207
+ applyCrossDomainFrameInit(init) {
5208
+ this.parentActive = true;
5209
+ this.crossDomainConnectionRequested = true;
5210
+ this.crossDomainFrameCapability = init.capability;
5211
+ this.crossDomainFrameCapabilityExpiresAt = init.capabilityExpiresAt;
5212
+ this.crossDomainFrameSession = init.session;
5213
+ this.crossDomainFrameCanvas = init.canvas ?? null;
5214
+ this.rootId = init.id;
5215
+ this.frameOderNumber = init.frameOrderNumber;
5216
+ this.frameLevel = init.frameLevel;
5217
+ this.allowAppStart();
5218
+ }
5219
+ clearCrossDomainFrameAuthorization() {
5220
+ this.parentActive = false;
5221
+ this.canStart = false;
5222
+ this.crossDomainFrameCapability = null;
5223
+ this.crossDomainFrameCapabilityExpiresAt = 0;
5224
+ this.crossDomainFrameSession = null;
5225
+ this.crossDomainFrameCanvas = null;
5226
+ this.rootId = null;
5227
+ }
5228
+ findCrossDomainFrame(source) {
5229
+ const frame = this.pageFrames.find((candidate) => candidate.contentWindow === source) ??
5230
+ Array.from(document.querySelectorAll('iframe')).find((candidate) => candidate.contentWindow === source);
5231
+ if (!frame)
5232
+ return null;
5233
+ if (!this.pageFrames.includes(frame)) {
5234
+ this.pageFrames.push(frame);
5235
+ }
5236
+ return frame;
5237
+ }
5238
+ revokeCrossDomainFrame(source, expectedState) {
5239
+ const state = this.crossDomainFrames.get(source);
5240
+ if (!state || (expectedState !== undefined && state !== expectedState)) {
5241
+ return;
5242
+ }
5243
+ this.crossDomainFrames.delete(source);
5244
+ }
5245
+ pruneDetachedCrossDomainFrames() {
5246
+ this.crossDomainFrames.forEach((state, source) => {
5247
+ if (!state.frame.isConnected || state.frame.contentWindow !== source) {
5248
+ this.revokeCrossDomainFrame(source);
5249
+ }
5250
+ });
5251
+ }
5252
+ currentCrossDomainSession() {
5253
+ const session = this.session.getInfo();
5254
+ if (!session.sessionID) {
5255
+ return null;
5256
+ }
5257
+ return {
5258
+ sessionID: session.sessionID,
5259
+ projectID: session.projectID,
5260
+ startedAt: session.timestamp,
5261
+ delayMs: this.delay,
5262
+ userUUID: this.localStorage.getItem(this.options.local_uuid_key) ?? '',
5263
+ };
5264
+ }
5265
+ frameInitPayload(state, id) {
5266
+ const session = this.currentCrossDomainSession();
5267
+ if (session === null ||
5268
+ session.sessionID !== state.sessionID ||
5269
+ Date.now() >= state.capabilityExpiresAt) {
5270
+ return null;
5271
+ }
5272
+ return {
5273
+ id,
5274
+ frameOrderNumber: state.frameOrderNumber,
5275
+ frameLevel: this.frameLevel + 1,
5276
+ capabilityExpiresAt: state.capabilityExpiresAt,
5277
+ session,
5278
+ ...(this.crossDomainCanvasConfig ? { canvas: this.crossDomainCanvasConfig } : {}),
5279
+ };
5280
+ }
5281
+ rotateCrossDomainFrameCapability(state, sessionID, now = Date.now()) {
5282
+ const capability = createCrossDomainSecret();
5283
+ if (capability === null) {
5284
+ return false;
5285
+ }
5286
+ const preservePreviousCapability = state.sessionID === sessionID && now < state.capabilityExpiresAt;
5287
+ state.previousCapability = preservePreviousCapability ? state.capability : null;
5288
+ state.previousCapabilityExpiresAt = preservePreviousCapability
5289
+ ? Math.min(state.capabilityExpiresAt, now + FRAME_PREVIOUS_CAPABILITY_GRACE_MS)
5290
+ : 0;
5291
+ state.capability = capability;
5292
+ state.sessionID = sessionID;
5293
+ state.capabilityExpiresAt = now + FRAME_CAPABILITY_TTL_MS;
5294
+ return true;
5295
+ }
5296
+ crossDomainFrameNeedsRotation(state, sessionID, now = Date.now()) {
5297
+ return (state.sessionID !== sessionID ||
5298
+ now >= state.capabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS);
5299
+ }
5300
+ postToCrossDomainFrame(state, line, extra = {}) {
5301
+ state.source.postMessage({
5302
+ line,
5303
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5304
+ context: state.context,
5305
+ nonce: state.nonce,
5306
+ capability: state.capability,
5307
+ ...extra,
5308
+ }, state.origin);
5309
+ }
5310
+ consumeCrossDomainEnvelopeBudget(source) {
5311
+ const now = Date.now();
5312
+ let budget = this.crossDomainEnvelopeBudgets.get(source);
5313
+ if (budget === undefined || now - budget.windowStartedAt >= FRAME_RATE_WINDOW_MS) {
5314
+ budget = {
5315
+ windowStartedAt: now,
5316
+ envelopesInWindow: 0,
5317
+ };
5318
+ this.crossDomainEnvelopeBudgets.set(source, budget);
5319
+ }
5320
+ if (budget.envelopesInWindow >= MAX_FRAME_ENVELOPES_PER_WINDOW) {
5321
+ return false;
5322
+ }
5323
+ budget.envelopesInWindow += 1;
5324
+ return true;
5325
+ }
5326
+ async establishCrossDomainFrame(event, handshake) {
5327
+ if (!this.active() ||
5328
+ event.source === null ||
5329
+ !this.crossDomainChildOrigins.has(event.origin)) {
5330
+ return;
5331
+ }
5332
+ const candidateSource = event.source;
5333
+ if (!this.consumeCrossDomainEnvelopeBudget(candidateSource)) {
5334
+ return;
5335
+ }
5336
+ const frame = this.findCrossDomainFrame(candidateSource);
5337
+ if (frame === null || !frame.isConnected || frame.contentWindow === null) {
5338
+ return;
5339
+ }
5340
+ const source = frame.contentWindow;
5341
+ const existing = this.crossDomainFrames.get(source);
5342
+ if (this.pendingCrossDomainFrames.has(source)) {
5343
+ return;
5344
+ }
5345
+ const existingMatches = existing !== undefined &&
5346
+ existing.origin === event.origin &&
5347
+ existing.context === handshake.context &&
5348
+ existing.nonce === handshake.nonce &&
5349
+ existing.frame === frame;
5350
+ if (existing !== undefined && !existingMatches) {
5351
+ return;
5352
+ }
5353
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
5354
+ const pendingToken = Symbol();
5355
+ this.pendingCrossDomainFrames.set(source, pendingToken);
5356
+ let navigated = false;
5357
+ const markNavigated = () => {
5358
+ navigated = true;
5359
+ };
5360
+ frame.addEventListener('load', markNavigated, { once: true });
5361
+ try {
5362
+ const id = await this.checkNodeId(source);
5363
+ if (id === null ||
5364
+ navigated ||
5365
+ !this.active() ||
5366
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
5367
+ !frame.isConnected ||
5368
+ frame.contentWindow !== source) {
5369
+ return;
5370
+ }
5371
+ const session = this.currentCrossDomainSession();
5372
+ if (session === null) {
5373
+ return;
5374
+ }
5375
+ const now = Date.now();
5376
+ let state;
5377
+ if (existingMatches) {
5378
+ state = existing;
5379
+ if (this.crossDomainFrameNeedsRotation(state, session.sessionID, now) &&
5380
+ !this.rotateCrossDomainFrameCapability(state, session.sessionID, now)) {
5381
+ return;
5382
+ }
5383
+ state.lastSeenAt = now;
5384
+ }
5385
+ else {
5386
+ const capability = createCrossDomainSecret();
5387
+ if (capability === null) {
5388
+ return;
5389
+ }
5390
+ state = {
5391
+ source,
5392
+ frame,
5393
+ origin: event.origin,
5394
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5395
+ context: handshake.context,
5396
+ nonce: handshake.nonce,
5397
+ capability,
5398
+ frameOrderNumber: this.nextCrossDomainFrameOrder++,
5399
+ sessionID: session.sessionID,
5400
+ capabilityExpiresAt: now + FRAME_CAPABILITY_TTL_MS,
5401
+ previousCapability: null,
5402
+ previousCapabilityExpiresAt: 0,
5403
+ lastSeenAt: now,
5404
+ rateWindowStartedAt: now,
5405
+ messagesInRateWindow: 0,
5406
+ bytesInRateWindow: 0,
5407
+ };
5408
+ }
5409
+ const payload = this.frameInitPayload(state, id);
5410
+ if (payload === null) {
5411
+ return;
5412
+ }
5413
+ if (!existingMatches) {
5414
+ if (existing !== undefined) {
5415
+ this.revokeCrossDomainFrame(source, existing);
5416
+ }
5417
+ this.crossDomainFrames.set(source, state);
5418
+ frame.addEventListener('load', () => {
5419
+ this.revokeCrossDomainFrame(source, state);
5420
+ }, { once: true });
5421
+ }
5422
+ this.postToCrossDomainFrame(state, proto.iframeId, payload);
5423
+ }
5424
+ finally {
5425
+ frame.removeEventListener('load', markNavigated);
5426
+ if (this.pendingCrossDomainFrames.get(source) === pendingToken) {
5427
+ this.pendingCrossDomainFrames.delete(source);
5428
+ }
5429
+ }
5430
+ }
5431
+ validatedCrossDomainFrame(event, expectedLine) {
5432
+ if (event.source === null ||
5433
+ !this.crossDomainChildOrigins.has(event.origin) ||
5434
+ event.source === window) {
5435
+ return null;
5436
+ }
5437
+ const source = event.source;
5438
+ const state = this.crossDomainFrames.get(source);
5439
+ const now = Date.now();
5440
+ const currentSessionID = this.session.getInfo().sessionID;
5441
+ if (!state ||
5442
+ state.origin !== event.origin ||
5443
+ !state.frame.isConnected ||
5444
+ state.frame.contentWindow !== source) {
5445
+ if (state) {
5446
+ this.revokeCrossDomainFrame(source);
5447
+ }
5448
+ return null;
5449
+ }
5450
+ if (currentSessionID === undefined || state.sessionID !== currentSessionID) {
5451
+ this.revokeCrossDomainFrame(source);
5452
+ return null;
5453
+ }
5454
+ const matchesCurrentCapability = now < state.capabilityExpiresAt && matchesCrossDomainEnvelope(event.data, expectedLine, state);
5455
+ const matchesPreviousCapability = state.previousCapability !== null &&
5456
+ now < state.previousCapabilityExpiresAt &&
5457
+ matchesCrossDomainEnvelope(event.data, expectedLine, {
5458
+ protocolVersion: state.protocolVersion,
5459
+ context: state.context,
5460
+ nonce: state.nonce,
5461
+ capability: state.previousCapability,
5462
+ });
5463
+ if (!matchesCurrentCapability && !matchesPreviousCapability) {
5464
+ if (now >= state.capabilityExpiresAt && now >= state.previousCapabilityExpiresAt) {
5465
+ this.revokeCrossDomainFrame(source);
5466
+ }
5467
+ return null;
5468
+ }
5469
+ if ((matchesCurrentCapability && state.previousCapability !== null) ||
5470
+ now >= state.previousCapabilityExpiresAt) {
5471
+ state.previousCapability = null;
5472
+ state.previousCapabilityExpiresAt = 0;
5473
+ }
5474
+ state.lastSeenAt = now;
5475
+ return state;
5476
+ }
5477
+ consumeCrossDomainFrameBudget(state, messageCount, bytes) {
5478
+ const now = Date.now();
5479
+ if (now - state.rateWindowStartedAt >= FRAME_RATE_WINDOW_MS) {
5480
+ state.rateWindowStartedAt = now;
5481
+ state.messagesInRateWindow = 0;
5482
+ state.bytesInRateWindow = 0;
5483
+ }
5484
+ if (state.messagesInRateWindow + messageCount > MAX_FRAME_MESSAGES_PER_WINDOW ||
5485
+ state.bytesInRateWindow + bytes > MAX_FRAME_BYTES_PER_WINDOW) {
5486
+ return false;
5487
+ }
5488
+ state.messagesInRateWindow += messageCount;
5489
+ state.bytesInRateWindow += bytes;
5490
+ return true;
5491
+ }
4942
5492
  allowAppStart() {
4943
5493
  this.canStart = true;
4944
5494
  if (this.startTimeout) {
@@ -4998,6 +5548,7 @@ class App {
4998
5548
  setTimeout(() => {
4999
5549
  closing = false;
5000
5550
  }, 500);
5551
+ this.canvasRecorder?.flushPendingSnapshots();
5001
5552
  if (this.worker) {
5002
5553
  this.worker.postMessage('closing');
5003
5554
  }
@@ -5038,22 +5589,39 @@ class App {
5038
5589
  }
5039
5590
  else if (data.type === 'compress') {
5040
5591
  const batch = data.batch;
5592
+ const dataType = data.dataType;
5041
5593
  const batchSize = batch.byteLength;
5042
- if (batchSize > this.compressionThreshold) {
5043
- gzip(data.batch, { mtime: 0 }, (err, result) => {
5044
- if (err) {
5045
- this.debug.error('OxvoSessions compression error:', err);
5046
- this.worker?.postMessage({ type: 'uncompressed', batch: batch });
5047
- }
5048
- else {
5049
- this.worker?.postMessage({ type: 'compressed', batch: result });
5050
- }
5594
+ const hasCompressionAPI = 'CompressionStream' in globalThis;
5595
+ if (batchSize > this.compressionThreshold && hasCompressionAPI) {
5596
+ const blob = new Blob([batch]);
5597
+ const stream = blob.stream().pipeThrough(new CompressionStream('gzip'));
5598
+ new Response(stream)
5599
+ .arrayBuffer()
5600
+ .then((compressedBuffer) => {
5601
+ this.worker?.postMessage({
5602
+ type: 'compressed',
5603
+ batch: new Uint8Array(compressedBuffer),
5604
+ dataType,
5605
+ });
5606
+ })
5607
+ .catch((err) => {
5608
+ this.debug.error('OxvoSessions compression error:', err);
5609
+ this.worker?.postMessage({ type: 'uncompressed', batch: batch, dataType });
5051
5610
  });
5052
5611
  }
5053
5612
  else {
5054
- this.worker?.postMessage({ type: 'uncompressed', batch: batch });
5613
+ this.worker?.postMessage({ type: 'uncompressed', batch: batch, dataType });
5055
5614
  }
5056
5615
  }
5616
+ else if (data.type === 'local_save') {
5617
+ const blob = new Blob([data.batch], { type: 'application/octet-stream' });
5618
+ const url = URL.createObjectURL(blob);
5619
+ const a = document.createElement('a');
5620
+ a.href = url;
5621
+ a.download = data.name;
5622
+ a.click();
5623
+ URL.revokeObjectURL(url);
5624
+ }
5057
5625
  else if (data.type === 'queue_empty') {
5058
5626
  this.onSessionSent();
5059
5627
  }
@@ -5084,27 +5652,36 @@ class App {
5084
5652
  return;
5085
5653
  }
5086
5654
  if (this.insideIframe) {
5087
- window.parent.postMessage({
5088
- line: proto.iframeBatch,
5089
- messages: this.messages,
5090
- }, this.options.crossdomain?.parentDomain ?? '*');
5655
+ this.postToParent(proto.iframeBatch, { messages: this.messages });
5091
5656
  this.commitCallbacks.forEach((cb) => cb(this.messages));
5092
5657
  this.messages.length = 0;
5093
5658
  return;
5094
5659
  }
5095
- if (this.worker === undefined || !this.messages.length) {
5660
+ if (this.worker === undefined) {
5096
5661
  return;
5097
5662
  }
5098
5663
  if (!this.messages.length) {
5099
- // Release empty batches every 30 secs (1000 * 30ms)
5664
+ // Send a keepalive batch every ~30s (1000 * 30ms) to keep the session active
5100
5665
  if (this.emptyBatchCounter < 1000) {
5101
5666
  this.emptyBatchCounter++;
5102
5667
  return;
5103
5668
  }
5669
+ // Keepalive: send just Timestamp + TabData so the session stays alive
5670
+ this.emptyBatchCounter = 0;
5671
+ try {
5672
+ this.worker?.postMessage([Timestamp(this.timestamp()), TabData(this.session.getTabId())]);
5673
+ }
5674
+ catch (e) {
5675
+ this._debug('worker_keepalive', e);
5676
+ }
5677
+ return;
5104
5678
  }
5105
5679
  this.emptyBatchCounter = 0;
5106
5680
  try {
5107
5681
  requestIdleCb(() => {
5682
+ if (!this.messages.length) {
5683
+ return;
5684
+ }
5108
5685
  this.messages.unshift(Timestamp(this.timestamp()), TabData(this.session.getTabId()));
5109
5686
  this.worker?.postMessage(this.messages);
5110
5687
  this.commitCallbacks.forEach((cb) => cb(this.messages));
@@ -5231,6 +5808,159 @@ class App {
5231
5808
  },
5232
5809
  };
5233
5810
  }
5811
+ getBootstrapAttemptStorageKey(kind) {
5812
+ return `${this.options.session_token_key}_bootstrap_idempotency_${kind}`;
5813
+ }
5814
+ isValidBootstrapAttempt(value, kind, currentTime) {
5815
+ if (typeof value !== 'object' || value === null) {
5816
+ return false;
5817
+ }
5818
+ const attempt = value;
5819
+ if (attempt.version !== 1 ||
5820
+ attempt.projectKey !== this.projectKey ||
5821
+ typeof attempt.createdAt !== 'number' ||
5822
+ !Number.isFinite(attempt.createdAt) ||
5823
+ currentTime - attempt.createdAt >= BOOTSTRAP_ATTEMPT_TTL_MS ||
5824
+ typeof attempt.idempotencyKey !== 'string' ||
5825
+ attempt.idempotencyKey.length < 8 ||
5826
+ attempt.idempotencyKey.length > 128 ||
5827
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
5828
+ typeof attempt.intentFingerprint !== 'string' ||
5829
+ attempt.intentFingerprint.length === 0 ||
5830
+ attempt.intentFingerprint.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH ||
5831
+ typeof attempt.requestTimestamp !== 'number' ||
5832
+ !Number.isFinite(attempt.requestTimestamp) ||
5833
+ typeof attempt.workerTimestamp !== 'number' ||
5834
+ !Number.isFinite(attempt.workerTimestamp) ||
5835
+ typeof attempt.requestBody !== 'string' ||
5836
+ attempt.requestBody.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
5837
+ return false;
5838
+ }
5839
+ try {
5840
+ const body = JSON.parse(attempt.requestBody);
5841
+ const intent = this.parseBootstrapIntent(attempt.intentFingerprint);
5842
+ return (intent?.projectKey === this.projectKey &&
5843
+ intent.kind === kind &&
5844
+ body.app?.key === this.projectKey &&
5845
+ body.session?.dry === false &&
5846
+ body.session?.offline === (kind === 'offline') &&
5847
+ typeof body.session?.token === 'string' &&
5848
+ body.session.token === intent.token &&
5849
+ body.session?.rule === intent.rule &&
5850
+ (body.user?.id ?? '') === intent.userID &&
5851
+ (body.user?.uuid ?? '') === intent.userUUID &&
5852
+ body.meta?.ts === attempt.requestTimestamp &&
5853
+ attempt.idempotencyKey.startsWith(`bootstrap:${kind}:`));
5854
+ }
5855
+ catch {
5856
+ return false;
5857
+ }
5858
+ }
5859
+ removeStoredBootstrapAttempt(kind) {
5860
+ delete this.bootstrapAttempts[kind];
5861
+ try {
5862
+ this.sessionStorage.removeItem(this.getBootstrapAttemptStorageKey(kind));
5863
+ }
5864
+ catch {
5865
+ // The in-memory copy is still cleared when storage is unavailable.
5866
+ }
5867
+ }
5868
+ loadBootstrapAttempt(kind) {
5869
+ const currentTime = Date.now();
5870
+ const inMemoryAttempt = this.bootstrapAttempts[kind];
5871
+ if (this.isValidBootstrapAttempt(inMemoryAttempt, kind, currentTime)) {
5872
+ return inMemoryAttempt;
5873
+ }
5874
+ delete this.bootstrapAttempts[kind];
5875
+ try {
5876
+ const storedAttempt = this.sessionStorage.getItem(this.getBootstrapAttemptStorageKey(kind));
5877
+ if (storedAttempt === null || storedAttempt.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
5878
+ if (storedAttempt !== null) {
5879
+ this.removeStoredBootstrapAttempt(kind);
5880
+ }
5881
+ return undefined;
5882
+ }
5883
+ const parsedAttempt = JSON.parse(storedAttempt);
5884
+ if (this.isValidBootstrapAttempt(parsedAttempt, kind, currentTime)) {
5885
+ this.bootstrapAttempts[kind] = parsedAttempt;
5886
+ return parsedAttempt;
5887
+ }
5888
+ }
5889
+ catch {
5890
+ // Treat corrupt or inaccessible storage as an absent pending attempt.
5891
+ }
5892
+ this.removeStoredBootstrapAttempt(kind);
5893
+ return undefined;
5894
+ }
5895
+ getOrCreateBootstrapAttempt(kind, request) {
5896
+ const existingAttempt = this.loadBootstrapAttempt(kind);
5897
+ if (existingAttempt &&
5898
+ this.bootstrapIntentsMatch(existingAttempt.intentFingerprint, request.intentFingerprint)) {
5899
+ return existingAttempt;
5900
+ }
5901
+ if (existingAttempt) {
5902
+ this.removeStoredBootstrapAttempt(kind);
5903
+ }
5904
+ const attempt = {
5905
+ version: 1,
5906
+ projectKey: this.projectKey,
5907
+ createdAt: Date.now(),
5908
+ idempotencyKey: `bootstrap:${kind}:${generateRandomId(40)}`,
5909
+ ...request,
5910
+ };
5911
+ this.bootstrapAttempts[kind] = attempt;
5912
+ try {
5913
+ this.sessionStorage.setItem(this.getBootstrapAttemptStorageKey(kind), JSON.stringify(attempt));
5914
+ }
5915
+ catch {
5916
+ // The in-memory copy still protects retries within this tracker instance.
5917
+ }
5918
+ return attempt;
5919
+ }
5920
+ clearBootstrapAttempt(kind) {
5921
+ this.removeStoredBootstrapAttempt(kind);
5922
+ }
5923
+ buildBootstrapIntentFingerprint(params) {
5924
+ const tracker = this.getTrackerInfo();
5925
+ return JSON.stringify({
5926
+ projectKey: this.projectKey,
5927
+ kind: params.kind,
5928
+ token: params.token ?? '',
5929
+ rule: params.rule ?? '',
5930
+ userID: this.session.getInfo().userID ?? '',
5931
+ userUUID: tracker.userUUID ?? '',
5932
+ });
5933
+ }
5934
+ parseBootstrapIntent(value) {
5935
+ try {
5936
+ const intent = JSON.parse(value);
5937
+ if (intent.projectKey !== this.projectKey ||
5938
+ (intent.kind !== 'recording' && intent.kind !== 'offline') ||
5939
+ typeof intent.token !== 'string' ||
5940
+ typeof intent.rule !== 'string' ||
5941
+ typeof intent.userID !== 'string' ||
5942
+ typeof intent.userUUID !== 'string') {
5943
+ return undefined;
5944
+ }
5945
+ return intent;
5946
+ }
5947
+ catch {
5948
+ return undefined;
5949
+ }
5950
+ }
5951
+ bootstrapIntentsMatch(existingFingerprint, currentFingerprint) {
5952
+ const existing = this.parseBootstrapIntent(existingFingerprint);
5953
+ const current = this.parseBootstrapIntent(currentFingerprint);
5954
+ if (!existing || !current) {
5955
+ return false;
5956
+ }
5957
+ return (existing.projectKey === current.projectKey &&
5958
+ existing.kind === current.kind &&
5959
+ existing.token === current.token &&
5960
+ (current.rule === '' || existing.rule === current.rule) &&
5961
+ (current.userID === '' || existing.userID === current.userID) &&
5962
+ (current.userUUID === '' || existing.userUUID === current.userUUID));
5963
+ }
5234
5964
  getSessionMeta() {
5235
5965
  return {
5236
5966
  ...this.session.getInfo(),
@@ -5298,10 +6028,22 @@ class App {
5298
6028
  }
5299
6029
  }
5300
6030
  checkSessionToken(startNewSession) {
5301
- const lsReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
5302
- const needNewSessionID = startNewSession || lsReset;
6031
+ const needReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
6032
+ let needNewSessionID = startNewSession || needReset;
5303
6033
  const sessionToken = this.session.getSessionSecret(this.projectKey);
5304
- return needNewSessionID || !sessionToken;
6034
+ if (needReset) {
6035
+ this.sessionStorage.removeItem(this.options.session_reset_key);
6036
+ }
6037
+ const storedVersion = this.sessionStorage.getItem(`${this.options.session_token_key}_version`);
6038
+ if (!storedVersion || storedVersion !== PROTO_VERSION) {
6039
+ needNewSessionID = true;
6040
+ this.sessionStorage.setItem(`${this.options.session_token_key}_version`, PROTO_VERSION);
6041
+ }
6042
+ const isNewSession = needNewSessionID || !sessionToken;
6043
+ if (isNewSession && sessionToken) {
6044
+ this.session.clearSessionSecret();
6045
+ }
6046
+ return isNewSession;
5305
6047
  }
5306
6048
  /**
5307
6049
  * start buffering messages without starting the actual session, which gives
@@ -5373,7 +6115,7 @@ class App {
5373
6115
  })),
5374
6116
  });
5375
6117
  const { session = {}, client = {}, device = {} } = await r.json();
5376
- const { token, projectId } = session;
6118
+ const { token, projectId, assistToken } = session;
5377
6119
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5378
6120
  this.session.assign({ projectID: projectId });
5379
6121
  this.session.setUserInfo({
@@ -5386,6 +6128,7 @@ class App {
5386
6128
  });
5387
6129
  const onStartInfo = {
5388
6130
  sessionToken: token,
6131
+ assistToken: typeof assistToken === 'string' ? assistToken : undefined,
5389
6132
  userUUID: device?.id || '',
5390
6133
  sessionID: session?.id || '',
5391
6134
  };
@@ -5401,7 +6144,6 @@ class App {
5401
6144
  offlineRecording(startOpts = {}, onSessionSent) {
5402
6145
  this.onSessionSent = onSessionSent;
5403
6146
  this.singleBuffer = true;
5404
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5405
6147
  adjustTimeOrigin();
5406
6148
  this.coldStartTs = now();
5407
6149
  const saverBuffer = this.localStorage.getItem(bufferStorageKey);
@@ -5419,6 +6161,10 @@ class App {
5419
6161
  if (startOpts.startNewSession) {
5420
6162
  this.session.reset();
5421
6163
  }
6164
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6165
+ this.offlineRecordingSessionToken = isNewSession
6166
+ ? undefined
6167
+ : this.session.getSessionSecret(this.projectKey);
5422
6168
  this.session.assign({
5423
6169
  userID: startOpts.userID,
5424
6170
  metadata: startOpts.metadata,
@@ -5468,39 +6214,60 @@ class App {
5468
6214
  * */
5469
6215
  async uploadOfflineRecording() {
5470
6216
  this.stop(false);
5471
- const timestamp = now();
6217
+ const freshTimestamp = now();
6218
+ const bootstrapToken = this.offlineRecordingSessionToken ?? '';
6219
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6220
+ timestamp: freshTimestamp,
6221
+ dry: false,
6222
+ offline: true,
6223
+ bufferMs: freshTimestamp - this.coldStartTs,
6224
+ token: bootstrapToken,
6225
+ }));
6226
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('offline', {
6227
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6228
+ kind: 'offline',
6229
+ token: bootstrapToken,
6230
+ }),
6231
+ requestTimestamp: freshTimestamp,
6232
+ workerTimestamp: this.coldStartTs,
6233
+ requestBody: freshRequestBody(),
6234
+ });
5472
6235
  this.worker?.postMessage({
5473
6236
  type: 'start',
5474
6237
  pageNo: this.session.incPageNo(),
5475
6238
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5476
- timestamp: this.coldStartTs,
6239
+ timestamp: bootstrapAttempt.workerTimestamp,
5477
6240
  url: document.URL,
5478
6241
  connAttemptCount: this.options.connAttemptCount,
5479
6242
  connAttemptGap: this.options.connAttemptGap,
5480
6243
  tabId: this.session.getTabId(),
6244
+ localDebug: this.options.__local_debug,
5481
6245
  });
5482
6246
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6247
+ const headers = {
6248
+ 'Content-Type': 'application/json',
6249
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6250
+ };
5483
6251
  const r = await fetch(ingestPoint + '/v1/bootstrap', {
5484
6252
  method: 'POST',
5485
- headers: {
5486
- 'Content-Type': 'application/json',
5487
- },
5488
- body: JSON.stringify(this.buildStartPayload({
5489
- timestamp,
5490
- dry: false,
5491
- bufferMs: timestamp - this.coldStartTs,
5492
- token: '',
5493
- })),
6253
+ headers,
6254
+ body: bootstrapAttempt.requestBody,
5494
6255
  });
5495
- const { session = {}, client = {}, limits = {} } = await r.json();
6256
+ if (r.status !== 200) {
6257
+ throw new Error(`Server error: ${r.status}. ${await r.text()}`);
6258
+ }
6259
+ const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion, } = await r.json();
5496
6260
  const { token, projectId } = session;
5497
6261
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5498
6262
  const { beacon: beaconSizeLimit } = limits;
5499
- this.worker?.postMessage({
5500
- type: 'auth',
5501
- token,
5502
- beaconSizeLimit,
5503
- });
6263
+ if (typeof token !== 'string' ||
6264
+ token.length === 0 ||
6265
+ (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6266
+ throw new Error('Incorrect server response for offline recording bootstrap');
6267
+ }
6268
+ this.session.setSessionSecret(token, this.projectKey);
6269
+ this.clearBootstrapAttempt('offline');
6270
+ this.offlineRecordingSessionToken = token;
5504
6271
  this.session.assign({ projectID: projectId });
5505
6272
  this.session.setUserInfo({
5506
6273
  userBrowser,
@@ -5510,16 +6277,84 @@ class App {
5510
6277
  userOS,
5511
6278
  userState,
5512
6279
  });
6280
+ this.worker?.postMessage({
6281
+ type: 'auth',
6282
+ token,
6283
+ beaconSizeLimit,
6284
+ protocolVersion: offlineProtocolVersion,
6285
+ });
5513
6286
  while (this.bufferedMessages1.length > 0) {
5514
6287
  await this.flushBuffer(this.bufferedMessages1);
5515
6288
  }
5516
6289
  this.postToWorker([[-1]]);
5517
6290
  this.clearBuffers();
5518
6291
  }
6292
+ startCrossDomainFrame(startOpts = {}) {
6293
+ if (this.activityState === ActivityState.Active ||
6294
+ this.activityState === ActivityState.Starting) {
6295
+ const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
6296
+ return UnsuccessfulStart(reason);
6297
+ }
6298
+ if (this.crossDomainFrameSession === null ||
6299
+ this.crossDomainFrameCapability === null ||
6300
+ this.rootId === null) {
6301
+ return UnsuccessfulStart('Cross-domain iframe handshake was not established.');
6302
+ }
6303
+ if (Object.keys(startOpts).length !== 0) {
6304
+ this.prevOpts = startOpts;
6305
+ }
6306
+ if (startOpts.startCallback) {
6307
+ this.userStartCallback = startOpts.startCallback;
6308
+ }
6309
+ try {
6310
+ adjustTimeOrigin();
6311
+ this.activityState = ActivityState.Starting;
6312
+ this.delay = this.crossDomainFrameSession.delayMs;
6313
+ this.session.clearSessionSecret();
6314
+ this.session.assign({
6315
+ sessionID: this.crossDomainFrameSession.sessionID,
6316
+ projectID: this.crossDomainFrameSession.projectID,
6317
+ timestamp: this.crossDomainFrameSession.startedAt,
6318
+ });
6319
+ const onStartInfo = {
6320
+ sessionToken: '',
6321
+ userUUID: this.crossDomainFrameSession.userUUID,
6322
+ sessionID: this.crossDomainFrameSession.sessionID,
6323
+ };
6324
+ this.startCallbacks.forEach((callback) => callback(onStartInfo));
6325
+ this.userStartCallback?.(SuccessfulStart(onStartInfo));
6326
+ this.activityState = ActivityState.Active;
6327
+ if (this.crossDomainFrameCanvas?.enabled && !this.options.canvas.disableCanvas) {
6328
+ this.canvasRecorder =
6329
+ this.canvasRecorder ??
6330
+ new CanvasRecorder(this, {
6331
+ fps: this.crossDomainFrameCanvas.fps ?? 15,
6332
+ quality: this.crossDomainFrameCanvas.quality ?? 'medium',
6333
+ isDebug: this.options.canvas.__save_canvas_locally,
6334
+ fixedScaling: this.options.canvas.fixedCanvasScaling,
6335
+ useAnimationFrame: this.options.canvas.useAnimationFrame,
6336
+ framesSupport: this.crossDomainFrameCanvas.framesSupport,
6337
+ });
6338
+ }
6339
+ this.observer.crossdomainObserve(this.rootId, this.frameOderNumber, this.frameLevel);
6340
+ this.ticker.start();
6341
+ this.canvasRecorder?.startTracking();
6342
+ return SuccessfulStart(onStartInfo);
6343
+ }
6344
+ catch (reason) {
6345
+ this.stop();
6346
+ const message = reason instanceof Error ? reason.message : String(reason);
6347
+ this.signalError(message, []);
6348
+ return UnsuccessfulStart(message);
6349
+ }
6350
+ }
5519
6351
  async _start(startOpts = {}, resetByWorker = false, conditionName) {
5520
6352
  if (Object.keys(startOpts).length !== 0) {
5521
6353
  this.prevOpts = startOpts;
5522
6354
  }
6355
+ if (startOpts.startCallback) {
6356
+ this.userStartCallback = startOpts.startCallback;
6357
+ }
5523
6358
  const isColdStart = this.activityState === ActivityState.ColdStart;
5524
6359
  if (isColdStart && this.coldInterval) {
5525
6360
  clearInterval(this.coldInterval);
@@ -5551,67 +6386,89 @@ class App {
5551
6386
  userID: userId || undefined,
5552
6387
  metadata: startOpts.metadata,
5553
6388
  });
5554
- const timestamp = now();
6389
+ const sessionToken = this.session.getSessionSecret(this.projectKey);
6390
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6391
+ const freshTimestamp = now();
6392
+ const freshWorkerTimestamp = isColdStart ? this.coldStartTs : freshTimestamp;
6393
+ const bootstrapToken = isNewSession ? '' : sessionToken;
6394
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6395
+ timestamp: freshTimestamp,
6396
+ dry: false,
6397
+ bufferMs: this.coldStartTs ? freshTimestamp - this.coldStartTs : 0,
6398
+ token: bootstrapToken,
6399
+ rule: conditionName,
6400
+ width: window.screen.width,
6401
+ height: window.screen.height,
6402
+ }));
6403
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('recording', {
6404
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6405
+ kind: 'recording',
6406
+ token: bootstrapToken,
6407
+ rule: conditionName,
6408
+ }),
6409
+ requestTimestamp: freshTimestamp,
6410
+ workerTimestamp: freshWorkerTimestamp,
6411
+ requestBody: freshRequestBody(),
6412
+ });
6413
+ const timestamp = bootstrapAttempt.requestTimestamp;
5555
6414
  this.worker?.postMessage({
5556
6415
  type: 'start',
5557
6416
  pageNo: this.session.incPageNo(),
5558
6417
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5559
- timestamp: isColdStart ? this.coldStartTs : timestamp,
6418
+ timestamp: bootstrapAttempt.workerTimestamp,
5560
6419
  url: document.URL,
5561
6420
  connAttemptCount: this.options.connAttemptCount,
5562
6421
  connAttemptGap: this.options.connAttemptGap,
5563
6422
  tabId: this.session.getTabId(),
6423
+ localDebug: this.options.__local_debug,
5564
6424
  });
5565
- const sessionToken = this.session.getSessionSecret(this.projectKey);
5566
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5567
- this.sessionStorage.removeItem(this.options.session_reset_key);
5568
6425
  this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'session token: ', sessionToken);
5569
6426
  try {
5570
6427
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6428
+ const headers = {
6429
+ 'Content-Type': 'application/json',
6430
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6431
+ };
5571
6432
  const r = await window.fetch(ingestPoint + '/v1/bootstrap', {
5572
6433
  method: 'POST',
5573
- headers: {
5574
- 'Content-Type': 'application/json',
5575
- },
5576
- body: JSON.stringify(this.buildStartPayload({
5577
- timestamp,
5578
- dry: false,
5579
- bufferMs: timestamp - this.coldStartTs,
5580
- token: isNewSession ? '' : sessionToken,
5581
- rule: conditionName,
5582
- width: window.screen.width,
5583
- height: window.screen.height,
5584
- })),
6434
+ headers,
6435
+ body: bootstrapAttempt.requestBody,
5585
6436
  });
5586
6437
  if (r.status !== 200) {
5587
6438
  const error = await r.text();
5588
6439
  const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
5589
- return UnsuccessfulStart(reason);
6440
+ throw reason;
5590
6441
  }
5591
6442
  if (!this.worker && !this.insideIframe) {
5592
6443
  const reason = 'no worker found after start request (this should not happen in real world)';
5593
- this.signalError(reason, []);
5594
- return UnsuccessfulStart(reason);
6444
+ throw new Error(reason);
5595
6445
  }
5596
- const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {} } = await r.json();
5597
- const { token, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
6446
+ const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {}, protocolVersion, } = await r.json();
6447
+ const { token, assistToken, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
5598
6448
  const { id: userUUID } = device;
5599
6449
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5600
6450
  const { beacon: beaconSizeLimit, compressAt: compressionThreshold } = limits;
5601
- const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS } = canvas;
6451
+ const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS, framesSupport, } = canvas;
5602
6452
  const socketOnly = flags?.socketOnly;
5603
6453
  if (typeof token !== 'string' ||
6454
+ (typeof assistToken !== 'string' && typeof assistToken !== 'undefined') ||
5604
6455
  typeof userUUID !== 'string' ||
5605
6456
  (typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined') ||
5606
6457
  typeof sessionID !== 'string' ||
5607
6458
  typeof delay !== 'number' ||
5608
6459
  (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
5609
6460
  const reason = `Incorrect server response: ${JSON.stringify(r)}`;
5610
- this.signalError(reason, []);
5611
- return UnsuccessfulStart(reason);
6461
+ throw new Error(reason);
5612
6462
  }
6463
+ this.crossDomainCanvasConfig = {
6464
+ enabled: canvasEnabled === true,
6465
+ ...(['low', 'medium', 'high'].includes(canvasQuality) ? { quality: canvasQuality } : {}),
6466
+ ...(typeof canvasFPS === 'number' ? { fps: canvasFPS } : {}),
6467
+ ...(typeof framesSupport === 'boolean' ? { framesSupport } : {}),
6468
+ };
5613
6469
  this.delay = delay;
5614
6470
  this.session.setSessionSecret(token, this.projectKey);
6471
+ this.clearBootstrapAttempt('recording');
5615
6472
  if (sessionToken && sessionToken !== token) {
5616
6473
  this.bc?.postMessage({
5617
6474
  type: proto.reset,
@@ -5640,6 +6497,7 @@ class App {
5640
6497
  type: 'auth',
5641
6498
  token,
5642
6499
  beaconSizeLimit,
6500
+ protocolVersion,
5643
6501
  });
5644
6502
  }
5645
6503
  if (!isNewSession && token === sessionToken) {
@@ -5651,14 +6509,13 @@ class App {
5651
6509
  Object.entries(this.session.getInfo().metadata).forEach(([key, value]) => this.send(Metadata(key, value)));
5652
6510
  this.localStorage.setItem(this.options.local_uuid_key, userUUID);
5653
6511
  this.compressionThreshold = compressionThreshold;
5654
- const onStartInfo = { sessionToken: token, userUUID, sessionID };
6512
+ const onStartInfo = { sessionToken: token, assistToken, userUUID, sessionID };
5655
6513
  // TODO: start as early as possible (before receiving the token)
5656
6514
  /** after start */
5657
6515
  this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
5658
- if (startOpts.startCallback) {
5659
- startOpts.startCallback(SuccessfulStart(onStartInfo));
6516
+ if (this.userStartCallback) {
6517
+ this.userStartCallback(SuccessfulStart(onStartInfo));
5660
6518
  }
5661
- await this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
5662
6519
  this.activityState = ActivityState.Active;
5663
6520
  if (this.options.crossdomain?.enabled) {
5664
6521
  void this.bootChildrenFrames();
@@ -5672,6 +6529,7 @@ class App {
5672
6529
  isDebug: this.options.canvas.__save_canvas_locally,
5673
6530
  fixedScaling: this.options.canvas.fixedCanvasScaling,
5674
6531
  useAnimationFrame: this.options.canvas.useAnimationFrame,
6532
+ framesSupport: !!framesSupport,
5675
6533
  });
5676
6534
  }
5677
6535
  /** --------------- COLD START BUFFER ------------------*/
@@ -5694,11 +6552,11 @@ class App {
5694
6552
  }
5695
6553
  this.ticker.start();
5696
6554
  this.canvasRecorder?.startTracking();
6555
+ void this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
5697
6556
  return SuccessfulStart(onStartInfo);
5698
6557
  }
5699
6558
  catch (reason) {
5700
6559
  this.stop();
5701
- this.session.reset();
5702
6560
  if (!reason) {
5703
6561
  console.error('Unknown error during start');
5704
6562
  this.signalError('Unknown error', []);
@@ -5724,6 +6582,23 @@ class App {
5724
6582
  }, 100);
5725
6583
  });
5726
6584
  }
6585
+ async waitCrossDomainStart(timeoutMs = 15000) {
6586
+ if (this.canStart)
6587
+ return true;
6588
+ return new Promise((resolve) => {
6589
+ const interval = setInterval(() => {
6590
+ if (!this.canStart)
6591
+ return;
6592
+ clearInterval(interval);
6593
+ clearTimeout(timeout);
6594
+ resolve(true);
6595
+ }, 100);
6596
+ const timeout = setTimeout(() => {
6597
+ clearInterval(interval);
6598
+ resolve(false);
6599
+ }, timeoutMs);
6600
+ });
6601
+ }
5727
6602
  async waitStarted() {
5728
6603
  return this.waitStatus(ActivityState.Active);
5729
6604
  }
@@ -5751,20 +6626,35 @@ class App {
5751
6626
  return Promise.resolve(UnsuccessfulStart(reason));
5752
6627
  }
5753
6628
  if (this.insideIframe) {
6629
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
6630
+ return UnsuccessfulStart('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
6631
+ }
5754
6632
  this.signalIframeTracker();
5755
6633
  }
5756
- if (!document.hidden) {
6634
+ const startWhenReady = async () => {
6635
+ if (this.insideIframe) {
6636
+ const ready = await this.waitCrossDomainStart();
6637
+ if (!ready) {
6638
+ this.crossDomainConnectionRequested = false;
6639
+ this.crossDomainResumeAfterHandshake = false;
6640
+ this.clearCrossDomainFrameAuthorization();
6641
+ return UnsuccessfulStart('Cross-domain iframe handshake timed out.');
6642
+ }
6643
+ return this.startCrossDomainFrame(args[0]);
6644
+ }
5757
6645
  await this.waitStart();
5758
6646
  return this._start(...args);
6647
+ };
6648
+ if (!document.hidden) {
6649
+ return startWhenReady();
5759
6650
  }
5760
6651
  else {
5761
6652
  return new Promise((resolve) => {
5762
6653
  const onVisibilityChange = async () => {
5763
6654
  if (!document.hidden) {
5764
- await this.waitStart();
5765
6655
  // eslint-disable-next-line
5766
6656
  document.removeEventListener('visibilitychange', onVisibilityChange);
5767
- resolve(this._start(...args));
6657
+ resolve(startWhenReady());
5768
6658
  }
5769
6659
  };
5770
6660
  // eslint-disable-next-line
@@ -5818,12 +6708,18 @@ class App {
5818
6708
  this.canvasRecorder?.clear();
5819
6709
  this.messages.length = 0;
5820
6710
  this.parentActive = false;
6711
+ this.pageFrames = [];
5821
6712
  }
5822
6713
  finally {
5823
6714
  this.activityState = ActivityState.NotActive;
5824
6715
  this.debug.log('OxvoSessions tracking stopped.');
5825
6716
  }
5826
6717
  }
6718
+ if (this.insideIframe) {
6719
+ this.crossDomainConnectionRequested = false;
6720
+ this.crossDomainResumeAfterHandshake = false;
6721
+ this.clearCrossDomainFrameAuthorization();
6722
+ }
5827
6723
  }
5828
6724
  }
5829
6725
 
@@ -6315,6 +7211,12 @@ function Img (app) {
6315
7211
  sendImgAttrs(node);
6316
7212
  observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] });
6317
7213
  });
7214
+ // On a runtime level change, re-evaluate placeholder vs real src for this image.
7215
+ app.attachResanitizeCallback((node) => {
7216
+ if (hasTag(node, 'img')) {
7217
+ sendImgAttrs(node);
7218
+ }
7219
+ });
6318
7220
  }
6319
7221
 
6320
7222
  const INPUT_TYPES = [
@@ -6372,19 +7274,24 @@ const labelElementFor = IN_BROWSER && 'labels' in HTMLInputElement.prototype
6372
7274
  }
6373
7275
  }
6374
7276
  };
6375
- function getInputLabel(node) {
6376
- let label = getLabelAttribute(node);
6377
- if (label === null) {
6378
- const labelElement = labelElementFor(node);
6379
- label =
6380
- (labelElement && labelElement.innerText) ||
6381
- node.placeholder ||
6382
- node.name ||
6383
- node.id ||
6384
- node.className ||
6385
- node.type;
6386
- }
6387
- return normSpaces(label).slice(0, 100);
7277
+ function getInputLabel(node, customAttributes) {
7278
+ const oxvosessionsLabel = getLabelAttribute(node);
7279
+ if (oxvosessionsLabel !== null)
7280
+ return normSpaces(oxvosessionsLabel).slice(0, 100);
7281
+ const customAttributeLabel = getCustomAttributeLabel(node, customAttributes);
7282
+ if (customAttributeLabel)
7283
+ return customAttributeLabel;
7284
+ if (node.id)
7285
+ return `#${node.id}`;
7286
+ const classLabel = getClassSelector(node);
7287
+ if (classLabel)
7288
+ return classLabel;
7289
+ const labelElement = labelElementFor(node);
7290
+ const label = node.name ||
7291
+ node.placeholder ||
7292
+ (labelElement && labelElement.innerText) ||
7293
+ node.type;
7294
+ return normSpaces(label || '').slice(0, 100);
6388
7295
  }
6389
7296
  const InputMode = {
6390
7297
  Plain: 0,
@@ -6406,9 +7313,12 @@ function Input (app, opts) {
6406
7313
  }
6407
7314
  else if (app.sanitizer.isObscured(id) ||
6408
7315
  (inputMode === InputMode.Plain &&
6409
- ((options.maskInputNumbers && node.type !== 'date' && /\d\d\d\d/.test(value)) ||
7316
+ ((options.maskInputNumbers &&
7317
+ node.type !== 'date' &&
7318
+ /\d\d\d\d/.test(value)) ||
6410
7319
  (options.maskInputDates && node.type === 'date') ||
6411
- (options.maskInputEmails && (node.type === 'email' || !!~value.indexOf('@')))))) {
7320
+ (options.maskInputEmails &&
7321
+ (node.type === 'email' || !!~value.indexOf('@')))))) {
6412
7322
  inputMode = InputMode.Obscured;
6413
7323
  }
6414
7324
  let mask = 0;
@@ -6428,11 +7338,24 @@ function Input (app, opts) {
6428
7338
  const { value, mask } = getInputValue(id, node);
6429
7339
  app.send(SetInputValue(id, value, mask));
6430
7340
  }
7341
+ const tagSelectorMap = new Map();
6431
7342
  const inputValues = new Map();
6432
7343
  const checkboxValues = new Map();
7344
+ function getTagSelector(id, node) {
7345
+ const cached = tagSelectorMap.get(id);
7346
+ if (cached !== undefined)
7347
+ return cached;
7348
+ const tagMatch = app.tagMatcher.match(node);
7349
+ if (tagMatch) {
7350
+ tagSelectorMap.set(id, tagMatch.selector);
7351
+ return tagMatch.selector;
7352
+ }
7353
+ return null;
7354
+ }
6433
7355
  app.attachStopCallback(() => {
6434
7356
  inputValues.clear();
6435
7357
  checkboxValues.clear();
7358
+ tagSelectorMap.clear();
6436
7359
  });
6437
7360
  function trackInputValue(id, node) {
6438
7361
  if (inputValues.get(id) === node.value) {
@@ -6465,12 +7388,19 @@ function Input (app, opts) {
6465
7388
  }, 3);
6466
7389
  function sendInputChange(id, node, hesitationTime, inputTime) {
6467
7390
  const { value, mask } = getInputValue(id, node);
6468
- let label = getInputLabel(node);
7391
+ let label = getTagSelector(id, node) || getInputLabel(node, options.customAttributes);
6469
7392
  if (app.sanitizer.maskAllByDefault) {
6470
7393
  label = label.replaceAll(/./g, '*');
6471
7394
  }
6472
7395
  app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime));
6473
7396
  }
7397
+ // Re-emit a field's value when its sanitization level changes at runtime.
7398
+ // getInputValue() reads the current level, so re-sending applies the new mask.
7399
+ app.attachResanitizeCallback((node, id) => {
7400
+ if (isTextFieldElement(node) || hasTag(node, 'select')) {
7401
+ sendInputValue(id, node);
7402
+ }
7403
+ });
6474
7404
  app.nodes.attachNodeCallback(app.safe((node) => {
6475
7405
  const id = app.nodes.getID(node);
6476
7406
  if (id === undefined) {
@@ -6574,21 +7504,28 @@ function _getTarget(target, document) {
6574
7504
  return target === document.documentElement ? null : target;
6575
7505
  }
6576
7506
  function Mouse (app, options) {
6577
- const { disableClickmaps = false } = options || {};
7507
+ const { disableClickmaps = false, customAttributes } = options || {};
6578
7508
  function getTargetLabel(target) {
6579
7509
  const dl = getLabelAttribute(target);
6580
7510
  if (dl !== null) {
6581
7511
  return dl;
6582
7512
  }
6583
7513
  if (hasTag(target, 'input')) {
6584
- return getInputLabel(target);
6585
- }
7514
+ return getInputLabel(target, customAttributes);
7515
+ }
7516
+ const customAttributeLabel = getCustomAttributeLabel(target, customAttributes);
7517
+ if (customAttributeLabel)
7518
+ return customAttributeLabel;
7519
+ if (target.id)
7520
+ return `#${target.id}`;
7521
+ const classLabel = getClassSelector(target);
7522
+ if (classLabel)
7523
+ return classLabel;
6586
7524
  if (isClickable(target)) {
6587
7525
  let label = '';
6588
7526
  if (target instanceof HTMLElement) {
6589
7527
  label = app.sanitizer.getInnerTextSecure(target);
6590
7528
  }
6591
- label = label || target.id || target.className;
6592
7529
  return normSpaces(label).slice(0, 100);
6593
7530
  }
6594
7531
  return '';
@@ -6641,7 +7578,13 @@ function Mouse (app, options) {
6641
7578
  };
6642
7579
  const patchDocument = (document, topframe = false) => {
6643
7580
  function getSelector(id, target) {
6644
- return (selectorMap[id] = selectorMap[id] || _getSelector(target));
7581
+ if (selectorMap[id])
7582
+ return selectorMap[id];
7583
+ const tagMatch = app.tagMatcher.match(target);
7584
+ if (tagMatch) {
7585
+ return (selectorMap[id] = tagMatch.selector);
7586
+ }
7587
+ return (selectorMap[id] = _getSelector(target));
6645
7588
  }
6646
7589
  const attachListener = topframe
6647
7590
  ? app.attachEventListener.bind(app) // attached/removed on start/stop
@@ -6732,6 +7675,11 @@ function getCSSPath(el) {
6732
7675
  return false;
6733
7676
  if (el.id)
6734
7677
  return `#${cssEscape(el.id)}`;
7678
+ // if has data attributes - use them as they are more likely to be stable and unique
7679
+ const dataAttr = Array.from(el.attributes).find(attr => attr.name.startsWith('data-'));
7680
+ if (dataAttr) {
7681
+ return `[${dataAttr.name}="${cssEscape(dataAttr.value)}"]`;
7682
+ }
6735
7683
  const parts = [];
6736
7684
  while (el && el.nodeType === 1 && el !== el.ownerDocument) {
6737
7685
  if (el.id) {
@@ -7072,11 +8020,27 @@ function Scroll (app, insideIframe) {
7072
8020
  }, 5, false);
7073
8021
  }
7074
8022
 
8023
+ const NativeURL = URL;
8024
+ const defaultUrlSanitizer = (url) => {
8025
+ const hiddenQueryParams = ['jwt', 'password', 'reset-password', 'invitation', 'secret', 'token'];
8026
+ try {
8027
+ const u = new NativeURL(url);
8028
+ u.searchParams.forEach((value, key) => {
8029
+ if (hiddenQueryParams.includes(key.toLowerCase())) {
8030
+ u.searchParams.set(key, '*'.repeat(value.length));
8031
+ }
8032
+ });
8033
+ return u.toString();
8034
+ }
8035
+ catch {
8036
+ return url;
8037
+ }
8038
+ };
7075
8039
  function Viewport (app, options) {
7076
8040
  let url, width, height;
7077
8041
  let navigationStart;
7078
8042
  let referrer = document.referrer;
7079
- const urlSanitizer = options?.urlSanitizer || ((u) => u);
8043
+ const urlSanitizer = options?.urlSanitizer || defaultUrlSanitizer;
7080
8044
  const titleSanitizer = options?.titleSanitizer || ((t) => t);
7081
8045
  const sendSetPageLocation = app.safe(() => {
7082
8046
  const currURL = document.URL;
@@ -8902,6 +9866,8 @@ function webAnimations(app, options = {}) {
8902
9866
  const lastKF = anim.effect.getKeyframes().at(-1);
8903
9867
  if (!lastKF)
8904
9868
  return;
9869
+ if (!el || !(el instanceof Element) || !el.isConnected)
9870
+ return;
8905
9871
  const computedStyle = getComputedStyle(el);
8906
9872
  const keys = Object.keys(lastKF).filter((p) => !toIgnore.includes(p));
8907
9873
  // @ts-ignore
@@ -9313,7 +10279,7 @@ class ConstantProperties {
9313
10279
  user_id: this.user_id,
9314
10280
  distinct_id: this.deviceId,
9315
10281
  sdk_edition: 'web',
9316
- sdk_version: '7.1.0',
10282
+ sdk_version: '7.3.4',
9317
10283
  timezone: getUTCOffsetString(),
9318
10284
  search_engine: this.searchEngine,
9319
10285
  };
@@ -9701,10 +10667,11 @@ class People {
9701
10667
  * Creates batches of events, then sends them at intervals.
9702
10668
  */
9703
10669
  class Batcher {
9704
- constructor(backendUrl, getToken, init) {
10670
+ constructor(backendUrl, getToken, init, standalone) {
9705
10671
  this.backendUrl = backendUrl;
9706
10672
  this.getToken = getToken;
9707
10673
  this.init = init;
10674
+ this.standalone = standalone;
9708
10675
  this.autosendInterval = 5 * 1000;
9709
10676
  this.retryTimeout = 3 * 1000;
9710
10677
  this.retryLimit = 3;
@@ -9714,10 +10681,20 @@ class Batcher {
9714
10681
  [categories.events]: [],
9715
10682
  };
9716
10683
  this.intervalId = null;
10684
+ this.stopped = false;
10685
+ this.paused = false;
10686
+ this.onVisibilityChange = () => {
10687
+ this.paused = document.hidden;
10688
+ };
9717
10689
  }
9718
10690
  getBatches() {
9719
10691
  this.batch[categories.people] = this.dedupePeopleEvents();
9720
- const finalData = { data: this.batch };
10692
+ const finalData = {
10693
+ data: {
10694
+ [categories.people]: [...this.batch[categories.people]],
10695
+ [categories.events]: [...this.batch[categories.events]],
10696
+ },
10697
+ };
9721
10698
  return finalData;
9722
10699
  }
9723
10700
  addEvent(event) {
@@ -9793,6 +10770,9 @@ class Batcher {
9793
10770
  return Array.from(uniqueEventsByType.values());
9794
10771
  }
9795
10772
  sendBatch(batch) {
10773
+ if (this.stopped) {
10774
+ return;
10775
+ }
9796
10776
  const sentBatch = batch;
9797
10777
  let attempts = 0;
9798
10778
  const send = () => {
@@ -9809,11 +10789,27 @@ class Batcher {
9809
10789
  },
9810
10790
  body: JSON.stringify(sentBatch),
9811
10791
  })
9812
- .then((response) => {
10792
+ .then(async (response) => {
10793
+ if (response.status === 401) {
10794
+ const body = await response.json().catch(() => null);
10795
+ if (!this.standalone && body?.error === 'token expired') {
10796
+ this.stop();
10797
+ return;
10798
+ }
10799
+ if (attempts < this.retryLimit) {
10800
+ return this.init().then(() => {
10801
+ send();
10802
+ });
10803
+ }
10804
+ return;
10805
+ }
9813
10806
  if (response.status === 403) {
9814
- this.init().then(() => {
9815
- send();
9816
- });
10807
+ if (attempts < this.retryLimit) {
10808
+ return this.init().then(() => {
10809
+ send();
10810
+ });
10811
+ }
10812
+ return;
9817
10813
  }
9818
10814
  if (!response.ok) {
9819
10815
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -9828,8 +10824,15 @@ class Batcher {
9828
10824
  void send();
9829
10825
  }
9830
10826
  startAutosend() {
10827
+ if (this.intervalId) {
10828
+ clearInterval(this.intervalId);
10829
+ }
10830
+ this.paused = document.hidden;
10831
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
9831
10832
  this.intervalId = setInterval(() => {
9832
- this.flush();
10833
+ if (!this.paused) {
10834
+ this.flush();
10835
+ }
9833
10836
  }, this.autosendInterval);
9834
10837
  }
9835
10838
  flush() {
@@ -9849,10 +10852,18 @@ class Batcher {
9849
10852
  clearInterval(this.intervalId);
9850
10853
  this.intervalId = null;
9851
10854
  }
10855
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
10856
+ this.stopped = true;
10857
+ }
10858
+ restart() {
10859
+ this.stopped = false;
10860
+ this.startAutosend();
9852
10861
  }
9853
10862
  }
9854
10863
 
9855
10864
  const STORAGEKEY = '__or_sdk_analytics_token';
10865
+ const START_ATTEMPT_STORAGE_KEY = '__or_sdk_analytics_start_attempt';
10866
+ const START_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
9856
10867
  const normalizeIngestPoint = (ingest) => {
9857
10868
  const base = ingest.replace(/\/$/, '');
9858
10869
  return base.endsWith('/oxvo') ? base : `${base}/oxvo`;
@@ -9876,11 +10887,14 @@ class Analytics {
9876
10887
  constructor(options) {
9877
10888
  this.token = null;
9878
10889
  this.standalone = false;
10890
+ this.pendingStartAttempt = null;
9879
10891
  this._getToken = () => {
9880
10892
  if (this.standalone) {
9881
10893
  return this.token;
9882
10894
  }
9883
- return this.getToken();
10895
+ else {
10896
+ return this.getToken();
10897
+ }
9884
10898
  };
9885
10899
  this._getTimestamp = () => {
9886
10900
  if (this.standalone) {
@@ -9917,9 +10931,14 @@ class Analytics {
9917
10931
  timezone: defaultFields.timezone,
9918
10932
  search_engine: defaultFields.search_engine,
9919
10933
  };
10934
+ const attempt = this.startAttempt(JSON.stringify(data));
9920
10935
  const resp = await fetch(apiEdp, {
9921
10936
  method: 'POST',
9922
- body: JSON.stringify(data),
10937
+ headers: {
10938
+ 'Content-Type': 'application/json',
10939
+ 'Idempotency-Key': attempt.idempotencyKey,
10940
+ },
10941
+ body: attempt.requestBody,
9923
10942
  });
9924
10943
  if (!resp.ok) {
9925
10944
  throw new Error(`HTTP error! status: ${resp.status}`);
@@ -9928,12 +10947,26 @@ class Analytics {
9928
10947
  if (result.token) {
9929
10948
  this.token = result.token;
9930
10949
  this.sessionStorage.setItem(STORAGEKEY, result.token);
10950
+ this.clearStartAttempt();
9931
10951
  }
9932
10952
  else {
9933
10953
  throw new Error('No token received from server');
9934
10954
  }
9935
10955
  }
9936
10956
  };
10957
+ /**
10958
+ * Used by tracker when running in blundled mode
10959
+ */
10960
+ this.onStart = () => {
10961
+ if (!this.standalone) {
10962
+ this.batcher.restart();
10963
+ }
10964
+ };
10965
+ this.onStop = () => {
10966
+ if (!this.standalone) {
10967
+ this.batcher.stop();
10968
+ }
10969
+ };
9937
10970
  this.reset = () => {
9938
10971
  this.people.reset(true);
9939
10972
  this.events.reset();
@@ -9941,6 +10974,7 @@ class Analytics {
9941
10974
  if (this.standalone) {
9942
10975
  this.token = null;
9943
10976
  this.sessionStorage.setItem(STORAGEKEY, '');
10977
+ this.clearStartAttempt();
9944
10978
  }
9945
10979
  };
9946
10980
  /**
@@ -9972,13 +11006,98 @@ class Analytics {
9972
11006
  this.standalone = !options.notStandalone;
9973
11007
  this.token = this.sessionStorage.getItem(STORAGEKEY);
9974
11008
  this.constantProperties = new ConstantProperties(this.localStorage, this.sessionStorage);
9975
- this.batcher = new Batcher(this.backendUrl, this._getToken, this.init);
11009
+ this.batcher = new Batcher(this.backendUrl, this._getToken, this.init, this.standalone);
9976
11010
  this.events = new Events(this.constantProperties, this._getTimestamp, this.batcher);
9977
11011
  this.people = new People(this.constantProperties, this._getTimestamp, this.setUserId, this.batcher);
9978
11012
  if (options.notStandalone) {
9979
11013
  this.init();
9980
11014
  }
9981
11015
  }
11016
+ loadStartAttempt() {
11017
+ if (this.isValidStartAttempt(this.pendingStartAttempt)) {
11018
+ return this.pendingStartAttempt;
11019
+ }
11020
+ this.pendingStartAttempt = null;
11021
+ let serialized;
11022
+ try {
11023
+ serialized = this.sessionStorage.getItem(START_ATTEMPT_STORAGE_KEY);
11024
+ }
11025
+ catch {
11026
+ return null;
11027
+ }
11028
+ if (!serialized || serialized.length > 1024 * 1024) {
11029
+ return null;
11030
+ }
11031
+ try {
11032
+ const attempt = JSON.parse(serialized);
11033
+ if (this.isValidStartAttempt(attempt)) {
11034
+ this.pendingStartAttempt = attempt;
11035
+ return attempt;
11036
+ }
11037
+ }
11038
+ catch {
11039
+ // Treat corrupt storage as an absent pending attempt.
11040
+ }
11041
+ return null;
11042
+ }
11043
+ isValidStartAttempt(value) {
11044
+ if (typeof value !== 'object' || value === null) {
11045
+ return false;
11046
+ }
11047
+ const attempt = value;
11048
+ const currentTime = Date.now();
11049
+ if (attempt.version !== 1 ||
11050
+ attempt.projectKey !== this.projectKey ||
11051
+ typeof attempt.createdAt !== 'number' ||
11052
+ !Number.isFinite(attempt.createdAt) ||
11053
+ attempt.createdAt > currentTime ||
11054
+ currentTime - attempt.createdAt >= START_ATTEMPT_TTL_MS ||
11055
+ typeof attempt.idempotencyKey !== 'string' ||
11056
+ attempt.idempotencyKey.length < 8 ||
11057
+ attempt.idempotencyKey.length > 128 ||
11058
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
11059
+ typeof attempt.requestBody !== 'string' ||
11060
+ attempt.requestBody.length > 1024 * 1024) {
11061
+ return false;
11062
+ }
11063
+ try {
11064
+ const request = JSON.parse(attempt.requestBody);
11065
+ return request.projectKey === this.projectKey;
11066
+ }
11067
+ catch {
11068
+ return false;
11069
+ }
11070
+ }
11071
+ startAttempt(requestBody) {
11072
+ const existing = this.loadStartAttempt();
11073
+ if (existing) {
11074
+ return existing;
11075
+ }
11076
+ const attempt = {
11077
+ version: 1,
11078
+ projectKey: this.projectKey,
11079
+ createdAt: Date.now(),
11080
+ idempotencyKey: `insights:start:${generateRandomId(40)}`,
11081
+ requestBody,
11082
+ };
11083
+ this.pendingStartAttempt = attempt;
11084
+ try {
11085
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, JSON.stringify(attempt));
11086
+ }
11087
+ catch {
11088
+ // The in-memory copy still protects retries within this Analytics instance.
11089
+ }
11090
+ return attempt;
11091
+ }
11092
+ clearStartAttempt() {
11093
+ this.pendingStartAttempt = null;
11094
+ try {
11095
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, '');
11096
+ }
11097
+ catch {
11098
+ // The in-memory copy is still cleared when storage is unavailable.
11099
+ }
11100
+ }
9982
11101
  }
9983
11102
 
9984
11103
  const Messages = _Messages;
@@ -10025,7 +11144,7 @@ class API {
10025
11144
  this.signalStartIssue = (reason, missingApi) => {
10026
11145
  const doNotTrack = this.checkDoNotTrack();
10027
11146
  console.log("Tracker couldn't start due to:", JSON.stringify({
10028
- trackerVersion: '7.1.0',
11147
+ trackerVersion: '7.3.4',
10029
11148
  siteKey: this.options.siteKey,
10030
11149
  doNotTrack,
10031
11150
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -10037,6 +11156,31 @@ class API {
10037
11156
  }
10038
11157
  this.app.restartCanvasTracking();
10039
11158
  };
11159
+ /**
11160
+ * Re-evaluates sanitization against the current DOM and re-emits whatever
11161
+ * changed, updating already-recorded nodes mid-session. Call after toggling
11162
+ * `data-oxvosessions-*` attributes or after changing whatever your `domSanitizer`
11163
+ * keys on (class/id/etc).
11164
+ *
11165
+ * @param el - the highest node you changed; omit to re-scan the whole document;
11166
+ * scanning the entire doc is O(dom size)
11167
+ * */
11168
+ this.resanitize = (el) => {
11169
+ if (this.app === null) {
11170
+ return;
11171
+ }
11172
+ this.app.resanitize(el);
11173
+ };
11174
+ /**
11175
+ * Returns the sanitization level the tracker currently has for a node
11176
+ * (0 = Plain, 1 = Obscured, 2 = Hidden), or undefined if it isn't tracked.
11177
+ * */
11178
+ this.checkSanitization = (el) => {
11179
+ if (this.app === null) {
11180
+ return undefined;
11181
+ }
11182
+ return this.app.checkSanitization(el);
11183
+ };
10040
11184
  this.getSessionLink = (options) => {
10041
11185
  if (this.app === null) {
10042
11186
  return undefined;
@@ -10179,6 +11323,12 @@ class API {
10179
11323
  : (options.analytics?.endpoint ?? options.endpoint ?? defaultEdp),
10180
11324
  projectKey: options.siteKey,
10181
11325
  });
11326
+ app.attachStartCallback(() => {
11327
+ this.analytics?.onStart();
11328
+ });
11329
+ app.attachStopCallback(() => {
11330
+ this.analytics?.onStop();
11331
+ });
10182
11332
  }
10183
11333
  if (!this.crossdomainMode) {
10184
11334
  // no need to send iframe viewport data since its a node for us
@@ -10190,7 +11340,7 @@ class API {
10190
11340
  // no tabs in iframes yet
10191
11341
  Tabs(app);
10192
11342
  }
10193
- Mouse(app, options.mouse);
11343
+ Mouse(app, { ...options.mouse, customAttributes: options.customAttributes });
10194
11344
  // inside iframe, we ignore viewport scroll
10195
11345
  Scroll(app, this.crossdomainMode);
10196
11346
  CSSRules(app, options.css);