@oxvo/browser 7.3.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 (35) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/entry.js +1452 -307
  3. package/dist/cjs/entry.js.map +1 -1
  4. package/dist/cjs/index.js +1452 -307
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/main/app/canvas.d.ts +5 -0
  7. package/dist/cjs/main/app/cross_domain_iframe.d.ts +44 -0
  8. package/dist/cjs/main/app/index.d.ts +53 -13
  9. package/dist/cjs/main/app/messages.gen.d.ts +2 -0
  10. package/dist/cjs/main/app/session.d.ts +1 -0
  11. package/dist/cjs/main/entry.d.ts +5 -5
  12. package/dist/cjs/main/index.d.ts +1 -1
  13. package/dist/cjs/main/modules/analytics/index.d.ts +5 -0
  14. package/dist/cjs/package.json +1 -0
  15. package/dist/lib/entry.js +1452 -307
  16. package/dist/lib/entry.js.map +1 -1
  17. package/dist/lib/index.js +1452 -307
  18. package/dist/lib/index.js.map +1 -1
  19. package/dist/lib/main/app/canvas.d.ts +5 -0
  20. package/dist/lib/main/app/cross_domain_iframe.d.ts +44 -0
  21. package/dist/lib/main/app/index.d.ts +53 -13
  22. package/dist/lib/main/app/messages.gen.d.ts +2 -0
  23. package/dist/lib/main/app/session.d.ts +1 -0
  24. package/dist/lib/main/entry.d.ts +5 -5
  25. package/dist/lib/main/index.d.ts +1 -1
  26. package/dist/lib/main/modules/analytics/index.d.ts +5 -0
  27. package/dist/types/main/app/canvas.d.ts +5 -0
  28. package/dist/types/main/app/cross_domain_iframe.d.ts +44 -0
  29. package/dist/types/main/app/index.d.ts +53 -13
  30. package/dist/types/main/app/messages.gen.d.ts +2 -0
  31. package/dist/types/main/app/session.d.ts +1 -0
  32. package/dist/types/main/entry.d.ts +5 -5
  33. package/dist/types/main/index.d.ts +1 -1
  34. package/dist/types/main/modules/analytics/index.d.ts +5 -0
  35. package/package.json +4 -3
package/dist/lib/index.js CHANGED
@@ -530,7 +530,7 @@ function dec2hex(dec) {
530
530
  return dec.toString(16).padStart(2, '0');
531
531
  }
