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