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