532
532
  function generateRandomId(len) {
533
- const arr = new Uint8Array((len) / 2);
533
+ const arr = new Uint8Array((len || 40) / 2);
534
534
  // msCrypto = IE11
535
535
  // @ts-ignore
536
536
  const safeCrypto = window.crypto || window.msCrypto;
@@ -539,7 +539,7 @@ function generateRandomId(len) {
539
539
  return Array.from(arr, dec2hex).join('');
540
540
  }
541
541
  else {
542
- return Array.from({ length: len }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
542
+ return Array.from({ length: len || 40 }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
543
543
  }
544
544
  }
545
545
  function inIframe() {
@@ -736,6 +736,88 @@ function throttleWithTrailing(fn, interval) {
736
736
 
737
737
  // Auto-generated, do not edit
738
738
  /* eslint-disable */
739
+ const MESSAGE_FIELD_TYPES = {
740
+ 0: ['number'],
741
+ 4: ['string', 'string', 'number'],
742
+ 5: ['number', 'number'],
743
+ 6: ['number', 'number'],
744
+ 7: [],
745
+ 8: ['number', 'number', 'number', 'string', 'boolean'],
746
+ 9: ['number', 'number', 'number'],
747
+ 10: ['number', 'number', 'number'],
748
+ 11: ['number'],
749
+ 12: ['number', 'string', 'string'],
750
+ 13: ['number', 'string'],
751
+ 14: ['number', 'string'],
752
+ 16: ['number', 'number', 'number'],
753
+ 17: ['number', 'string'],
754
+ 18: ['number', 'string', 'number'],
755
+ 19: ['number', 'boolean'],
756
+ 20: ['number', 'number'],
757
+ 21: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number'],
758
+ 22: ['string', 'string'],
759
+ 23: ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
760
+ 24: ['number', 'number', 'number'],
761
+ 27: ['string', 'string'],
762
+ 28: ['string'],
763
+ 29: ['string'],
764
+ 30: ['string', 'string'],
765
+ 34: ['number', 'string'],
766
+ 35: ['number', 'number', 'number'],
767
+ 36: ['number', 'string'],
768
+ 40: ['string', 'number', 'string', 'string'],
769
+ 41: ['string', 'string'],
770
+ 42: ['string'],
771
+ 44: ['string', 'string', 'number'],
772
+ 45: ['string', 'string'],
773
+ 46: ['string', 'string'],
774
+ 47: ['string', 'string', 'number'],
775
+ 48: ['string', 'string', 'string', 'string', 'number'],
776
+ 49: ['number', 'number', 'number', 'number'],
777
+ 50: ['number', 'string'],
778
+ 51: ['number', 'number', 'number'],
779
+ 43: ['string', 'string'],
780
+ 52: ['number', 'string', 'string'],
781
+ 53: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string'],
782
+ 54: ['number', 'string'],
783
+ 55: ['boolean'],
784
+ 57: ['number', 'string', 'string', 'string'],
785
+ 58: ['number'],
786
+ 60: ['number', 'string', 'string', 'string'],
787
+ 61: ['number', 'string', 'string'],
788
+ 63: ['string', 'string'],
789
+ 64: ['string', 'string'],
790
+ 65: ['number', 'number'],
791
+ 68: ['number', 'number', 'string', 'string', 'number', 'number'],
792
+ 69: ['number', 'number', 'string', 'string'],
793
+ 70: ['number', 'number'],
794
+ 71: ['number', 'string', 'string'],
795
+ 73: ['number', 'string', 'number', 'string'],
796
+ 75: ['number', 'number'],
797
+ 76: ['number', 'number'],
798
+ 77: ['number', 'number'],
799
+ 78: ['string', 'string', 'string', 'string'],
800
+ 79: ['string', 'string'],
801
+ 81: ['number', 'number', 'number', 'number', 'string'],
802
+ 83: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number', 'number'],
803
+ 84: ['string', 'string', 'string', 'number', 'string', 'string'],
804
+ 85: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
805
+ 87: ['string', 'number', 'number'],
806
+ 89: ['string', 'number', 'number', 'number', 'number', 'string'],
807
+ 112: ['number', 'string', 'boolean', 'string', 'number', 'number'],
808
+ 113: ['number', 'number', 'string'],
809
+ 114: ['number'],
810
+ 115: ['number'],
811
+ 116: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean'],
812
+ 117: ['string'],
813
+ 118: ['string'],
814
+ 119: ['string', 'number'],
815
+ 120: ['number'],
816
+ 121: ['string', 'string', 'number', 'number'],
817
+ 122: ['string', 'string', 'number', 'string'],
818
+ 123: ['string', 'string', 'string', 'string', 'number'],
819
+ 124: ['string', 'string'],
820
+ };
739
821
  function Timestamp(timestamp) {
740
822
  return [
741
823
  0 /* Messages.Type.Timestamp */,
@@ -1418,6 +1500,7 @@ var _Messages = /*#__PURE__*/Object.freeze({
1418
1500
  JSException: JSException,
1419
1501
  LoadFontFace: LoadFontFace,
1420
1502
  LongAnimationTask: LongAnimationTask$1,
1503
+ MESSAGE_FIELD_TYPES: MESSAGE_FIELD_TYPES,
1421
1504
  Metadata: Metadata,
1422
1505
  MobX: MobX,
1423
1506
  MouseClick: MouseClick,
@@ -1798,6 +1881,8 @@ class CanvasRecorder {
1798
1881
  this.uploadQueue = 0;
1799
1882
  this.MAX_CONCURRENT_UPLOADS = 2;
1800
1883
  this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
1884
+ this.MAX_UPLOAD_ATTEMPTS = 5;
1885
+ this.MAX_RETRY_DELAY_MS = 10000;
1801
1886
  this.pendingBatches = [];
1802
1887
  this.isProcessingQueue = false;
1803
1888
  /**
@@ -1938,26 +2023,41 @@ class CanvasRecorder {
1938
2023
  this.app.debug.warn('Upload queue full, dropping canvas batch');
1939
2024
  return;
1940
2025
  }
1941
- this.pendingBatches.push({ images, canvasId, createdAt });
2026
+ const token = this.app.session.getSessionSecret();
2027
+ if (!token) {
2028
+ this.app.debug.warn('Canvas batch has no session token');
2029
+ return;
2030
+ }
2031
+ this.pendingBatches.push({ images, canvasId, createdAt, token });
1942
2032
  if (!this.isProcessingQueue) {
1943
- this.processUploadQueue();
2033
+ void this.processUploadQueue();
1944
2034
  }
1945
2035
  }
1946
2036
  async processUploadQueue() {
2037
+ if (this.isProcessingQueue)
2038
+ return;
1947
2039
  this.isProcessingQueue = true;
1948
- while (this.pendingBatches.length > 0) {
1949
- if (this.uploadQueue >= this.MAX_CONCURRENT_UPLOADS) {
1950
- await new Promise((resolve) => setTimeout(resolve, 100));
1951
- continue;
2040
+ const worker = async () => {
2041
+ while (true) {
2042
+ const batch = this.pendingBatches.shift();
2043
+ if (!batch)
2044
+ return;
2045
+ await this.uploadBatch(batch.images, batch.canvasId, batch.createdAt, batch.token);
2046
+ }
2047
+ };
2048
+ try {
2049
+ await Promise.all(Array.from({ length: this.MAX_CONCURRENT_UPLOADS }, () => worker()));
2050
+ }
2051
+ finally {
2052
+ this.isProcessingQueue = false;
2053
+ // A batch can be appended between the final worker check and the flag
2054
+ // reset. Restart once so that race cannot strand queued frames.
2055
+ if (this.pendingBatches.length > 0) {
2056
+ void this.processUploadQueue();
1952
2057
  }
1953
- const batch = this.pendingBatches.shift();
1954
- if (!batch)
1955
- break;
1956
- this.uploadBatch(batch.images, batch.canvasId, batch.createdAt);
1957
2058
  }
1958
- this.isProcessingQueue = false;
1959
2059
  }
1960
- async uploadBatch(images, canvasId, createdAt) {
2060
+ async uploadBatch(images, canvasId, createdAt, token) {
1961
2061
  if (this.options.isDebug) {
1962
2062
  const packed = await packFrames(images);
1963
2063
  if (packed) {
@@ -2025,6 +2125,9 @@ class CanvasRecorder {
2025
2125
  });
2026
2126
  }
2027
2127
  const initRestart = () => {
2128
+ if (this.app.session.getSessionSecret() !== token) {
2129
+ return;
2130
+ }
2028
2131
  this.app.debug.log('Restarting tracker; token expired');
2029
2132
  this.app.stop(false);
2030
2133
  setTimeout(() => {
@@ -2034,25 +2137,72 @@ class CanvasRecorder {
2034
2137
  this.uploadQueue++;
2035
2138
  const base = this.app.options.endpoint.replace(/\/$/, '');
2036
2139
  const ingestPoint = base.endsWith('/oxvo') ? base : `${base}/oxvo`;
2037
- fetch(ingestPoint + '/v1/assets', {
2038
- method: 'POST',
2039
- headers: {
2040
- Authorization: `Bearer ${this.app.session.getSessionSecret() ?? ''}`,
2041
- },
2042
- body: formData,
2043
- })
2044
- .then((r) => {
2045
- if (r.status === 401) {
2046
- return initRestart();
2140
+ try {
2141
+ for (let attempt = 1; attempt <= this.MAX_UPLOAD_ATTEMPTS; attempt++) {
2142
+ try {
2143
+ const response = await fetch(ingestPoint + '/v1/assets', {
2144
+ method: 'POST',
2145
+ headers: {
2146
+ Authorization: `Bearer ${token}`,
2147
+ },
2148
+ body: formData,
2149
+ });
2150
+ if (response.ok)
2151
+ return;
2152
+ if (response.status === 401) {
2153
+ initRestart();
2154
+ return;
2155
+ }
2156
+ if (response.status === 413) {
2157
+ this.app.debug.warn('Canvas batch exceeds the upload size limit');
2158
+ return;
2159
+ }
2160
+ if (!this.isRetryableUploadStatus(response.status)) {
2161
+ this.app.debug.error(`Canvas upload failed with status ${response.status}`);
2162
+ return;
2163
+ }
2164
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2165
+ this.app.debug.error(`Canvas upload failed after ${attempt} attempts (status ${response.status})`);
2166
+ return;
2167
+ }
2168
+ await this.waitBeforeUploadRetry(response, attempt);
2169
+ }
2170
+ catch (error) {
2171
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2172
+ this.app.debug.error('Canvas upload failed after retrying', error);
2173
+ return;
2174
+ }
2175
+ await this.waitBeforeUploadRetry(undefined, attempt);
2176
+ }
2047
2177
  }
2048
- return true;
2049
- })
2050
- .catch((e) => {
2051
- this.app.debug.error('error saving canvas', e);
2052
- })
2053
- .finally(() => {
2178
+ }
2179
+ finally {
2054
2180
  this.uploadQueue--;
2055
- });
2181
+ }
2182
+ }
2183
+ isRetryableUploadStatus(status) {
2184
+ return status === 429 || status === 408 || status >= 500;
2185
+ }
2186
+ async waitBeforeUploadRetry(response, attempt) {
2187
+ const retryAfter = response?.headers.get('Retry-After');
2188
+ let retryDelayMs = 0;
2189
+ if (retryAfter) {
2190
+ const seconds = Number(retryAfter);
2191
+ if (Number.isFinite(seconds) && seconds >= 0) {
2192
+ retryDelayMs = seconds * 1000;
2193
+ }
2194
+ else {
2195
+ const retryAt = Date.parse(retryAfter);
2196
+ if (Number.isFinite(retryAt)) {
2197
+ retryDelayMs = Math.max(0, retryAt - Date.now());
2198
+ }
2199
+ }
2200
+ }
2201
+ if (retryDelayMs === 0) {
2202
+ retryDelayMs = 250 * 2 ** (attempt - 1);
2203
+ }
2204
+ retryDelayMs = Math.min(retryDelayMs, this.MAX_RETRY_DELAY_MS);
2205
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
2056
2206
  }
2057
2207
  cleanupCanvas(id) {
2058
2208
  if (this.snapshots[id]) {
@@ -2075,8 +2225,7 @@ class CanvasRecorder {
2075
2225
  }
2076
2226
  delete this.snapshots[id];
2077
2227
  }
2078
- clear() {
2079
- // Flush remaining images before cleanup
2228
+ flushPendingSnapshots() {
2080
2229
  Object.keys(this.snapshots).forEach((idStr) => {
2081
2230
  const id = parseInt(idStr, 10);
2082
2231
  const snapshot = this.snapshots[id];
@@ -2085,6 +2234,9 @@ class CanvasRecorder {
2085
2234
  snapshot.images = [];
2086
2235
  }
2087
2236
  });
2237
+ }
2238
+ clear() {
2239
+ this.flushPendingSnapshots();
2088
2240
  Object.keys(this.snapshots).forEach((idStr) => {
2089
2241
  const id = parseInt(idStr, 10);
2090
2242
  this.cleanupCanvas(id);
@@ -2151,6 +2303,291 @@ async function packFrames(images) {
2151
2303
  return packed;
2152
2304
  }
2153
2305
 
2306
+ const CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION = 1;
2307
+ const SECRET_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
2308
+ const CONTEXT_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
2309
+ const MAX_MESSAGE_FIELDS = 24;
2310
+ const MAX_MESSAGE_STRING_LENGTH = 100000;
2311
+ const MAX_BATCH_MESSAGES = 25000;
2312
+ const MAX_BATCH_BYTES = 2 * 1024 * 1024;
2313
+ // Cross-frame batches may contain only recording events. Identity, dictionary,
2314
+ // batch-header, and tab/session control opcodes remain owned by the top-level
2315
+ // tracker.
2316
+ const CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES = new Set([
2317
+ 4 /* MType.SetPageLocationDeprecated */,
2318
+ 5 /* MType.SetViewportSize */,
2319
+ 6 /* MType.SetViewportScroll */,
2320
+ 7 /* MType.CreateDocument */,
2321
+ 8 /* MType.CreateElementNode */,
2322
+ 9 /* MType.CreateTextNode */,
2323
+ 10 /* MType.MoveNode */,
2324
+ 11 /* MType.RemoveNode */,
2325
+ 12 /* MType.SetNodeAttribute */,
2326
+ 13 /* MType.RemoveNodeAttribute */,
2327
+ 14 /* MType.SetNodeData */,
2328
+ 16 /* MType.SetNodeScroll */,
2329
+ 17 /* MType.SetInputTarget */,
2330
+ 18 /* MType.SetInputValue */,
2331
+ 19 /* MType.SetInputChecked */,
2332
+ 20 /* MType.MouseMove */,
2333
+ 21 /* MType.NetworkRequestDeprecated */,
2334
+ 22 /* MType.ConsoleLog */,
2335
+ 23 /* MType.PageLoadTiming */,
2336
+ 24 /* MType.PageRenderTiming */,
2337
+ 27 /* MType.CustomEvent */,
2338
+ 36 /* MType.NodeAnimationResult */,
2339
+ 40 /* MType.Profiler */,
2340
+ 41 /* MType.OTable */,
2341
+ 42 /* MType.StateAction */,
2342
+ 44 /* MType.ReduxDeprecated */,
2343
+ 45 /* MType.Vuex */,
2344
+ 46 /* MType.MobX */,
2345
+ 47 /* MType.NgRx */,
2346
+ 48 /* MType.GraphQLDeprecated */,
2347
+ 49 /* MType.PerformanceTrack */,
2348
+ 53 /* MType.ResourceTimingDeprecatedDeprecated */,
2349
+ 54 /* MType.ConnectionInformation */,
2350
+ 55 /* MType.SetPageVisibility */,
2351
+ 57 /* MType.LoadFontFace */,
2352
+ 58 /* MType.SetNodeFocus */,
2353
+ 60 /* MType.SetNodeAttributeURLBased */,
2354
+ 61 /* MType.SetCSSDataURLBased */,
2355
+ 63 /* MType.TechnicalInfo */,
2356
+ 64 /* MType.CustomIssue */,
2357
+ 65 /* MType.SetNodeSlot */,
2358
+ 68 /* MType.MouseClick */,
2359
+ 69 /* MType.MouseClickDeprecated */,
2360
+ 70 /* MType.CreateIFrameDocument */,
2361
+ 71 /* MType.AdoptedSSReplaceURLBased */,
2362
+ 73 /* MType.AdoptedSSInsertRuleURLBased */,
2363
+ 75 /* MType.AdoptedSSDeleteRule */,
2364
+ 76 /* MType.AdoptedSSAddOwner */,
2365
+ 77 /* MType.AdoptedSSRemoveOwner */,
2366
+ 78 /* MType.JSException */,
2367
+ 79 /* MType.Zustand */,
2368
+ 83 /* MType.NetworkRequest */,
2369
+ 84 /* MType.WSChannel */,
2370
+ 85 /* MType.ResourceTiming */,
2371
+ 87 /* MType.Incident */,
2372
+ 89 /* MType.LongAnimationTask */,
2373
+ 112 /* MType.InputChange */,
2374
+ 113 /* MType.SelectionChange */,
2375
+ 114 /* MType.MouseThrashing */,
2376
+ 115 /* MType.UnbindNodes */,
2377
+ 116 /* MType.ResourceTimingDeprecated */,
2378
+ 119 /* MType.CanvasNode */,
2379
+ 120 /* MType.TagTrigger */,
2380
+ 121 /* MType.Redux */,
2381
+ 122 /* MType.SetPageLocation */,
2382
+ 123 /* MType.GraphQL */,
2383
+ 124 /* MType.WebVitals */,
2384
+ ]);
2385
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2386
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
2387
+ const isSafeIntegerAtLeast = (value, minimum) => typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
2388
+ const isBoundedString = (value, maxLength) => typeof value === 'string' && value.length > 0 && value.length <= maxLength;
2389
+ const isCanvasQuality = (value) => value === 'low' || value === 'medium' || value === 'high';
2390
+ function normalizeCrossDomainOrigin(value) {
2391
+ if (typeof value !== 'string' || value.trim() === '' || value.trim() === '*') {
2392
+ return null;
2393
+ }
2394
+ try {
2395
+ const url = new URL(value.trim());
2396
+ if (!['http:', 'https:'].includes(url.protocol) ||
2397
+ url.origin === 'null' ||
2398
+ url.username !== '' ||
2399
+ url.password !== '' ||
2400
+ (url.pathname !== '' && url.pathname !== '/') ||
2401
+ url.search !== '' ||
2402
+ url.hash !== '') {
2403
+ return null;
2404
+ }
2405
+ return url.origin;
2406
+ }
2407
+ catch {
2408
+ return null;
2409
+ }
2410
+ }
2411
+ function normalizeCrossDomainOrigins(values) {
2412
+ if (!Array.isArray(values)) {
2413
+ return new Set();
2414
+ }
2415
+ return new Set(values.map(normalizeCrossDomainOrigin).filter((origin) => origin !== null));
2416
+ }
2417
+ function createCrossDomainSecret() {
2418
+ if (!globalThis.crypto?.getRandomValues) {
2419
+ return null;
2420
+ }
2421
+ const bytes = new Uint8Array(24);
2422
+ globalThis.crypto.getRandomValues(bytes);
2423
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
2424
+ }
2425
+ function parseCrossDomainHandshake(data, expectedLine, expectedProjectKey) {
2426
+ if (!isRecord(data) ||
2427
+ data.line !== expectedLine ||
2428
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2429
+ data.projectKey !== expectedProjectKey ||
2430
+ !isBoundedString(data.context, 128) ||
2431
+ !CONTEXT_PATTERN.test(data.context) ||
2432
+ !isBoundedString(data.nonce, 128) ||
2433
+ !SECRET_PATTERN.test(data.nonce)) {
2434
+ return null;
2435
+ }
2436
+ return {
2437
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2438
+ context: data.context,
2439
+ nonce: data.nonce,
2440
+ projectKey: expectedProjectKey,
2441
+ };
2442
+ }
2443
+ function matchesCrossDomainEnvelope(data, expectedLine, expected) {
2444
+ return (isRecord(data) &&
2445
+ data.line === expectedLine &&
2446
+ data.protocolVersion === CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION &&
2447
+ data.context === expected.context &&
2448
+ data.nonce === expected.nonce &&
2449
+ data.capability === expected.capability);
2450
+ }
2451
+ function parseCrossDomainFrameInit(data, expectedLine, expected) {
2452
+ if (!isRecord(data) ||
2453
+ data.line !== expectedLine ||
2454
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2455
+ data.context !== expected.context ||
2456
+ data.nonce !== expected.nonce ||
2457
+ !isBoundedString(data.capability, 128) ||
2458
+ !SECRET_PATTERN.test(data.capability) ||
2459
+ !isSafeIntegerAtLeast(data.id, 0) ||
2460
+ !isSafeIntegerAtLeast(data.frameOrderNumber, 1) ||
2461
+ !isSafeIntegerAtLeast(data.frameLevel, 0) ||
2462
+ !isSafeIntegerAtLeast(data.capabilityExpiresAt, 1) ||
2463
+ data.capabilityExpiresAt <= Date.now() ||
2464
+ !isRecord(data.session)) {
2465
+ return null;
2466
+ }
2467
+ const session = data.session;
2468
+ if (!isBoundedString(session.sessionID, 256) ||
2469
+ !isFiniteNumber(session.startedAt) ||
2470
+ !isFiniteNumber(session.delayMs) ||
2471
+ typeof session.userUUID !== 'string' ||
2472
+ session.userUUID.length > 256) {
2473
+ return null;
2474
+ }
2475
+ let projectID;
2476
+ if (session.projectID !== undefined) {
2477
+ if (!isBoundedString(session.projectID, 256)) {
2478
+ return null;
2479
+ }
2480
+ projectID = session.projectID;
2481
+ }
2482
+ const canvas = parseCanvasConfig(data.canvas);
2483
+ if (data.canvas !== undefined && canvas === null) {
2484
+ return null;
2485
+ }
2486
+ return {
2487
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2488
+ context: data.context,
2489
+ nonce: data.nonce,
2490
+ capability: data.capability,
2491
+ id: data.id,
2492
+ frameOrderNumber: data.frameOrderNumber,
2493
+ frameLevel: data.frameLevel,
2494
+ capabilityExpiresAt: data.capabilityExpiresAt,
2495
+ session: {
2496
+ sessionID: session.sessionID,
2497
+ projectID,
2498
+ startedAt: session.startedAt,
2499
+ delayMs: session.delayMs,
2500
+ userUUID: session.userUUID,
2501
+ },
2502
+ ...(canvas ? { canvas } : {}),
2503
+ };
2504
+ }
2505
+ function parseCanvasConfig(value) {
2506
+ if (value === undefined) {
2507
+ return null;
2508
+ }
2509
+ if (!isRecord(value) || typeof value.enabled !== 'boolean') {
2510
+ return null;
2511
+ }
2512
+ let quality;
2513
+ if (value.quality !== undefined) {
2514
+ if (!isCanvasQuality(value.quality)) {
2515
+ return null;
2516
+ }
2517
+ quality = value.quality;
2518
+ }
2519
+ let fps;
2520
+ if (value.fps !== undefined) {
2521
+ if (!isFiniteNumber(value.fps)) {
2522
+ return null;
2523
+ }
2524
+ fps = value.fps;
2525
+ }
2526
+ let framesSupport;
2527
+ if (value.framesSupport !== undefined) {
2528
+ if (typeof value.framesSupport !== 'boolean') {
2529
+ return null;
2530
+ }
2531
+ framesSupport = value.framesSupport;
2532
+ }
2533
+ return {
2534
+ enabled: value.enabled,
2535
+ quality,
2536
+ fps,
2537
+ framesSupport,
2538
+ };
2539
+ }
2540
+ function parseCrossDomainIframeBatch(messages) {
2541
+ if (!Array.isArray(messages) || messages.length > MAX_BATCH_MESSAGES) {
2542
+ return null;
2543
+ }
2544
+ let bytes = 0;
2545
+ for (const message of messages) {
2546
+ if (!Array.isArray(message) || message.length < 1 || message.length > MAX_MESSAGE_FIELDS) {
2547
+ return null;
2548
+ }
2549
+ const messageType = message[0];
2550
+ if (typeof messageType !== 'number' ||
2551
+ !Number.isInteger(messageType) ||
2552
+ !CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES.has(messageType)) {
2553
+ return null;
2554
+ }
2555
+ const expectedFieldTypes = MESSAGE_FIELD_TYPES[messageType];
2556
+ if (expectedFieldTypes === undefined || message.length !== expectedFieldTypes.length + 1) {
2557
+ return null;
2558
+ }
2559
+ bytes += 8;
2560
+ for (let fieldIndex = 0; fieldIndex < expectedFieldTypes.length; fieldIndex++) {
2561
+ const field = message[fieldIndex + 1];
2562
+ if (typeof field !== expectedFieldTypes[fieldIndex]) {
2563
+ return null;
2564
+ }
2565
+ if (typeof field === 'string') {
2566
+ if (field.length > MAX_MESSAGE_STRING_LENGTH) {
2567
+ return null;
2568
+ }
2569
+ bytes += field.length * 3;
2570
+ }
2571
+ else if (typeof field === 'number') {
2572
+ if (!Number.isFinite(field)) {
2573
+ return null;
2574
+ }
2575
+ bytes += 8;
2576
+ }
2577
+ else if (typeof field === 'boolean') {
2578
+ bytes += 1;
2579
+ }
2580
+ else {
2581
+ return null;
2582
+ }
2583
+ if (bytes > MAX_BATCH_BYTES) {
2584
+ return null;
2585
+ }
2586
+ }
2587
+ }
2588
+ return { messages: messages, bytes };
2589
+ }
2590
+
2154
2591
  const LogLevel = {
2155
2592
  Verbose: 5,
2156
2593
  Log: 4,
@@ -3954,6 +4391,10 @@ class Session {
3954
4391
  this.token = `${token}${tokenSeparator}${projectKey}`;
3955
4392
  this.app.sessionStorage.setItem(this.options.session_token_key, `${token}${tokenSeparator}${projectKey}`);
3956
4393
  };
4394
+ this.clearSessionSecret = () => {
4395
+ this.token = undefined;
4396
+ this.app.sessionStorage.removeItem(this.options.session_token_key);
4397
+ };
3957
4398
  this.app = params.app;
3958
4399
  this.options = params.options;
3959
4400
  this.createTabId();
@@ -4050,7 +4491,7 @@ class Session {
4050
4491
  };
4051
4492
  }
4052
4493
  reset() {
4053
- this.app.sessionStorage.removeItem(this.options.session_token_key);
4494
+ this.clearSessionSecret();
4054
4495
  this.metadata = {};
4055
4496
  this.userID = null;
4056
4497
  this.sessionID = undefined;
@@ -4108,10 +4549,12 @@ class Ticker {
4108
4549
  * this value is injected during build time via rollup
4109
4550
  * */
4110
4551
  // @ts-ignore
4111
- 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.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\"}`;fetch(`${this.ingestURL}?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!==b&&(clearInterval(b),b=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,b=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===b&&(b=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";
4552
+ const workerBodyFn = "!function(){\"use strict\";class t{constructor(t,s,i,e=10,n=250,h,r){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.pageNo=r,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.lastBatchNum=0,this.inflightKeepaliveBytes=0,this.ingestURL=t+\"/v1/events\",this.analyticsIngestURL=t+\"/v1/analytics\",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t,s=\"player\"){if(this.busy||!this.token)this.queue.push({batch:t,dataType:s});else if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t,s);else{const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}}sendNext(){const t=this.queue.shift();if(t)if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t.batch,t.dataType);else{const s=++this.lastBatchNum;this.sendBatch(t.batch,!1,s,t.dataType)}else this.busy=!1}retry(t,s,i,e=\"player\"){if(this.attemptsCount>=this.MAX_ATTEMPTS_COUNT)return void this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`);this.attemptsCount++;const n=new Uint8Array(t);setTimeout((()=>this.sendBatch(n,s,i,e)),this.ATTEMPT_TIMEOUT*this.attemptsCount)}sendBatch(t,s,i,e=\"player\"){var n;if(0===t.length)return console.error(\"OxvoSessions: refusing to send 0-byte batch.\",{batchNum:i,dataType:e,isCompressed:s,batch:t}),this.attemptsCount=0,void this.sendNext();const h=null==i?void 0:i.toString().replace(/^([^_]+)_([^_]+).*/,\"$1_$2_$3\");this.busy=!0;const r={Authorization:`Bearer ${this.token}`,DataType:e};if(s&&(r[\"Content-Encoding\"]=\"gzip\"),null===this.token)return void setTimeout((()=>{this.sendBatch(t,s,`${null!=i?i:\"noBatchNum\"}_newToken`,e)}),500);const a=t.length<65536&&this.inflightKeepaliveBytes+t.length<=65536;a&&(this.inflightKeepaliveBytes+=t.length);const o=()=>{a&&(this.inflightKeepaliveBytes-=t.length)},u=`${null!==(n=this.pageNo)&&void 0!==n?n:\"noPageNum\"}_${null!=h?h:\"noBatchNum\"}`,c=\"analytics\"===e?this.analyticsIngestURL:this.ingestURL;fetch(`${c}?seq=${u}&keepalive=${a?\"yes\":\"no\"}`,{body:t,method:\"POST\",headers:r,keepalive:a}).then((n=>{var h;if(o(),null===(h=n.body)||void 0===h||h.cancel().catch((()=>{})),401===n.status)return this.busy=!1,void this.onUnauthorised();n.status>=400?this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_network:${n.status}`,e):(this.attemptsCount=0,this.sendNext())})).catch((n=>{o(),console.warn(\"OxvoSessions:\",n),this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_reject:${n.message}`,e)}))}sendCompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!0,i,s)}sendUncompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s=new Set([60,61,71,73]),i=new Set([21,22,40,41,44,45,46,47,48,79,83,84,85,87,89,116,120,121,123]),e=new Set([17,23,24,27,28,29,30,42,63,64,78,112,115,124]),n=\"function\"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class h{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}getCurrentCheckpoint(){return this.checkpointOffset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const s=n.encode(t),i=s.byteLength;return!(!this.uint(i)||this.offset+i>this.size)&&(this.data.set(s,this.offset),this.offset+=i,!0)}reset(){this.offset=0,this.checkpointOffset=0}rewind(t,s){t>this.offset||s>this.checkpointOffset||(this.offset=t,this.checkpointOffset=s)}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class r extends h{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 65:case 70:case 75:case 76:case 77:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 35:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 52:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17:case 34:case 36:case 50:case 54:return this.uint(t[1])&&this.string(t[2]);case 16:return this.uint(t[1])&&this.int(t[2])&&this.int(t[3]);case 18:return this.uint(t[1])&&this.string(t[2])&&this.int(t[3]);case 19:return this.uint(t[1])&&this.boolean(t[2]);case 21:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8]);case 22:case 27:case 30:case 41:case 45:case 46:case 43:case 63:case 64:case 79:case 124:return this.string(t[1])&&this.string(t[2]);case 23:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 28:case 29:case 42:case 117:case 118:return this.string(t[1]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.int(t[5]);case 49:return this.int(t[1])&&this.int(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 53:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8]);case 55:return this.boolean(t[1]);case 57:case 60:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 58:case 120:return this.int(t[1]);case 68:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6]);case 69:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 81:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.int(t[4])&&this.string(t[5]);case 83:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 84:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6]);case 85:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10])&&this.uint(t[11])&&this.uint(t[12])&&this.uint(t[13])&&this.uint(t[14])&&this.uint(t[15])&&this.uint(t[16])&&this.uint(t[17]);case 87:return this.string(t[1])&&this.int(t[2])&&this.int(t[3]);case 89:return this.string(t[1])&&this.int(t[2])&&this.int(t[3])&&this.int(t[4])&&this.int(t[5])&&this.string(t[6]);case 112:return this.uint(t[1])&&this.string(t[2])&&this.boolean(t[3])&&this.string(t[4])&&this.int(t[5])&&this.int(t[6]);case 113:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3]);case 116:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10]);case 119:return this.string(t[1])&&this.uint(t[2]);case 121:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 122:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 123:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])}}}class a{constructor(t,s,i){this.bufferSize=t,this.version=s,this.dataType=i,this.sizeBuffer=new Uint8Array(3),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,this.encoder=new r(t)}push(t,s){const i=this.encoder,e=null===this.snap,n=i.getCurrentOffset(),h=i.getCurrentCheckpoint();if(e){const t={pageNo:s.pageNo,firstIndex:s.index,timestamp:s.timestamp,url:s.url,tabId:s.tabId};if(!this.writeHeader(t))return i.rewind(n,h),!1;this.snap=t,this.lastPushedTs=s.timestamp}return 0===t[0]||s.timestamp===this.lastPushedTs||this.writeMessageWithSize([0,s.timestamp])?this.writeMessageWithSize(t)?(this.lastPushedTs=s.timestamp,0!==t[0]&&(this.hasNonTimestamp=!0),!0):(i.rewind(n,h),e&&(this.snap=null),!1):(i.rewind(n,h),!1)}hasContent(){return null!==this.snap&&this.hasNonTimestamp}flush(){if(!this.hasContent())return this.reset(),null;const t=this.encoder.flush();return this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,t}reset(){this.encoder.reset(),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0}writeHeader(t){const s=this.encoder,i=[81,this.version,t.pageNo,t.firstIndex,t.timestamp,t.url];return!!s.uint(i[0])&&(!!s.encode(i)&&(!(s.getCurrentOffset()>this.bufferSize)&&(s.checkpoint(),!!this.writeMessageWithSize([0,t.timestamp])&&!!this.writeMessageWithSize([118,t.tabId]))))}writeMessageWithSize(t){const s=this.encoder;if(!s.uint(t[0])||!s.skip(3))return!1;const i=s.getCurrentOffset();if(!s.encode(t))return!1;const e=s.getCurrentOffset(),n=e-i;return n>16777215?(console.warn(\"OxvoSessions: max message size overflow.\"),!1):!(e>this.bufferSize)&&(this.writeSizeAt(n,i-3),s.checkpoint(),!0)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}}class o{constructor(t,s,i,e,n,h,r=!1,o){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=e,this.tabId=n,this.onOfflineEnd=h,this.localDebug=r,this.onLocalSave=o,this.nextIndex=0,this.beaconSize=2e5,this.beaconSizeLimit=1e6,this.protocolVersion=1,this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"),this.assetBuilder=new a(this.beaconSize,3,\"assets\"),this.devtoolsBuilder=new a(this.beaconSize,4,\"devtools\"),this.analyticsBuilder=new a(this.beaconSize,5,\"analytics\")}playerVersion(){return 2===this.protocolVersion?2:1}currentCtx(){return{pageNo:this.pageNo,index:this.nextIndex,timestamp:this.timestamp,url:this.url,tabId:this.tabId}}setBeaconSizeLimit(t){this.beaconSizeLimit=t}setProtocolVersion(t){this.protocolVersion!==t&&(this.protocolVersion=t,this.playerBuilder.reset(),this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"))}writeMessage(t){if(-1===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),122===t[0]&&(this.url=t[1]);const s=this.routeMessage(t);this.pushTo(s,t)}routeMessage(t){if(2===this.protocolVersion){const n=t[0];if(s.has(n))return this.assetBuilder;if(i.has(n))return this.devtoolsBuilder;if(e.has(n))return this.analyticsBuilder}return this.playerBuilder}pushTo(t,s){const i=this.currentCtx();if(t.push(s,i))return void this.nextIndex++;if(this.flushBuilder(t),t.push(s,i))return void this.nextIndex++;const e=new a(this.beaconSizeLimit,t.version,t.dataType);if(!e.push(s,i))return void console.warn(\"OxvoSessions: beacon size overflow. Skipping large message.\",s);this.nextIndex++;const n=e.flush();n&&this.emitBatch(n,t.dataType,!1)}flushBuilder(t,s=!1){const i=t.flush();return!!i&&(this.emitBatch(i,t.dataType,s),!0)}emitBatch(t,s,i){this.localDebug&&this.onLocalSave&&this.onLocalSave(`${s}-${Date.now()}`,t.slice()),this.onBatch(t,i,s)}finaliseBatch(t=!1){this.flushBuilder(this.playerBuilder,t),this.flushBuilder(this.assetBuilder,t),this.flushBuilder(this.devtoolsBuilder,t),this.flushBuilder(this.analyticsBuilder,t)}clean(){this.playerBuilder.reset(),this.assetBuilder.reset(),this.devtoolsBuilder.reset(),this.analyticsBuilder.reset()}}var u;!function(t){t[t.NotActive=0]=\"NotActive\",t[t.Starting=1]=\"Starting\",t[t.Stopping=2]=\"Stopping\",t[t.Active=3]=\"Active\",t[t.Stopped=4]=\"Stopped\"}(u||(u={}));let c=null,l=null,d=u.NotActive;function p(t){l&&l.finaliseBatch(t)}function f(){return new Promise((t=>{d=u.Stopping,null!==y&&(clearInterval(y),y=null),l&&(l.clean(),l=null),c&&(c.clean(),setTimeout((()=>{c=null}),20)),setTimeout((()=>{d=u.NotActive,t(null)}),100)}))}function g(){[u.Stopped,u.Stopping].includes(d)||(postMessage(\"a_stop\"),f().then((()=>{postMessage(\"a_start\")})))}let m,y=null;self.onmessage=({data:s})=>{var i;if(\"stop\"===s)return p(),void f().then((()=>{d=u.Stopped}));if(\"forceFlushBatch\"!==s)if(\"closing\"!==s){if(!Array.isArray(s)){if(\"compressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Compressed batch.\"),void g();s.batch&&c.sendCompressed(s.batch,s.dataType)}if(\"uncompressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Uncompressed batch.\"),void g();s.batch&&c.sendUncompressed(s.batch,s.dataType)}return\"start\"===s.type?(d=u.Starting,c=new t(s.ingestPoint,(()=>{g()}),(t=>{!function(t){postMessage({type:\"failure\",reason:t}),f()}(t)}),s.connAttemptCount,s.connAttemptGap,((t,s)=>{postMessage({type:\"compress\",batch:t,dataType:s},[t.buffer])}),s.pageNo),l=new o(s.pageNo,s.timestamp,s.url,((t,s,i=\"player\")=>{c&&(s?c.sendUncompressed(t,i):c.push(t,i))}),s.tabId,(()=>postMessage({type:\"queue_empty\"})),null!==(i=s.localDebug)&&void 0!==i&&i,((t,s)=>{postMessage({type:\"local_save\",name:t,batch:s},[s.buffer])})),null===y&&(y=setInterval(p,3e4)),d=u.Active):\"auth\"===s.type?c?l?(c.authorise(s.token),s.beaconSizeLimit&&l.setBeaconSizeLimit(s.beaconSizeLimit),void(s.protocolVersion&&l.setProtocolVersion(s.protocolVersion))):(console.debug(\"OR WebWorker: writer not initialised. Received auth.\"),void g()):(console.debug(\"OR WebWorker: sender not initialised. Received auth.\"),void g()):void 0}if(l){const t=l;s.forEach((s=>{55===s[0]&&(s[1]?m=setTimeout((()=>g()),18e5):clearTimeout(m)),t.writeMessage(s)}))}else postMessage(\"not_init\"),g()}else p(!0);else p()}}();\n";
4112
4553
  const CANCELED = 'canceled';
4113
4554
  const bufferStorageKey = 'or_buffer_1';
4114
4555
  const PROTO_VERSION = '2';
4556
+ const BOOTSTRAP_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
4557
+ const MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH = 1024 * 1024;
4115
4558
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
4116
4559
  const SuccessfulStart = (body) => ({ ...body, success: true });
4117
4560
  var ActivityState;
@@ -4142,18 +4585,25 @@ const proto = {
4142
4585
  resp: 'never-gonna-let-you-down',
4143
4586
  // regenerating id (copied other tab)
4144
4587
  reg: 'never-gonna-run-around-and-desert-you',
4145
- iframeSignal: 'tracker inside a child iframe',
4146
- iframeId: 'getting node id for child iframe',
4147
- iframeBatch: 'batch of messages from an iframe window',
4148
- parentAlive: 'signal that parent is live',
4149
- killIframe: 'stop tracker inside frame',
4150
- startIframe: 'start tracker inside frame',
4588
+ iframeSignal: 'oxvo:cross-frame:v1:handshake',
4589
+ iframeId: 'oxvo:cross-frame:v1:ready',
4590
+ iframeBatch: 'oxvo:cross-frame:v1:batch',
4591
+ parentAlive: 'oxvo:cross-frame:v1:pong',
4592
+ killIframe: 'oxvo:cross-frame:v1:stop',
4593
+ startIframe: 'oxvo:cross-frame:v1:start',
4151
4594
  // checking updates
4152
- polling: 'hello-how-are-you-im-under-the-water-please-help-me',
4595
+ polling: 'oxvo:cross-frame:v1:ping',
4153
4596
  // happens if tab is old and has outdated token but
4154
4597
  // not communicating with backend to update it (for whatever reason)
4155
4598
  reset: 'reset-your-session-please',
4156
4599
  };
4600
+ const FRAME_RATE_WINDOW_MS = 1000;
4601
+ const MAX_FRAME_ENVELOPES_PER_WINDOW = 120;
4602
+ const MAX_FRAME_MESSAGES_PER_WINDOW = 50000;
4603
+ const MAX_FRAME_BYTES_PER_WINDOW = 4 * 1024 * 1024;
4604
+ const FRAME_CAPABILITY_TTL_MS = 5 * 60 * 1000;
4605
+ const FRAME_CAPABILITY_REFRESH_LEAD_MS = 60 * 1000;
4606
+ const FRAME_PREVIOUS_CAPABILITY_GRACE_MS = 5 * 1000;
4157
4607
  class App {
4158
4608
  get tagMatcher() {
4159
4609
  return this.markerWatcher.matcher;
@@ -4174,18 +4624,31 @@ class App {
4174
4624
  this.stopCallbacks = [];
4175
4625
  this.commitCallbacks = [];
4176
4626
  this.activityState = ActivityState.NotActive;
4177
- this.version = '7.3.0'; // TODO: version compatability check inside each plugin.
4627
+ this.version = '7.3.4'; // TODO: version compatability check inside each plugin.
4178
4628
  this.socketMode = false;
4179
4629
  this.compressionThreshold = 24 * 1000;
4180
4630
  this.bc = null;
4181
4631
  this.canvasRecorder = null;
4182
4632
  this.conditionsManager = null;
4633
+ this.bootstrapAttempts = {};
4183
4634
  this.canStart = false;
4184
4635
  this.rootId = null;
4185
4636
  this.pageFrames = [];
4186
4637
  this.frameOderNumber = 0;
4187
4638
  this.frameLevel = 0;
4188
4639
  this.emptyBatchCounter = 0;
4640
+ this.crossDomainFrameCapability = null;
4641
+ this.crossDomainFrameCapabilityExpiresAt = 0;
4642
+ this.crossDomainFrameSession = null;
4643
+ this.crossDomainFrameCanvas = null;
4644
+ this.crossDomainResumeAfterHandshake = false;
4645
+ this.crossDomainConnectionRequested = false;
4646
+ this.crossDomainFrames = new Map();
4647
+ this.crossDomainEnvelopeBudgets = new WeakMap();
4648
+ this.pendingCrossDomainFrames = new Map();
4649
+ this.crossDomainAuthorizationGeneration = 0;
4650
+ this.nextCrossDomainFrameOrder = 1;
4651
+ this.crossDomainCanvasConfig = null;
4189
4652
  /** used by child iframes for crossdomain only */
4190
4653
  this.parentActive = false;
4191
4654
  this.checkStatus = () => {
@@ -4193,230 +4656,201 @@ class App {
4193
4656
  };
4194
4657
  this.parentCrossDomainFrameListener = (event) => {
4195
4658
  const { data } = event;
4196
- if (!data || event.source === window)
4659
+ if (!data ||
4660
+ this.crossDomainParentOrigin === null ||
4661
+ this.crossDomainFrameNonce === null ||
4662
+ event.source !== window.parent ||
4663
+ event.origin !== this.crossDomainParentOrigin) {
4664
+ return;
4665
+ }
4666
+ if (data.line === proto.iframeId) {
4667
+ const init = parseCrossDomainFrameInit(data, proto.iframeId, {
4668
+ context: this.contextId,
4669
+ nonce: this.crossDomainFrameNonce,
4670
+ });
4671
+ if (init === null)
4672
+ return;
4673
+ const sessionChanged = this.crossDomainFrameSession !== null &&
4674
+ this.crossDomainFrameSession.sessionID !== init.session.sessionID;
4675
+ if (sessionChanged && this.activityState === ActivityState.Starting) {
4676
+ this.postHandshakeToParent();
4677
+ return;
4678
+ }
4679
+ const shouldResume = this.crossDomainResumeAfterHandshake || (sessionChanged && this.active());
4680
+ if (sessionChanged && this.active()) {
4681
+ this.stop();
4682
+ }
4683
+ this.applyCrossDomainFrameInit(init);
4684
+ if (shouldResume &&
4685
+ this.activityState !== ActivityState.Active &&
4686
+ this.activityState !== ActivityState.Starting) {
4687
+ this.crossDomainResumeAfterHandshake = false;
4688
+ this.startCrossDomainFrame(this.prevOpts);
4689
+ }
4690
+ this.debug.log('Starting secure cross-domain iframe tracking');
4691
+ return;
4692
+ }
4693
+ const expectedLine = data.line === proto.parentAlive
4694
+ ? proto.parentAlive
4695
+ : data.line === proto.startIframe
4696
+ ? proto.startIframe
4697
+ : data.line === proto.killIframe
4698
+ ? proto.killIframe
4699
+ : null;
4700
+ if (this.crossDomainFrameCapability === null ||
4701
+ expectedLine === null ||
4702
+ (expectedLine !== proto.killIframe &&
4703
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) ||
4704
+ !matchesCrossDomainEnvelope(data, expectedLine, {
4705
+ context: this.contextId,
4706
+ nonce: this.crossDomainFrameNonce,
4707
+ capability: this.crossDomainFrameCapability,
4708
+ })) {
4709
+ return;
4710
+ }
4711
+ if (data.line === proto.parentAlive) {
4712
+ this.parentActive = true;
4197
4713
  return;
4714
+ }
4198
4715
  if (data.line === proto.startIframe) {
4199
4716
  // Avoid corrupting an in-flight start; let it complete.
4200
4717
  if (this.activityState === ActivityState.Starting)
4201
4718
  return;
4719
+ const init = parseCrossDomainFrameInit(data, proto.startIframe, {
4720
+ context: this.contextId,
4721
+ nonce: this.crossDomainFrameNonce,
4722
+ });
4723
+ if (init === null)
4724
+ return;
4202
4725
  try {
4203
4726
  if (this.active()) {
4204
4727
  this.stop();
4205
4728
  }
4206
- if (data.token) {
4207
- this.session.setSessionSecret(data.token, this.projectKey);
4208
- }
4209
- if (data.id !== undefined) {
4210
- this.rootId = data.id;
4211
- }
4212
- this.allowAppStart();
4213
- void this.start();
4729
+ this.applyCrossDomainFrameInit(init);
4730
+ this.startCrossDomainFrame(this.prevOpts);
4214
4731
  }
4215
4732
  catch (e) {
4216
4733
  console.error('children frame restart failed:', e);
4217
4734
  }
4218
- }
4219
- if (data.line === proto.parentAlive) {
4220
- this.parentActive = true;
4221
- }
4222
- if (data.line === proto.iframeId) {
4223
- this.parentActive = true;
4224
- this.rootId = data.id;
4225
- this.session.setSessionSecret(data.token, this.projectKey);
4226
- this.frameOderNumber = data.frameOrderNumber;
4227
- this.frameLevel = data.frameLevel;
4228
- this.debug.log('starting iframe tracking', data);
4229
- this.allowAppStart();
4735
+ return;
4230
4736
  }
4231
4737
  if (data.line === proto.killIframe) {
4232
- if (this.active()) {
4738
+ const shouldResume = this.activityState === ActivityState.Active || this.activityState === ActivityState.Starting;
4739
+ if (this.activityState !== ActivityState.NotActive) {
4233
4740
  this.stop();
4234
4741
  }
4742
+ this.clearCrossDomainFrameAuthorization();
4743
+ this.crossDomainResumeAfterHandshake = shouldResume;
4744
+ this.crossDomainConnectionRequested = shouldResume;
4235
4745
  }
4236
4746
  };
4237
- this.trackedFrames = [];
4238
- this.frameLastSeen = new Map();
4239
- this.FRAME_STALE_MS = 1500;
4240
4747
  this.crossDomainIframeListener = (event) => {
4241
- if (event.source === window)
4242
- return;
4243
4748
  const { data } = event;
4244
- if (!data)
4749
+ if (!data || event.source === window)
4750
+ return;
4751
+ if (data.line === proto.iframeSignal) {
4752
+ const handshake = parseCrossDomainHandshake(data, proto.iframeSignal, this.projectKey);
4753
+ if (handshake !== null) {
4754
+ void this.establishCrossDomainFrame(event, handshake);
4755
+ }
4245
4756
  return;
4246
- // Record liveness regardless of our own active state so the queue can prune
4247
- // stale contexts reliably once we resume dispatching commands after a cycle.
4248
- if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
4249
- this.frameLastSeen.set(data.context, Date.now());
4250
4757
  }
4251
4758
  if (!this.active())
4252
4759
  return;
4253
- if (data.line === proto.iframeSignal) {
4254
- // @ts-ignore
4255
- event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
4256
- const signalId = async () => {
4257
- if (event.source === null) {
4258
- return console.error('Couldnt connect to event.source for child iframe tracking');
4259
- }
4260
- const id = await this.checkNodeId(event.source);
4261
- if (!id) {
4262
- this.debug.log('Couldnt get node id for iframe', event.source);
4263
- return;
4264
- }
4265
- try {
4266
- if (this.trackedFrames.includes(data.context)) {
4267
- this.debug.log('Trying to observe already added iframe; ignore if its a restart');
4268
- }
4269
- else {
4270
- this.trackedFrames.push(data.context);
4271
- }
4272
- await this.waitStarted();
4273
- const token = this.session.getSessionSecret(this.projectKey);
4274
- const order = this.trackedFrames.findIndex((f) => f === data.context) + 1;
4275
- if (order === 0) {
4276
- this.debug.error('Couldnt get order number for iframe', data.context, this.trackedFrames);
4277
- }
4278
- const iframeData = {
4279
- line: proto.iframeId,
4280
- id,
4281
- token,
4282
- frameOrderNumber: order,
4283
- frameLevel: this.frameLevel + 1,
4284
- };
4285
- this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
4286
- // @ts-ignore
4287
- event.source?.postMessage(iframeData, '*');
4288
- }
4289
- catch (e) {
4290
- console.error(e);
4291
- }
4292
- };
4293
- void signalId();
4760
+ if (data.line === proto.polling) {
4761
+ const state = this.validatedCrossDomainFrame(event, proto.polling);
4762
+ if (state !== null && this.consumeCrossDomainEnvelopeBudget(state.source)) {
4763
+ this.postToCrossDomainFrame(state, proto.parentAlive);
4764
+ }
4765
+ return;
4294
4766
  }
4295
4767
  /**
4296
4768
  * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
4297
4769
  * plus we rewrite some of the messages to be relative to the main context/window
4298
4770
  * */
4299
4771
  if (data.line === proto.iframeBatch) {
4300
- const msgBatch = data.messages;
4772
+ const state = this.validatedCrossDomainFrame(event, proto.iframeBatch);
4773
+ if (state === null || !this.consumeCrossDomainEnvelopeBudget(state.source)) {
4774
+ return;
4775
+ }
4776
+ const batch = parseCrossDomainIframeBatch(data.messages);
4777
+ if (batch === null ||
4778
+ !this.consumeCrossDomainFrameBudget(state, batch.messages.length, batch.bytes)) {
4779
+ return;
4780
+ }
4301
4781
  const mappedMessages = [];
4302
- msgBatch.forEach((msg) => {
4782
+ batch.messages.forEach((msg) => {
4783
+ let fixedMessage = msg;
4303
4784
  if (msg[0] === 20 /* MType.MouseMove */) {
4304
- let fixedMessage = msg;
4305
- this.pageFrames.forEach((frame) => {
4306
- if (frame.contentWindow === event.source) {
4307
- const [type, x, y] = msg;
4308
- const { left, top } = frame.getBoundingClientRect();
4309
- fixedMessage = [type, x + left, y + top];
4310
- }
4311
- });
4312
- mappedMessages.push(fixedMessage);
4785
+ const [type, x, y] = msg;
4786
+ const { left, top } = state.frame.getBoundingClientRect();
4787
+ fixedMessage = [type, x + left, y + top];
4313
4788
  }
4314
4789
  if (msg[0] === 68 /* MType.MouseClick */) {
4315
- let fixedMessage = msg;
4316
- this.pageFrames.forEach((frame) => {
4317
- if (frame.contentWindow === event.source) {
4318
- const [type, id, hesitationTime, label, selector, normX, normY] = msg;
4319
- const { left, top, width, height } = frame.getBoundingClientRect();
4320
- const contentWidth = document.documentElement.scrollWidth;
4321
- const contentHeight = document.documentElement.scrollHeight;
4322
- // (normalizedX * frameWidth + frameLeftOffset)/docSize
4323
- const fullX = (normX / 100) * width + left;
4324
- const fullY = (normY / 100) * height + top;
4325
- const fixedX = fullX / contentWidth;
4326
- const fixedY = fullY / contentHeight;
4327
- fixedMessage = [
4328
- type,
4329
- id,
4330
- hesitationTime,
4331
- label,
4332
- selector,
4333
- Math.round(fixedX * 1e3) / 1e1,
4334
- Math.round(fixedY * 1e3) / 1e1,
4335
- ];
4336
- }
4337
- });
4338
- mappedMessages.push(fixedMessage);
4339
- }
4340
- if (![28 /* MType.UserID */, 29 /* MType.UserAnonymousID */, 30 /* MType.Metadata */].includes(msg[0])) {
4341
- mappedMessages.push(msg);
4790
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
4791
+ const { left, top, width, height } = state.frame.getBoundingClientRect();
4792
+ const contentWidth = document.documentElement.scrollWidth;
4793
+ const contentHeight = document.documentElement.scrollHeight;
4794
+ const fullX = (normX / 100) * width + left;
4795
+ const fullY = (normY / 100) * height + top;
4796
+ const fixedX = fullX / contentWidth;
4797
+ const fixedY = fullY / contentHeight;
4798
+ fixedMessage = [
4799
+ type,
4800
+ id,
4801
+ hesitationTime,
4802
+ label,
4803
+ selector,
4804
+ Math.round(fixedX * 1e3) / 1e1,
4805
+ Math.round(fixedY * 1e3) / 1e1,
4806
+ ];
4342
4807
  }
4808
+ mappedMessages.push(fixedMessage);
4343
4809
  });
4344
4810
  this.messages.push(...mappedMessages);
4345
4811
  }
4346
- if (data.line === proto.polling) {
4347
- if (!this.pollingQueue.order.length) {
4348
- return;
4349
- }
4350
- this.pruneStaleFrames();
4351
- const liveSet = new Set(this.trackedFrames);
4352
- while (this.pollingQueue.order.length > 0) {
4353
- const head = this.pollingQueue.order[0];
4354
- this.pollingQueue[head] = this.pollingQueue[head].filter((ctx) => liveSet.has(ctx));
4355
- if (this.pollingQueue[head].length === 0) {
4356
- delete this.pollingQueue[head];
4357
- this.pollingQueue.order.shift();
4358
- }
4359
- else {
4360
- break;
4361
- }
4812
+ };
4813
+ this.bootChildrenFrames = async () => {
4814
+ await this.waitStarted();
4815
+ this.pruneDetachedCrossDomainFrames();
4816
+ for (const state of this.crossDomainFrames.values()) {
4817
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
4818
+ const id = await this.checkNodeId(state.source);
4819
+ const session = this.currentCrossDomainSession();
4820
+ if (id === null ||
4821
+ session === null ||
4822
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
4823
+ this.crossDomainFrames.get(state.source) !== state ||
4824
+ !this.active()) {
4825
+ continue;
4362
4826
  }
4363
- if (!this.pollingQueue.order.length) {
4364
- return;
4827
+ const rotated = this.crossDomainFrameNeedsRotation(state, session.sessionID);
4828
+ if (rotated && !this.rotateCrossDomainFrameCapability(state, session.sessionID)) {
4829
+ continue;
4365
4830
  }
4366
- const nextCommand = this.pollingQueue.order[0];
4367
- if (this.pollingQueue[nextCommand].includes(data.context)) {
4368
- this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter((c) => c !== data.context);
4369
- const message = { line: nextCommand };
4370
- if (nextCommand === proto.startIframe) {
4371
- message.token = this.session.getSessionSecret(this.projectKey);
4372
- const targetFrame = this.pageFrames.find((f) => f.contentWindow === event.source) ||
4373
- Array.from(document.querySelectorAll('iframe')).find((f) => f.contentWindow === event.source);
4374
- if (targetFrame) {
4375
- const nodeId = targetFrame[this.options.node_id];
4376
- if (nodeId !== undefined) {
4377
- message.id = nodeId;
4378
- }
4379
- }
4380
- }
4381
- // @ts-ignore
4382
- event.source?.postMessage(message, '*');
4383
- if (this.pollingQueue[nextCommand].length === 0) {
4384
- delete this.pollingQueue[nextCommand];
4385
- this.pollingQueue.order.shift();
4386
- }
4831
+ const payload = id === null ? null : this.frameInitPayload(state, id);
4832
+ if (payload) {
4833
+ this.postToCrossDomainFrame(state, rotated ? proto.iframeId : proto.startIframe, payload);
4387
4834
  }
4388
4835
  }
4389
4836
  };
4390
- /**
4391
- * { command : [remaining iframes] }
4392
- * + order of commands
4393
- **/
4394
- this.pollingQueue = {
4395
- order: [],
4396
- };
4397
- this.addCommand = (cmd) => {
4398
- this.pruneStaleFrames();
4399
- if (this.pollingQueue[cmd]) {
4400
- this.pollingQueue[cmd] = Array.from(new Set([...this.pollingQueue[cmd], ...this.trackedFrames]));
4401
- return;
4402
- }
4403
- this.pollingQueue.order.push(cmd);
4404
- this.pollingQueue[cmd] = [...this.trackedFrames];
4405
- };
4406
- this.bootChildrenFrames = async () => {
4407
- await this.waitStarted();
4408
- this.addCommand(proto.startIframe);
4409
- };
4410
4837
  this.killChildrenFrames = () => {
4411
- this.addCommand(proto.killIframe);
4838
+ this.pruneDetachedCrossDomainFrames();
4839
+ const states = Array.from(this.crossDomainFrames.values());
4840
+ this.crossDomainAuthorizationGeneration += 1;
4841
+ this.crossDomainFrames.clear();
4842
+ this.pendingCrossDomainFrames.clear();
4843
+ states.forEach((state) => {
4844
+ this.postToCrossDomainFrame(state, proto.killIframe);
4845
+ });
4412
4846
  };
4413
4847
  this.signalIframeTracker = () => {
4414
- const thisTab = this.session.getTabId();
4415
- window.parent.postMessage({
4416
- line: proto.iframeSignal,
4417
- source: thisTab,
4418
- context: this.contextId,
4419
- }, this.options.crossdomain?.parentDomain ?? '*');
4848
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
4849
+ this.debug.error('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
4850
+ return;
4851
+ }
4852
+ this.crossDomainConnectionRequested = true;
4853
+ this.postHandshakeToParent();
4420
4854
  /**
4421
4855
  * since we need to wait uncertain amount of time
4422
4856
  * and I don't want to have recursion going on,
@@ -4428,20 +4862,16 @@ class App {
4428
4862
  let cumulativeDelay = 0;
4429
4863
  let stopAttempts = false;
4430
4864
  const checkAndSendMessage = () => {
4431
- if (stopAttempts || this.checkStatus()) {
4865
+ if (stopAttempts || !this.crossDomainConnectionRequested || this.checkStatus()) {
4432
4866
  stopAttempts = true;
4433
4867
  return;
4434
4868
  }
4435
- window.parent.postMessage({
4436
- line: proto.iframeSignal,
4437
- source: thisTab,
4438
- context: this.contextId,
4439
- }, this.options.crossdomain?.parentDomain ?? '*');
4869
+ this.postHandshakeToParent();
4440
4870
  this.debug.info('Trying to signal to parent, attempt:', retries + 1);
4441
4871
  retries++;
4442
4872
  };
4443
4873
  for (let i = 0; i < maxRetries; i++) {
4444
- if (this.checkStatus()) {
4874
+ if (!this.crossDomainConnectionRequested || this.checkStatus()) {
4445
4875
  stopAttempts = true;
4446
4876
  break;
4447
4877
  }
@@ -4598,9 +5028,7 @@ class App {
4598
5028
  maskTextEmails: false,
4599
5029
  maskTextNumbers: false,
4600
5030
  disableStringDict: false,
4601
- crossdomain: {
4602
- parentDomain: '*',
4603
- },
5031
+ crossdomain: {},
4604
5032
  canvas: {
4605
5033
  disableCanvas: false,
4606
5034
  fixedCanvasScaling: false,
@@ -4613,6 +5041,9 @@ class App {
4613
5041
  disableThrottling: false,
4614
5042
  };
4615
5043
  this.options = simpleMerge(defaultOptions, options);
5044
+ this.crossDomainChildOrigins = normalizeCrossDomainOrigins(this.options.crossdomain?.childOrigins);
5045
+ this.crossDomainParentOrigin = normalizeCrossDomainOrigin(this.options.crossdomain?.parentDomain);
5046
+ this.crossDomainFrameNonce = this.insideIframe ? createCrossDomainSecret() : null;
4616
5047
  if (!this.insideIframe &&
4617
5048
  !this.options.forceSingleTab &&
4618
5049
  globalThis &&
@@ -4659,22 +5090,33 @@ class App {
4659
5090
  }
4660
5091
  });
4661
5092
  // @deprecated (use sessionHash on start instead)
4662
- if (sessionToken != null) {
5093
+ if (sessionToken != null && !this.insideIframe) {
4663
5094
  this.session.applySessionHash(sessionToken);
4664
5095
  }
5096
+ if (this.insideIframe) {
5097
+ this.session.clearSessionSecret();
5098
+ }
4665
5099
  const thisTab = this.session.getTabId();
4666
5100
  if (this.insideIframe) {
4667
- /**
4668
- * listen for messages from parent window, so we can signal that we're alive
4669
- * */
4670
- window.addEventListener('message', this.parentCrossDomainFrameListener);
4671
- window.addEventListener('message', this.crossDomainIframeListener);
4672
- setInterval(() => {
4673
- window.parent.postMessage({
4674
- line: proto.polling,
4675
- context: this.contextId,
4676
- }, options.crossdomain?.parentDomain ?? '*');
4677
- }, 250);
5101
+ if (this.crossDomainParentOrigin !== null && this.crossDomainFrameNonce !== null) {
5102
+ /**
5103
+ * listen for messages from parent window, so we can signal that we're alive
5104
+ * */
5105
+ window.addEventListener('message', this.parentCrossDomainFrameListener);
5106
+ setInterval(() => {
5107
+ if (!this.crossDomainConnectionRequested) {
5108
+ return;
5109
+ }
5110
+ if (this.crossDomainFrameCapability === null ||
5111
+ Date.now() >=
5112
+ this.crossDomainFrameCapabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS) {
5113
+ this.postHandshakeToParent();
5114
+ }
5115
+ else {
5116
+ this.postToParent(proto.polling);
5117
+ }
5118
+ }, 1000);
5119
+ }
4678
5120
  }
4679
5121
  else {
4680
5122
  this.initWorker();
@@ -4682,7 +5124,9 @@ class App {
4682
5124
  * if we get a signal from child iframes, we check for their node_id and send it back,
4683
5125
  * so they can act as if it was just a same-domain iframe
4684
5126
  * */
4685
- window.addEventListener('message', this.crossDomainIframeListener);
5127
+ if (this.options.crossdomain?.enabled && this.crossDomainChildOrigins.size > 0) {
5128
+ window.addEventListener('message', this.crossDomainIframeListener);
5129
+ }
4686
5130
  }
4687
5131
  if (this.bc !== null) {
4688
5132
  this.bc.postMessage({
@@ -4732,16 +5176,319 @@ class App {
4732
5176
  };
4733
5177
  }
4734
5178
  }
4735
- pruneStaleFrames() {
4736
- const staleAfter = Date.now() - this.FRAME_STALE_MS;
4737
- this.trackedFrames = this.trackedFrames.filter((ctx) => {
4738
- const last = this.frameLastSeen.get(ctx);
4739
- if (last !== undefined && last >= staleAfter)
4740
- return true;
4741
- this.frameLastSeen.delete(ctx);
4742
- return false;
5179
+ postToParent(line, extra = {}) {
5180
+ if (this.crossDomainParentOrigin === null ||
5181
+ this.crossDomainFrameNonce === null ||
5182
+ this.crossDomainFrameCapability === null ||
5183
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) {
5184
+ return;
5185
+ }
5186
+ window.parent.postMessage({
5187
+ line,
5188
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5189
+ context: this.contextId,
5190
+ nonce: this.crossDomainFrameNonce,
5191
+ capability: this.crossDomainFrameCapability,
5192
+ ...extra,
5193
+ }, this.crossDomainParentOrigin);
5194
+ }
5195
+ postHandshakeToParent() {
5196
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
5197
+ return;
5198
+ }
5199
+ window.parent.postMessage({
5200
+ line: proto.iframeSignal,
5201
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5202
+ context: this.contextId,
5203
+ nonce: this.crossDomainFrameNonce,
5204
+ projectKey: this.projectKey,
5205
+ }, this.crossDomainParentOrigin);
5206
+ }
5207
+ applyCrossDomainFrameInit(init) {
5208
+ this.parentActive = true;
5209
+ this.crossDomainConnectionRequested = true;
5210
+ this.crossDomainFrameCapability = init.capability;
5211
+ this.crossDomainFrameCapabilityExpiresAt = init.capabilityExpiresAt;
5212
+ this.crossDomainFrameSession = init.session;
5213
+ this.crossDomainFrameCanvas = init.canvas ?? null;
5214
+ this.rootId = init.id;
5215
+ this.frameOderNumber = init.frameOrderNumber;
5216
+ this.frameLevel = init.frameLevel;
5217
+ this.allowAppStart();
5218
+ }
5219
+ clearCrossDomainFrameAuthorization() {
5220
+ this.parentActive = false;
5221
+ this.canStart = false;
5222
+ this.crossDomainFrameCapability = null;
5223
+ this.crossDomainFrameCapabilityExpiresAt = 0;
5224
+ this.crossDomainFrameSession = null;
5225
+ this.crossDomainFrameCanvas = null;
5226
+ this.rootId = null;
5227
+ }
5228
+ findCrossDomainFrame(source) {
5229
+ const frame = this.pageFrames.find((candidate) => candidate.contentWindow === source) ??
5230
+ Array.from(document.querySelectorAll('iframe')).find((candidate) => candidate.contentWindow === source);
5231
+ if (!frame)
5232
+ return null;
5233
+ if (!this.pageFrames.includes(frame)) {
5234
+ this.pageFrames.push(frame);
5235
+ }
5236
+ return frame;
5237
+ }
5238
+ revokeCrossDomainFrame(source, expectedState) {
5239
+ const state = this.crossDomainFrames.get(source);
5240
+ if (!state || (expectedState !== undefined && state !== expectedState)) {
5241
+ return;
5242
+ }
5243
+ this.crossDomainFrames.delete(source);
5244
+ }
5245
+ pruneDetachedCrossDomainFrames() {
5246
+ this.crossDomainFrames.forEach((state, source) => {
5247
+ if (!state.frame.isConnected || state.frame.contentWindow !== source) {
5248
+ this.revokeCrossDomainFrame(source);
5249
+ }
4743
5250
  });
4744
5251
  }
5252
+ currentCrossDomainSession() {
5253
+ const session = this.session.getInfo();
5254
+ if (!session.sessionID) {
5255
+ return null;
5256
+ }
5257
+ return {
5258
+ sessionID: session.sessionID,
5259
+ projectID: session.projectID,
5260
+ startedAt: session.timestamp,
5261
+ delayMs: this.delay,
5262
+ userUUID: this.localStorage.getItem(this.options.local_uuid_key) ?? '',
5263
+ };
5264
+ }
5265
+ frameInitPayload(state, id) {
5266
+ const session = this.currentCrossDomainSession();
5267
+ if (session === null ||
5268
+ session.sessionID !== state.sessionID ||
5269
+ Date.now() >= state.capabilityExpiresAt) {
5270
+ return null;
5271
+ }
5272
+ return {
5273
+ id,
5274
+ frameOrderNumber: state.frameOrderNumber,
5275
+ frameLevel: this.frameLevel + 1,
5276
+ capabilityExpiresAt: state.capabilityExpiresAt,
5277
+ session,
5278
+ ...(this.crossDomainCanvasConfig ? { canvas: this.crossDomainCanvasConfig } : {}),
5279
+ };
5280
+ }
5281
+ rotateCrossDomainFrameCapability(state, sessionID, now = Date.now()) {
5282
+ const capability = createCrossDomainSecret();
5283
+ if (capability === null) {
5284
+ return false;
5285
+ }
5286
+ const preservePreviousCapability = state.sessionID === sessionID && now < state.capabilityExpiresAt;
5287
+ state.previousCapability = preservePreviousCapability ? state.capability : null;
5288
+ state.previousCapabilityExpiresAt = preservePreviousCapability
5289
+ ? Math.min(state.capabilityExpiresAt, now + FRAME_PREVIOUS_CAPABILITY_GRACE_MS)
5290
+ : 0;
5291
+ state.capability = capability;
5292
+ state.sessionID = sessionID;
5293
+ state.capabilityExpiresAt = now + FRAME_CAPABILITY_TTL_MS;
5294
+ return true;
5295
+ }
5296
+ crossDomainFrameNeedsRotation(state, sessionID, now = Date.now()) {
5297
+ return (state.sessionID !== sessionID ||
5298
+ now >= state.capabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS);
5299
+ }
5300
+ postToCrossDomainFrame(state, line, extra = {}) {
5301
+ state.source.postMessage({
5302
+ line,
5303
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5304
+ context: state.context,
5305
+ nonce: state.nonce,
5306
+ capability: state.capability,
5307
+ ...extra,
5308
+ }, state.origin);
5309
+ }
5310
+ consumeCrossDomainEnvelopeBudget(source) {
5311
+ const now = Date.now();
5312
+ let budget = this.crossDomainEnvelopeBudgets.get(source);
5313
+ if (budget === undefined || now - budget.windowStartedAt >= FRAME_RATE_WINDOW_MS) {
5314
+ budget = {
5315
+ windowStartedAt: now,
5316
+ envelopesInWindow: 0,
5317
+ };
5318
+ this.crossDomainEnvelopeBudgets.set(source, budget);
5319
+ }
5320
+ if (budget.envelopesInWindow >= MAX_FRAME_ENVELOPES_PER_WINDOW) {
5321
+ return false;
5322
+ }
5323
+ budget.envelopesInWindow += 1;
5324
+ return true;
5325
+ }
5326
+ async establishCrossDomainFrame(event, handshake) {
5327
+ if (!this.active() ||
5328
+ event.source === null ||
5329
+ !this.crossDomainChildOrigins.has(event.origin)) {
5330
+ return;
5331
+ }
5332
+ const candidateSource = event.source;
5333
+ if (!this.consumeCrossDomainEnvelopeBudget(candidateSource)) {
5334
+ return;
5335
+ }
5336
+ const frame = this.findCrossDomainFrame(candidateSource);
5337
+ if (frame === null || !frame.isConnected || frame.contentWindow === null) {
5338
+ return;
5339
+ }
5340
+ const source = frame.contentWindow;
5341
+ const existing = this.crossDomainFrames.get(source);
5342
+ if (this.pendingCrossDomainFrames.has(source)) {
5343
+ return;
5344
+ }
5345
+ const existingMatches = existing !== undefined &&
5346
+ existing.origin === event.origin &&
5347
+ existing.context === handshake.context &&
5348
+ existing.nonce === handshake.nonce &&
5349
+ existing.frame === frame;
5350
+ if (existing !== undefined && !existingMatches) {
5351
+ return;
5352
+ }
5353
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
5354
+ const pendingToken = Symbol();
5355
+ this.pendingCrossDomainFrames.set(source, pendingToken);
5356
+ let navigated = false;
5357
+ const markNavigated = () => {
5358
+ navigated = true;
5359
+ };
5360
+ frame.addEventListener('load', markNavigated, { once: true });
5361
+ try {
5362
+ const id = await this.checkNodeId(source);
5363
+ if (id === null ||
5364
+ navigated ||
5365
+ !this.active() ||
5366
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
5367
+ !frame.isConnected ||
5368
+ frame.contentWindow !== source) {
5369
+ return;
5370
+ }
5371
+ const session = this.currentCrossDomainSession();
5372
+ if (session === null) {
5373
+ return;
5374
+ }
5375
+ const now = Date.now();
5376
+ let state;
5377
+ if (existingMatches) {
5378
+ state = existing;
5379
+ if (this.crossDomainFrameNeedsRotation(state, session.sessionID, now) &&
5380
+ !this.rotateCrossDomainFrameCapability(state, session.sessionID, now)) {
5381
+ return;
5382
+ }
5383
+ state.lastSeenAt = now;
5384
+ }
5385
+ else {
5386
+ const capability = createCrossDomainSecret();
5387
+ if (capability === null) {
5388
+ return;
5389
+ }
5390
+ state = {
5391
+ source,
5392
+ frame,
5393
+ origin: event.origin,
5394
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5395
+ context: handshake.context,
5396
+ nonce: handshake.nonce,
5397
+ capability,
5398
+ frameOrderNumber: this.nextCrossDomainFrameOrder++,
5399
+ sessionID: session.sessionID,
5400
+ capabilityExpiresAt: now + FRAME_CAPABILITY_TTL_MS,
5401
+ previousCapability: null,
5402
+ previousCapabilityExpiresAt: 0,
5403
+ lastSeenAt: now,
5404
+ rateWindowStartedAt: now,
5405
+ messagesInRateWindow: 0,
5406
+ bytesInRateWindow: 0,
5407
+ };
5408
+ }
5409
+ const payload = this.frameInitPayload(state, id);
5410
+ if (payload === null) {
5411
+ return;
5412
+ }
5413
+ if (!existingMatches) {
5414
+ if (existing !== undefined) {
5415
+ this.revokeCrossDomainFrame(source, existing);
5416
+ }
5417
+ this.crossDomainFrames.set(source, state);
5418
+ frame.addEventListener('load', () => {
5419
+ this.revokeCrossDomainFrame(source, state);
5420
+ }, { once: true });
5421
+ }
5422
+ this.postToCrossDomainFrame(state, proto.iframeId, payload);
5423
+ }
5424
+ finally {
5425
+ frame.removeEventListener('load', markNavigated);
5426
+ if (this.pendingCrossDomainFrames.get(source) === pendingToken) {
5427
+ this.pendingCrossDomainFrames.delete(source);
5428
+ }
5429
+ }
5430
+ }
5431
+ validatedCrossDomainFrame(event, expectedLine) {
5432
+ if (event.source === null ||
5433
+ !this.crossDomainChildOrigins.has(event.origin) ||
5434
+ event.source === window) {
5435
+ return null;
5436
+ }
5437
+ const source = event.source;
5438
+ const state = this.crossDomainFrames.get(source);
5439
+ const now = Date.now();
5440
+ const currentSessionID = this.session.getInfo().sessionID;
5441
+ if (!state ||
5442
+ state.origin !== event.origin ||
5443
+ !state.frame.isConnected ||
5444
+ state.frame.contentWindow !== source) {
5445
+ if (state) {
5446
+ this.revokeCrossDomainFrame(source);
5447
+ }
5448
+ return null;
5449
+ }
5450
+ if (currentSessionID === undefined || state.sessionID !== currentSessionID) {
5451
+ this.revokeCrossDomainFrame(source);
5452
+ return null;
5453
+ }
5454
+ const matchesCurrentCapability = now < state.capabilityExpiresAt && matchesCrossDomainEnvelope(event.data, expectedLine, state);
5455
+ const matchesPreviousCapability = state.previousCapability !== null &&
5456
+ now < state.previousCapabilityExpiresAt &&
5457
+ matchesCrossDomainEnvelope(event.data, expectedLine, {
5458
+ protocolVersion: state.protocolVersion,
5459
+ context: state.context,
5460
+ nonce: state.nonce,
5461
+ capability: state.previousCapability,
5462
+ });
5463
+ if (!matchesCurrentCapability && !matchesPreviousCapability) {
5464
+ if (now >= state.capabilityExpiresAt && now >= state.previousCapabilityExpiresAt) {
5465
+ this.revokeCrossDomainFrame(source);
5466
+ }
5467
+ return null;
5468
+ }
5469
+ if ((matchesCurrentCapability && state.previousCapability !== null) ||
5470
+ now >= state.previousCapabilityExpiresAt) {
5471
+ state.previousCapability = null;
5472
+ state.previousCapabilityExpiresAt = 0;
5473
+ }
5474
+ state.lastSeenAt = now;
5475
+ return state;
5476
+ }
5477
+ consumeCrossDomainFrameBudget(state, messageCount, bytes) {
5478
+ const now = Date.now();
5479
+ if (now - state.rateWindowStartedAt >= FRAME_RATE_WINDOW_MS) {
5480
+ state.rateWindowStartedAt = now;
5481
+ state.messagesInRateWindow = 0;
5482
+ state.bytesInRateWindow = 0;
5483
+ }
5484
+ if (state.messagesInRateWindow + messageCount > MAX_FRAME_MESSAGES_PER_WINDOW ||
5485
+ state.bytesInRateWindow + bytes > MAX_FRAME_BYTES_PER_WINDOW) {
5486
+ return false;
5487
+ }
5488
+ state.messagesInRateWindow += messageCount;
5489
+ state.bytesInRateWindow += bytes;
5490
+ return true;
5491
+ }
4745
5492
  allowAppStart() {
4746
5493
  this.canStart = true;
4747
5494
  if (this.startTimeout) {
@@ -4801,6 +5548,7 @@ class App {
4801
5548
  setTimeout(() => {
4802
5549
  closing = false;
4803
5550
  }, 500);
5551
+ this.canvasRecorder?.flushPendingSnapshots();
4804
5552
  if (this.worker) {
4805
5553
  this.worker.postMessage('closing');
4806
5554
  }
@@ -4904,10 +5652,7 @@ class App {
4904
5652
  return;
4905
5653
  }
4906
5654
  if (this.insideIframe) {
4907
- window.parent.postMessage({
4908
- line: proto.iframeBatch,
4909
- messages: this.messages,
4910
- }, this.options.crossdomain?.parentDomain ?? '*');
5655
+ this.postToParent(proto.iframeBatch, { messages: this.messages });
4911
5656
  this.commitCallbacks.forEach((cb) => cb(this.messages));
4912
5657
  this.messages.length = 0;
4913
5658
  return;
@@ -5063,6 +5808,159 @@ class App {
5063
5808
  },
5064
5809
  };
5065
5810
  }
5811
+ getBootstrapAttemptStorageKey(kind) {
5812
+ return `${this.options.session_token_key}_bootstrap_idempotency_${kind}`;
5813
+ }
5814
+ isValidBootstrapAttempt(value, kind, currentTime) {
5815
+ if (typeof value !== 'object' || value === null) {
5816
+ return false;
5817
+ }
5818
+ const attempt = value;
5819
+ if (attempt.version !== 1 ||
5820
+ attempt.projectKey !== this.projectKey ||
5821
+ typeof attempt.createdAt !== 'number' ||
5822
+ !Number.isFinite(attempt.createdAt) ||
5823
+ currentTime - attempt.createdAt >= BOOTSTRAP_ATTEMPT_TTL_MS ||
5824
+ typeof attempt.idempotencyKey !== 'string' ||
5825
+ attempt.idempotencyKey.length < 8 ||
5826
+ attempt.idempotencyKey.length > 128 ||
5827
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
5828
+ typeof attempt.intentFingerprint !== 'string' ||
5829
+ attempt.intentFingerprint.length === 0 ||
5830
+ attempt.intentFingerprint.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH ||
5831
+ typeof attempt.requestTimestamp !== 'number' ||
5832
+ !Number.isFinite(attempt.requestTimestamp) ||
5833
+ typeof attempt.workerTimestamp !== 'number' ||
5834
+ !Number.isFinite(attempt.workerTimestamp) ||
5835
+ typeof attempt.requestBody !== 'string' ||
5836
+ attempt.requestBody.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
5837
+ return false;
5838
+ }
5839
+ try {
5840
+ const body = JSON.parse(attempt.requestBody);
5841
+ const intent = this.parseBootstrapIntent(attempt.intentFingerprint);
5842
+ return (intent?.projectKey === this.projectKey &&
5843
+ intent.kind === kind &&
5844
+ body.app?.key === this.projectKey &&
5845
+ body.session?.dry === false &&
5846
+ body.session?.offline === (kind === 'offline') &&
5847
+ typeof body.session?.token === 'string' &&
5848
+ body.session.token === intent.token &&
5849
+ body.session?.rule === intent.rule &&
5850
+ (body.user?.id ?? '') === intent.userID &&
5851
+ (body.user?.uuid ?? '') === intent.userUUID &&
5852
+ body.meta?.ts === attempt.requestTimestamp &&
5853
+ attempt.idempotencyKey.startsWith(`bootstrap:${kind}:`));
5854
+ }
5855
+ catch {
5856
+ return false;
5857
+ }
5858
+ }
5859
+ removeStoredBootstrapAttempt(kind) {
5860
+ delete this.bootstrapAttempts[kind];
5861
+ try {
5862
+ this.sessionStorage.removeItem(this.getBootstrapAttemptStorageKey(kind));
5863
+ }
5864
+ catch {
5865
+ // The in-memory copy is still cleared when storage is unavailable.
5866
+ }
5867
+ }
5868
+ loadBootstrapAttempt(kind) {
5869
+ const currentTime = Date.now();
5870
+ const inMemoryAttempt = this.bootstrapAttempts[kind];
5871
+ if (this.isValidBootstrapAttempt(inMemoryAttempt, kind, currentTime)) {
5872
+ return inMemoryAttempt;
5873
+ }
5874
+ delete this.bootstrapAttempts[kind];
5875
+ try {
5876
+ const storedAttempt = this.sessionStorage.getItem(this.getBootstrapAttemptStorageKey(kind));
5877
+ if (storedAttempt === null || storedAttempt.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
5878
+ if (storedAttempt !== null) {
5879
+ this.removeStoredBootstrapAttempt(kind);
5880
+ }
5881
+ return undefined;
5882
+ }
5883
+ const parsedAttempt = JSON.parse(storedAttempt);
5884
+ if (this.isValidBootstrapAttempt(parsedAttempt, kind, currentTime)) {
5885
+ this.bootstrapAttempts[kind] = parsedAttempt;
5886
+ return parsedAttempt;
5887
+ }
5888
+ }
5889
+ catch {
5890
+ // Treat corrupt or inaccessible storage as an absent pending attempt.
5891
+ }
5892
+ this.removeStoredBootstrapAttempt(kind);
5893
+ return undefined;
5894
+ }
5895
+ getOrCreateBootstrapAttempt(kind, request) {
5896
+ const existingAttempt = this.loadBootstrapAttempt(kind);
5897
+ if (existingAttempt &&
5898
+ this.bootstrapIntentsMatch(existingAttempt.intentFingerprint, request.intentFingerprint)) {
5899
+ return existingAttempt;
5900
+ }
5901
+ if (existingAttempt) {
5902
+ this.removeStoredBootstrapAttempt(kind);
5903
+ }
5904
+ const attempt = {
5905
+ version: 1,
5906
+ projectKey: this.projectKey,
5907
+ createdAt: Date.now(),
5908
+ idempotencyKey: `bootstrap:${kind}:${generateRandomId(40)}`,
5909
+ ...request,
5910
+ };
5911
+ this.bootstrapAttempts[kind] = attempt;
5912
+ try {
5913
+ this.sessionStorage.setItem(this.getBootstrapAttemptStorageKey(kind), JSON.stringify(attempt));
5914
+ }
5915
+ catch {
5916
+ // The in-memory copy still protects retries within this tracker instance.
5917
+ }
5918
+ return attempt;
5919
+ }
5920
+ clearBootstrapAttempt(kind) {
5921
+ this.removeStoredBootstrapAttempt(kind);
5922
+ }
5923
+ buildBootstrapIntentFingerprint(params) {
5924
+ const tracker = this.getTrackerInfo();
5925
+ return JSON.stringify({
5926
+ projectKey: this.projectKey,
5927
+ kind: params.kind,
5928
+ token: params.token ?? '',
5929
+ rule: params.rule ?? '',
5930
+ userID: this.session.getInfo().userID ?? '',
5931
+ userUUID: tracker.userUUID ?? '',
5932
+ });
5933
+ }
5934
+ parseBootstrapIntent(value) {
5935
+ try {
5936
+ const intent = JSON.parse(value);
5937
+ if (intent.projectKey !== this.projectKey ||
5938
+ (intent.kind !== 'recording' && intent.kind !== 'offline') ||
5939
+ typeof intent.token !== 'string' ||
5940
+ typeof intent.rule !== 'string' ||
5941
+ typeof intent.userID !== 'string' ||
5942
+ typeof intent.userUUID !== 'string') {
5943
+ return undefined;
5944
+ }
5945
+ return intent;
5946
+ }
5947
+ catch {
5948
+ return undefined;
5949
+ }
5950
+ }
5951
+ bootstrapIntentsMatch(existingFingerprint, currentFingerprint) {
5952
+ const existing = this.parseBootstrapIntent(existingFingerprint);
5953
+ const current = this.parseBootstrapIntent(currentFingerprint);
5954
+ if (!existing || !current) {
5955
+ return false;
5956
+ }
5957
+ return (existing.projectKey === current.projectKey &&
5958
+ existing.kind === current.kind &&
5959
+ existing.token === current.token &&
5960
+ (current.rule === '' || existing.rule === current.rule) &&
5961
+ (current.userID === '' || existing.userID === current.userID) &&
5962
+ (current.userUUID === '' || existing.userUUID === current.userUUID));
5963
+ }
5066
5964
  getSessionMeta() {
5067
5965
  return {
5068
5966
  ...this.session.getInfo(),
@@ -5133,12 +6031,19 @@ class App {
5133
6031
  const needReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
5134
6032
  let needNewSessionID = startNewSession || needReset;
5135
6033
  const sessionToken = this.session.getSessionSecret(this.projectKey);
6034
+ if (needReset) {
6035
+ this.sessionStorage.removeItem(this.options.session_reset_key);
6036
+ }
5136
6037
  const storedVersion = this.sessionStorage.getItem(`${this.options.session_token_key}_version`);
5137
6038
  if (!storedVersion || storedVersion !== PROTO_VERSION) {
5138
6039
  needNewSessionID = true;
5139
6040
  this.sessionStorage.setItem(`${this.options.session_token_key}_version`, PROTO_VERSION);
5140
6041
  }
5141
- return needNewSessionID || !sessionToken;
6042
+ const isNewSession = needNewSessionID || !sessionToken;
6043
+ if (isNewSession && sessionToken) {
6044
+ this.session.clearSessionSecret();
6045
+ }
6046
+ return isNewSession;
5142
6047
  }
5143
6048
  /**
5144
6049
  * start buffering messages without starting the actual session, which gives
@@ -5210,7 +6115,7 @@ class App {
5210
6115
  })),
5211
6116
  });
5212
6117
  const { session = {}, client = {}, device = {} } = await r.json();
5213
- const { token, projectId } = session;
6118
+ const { token, projectId, assistToken } = session;
5214
6119
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5215
6120
  this.session.assign({ projectID: projectId });
5216
6121
  this.session.setUserInfo({
@@ -5223,6 +6128,7 @@ class App {
5223
6128
  });
5224
6129
  const onStartInfo = {
5225
6130
  sessionToken: token,
6131
+ assistToken: typeof assistToken === 'string' ? assistToken : undefined,
5226
6132
  userUUID: device?.id || '',
5227
6133
  sessionID: session?.id || '',
5228
6134
  };
@@ -5238,7 +6144,6 @@ class App {
5238
6144
  offlineRecording(startOpts = {}, onSessionSent) {
5239
6145
  this.onSessionSent = onSessionSent;
5240
6146
  this.singleBuffer = true;
5241
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5242
6147
  adjustTimeOrigin();
5243
6148
  this.coldStartTs = now();
5244
6149
  const saverBuffer = this.localStorage.getItem(bufferStorageKey);
@@ -5256,6 +6161,10 @@ class App {
5256
6161
  if (startOpts.startNewSession) {
5257
6162
  this.session.reset();
5258
6163
  }
6164
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6165
+ this.offlineRecordingSessionToken = isNewSession
6166
+ ? undefined
6167
+ : this.session.getSessionSecret(this.projectKey);
5259
6168
  this.session.assign({
5260
6169
  userID: startOpts.userID,
5261
6170
  metadata: startOpts.metadata,
@@ -5305,12 +6214,29 @@ class App {
5305
6214
  * */
5306
6215
  async uploadOfflineRecording() {
5307
6216
  this.stop(false);
5308
- const timestamp = now();
6217
+ const freshTimestamp = now();
6218
+ const bootstrapToken = this.offlineRecordingSessionToken ?? '';
6219
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6220
+ timestamp: freshTimestamp,
6221
+ dry: false,
6222
+ offline: true,
6223
+ bufferMs: freshTimestamp - this.coldStartTs,
6224
+ token: bootstrapToken,
6225
+ }));
6226
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('offline', {
6227
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6228
+ kind: 'offline',
6229
+ token: bootstrapToken,
6230
+ }),
6231
+ requestTimestamp: freshTimestamp,
6232
+ workerTimestamp: this.coldStartTs,
6233
+ requestBody: freshRequestBody(),
6234
+ });
5309
6235
  this.worker?.postMessage({
5310
6236
  type: 'start',
5311
6237
  pageNo: this.session.incPageNo(),
5312
6238
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5313
- timestamp: this.coldStartTs,
6239
+ timestamp: bootstrapAttempt.workerTimestamp,
5314
6240
  url: document.URL,
5315
6241
  connAttemptCount: this.options.connAttemptCount,
5316
6242
  connAttemptGap: this.options.connAttemptGap,
@@ -5318,28 +6244,30 @@ class App {
5318
6244
  localDebug: this.options.__local_debug,
5319
6245
  });
5320
6246
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6247
+ const headers = {
6248
+ 'Content-Type': 'application/json',
6249
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6250
+ };
5321
6251
  const r = await fetch(ingestPoint + '/v1/bootstrap', {
5322
6252
  method: 'POST',
5323
- headers: {
5324
- 'Content-Type': 'application/json',
5325
- },
5326
- body: JSON.stringify(this.buildStartPayload({
5327
- timestamp,
5328
- dry: false,
5329
- bufferMs: timestamp - this.coldStartTs,
5330
- token: '',
5331
- })),
6253
+ headers,
6254
+ body: bootstrapAttempt.requestBody,
5332
6255
  });
5333
- const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion } = await r.json();
6256
+ if (r.status !== 200) {
6257
+ throw new Error(`Server error: ${r.status}. ${await r.text()}`);
6258
+ }
6259
+ const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion, } = await r.json();
5334
6260
  const { token, projectId } = session;
5335
6261
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5336
6262
  const { beacon: beaconSizeLimit } = limits;
5337
- this.worker?.postMessage({
5338
- type: 'auth',
5339
- token,
5340
- beaconSizeLimit,
5341
- protocolVersion: offlineProtocolVersion,
5342
- });
6263
+ if (typeof token !== 'string' ||
6264
+ token.length === 0 ||
6265
+ (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6266
+ throw new Error('Incorrect server response for offline recording bootstrap');
6267
+ }
6268
+ this.session.setSessionSecret(token, this.projectKey);
6269
+ this.clearBootstrapAttempt('offline');
6270
+ this.offlineRecordingSessionToken = token;
5343
6271
  this.session.assign({ projectID: projectId });
5344
6272
  this.session.setUserInfo({
5345
6273
  userBrowser,
@@ -5349,12 +6277,77 @@ class App {
5349
6277
  userOS,
5350
6278
  userState,
5351
6279
  });
6280
+ this.worker?.postMessage({
6281
+ type: 'auth',
6282
+ token,
6283
+ beaconSizeLimit,
6284
+ protocolVersion: offlineProtocolVersion,
6285
+ });
5352
6286
  while (this.bufferedMessages1.length > 0) {
5353
6287
  await this.flushBuffer(this.bufferedMessages1);
5354
6288
  }
5355
6289
  this.postToWorker([[-1]]);
5356
6290
  this.clearBuffers();
5357
6291
  }
6292
+ startCrossDomainFrame(startOpts = {}) {
6293
+ if (this.activityState === ActivityState.Active ||
6294
+ this.activityState === ActivityState.Starting) {
6295
+ const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
6296
+ return UnsuccessfulStart(reason);
6297
+ }
6298
+ if (this.crossDomainFrameSession === null ||
6299
+ this.crossDomainFrameCapability === null ||
6300
+ this.rootId === null) {
6301
+ return UnsuccessfulStart('Cross-domain iframe handshake was not established.');
6302
+ }
6303
+ if (Object.keys(startOpts).length !== 0) {
6304
+ this.prevOpts = startOpts;
6305
+ }
6306
+ if (startOpts.startCallback) {
6307
+ this.userStartCallback = startOpts.startCallback;
6308
+ }
6309
+ try {
6310
+ adjustTimeOrigin();
6311
+ this.activityState = ActivityState.Starting;
6312
+ this.delay = this.crossDomainFrameSession.delayMs;
6313
+ this.session.clearSessionSecret();
6314
+ this.session.assign({
6315
+ sessionID: this.crossDomainFrameSession.sessionID,
6316
+ projectID: this.crossDomainFrameSession.projectID,
6317
+ timestamp: this.crossDomainFrameSession.startedAt,
6318
+ });
6319
+ const onStartInfo = {
6320
+ sessionToken: '',
6321
+ userUUID: this.crossDomainFrameSession.userUUID,
6322
+ sessionID: this.crossDomainFrameSession.sessionID,
6323
+ };
6324
+ this.startCallbacks.forEach((callback) => callback(onStartInfo));
6325
+ this.userStartCallback?.(SuccessfulStart(onStartInfo));
6326
+ this.activityState = ActivityState.Active;
6327
+ if (this.crossDomainFrameCanvas?.enabled && !this.options.canvas.disableCanvas) {
6328
+ this.canvasRecorder =
6329
+ this.canvasRecorder ??
6330
+ new CanvasRecorder(this, {
6331
+ fps: this.crossDomainFrameCanvas.fps ?? 15,
6332
+ quality: this.crossDomainFrameCanvas.quality ?? 'medium',
6333
+ isDebug: this.options.canvas.__save_canvas_locally,
6334
+ fixedScaling: this.options.canvas.fixedCanvasScaling,
6335
+ useAnimationFrame: this.options.canvas.useAnimationFrame,
6336
+ framesSupport: this.crossDomainFrameCanvas.framesSupport,
6337
+ });
6338
+ }
6339
+ this.observer.crossdomainObserve(this.rootId, this.frameOderNumber, this.frameLevel);
6340
+ this.ticker.start();
6341
+ this.canvasRecorder?.startTracking();
6342
+ return SuccessfulStart(onStartInfo);
6343
+ }
6344
+ catch (reason) {
6345
+ this.stop();
6346
+ const message = reason instanceof Error ? reason.message : String(reason);
6347
+ this.signalError(message, []);
6348
+ return UnsuccessfulStart(message);
6349
+ }
6350
+ }
5358
6351
  async _start(startOpts = {}, resetByWorker = false, conditionName) {
5359
6352
  if (Object.keys(startOpts).length !== 0) {
5360
6353
  this.prevOpts = startOpts;
@@ -5393,68 +6386,89 @@ class App {
5393
6386
  userID: userId || undefined,
5394
6387
  metadata: startOpts.metadata,
5395
6388
  });
5396
- const timestamp = now();
6389
+ const sessionToken = this.session.getSessionSecret(this.projectKey);
6390
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6391
+ const freshTimestamp = now();
6392
+ const freshWorkerTimestamp = isColdStart ? this.coldStartTs : freshTimestamp;
6393
+ const bootstrapToken = isNewSession ? '' : sessionToken;
6394
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6395
+ timestamp: freshTimestamp,
6396
+ dry: false,
6397
+ bufferMs: this.coldStartTs ? freshTimestamp - this.coldStartTs : 0,
6398
+ token: bootstrapToken,
6399
+ rule: conditionName,
6400
+ width: window.screen.width,
6401
+ height: window.screen.height,
6402
+ }));
6403
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('recording', {
6404
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6405
+ kind: 'recording',
6406
+ token: bootstrapToken,
6407
+ rule: conditionName,
6408
+ }),
6409
+ requestTimestamp: freshTimestamp,
6410
+ workerTimestamp: freshWorkerTimestamp,
6411
+ requestBody: freshRequestBody(),
6412
+ });
6413
+ const timestamp = bootstrapAttempt.requestTimestamp;
5397
6414
  this.worker?.postMessage({
5398
6415
  type: 'start',
5399
6416
  pageNo: this.session.incPageNo(),
5400
6417
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5401
- timestamp: isColdStart ? this.coldStartTs : timestamp,
6418
+ timestamp: bootstrapAttempt.workerTimestamp,
5402
6419
  url: document.URL,
5403
6420
  connAttemptCount: this.options.connAttemptCount,
5404
6421
  connAttemptGap: this.options.connAttemptGap,
5405
6422
  tabId: this.session.getTabId(),
5406
6423
  localDebug: this.options.__local_debug,
5407
6424
  });
5408
- const sessionToken = this.session.getSessionSecret(this.projectKey);
5409
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5410
- this.sessionStorage.removeItem(this.options.session_reset_key);
5411
6425
  this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'session token: ', sessionToken);
5412
6426
  try {
5413
6427
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6428
+ const headers = {
6429
+ 'Content-Type': 'application/json',
6430
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6431
+ };
5414
6432
  const r = await window.fetch(ingestPoint + '/v1/bootstrap', {
5415
6433
  method: 'POST',
5416
- headers: {
5417
- 'Content-Type': 'application/json',
5418
- },
5419
- body: JSON.stringify(this.buildStartPayload({
5420
- timestamp,
5421
- dry: false,
5422
- bufferMs: this.coldStartTs ? timestamp - this.coldStartTs : 0,
5423
- token: isNewSession ? '' : sessionToken,
5424
- rule: conditionName,
5425
- width: window.screen.width,
5426
- height: window.screen.height,
5427
- })),
6434
+ headers,
6435
+ body: bootstrapAttempt.requestBody,
5428
6436
  });
5429
6437
  if (r.status !== 200) {
5430
6438
  const error = await r.text();
5431
6439
  const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
5432
- return UnsuccessfulStart(reason);
6440
+ throw reason;
5433
6441
  }
5434
6442
  if (!this.worker && !this.insideIframe) {
5435
6443
  const reason = 'no worker found after start request (this should not happen in real world)';
5436
- this.signalError(reason, []);
5437
- return UnsuccessfulStart(reason);
6444
+ throw new Error(reason);
5438
6445
  }
5439
6446
  const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {}, protocolVersion, } = await r.json();
5440
- const { token, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
6447
+ const { token, assistToken, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
5441
6448
  const { id: userUUID } = device;
5442
6449
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5443
6450
  const { beacon: beaconSizeLimit, compressAt: compressionThreshold } = limits;
5444
6451
  const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS, framesSupport, } = canvas;
5445
6452
  const socketOnly = flags?.socketOnly;
5446
6453
  if (typeof token !== 'string' ||
6454
+ (typeof assistToken !== 'string' && typeof assistToken !== 'undefined') ||
5447
6455
  typeof userUUID !== 'string' ||
5448
6456
  (typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined') ||
5449
6457
  typeof sessionID !== 'string' ||
5450
6458
  typeof delay !== 'number' ||
5451
6459
  (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
5452
6460
  const reason = `Incorrect server response: ${JSON.stringify(r)}`;
5453
- this.signalError(reason, []);
5454
- return UnsuccessfulStart(reason);
6461
+ throw new Error(reason);
5455
6462
  }
6463
+ this.crossDomainCanvasConfig = {
6464
+ enabled: canvasEnabled === true,
6465
+ ...(['low', 'medium', 'high'].includes(canvasQuality) ? { quality: canvasQuality } : {}),
6466
+ ...(typeof canvasFPS === 'number' ? { fps: canvasFPS } : {}),
6467
+ ...(typeof framesSupport === 'boolean' ? { framesSupport } : {}),
6468
+ };
5456
6469
  this.delay = delay;
5457
6470
  this.session.setSessionSecret(token, this.projectKey);
6471
+ this.clearBootstrapAttempt('recording');
5458
6472
  if (sessionToken && sessionToken !== token) {
5459
6473
  this.bc?.postMessage({
5460
6474
  type: proto.reset,
@@ -5495,7 +6509,7 @@ class App {
5495
6509
  Object.entries(this.session.getInfo().metadata).forEach(([key, value]) => this.send(Metadata(key, value)));
5496
6510
  this.localStorage.setItem(this.options.local_uuid_key, userUUID);
5497
6511
  this.compressionThreshold = compressionThreshold;
5498
- const onStartInfo = { sessionToken: token, userUUID, sessionID };
6512
+ const onStartInfo = { sessionToken: token, assistToken, userUUID, sessionID };
5499
6513
  // TODO: start as early as possible (before receiving the token)
5500
6514
  /** after start */
5501
6515
  this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
@@ -5543,7 +6557,6 @@ class App {
5543
6557
  }
5544
6558
  catch (reason) {
5545
6559
  this.stop();
5546
- this.session.reset();
5547
6560
  if (!reason) {
5548
6561
  console.error('Unknown error during start');
5549
6562
  this.signalError('Unknown error', []);
@@ -5569,6 +6582,23 @@ class App {
5569
6582
  }, 100);
5570
6583
  });
5571
6584
  }
6585
+ async waitCrossDomainStart(timeoutMs = 15000) {
6586
+ if (this.canStart)
6587
+ return true;
6588
+ return new Promise((resolve) => {
6589
+ const interval = setInterval(() => {
6590
+ if (!this.canStart)
6591
+ return;
6592
+ clearInterval(interval);
6593
+ clearTimeout(timeout);
6594
+ resolve(true);
6595
+ }, 100);
6596
+ const timeout = setTimeout(() => {
6597
+ clearInterval(interval);
6598
+ resolve(false);
6599
+ }, timeoutMs);
6600
+ });
6601
+ }
5572
6602
  async waitStarted() {
5573
6603
  return this.waitStatus(ActivityState.Active);
5574
6604
  }
@@ -5596,20 +6626,35 @@ class App {
5596
6626
  return Promise.resolve(UnsuccessfulStart(reason));
5597
6627
  }
5598
6628
  if (this.insideIframe) {
6629
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
6630
+ return UnsuccessfulStart('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
6631
+ }
5599
6632
  this.signalIframeTracker();
5600
6633
  }
5601
- if (!document.hidden) {
6634
+ const startWhenReady = async () => {
6635
+ if (this.insideIframe) {
6636
+ const ready = await this.waitCrossDomainStart();
6637
+ if (!ready) {
6638
+ this.crossDomainConnectionRequested = false;
6639
+ this.crossDomainResumeAfterHandshake = false;
6640
+ this.clearCrossDomainFrameAuthorization();
6641
+ return UnsuccessfulStart('Cross-domain iframe handshake timed out.');
6642
+ }
6643
+ return this.startCrossDomainFrame(args[0]);
6644
+ }
5602
6645
  await this.waitStart();
5603
6646
  return this._start(...args);
6647
+ };
6648
+ if (!document.hidden) {
6649
+ return startWhenReady();
5604
6650
  }
5605
6651
  else {
5606
6652
  return new Promise((resolve) => {
5607
6653
  const onVisibilityChange = async () => {
5608
6654
  if (!document.hidden) {
5609
- await this.waitStart();
5610
6655
  // eslint-disable-next-line
5611
6656
  document.removeEventListener('visibilitychange', onVisibilityChange);
5612
- resolve(this._start(...args));
6657
+ resolve(startWhenReady());
5613
6658
  }
5614
6659
  };
5615
6660
  // eslint-disable-next-line
@@ -5670,6 +6715,11 @@ class App {
5670
6715
  this.debug.log('OxvoSessions tracking stopped.');
5671
6716
  }
5672
6717
  }
6718
+ if (this.insideIframe) {
6719
+ this.crossDomainConnectionRequested = false;
6720
+ this.crossDomainResumeAfterHandshake = false;
6721
+ this.clearCrossDomainFrameAuthorization();
6722
+ }
5673
6723
  }
5674
6724
  }
5675
6725
 
@@ -9229,7 +10279,7 @@ class ConstantProperties {
9229
10279
  user_id: this.user_id,
9230
10280
  distinct_id: this.deviceId,
9231
10281
  sdk_edition: 'web',
9232
- sdk_version: '7.3.0',
10282
+ sdk_version: '7.3.4',
9233
10283
  timezone: getUTCOffsetString(),
9234
10284
  search_engine: this.searchEngine,
9235
10285
  };
@@ -9812,6 +10862,8 @@ class Batcher {
9812
10862
  }
9813
10863
 
9814
10864
  const STORAGEKEY = '__or_sdk_analytics_token';
10865
+ const START_ATTEMPT_STORAGE_KEY = '__or_sdk_analytics_start_attempt';
10866
+ const START_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
9815
10867
  const normalizeIngestPoint = (ingest) => {
9816
10868
  const base = ingest.replace(/\/$/, '');
9817
10869
  return base.endsWith('/oxvo') ? base : `${base}/oxvo`;
@@ -9835,6 +10887,7 @@ class Analytics {
9835
10887
  constructor(options) {
9836
10888
  this.token = null;
9837
10889
  this.standalone = false;
10890
+ this.pendingStartAttempt = null;
9838
10891
  this._getToken = () => {
9839
10892
  if (this.standalone) {
9840
10893
  return this.token;
@@ -9878,9 +10931,14 @@ class Analytics {
9878
10931
  timezone: defaultFields.timezone,
9879
10932
  search_engine: defaultFields.search_engine,
9880
10933
  };
10934
+ const attempt = this.startAttempt(JSON.stringify(data));
9881
10935
  const resp = await fetch(apiEdp, {
9882
10936
  method: 'POST',
9883
- body: JSON.stringify(data),
10937
+ headers: {
10938
+ 'Content-Type': 'application/json',
10939
+ 'Idempotency-Key': attempt.idempotencyKey,
10940
+ },
10941
+ body: attempt.requestBody,
9884
10942
  });
9885
10943
  if (!resp.ok) {
9886
10944
  throw new Error(`HTTP error! status: ${resp.status}`);
@@ -9889,6 +10947,7 @@ class Analytics {
9889
10947
  if (result.token) {
9890
10948
  this.token = result.token;
9891
10949
  this.sessionStorage.setItem(STORAGEKEY, result.token);
10950
+ this.clearStartAttempt();
9892
10951
  }
9893
10952
  else {
9894
10953
  throw new Error('No token received from server');
@@ -9915,6 +10974,7 @@ class Analytics {
9915
10974
  if (this.standalone) {
9916
10975
  this.token = null;
9917
10976
  this.sessionStorage.setItem(STORAGEKEY, '');
10977
+ this.clearStartAttempt();
9918
10978
  }
9919
10979
  };
9920
10980
  /**
@@ -9953,6 +11013,91 @@ class Analytics {
9953
11013
  this.init();
9954
11014
  }
9955
11015
  }
11016
+ loadStartAttempt() {
11017
+ if (this.isValidStartAttempt(this.pendingStartAttempt)) {
11018
+ return this.pendingStartAttempt;
11019
+ }
11020
+ this.pendingStartAttempt = null;
11021
+ let serialized;
11022
+ try {
11023
+ serialized = this.sessionStorage.getItem(START_ATTEMPT_STORAGE_KEY);
11024
+ }
11025
+ catch {
11026
+ return null;
11027
+ }
11028
+ if (!serialized || serialized.length > 1024 * 1024) {
11029
+ return null;
11030
+ }
11031
+ try {
11032
+ const attempt = JSON.parse(serialized);
11033
+ if (this.isValidStartAttempt(attempt)) {
11034
+ this.pendingStartAttempt = attempt;
11035
+ return attempt;
11036
+ }
11037
+ }
11038
+ catch {
11039
+ // Treat corrupt storage as an absent pending attempt.
11040
+ }
11041
+ return null;
11042
+ }
11043
+ isValidStartAttempt(value) {
11044
+ if (typeof value !== 'object' || value === null) {
11045
+ return false;
11046
+ }
11047
+ const attempt = value;
11048
+ const currentTime = Date.now();
11049
+ if (attempt.version !== 1 ||
11050
+ attempt.projectKey !== this.projectKey ||
11051
+ typeof attempt.createdAt !== 'number' ||
11052
+ !Number.isFinite(attempt.createdAt) ||
11053
+ attempt.createdAt > currentTime ||
11054
+ currentTime - attempt.createdAt >= START_ATTEMPT_TTL_MS ||
11055
+ typeof attempt.idempotencyKey !== 'string' ||
11056
+ attempt.idempotencyKey.length < 8 ||
11057
+ attempt.idempotencyKey.length > 128 ||
11058
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
11059
+ typeof attempt.requestBody !== 'string' ||
11060
+ attempt.requestBody.length > 1024 * 1024) {
11061
+ return false;
11062
+ }
11063
+ try {
11064
+ const request = JSON.parse(attempt.requestBody);
11065
+ return request.projectKey === this.projectKey;
11066
+ }
11067
+ catch {
11068
+ return false;
11069
+ }
11070
+ }
11071
+ startAttempt(requestBody) {
11072
+ const existing = this.loadStartAttempt();
11073
+ if (existing) {
11074
+ return existing;
11075
+ }
11076
+ const attempt = {
11077
+ version: 1,
11078
+ projectKey: this.projectKey,
11079
+ createdAt: Date.now(),
11080
+ idempotencyKey: `insights:start:${generateRandomId(40)}`,
11081
+ requestBody,
11082
+ };
11083
+ this.pendingStartAttempt = attempt;
11084
+ try {
11085
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, JSON.stringify(attempt));
11086
+ }
11087
+ catch {
11088
+ // The in-memory copy still protects retries within this Analytics instance.
11089
+ }
11090
+ return attempt;
11091
+ }
11092
+ clearStartAttempt() {
11093
+ this.pendingStartAttempt = null;
11094
+ try {
11095
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, '');
11096
+ }
11097
+ catch {
11098
+ // The in-memory copy is still cleared when storage is unavailable.
11099
+ }
11100
+ }
9956
11101
  }
9957
11102
 
9958
11103
  const Messages = _Messages;
@@ -9999,7 +11144,7 @@ class API {
9999
11144
  this.signalStartIssue = (reason, missingApi) => {
10000
11145
  const doNotTrack = this.checkDoNotTrack();
10001
11146
  console.log("Tracker couldn't start due to:", JSON.stringify({
10002
- trackerVersion: '7.3.0',
11147
+ trackerVersion: '7.3.4',
10003
11148
  siteKey: this.options.siteKey,
10004
11149
  doNotTrack,
10005
11150
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,