@openreplay/tracker 11.0.6 → 12.0.0

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 (113) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/cjs/app/index.d.ts +84 -6
  3. package/cjs/app/index.js +427 -58
  4. package/cjs/app/logger.d.ts +7 -17
  5. package/cjs/app/logger.js +11 -19
  6. package/cjs/app/messages.gen.d.ts +2 -0
  7. package/cjs/app/messages.gen.js +20 -1
  8. package/cjs/app/observer/iframe_observer.js +4 -1
  9. package/cjs/app/observer/shadow_root_observer.js +4 -1
  10. package/cjs/app/observer/top_observer.js +7 -4
  11. package/cjs/common/interaction.d.ts +5 -2
  12. package/cjs/common/messages.gen.d.ts +17 -2
  13. package/cjs/index.d.ts +45 -2
  14. package/cjs/index.js +237 -106
  15. package/cjs/modules/Network/beaconProxy.js +4 -1
  16. package/cjs/modules/Network/fetchProxy.js +24 -1
  17. package/cjs/modules/Network/index.js +6 -3
  18. package/cjs/modules/Network/xhrProxy.js +24 -1
  19. package/cjs/modules/conditionsManager.d.ts +84 -0
  20. package/cjs/modules/conditionsManager.js +343 -0
  21. package/cjs/modules/exception.js +4 -1
  22. package/cjs/modules/featureFlags.d.ts +1 -1
  23. package/cjs/modules/featureFlags.js +36 -46
  24. package/cjs/modules/network.js +5 -2
  25. package/cjs/modules/tagWatcher.d.ts +21 -0
  26. package/cjs/modules/tagWatcher.js +77 -0
  27. package/cjs/modules/userTesting/index.js +30 -4
  28. package/cjs/modules/userTesting/recorder.js +71 -88
  29. package/coverage/clover.xml +577 -544
  30. package/coverage/coverage-final.json +8 -8
  31. package/coverage/lcov-report/index.html +28 -28
  32. package/coverage/lcov-report/main/app/canvas.ts.html +97 -46
  33. package/coverage/lcov-report/main/app/guards.ts.html +1 -1
  34. package/coverage/lcov-report/main/app/index.html +19 -19
  35. package/coverage/lcov-report/main/app/index.ts.html +62 -35
  36. package/coverage/lcov-report/main/app/logger.ts.html +1 -1
  37. package/coverage/lcov-report/main/app/messages.gen.ts.html +32 -5
  38. package/coverage/lcov-report/main/app/nodes.ts.html +17 -5
  39. package/coverage/lcov-report/main/app/observer/iframe_observer.ts.html +1 -1
  40. package/coverage/lcov-report/main/app/observer/iframe_offsets.ts.html +1 -1
  41. package/coverage/lcov-report/main/app/observer/index.html +1 -1
  42. package/coverage/lcov-report/main/app/observer/shadow_root_observer.ts.html +1 -1
  43. package/coverage/lcov-report/main/app/observer/top_observer.ts.html +1 -1
  44. package/coverage/lcov-report/main/app/sanitizer.ts.html +1 -1
  45. package/coverage/lcov-report/main/app/session.ts.html +1 -1
  46. package/coverage/lcov-report/main/app/ticker.ts.html +1 -1
  47. package/coverage/lcov-report/main/index.html +9 -9
  48. package/coverage/lcov-report/main/index.ts.html +27 -6
  49. package/coverage/lcov-report/main/modules/Network/beaconProxy.ts.html +1 -1
  50. package/coverage/lcov-report/main/modules/Network/fetchProxy.ts.html +1 -1
  51. package/coverage/lcov-report/main/modules/Network/index.html +1 -1
  52. package/coverage/lcov-report/main/modules/Network/index.ts.html +1 -1
  53. package/coverage/lcov-report/main/modules/Network/networkMessage.ts.html +1 -1
  54. package/coverage/lcov-report/main/modules/Network/utils.ts.html +1 -1
  55. package/coverage/lcov-report/main/modules/Network/xhrProxy.ts.html +1 -1
  56. package/coverage/lcov-report/main/modules/attributeSender.ts.html +1 -1
  57. package/coverage/lcov-report/main/modules/axiosSpy.ts.html +1 -1
  58. package/coverage/lcov-report/main/modules/conditionsManager.ts.html +92 -38
  59. package/coverage/lcov-report/main/modules/connection.ts.html +1 -1
  60. package/coverage/lcov-report/main/modules/console.ts.html +1 -1
  61. package/coverage/lcov-report/main/modules/constructedStyleSheets.ts.html +1 -1
  62. package/coverage/lcov-report/main/modules/cssrules.ts.html +1 -1
  63. package/coverage/lcov-report/main/modules/exception.ts.html +1 -1
  64. package/coverage/lcov-report/main/modules/featureFlags.ts.html +1 -1
  65. package/coverage/lcov-report/main/modules/focus.ts.html +1 -1
  66. package/coverage/lcov-report/main/modules/fonts.ts.html +1 -1
  67. package/coverage/lcov-report/main/modules/img.ts.html +1 -1
  68. package/coverage/lcov-report/main/modules/index.html +21 -21
  69. package/coverage/lcov-report/main/modules/input.ts.html +1 -1
  70. package/coverage/lcov-report/main/modules/mouse.ts.html +1 -1
  71. package/coverage/lcov-report/main/modules/network.ts.html +1 -1
  72. package/coverage/lcov-report/main/modules/performance.ts.html +1 -1
  73. package/coverage/lcov-report/main/modules/scroll.ts.html +1 -1
  74. package/coverage/lcov-report/main/modules/selection.ts.html +1 -1
  75. package/coverage/lcov-report/main/modules/tabs.ts.html +1 -1
  76. package/coverage/lcov-report/main/modules/tagWatcher.ts.html +54 -27
  77. package/coverage/lcov-report/main/modules/timing.ts.html +1 -1
  78. package/coverage/lcov-report/main/modules/userTesting/SignalManager.ts.html +1 -1
  79. package/coverage/lcov-report/main/modules/userTesting/dnd.ts.html +1 -1
  80. package/coverage/lcov-report/main/modules/userTesting/index.html +1 -1
  81. package/coverage/lcov-report/main/modules/userTesting/index.ts.html +1 -1
  82. package/coverage/lcov-report/main/modules/userTesting/recorder.ts.html +1 -1
  83. package/coverage/lcov-report/main/modules/userTesting/styles.ts.html +1 -1
  84. package/coverage/lcov-report/main/modules/userTesting/utils.ts.html +1 -1
  85. package/coverage/lcov-report/main/modules/viewport.ts.html +1 -1
  86. package/coverage/lcov-report/main/utils.ts.html +1 -1
  87. package/coverage/lcov-report/webworker/BatchWriter.ts.html +1 -1
  88. package/coverage/lcov-report/webworker/MessageEncoder.gen.ts.html +17 -5
  89. package/coverage/lcov-report/webworker/PrimitiveEncoder.ts.html +1 -1
  90. package/coverage/lcov-report/webworker/QueueSender.ts.html +1 -1
  91. package/coverage/lcov-report/webworker/index.html +7 -7
  92. package/coverage/lcov-report/webworker/index.ts.html +1 -1
  93. package/coverage/lcov.info +1100 -1033
  94. package/lib/app/index.d.ts +84 -6
  95. package/lib/app/index.js +387 -44
  96. package/lib/app/logger.d.ts +7 -17
  97. package/lib/app/logger.js +11 -19
  98. package/lib/app/messages.gen.d.ts +2 -0
  99. package/lib/app/messages.gen.js +17 -0
  100. package/lib/common/interaction.d.ts +5 -2
  101. package/lib/common/messages.gen.d.ts +17 -2
  102. package/lib/common/tsconfig.tsbuildinfo +1 -1
  103. package/lib/index.d.ts +45 -2
  104. package/lib/index.js +191 -86
  105. package/lib/modules/conditionsManager.d.ts +84 -0
  106. package/lib/modules/conditionsManager.js +340 -0
  107. package/lib/modules/featureFlags.d.ts +1 -1
  108. package/lib/modules/featureFlags.js +36 -46
  109. package/lib/modules/tagWatcher.d.ts +21 -0
  110. package/lib/modules/tagWatcher.js +74 -0
  111. package/lib/modules/userTesting/recorder.js +71 -88
  112. package/package.json +1 -1
  113. package/tsconfig-base.json +3 -2
package/lib/app/index.js CHANGED
@@ -1,4 +1,7 @@
1
- import { Timestamp, Metadata, UserID, TabChange, TabData } from './messages.gen.js';
1
+ import ConditionsManager from '../modules/conditionsManager.js';
2
+ import FeatureFlags from '../modules/featureFlags.js';
3
+ import { TagTrigger } from './messages.gen.js';
4
+ import { Timestamp, Metadata, UserID, TabChange, TabData, WSChannel, } from './messages.gen.js';
2
5
  import { now, adjustTimeOrigin, deprecationWarn, inIframe, createEventListener, deleteEventListener, requestIdleCb, } from '../utils.js';
3
6
  import Nodes from './nodes.js';
4
7
  import Observer from './observer/top_observer.js';
@@ -11,8 +14,10 @@ import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js';
11
14
  import AttributeSender from '../modules/attributeSender.js';
12
15
  import CanvasRecorder from './canvas.js';
13
16
  import UserTestManager from '../modules/userTesting/index.js';
17
+ import TagWatcher from '../modules/tagWatcher.js';
14
18
  const CANCELED = 'canceled';
15
19
  const uxtStorageKey = 'or_uxt_active';
20
+ const bufferStorageKey = 'or_buffer_1';
16
21
  const START_ERROR = ':(';
17
22
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
18
23
  const SuccessfulStart = (body) => (Object.assign(Object.assign({}, body), { success: true }));
@@ -21,6 +26,7 @@ var ActivityState;
21
26
  ActivityState[ActivityState["NotActive"] = 0] = "NotActive";
22
27
  ActivityState[ActivityState["Starting"] = 1] = "Starting";
23
28
  ActivityState[ActivityState["Active"] = 2] = "Active";
29
+ ActivityState[ActivityState["ColdStart"] = 3] = "ColdStart";
24
30
  })(ActivityState || (ActivityState = {}));
25
31
  // TODO: use backendHost only
26
32
  export const DEFAULT_INGEST_POINT = 'https://api.openreplay.com/ingest';
@@ -32,28 +38,58 @@ function getTimezone() {
32
38
  return `UTC${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
33
39
  }
34
40
  export default class App {
35
- constructor(projectKey, sessionToken, options) {
41
+ constructor(projectKey, sessionToken, options, signalError) {
36
42
  var _a, _b;
43
+ this.signalError = signalError;
37
44
  this.messages = [];
45
+ /**
46
+ * we need 2 buffers, so we don't lose anything
47
+ * @read coldStart implementation
48
+ * */
49
+ this.bufferedMessages1 = [];
50
+ this.bufferedMessages2 = [];
38
51
  this.startCallbacks = [];
39
52
  this.stopCallbacks = [];
40
53
  this.commitCallbacks = [];
41
54
  this.activityState = ActivityState.NotActive;
42
- this.version = '11.0.6'; // TODO: version compatability check inside each plugin.
55
+ this.version = '12.0.0'; // TODO: version compatability check inside each plugin.
43
56
  this.compressionThreshold = 24 * 1000;
44
57
  this.restartAttempts = 0;
45
58
  this.bc = null;
46
59
  this.canvasRecorder = null;
60
+ this.conditionsManager = null;
47
61
  this._usingOldFetchPlugin = false;
62
+ this.coldStartCommitN = 0;
48
63
  this.delay = 0;
64
+ this.coldInterval = null;
65
+ this.orderNumber = 0;
66
+ this.coldStartTs = 0;
67
+ this.singleBuffer = false;
68
+ this.onSessionSent = () => {
69
+ return;
70
+ };
49
71
  this.restartCanvasTracking = () => {
50
72
  var _a;
51
73
  (_a = this.canvasRecorder) === null || _a === void 0 ? void 0 : _a.restartTracking();
52
74
  };
75
+ this.flushBuffer = async (buffer) => {
76
+ return new Promise((res) => {
77
+ let ended = false;
78
+ const messagesBatch = [buffer.shift()];
79
+ while (!ended) {
80
+ const nextMsg = buffer[0];
81
+ if (!nextMsg || nextMsg[0] === 0 /* MType.Timestamp */) {
82
+ ended = true;
83
+ }
84
+ else {
85
+ messagesBatch.push(buffer.shift());
86
+ }
87
+ }
88
+ this.postToWorker(messagesBatch);
89
+ res(null);
90
+ });
91
+ };
53
92
  this.onUxtCb = [];
54
- // if (options.onStart !== undefined) {
55
- // deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)")
56
- // } ?? maybe onStart is good
57
93
  this.contextId = Math.random().toString(36).slice(2);
58
94
  this.projectKey = projectKey;
59
95
  this.networkOptions = options.network;
@@ -67,9 +103,9 @@ export default class App {
67
103
  local_uuid_key: '__openreplay_uuid',
68
104
  ingestPoint: DEFAULT_INGEST_POINT,
69
105
  resourceBaseHref: null,
70
- verbose: false,
71
106
  __is_snippet: false,
72
107
  __debug_report_edp: null,
108
+ __debug__: LogLevel.Silent,
73
109
  __save_canvas_locally: false,
74
110
  localStorage: null,
75
111
  sessionStorage: null,
@@ -90,9 +126,12 @@ export default class App {
90
126
  this.ticker = new Ticker(this);
91
127
  this.ticker.attach(() => this.commit());
92
128
  this.debug = new Logger(this.options.__debug__);
93
- this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent);
94
129
  this.session = new Session(this, this.options);
95
130
  this.attributeSender = new AttributeSender(this, Boolean(this.options.disableStringDict));
131
+ this.featureFlags = new FeatureFlags(this);
132
+ this.tagWatcher = new TagWatcher(this.sessionStorage, this.debug.error, (tag) => {
133
+ this.send(TagTrigger(tag));
134
+ });
96
135
  this.session.attachUpdateCallback(({ userID, metadata }) => {
97
136
  if (userID != null) {
98
137
  // TODO: nullable userID
@@ -107,7 +146,7 @@ export default class App {
107
146
  this.session.applySessionHash(sessionToken);
108
147
  }
109
148
  try {
110
- this.worker = new Worker(URL.createObjectURL(new Blob(['"use strict";class t{constructor(t,s,i,e=10,n=1e3,h){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.ingestURL=t+"/v1/web/i",this.isCompressing=void 0!==h}authorise(t){this.token=t,this.busy||this.sendNext()}push(t){this.busy||!this.token?this.queue.push(t):(this.busy=!0,this.isCompressing&&this.onCompress?this.onCompress(t):this.sendBatch(t))}sendNext(){const t=this.queue.shift();t?(this.busy=!0,this.isCompressing&&this.onCompress?this.onCompress(t):this.sendBatch(t)):this.busy=!1}retry(t,s){this.attemptsCount>=this.MAX_ATTEMPTS_COUNT?this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`):(this.attemptsCount++,setTimeout((()=>this.sendBatch(t,s)),this.ATTEMPT_TIMEOUT*this.attemptsCount))}sendBatch(t,s){this.busy=!0;const i={Authorization:`Bearer ${this.token}`};s&&(i["Content-Encoding"]="gzip"),null!==this.token?fetch(this.ingestURL,{body:t,method:"POST",headers:i,keepalive:t.length<65536}).then((i=>{if(401===i.status)return this.busy=!1,void this.onUnauthorised();i.status>=400?this.retry(t,s):(this.attemptsCount=0,this.sendNext())})).catch((i=>{console.warn("OpenReplay:",i),this.retry(t,s)})):setTimeout((()=>{this.sendBatch(t,s)}),500)}sendCompressed(t){this.sendBatch(t,!0)}sendUncompressed(t){this.sendBatch(t,!1)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s="function"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class i{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const i=s.encode(t),e=i.byteLength;return!(!this.uint(e)||this.offset+e>this.size)&&(this.data.set(i,this.offset),this.offset+=e,!0)}reset(){this.offset=0,this.checkpointOffset=0}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class e extends i{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 38:case 70:case 75:case 76:case 77:case 82:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17: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 63:case 64:case 79: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 37:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3]);case 39:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);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:return this.int(t[1]);case 59:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6])&&this.string(t[7]);case 67:case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 69:return this.uint(t[1])&&this.uint(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 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])}}}class n{constructor(t,s,i,n,h){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=n,this.tabId=h,this.nextIndex=0,this.beaconSize=2e5,this.encoder=new e(this.beaconSize),this.sizeBuffer=new Uint8Array(3),this.isEmpty=!0,this.beaconSizeLimit=1e6,this.prepare()}writeType(t){return this.encoder.uint(t[0])}writeFields(t){return this.encoder.encode(t)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}prepare(){if(!this.encoder.isEmpty)return;const t=[81,1,this.pageNo,this.nextIndex,this.timestamp,this.url],s=[118,this.tabId];this.writeType(t),this.writeFields(t),this.writeWithSize(s),this.isEmpty=!0}writeWithSize(t){const s=this.encoder;if(!this.writeType(t)||!s.skip(3))return!1;const i=s.getCurrentOffset(),e=this.writeFields(t);if(e){const e=s.getCurrentOffset()-i;if(e>16777215)return console.warn("OpenReplay: max message size overflow."),!1;this.writeSizeAt(e,i-3),s.checkpoint(),this.isEmpty=this.isEmpty&&0===t[0],this.nextIndex++}return e}setBeaconSizeLimit(t){this.beaconSizeLimit=t}writeMessage(t){0===t[0]&&(this.timestamp=t[1]),4===t[0]&&(this.url=t[1]),this.writeWithSize(t)||(this.finaliseBatch(),this.writeWithSize(t)||(this.encoder=new e(this.beaconSizeLimit),this.prepare(),this.writeWithSize(t)?this.finaliseBatch():console.warn("OpenReplay: beacon size overflow. Skipping large message.",t,this),this.encoder=new e(this.beaconSize),this.prepare()))}finaliseBatch(){if(this.isEmpty)return;const t=this.encoder.flush();this.onBatch(t),this.prepare()}clean(){this.encoder.reset()}}var h;!function(t){t[t.NotActive=0]="NotActive",t[t.Starting=1]="Starting",t[t.Stopping=2]="Stopping",t[t.Active=3]="Active",t[t.Stopped=4]="Stopped"}(h||(h={}));let r=null,a=null,u=h.NotActive;function o(){a&&a.finaliseBatch()}function c(){u=h.Stopping,null!==d&&(clearInterval(d),d=null),a&&(a.clean(),a=null),r&&(r.clean(),setTimeout((()=>{r=null}),20)),setTimeout((()=>{u=h.NotActive}),100)}function p(){u!==h.Stopped&&(postMessage("restart"),c())}let f,d=null;self.onmessage=({data:s})=>{if(null!=s){if("stop"===s)return o(),c(),u=h.Stopped;if("forceFlushBatch"!==s){if(!Array.isArray(s)){if("compressed"===s.type){if(!r)return console.debug("OR WebWorker: sender not initialised. Compressed batch."),void p();r.sendCompressed(s.batch)}if("uncompressed"===s.type){if(!r)return console.debug("OR WebWorker: sender not initialised. Uncompressed batch."),void p();r.sendUncompressed(s.batch)}return"start"===s.type?(u=h.Starting,r=new t(s.ingestPoint,(()=>{p()}),(t=>{!function(t){postMessage({type:"failure",reason:t}),c()}(t)}),s.connAttemptCount,s.connAttemptGap,(t=>{postMessage({type:"compress",batch:t},[t.buffer])})),a=new n(s.pageNo,s.timestamp,s.url,(t=>r&&r.push(t)),s.tabId),null===d&&(d=setInterval(o,1e4)),u=h.Active):"auth"===s.type?r?a?(r.authorise(s.token),void(s.beaconSizeLimit&&a.setBeaconSizeLimit(s.beaconSizeLimit))):(console.debug("OR WebWorker: writer not initialised. Received auth."),void p()):(console.debug("OR WebWorker: sender not initialised. Received auth."),void p()):void 0}if(null!==a){const t=a;s.forEach((s=>{55===s[0]&&(s[1]?f=setTimeout((()=>p()),18e5):clearTimeout(f)),t.writeMessage(s)}))}a||(postMessage("not_init"),p())}else o()}else o()};'], { type: 'text/javascript' })));
149
+ this.worker = new Worker(URL.createObjectURL(new Blob(['"use strict";class t{constructor(t,s,i,e=10,n=1e3,h){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.ingestURL=t+"/v1/web/i",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t){this.busy||!this.token?this.queue.push(t):(this.busy=!0,this.isCompressing&&this.onCompress?this.onCompress(t):this.sendBatch(t))}sendNext(){const t=this.queue.shift();t?(this.busy=!0,this.isCompressing&&this.onCompress?this.onCompress(t):this.sendBatch(t)):this.busy=!1}retry(t,s){this.attemptsCount>=this.MAX_ATTEMPTS_COUNT?this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`):(this.attemptsCount++,setTimeout((()=>this.sendBatch(t,s)),this.ATTEMPT_TIMEOUT*this.attemptsCount))}sendBatch(t,s){this.busy=!0;const i={Authorization:`Bearer ${this.token}`};s&&(i["Content-Encoding"]="gzip"),null!==this.token?fetch(this.ingestURL,{body:t,method:"POST",headers:i,keepalive:t.length<65536}).then((i=>{if(401===i.status)return this.busy=!1,void this.onUnauthorised();i.status>=400?this.retry(t,s):(this.attemptsCount=0,this.sendNext())})).catch((i=>{console.warn("OpenReplay:",i),this.retry(t,s)})):setTimeout((()=>{this.sendBatch(t,s)}),500)}sendCompressed(t){this.sendBatch(t,!0)}sendUncompressed(t){this.sendBatch(t,!1)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s="function"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class i{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const i=s.encode(t),e=i.byteLength;return!(!this.uint(e)||this.offset+e>this.size)&&(this.data.set(i,this.offset),this.offset+=e,!0)}reset(){this.offset=0,this.checkpointOffset=0}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class e extends i{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 38:case 70:case 75:case 76:case 77:case 82:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17: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 63:case 64:case 79: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 37:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3]);case 39:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);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 59:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6])&&this.string(t[7]);case 67:case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 69:return this.uint(t[1])&&this.uint(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 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])}}}class n{constructor(t,s,i,n,h,r){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=n,this.tabId=h,this.onOfflineEnd=r,this.nextIndex=0,this.beaconSize=2e5,this.encoder=new e(this.beaconSize),this.sizeBuffer=new Uint8Array(3),this.isEmpty=!0,this.beaconSizeLimit=1e6,this.prepare()}writeType(t){return this.encoder.uint(t[0])}writeFields(t){return this.encoder.encode(t)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}prepare(){if(!this.encoder.isEmpty)return;const t=[81,1,this.pageNo,this.nextIndex,this.timestamp,this.url],s=[118,this.tabId];this.writeType(t),this.writeFields(t),this.writeWithSize(s),this.isEmpty=!0}writeWithSize(t){const s=this.encoder;if(!this.writeType(t)||!s.skip(3))return!1;const i=s.getCurrentOffset(),e=this.writeFields(t);if(e){const e=s.getCurrentOffset()-i;if(e>16777215)return console.warn("OpenReplay: max message size overflow."),!1;this.writeSizeAt(e,i-3),s.checkpoint(),this.isEmpty=this.isEmpty&&0===t[0],this.nextIndex++}return e}setBeaconSizeLimit(t){this.beaconSizeLimit=t}writeMessage(t){if("q_end"===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),4===t[0]&&(this.url=t[1]),this.writeWithSize(t)||(this.finaliseBatch(),this.writeWithSize(t)||(this.encoder=new e(this.beaconSizeLimit),this.prepare(),this.writeWithSize(t)?this.finaliseBatch():console.warn("OpenReplay: beacon size overflow. Skipping large message.",t,this),this.encoder=new e(this.beaconSize),this.prepare()))}finaliseBatch(){if(this.isEmpty)return;const t=this.encoder.flush();this.onBatch(t),this.prepare()}clean(){this.encoder.reset()}}var h;!function(t){t[t.NotActive=0]="NotActive",t[t.Starting=1]="Starting",t[t.Stopping=2]="Stopping",t[t.Active=3]="Active",t[t.Stopped=4]="Stopped"}(h||(h={}));let r=null,u=null,a=h.NotActive;function o(){u&&u.finaliseBatch()}function c(){a=h.Stopping,null!==g&&(clearInterval(g),g=null),u&&(u.clean(),u=null),r&&(r.clean(),setTimeout((()=>{r=null}),20)),setTimeout((()=>{a=h.NotActive}),100)}function p(){a!==h.Stopped&&(postMessage("restart"),c())}let f,g=null;self.onmessage=({data:s})=>{if(null!=s){if("stop"===s)return o(),c(),a=h.Stopped;if("forceFlushBatch"!==s){if(!Array.isArray(s)){if("compressed"===s.type){if(!r)return console.debug("OR WebWorker: sender not initialised. Compressed batch."),void p();s.batch&&r.sendCompressed(s.batch)}if("uncompressed"===s.type){if(!r)return console.debug("OR WebWorker: sender not initialised. Uncompressed batch."),void p();s.batch&&r.sendUncompressed(s.batch)}return"start"===s.type?(a=h.Starting,r=new t(s.ingestPoint,(()=>{p()}),(t=>{!function(t){postMessage({type:"failure",reason:t}),c()}(t)}),s.connAttemptCount,s.connAttemptGap,(t=>{postMessage({type:"compress",batch:t},[t.buffer])})),u=new n(s.pageNo,s.timestamp,s.url,(t=>{r&&r.push(t)}),s.tabId,(()=>postMessage({type:"queue_empty"}))),null===g&&(g=setInterval(o,1e4)),a=h.Active):"auth"===s.type?r?u?(r.authorise(s.token),void(s.beaconSizeLimit&&u.setBeaconSizeLimit(s.beaconSizeLimit))):(console.debug("OR WebWorker: writer not initialised. Received auth."),void p()):(console.debug("OR WebWorker: sender not initialised. Received auth."),void p()):void 0}if(u){const t=u;s.forEach((s=>{55===s[0]&&(s[1]?f=setTimeout((()=>p()),18e5):clearTimeout(f)),t.writeMessage(s)}))}else postMessage("not_init"),p()}else o()}else o()};'], { type: 'text/javascript' })));
111
150
  this.worker.onerror = (e) => {
112
151
  this._debug('webworker_error', e);
113
152
  };
@@ -139,14 +178,18 @@ export default class App {
139
178
  void this.start({}, true);
140
179
  }
141
180
  }
142
- // @ts-ignore
143
- (_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage({ type: 'compressed', batch: result });
181
+ else {
182
+ (_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage({ type: 'compressed', batch: result });
183
+ }
144
184
  });
145
185
  }
146
186
  else {
147
187
  (_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage({ type: 'uncompressed', batch: batch });
148
188
  }
149
189
  }
190
+ else if (data.type === 'queue_empty') {
191
+ this.onSessionSent();
192
+ }
150
193
  };
151
194
  const alertWorker = () => {
152
195
  if (this.worker) {
@@ -221,6 +264,7 @@ export default class App {
221
264
  this.debug.error('OpenReplay error: ', context, e);
222
265
  }
223
266
  send(message, urgent = false) {
267
+ var _a;
224
268
  if (this.activityState === ActivityState.NotActive) {
225
269
  return;
226
270
  }
@@ -234,17 +278,30 @@ export default class App {
234
278
  return;
235
279
  }
236
280
  // ====================================================
237
- this.messages.push(message);
281
+ if (this.activityState === ActivityState.ColdStart) {
282
+ this.bufferedMessages1.push(message);
283
+ if (!this.singleBuffer) {
284
+ this.bufferedMessages2.push(message);
285
+ }
286
+ (_a = this.conditionsManager) === null || _a === void 0 ? void 0 : _a.processMessage(message);
287
+ }
288
+ else {
289
+ this.messages.push(message);
290
+ }
238
291
  // TODO: commit on start if there were `urgent` sends;
239
292
  // Clarify where urgent can be used for;
240
293
  // Clarify workflow for each type of message in case it was sent before start
241
294
  // (like Fetch before start; maybe add an option "preCapture: boolean" or sth alike)
242
- // Careful: `this.delay` is equal to zero before start hense all Timestamp-s will have to be updated on start
295
+ // Careful: `this.delay` is equal to zero before start so all Timestamp-s will have to be updated on start
243
296
  if (this.activityState === ActivityState.Active && urgent) {
244
297
  this.commit();
245
298
  }
246
299
  }
247
- commit() {
300
+ /**
301
+ * Normal workflow: add timestamp and tab data to batch, then commit it
302
+ * every ~30ms
303
+ * */
304
+ _nCommit() {
248
305
  if (this.worker !== undefined && this.messages.length) {
249
306
  requestIdleCb(() => {
250
307
  var _a;
@@ -257,6 +314,36 @@ export default class App {
257
314
  });
258
315
  }
259
316
  }
317
+ /**
318
+ * Cold start: add timestamp and tab data to both batches
319
+ * every 2nd tick, ~60ms
320
+ * this will make batches a bit larger and replay will work with bigger jumps every frame
321
+ * but in turn we don't overload batch writer on session start with 1000 batches
322
+ * */
323
+ _cStartCommit() {
324
+ this.coldStartCommitN += 1;
325
+ if (this.coldStartCommitN === 2) {
326
+ this.bufferedMessages1.push(Timestamp(this.timestamp()));
327
+ this.bufferedMessages1.push(TabData(this.session.getTabId()));
328
+ this.bufferedMessages2.push(Timestamp(this.timestamp()));
329
+ this.bufferedMessages2.push(TabData(this.session.getTabId()));
330
+ this.coldStartCommitN = 0;
331
+ }
332
+ }
333
+ commit() {
334
+ if (this.activityState === ActivityState.ColdStart) {
335
+ this._cStartCommit();
336
+ }
337
+ else {
338
+ this._nCommit();
339
+ }
340
+ }
341
+ postToWorker(messages) {
342
+ var _a;
343
+ (_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage(messages);
344
+ this.commitCallbacks.forEach((cb) => cb(messages));
345
+ messages.length = 0;
346
+ }
260
347
  timestamp() {
261
348
  return now() + this.delay;
262
349
  }
@@ -390,15 +477,228 @@ export default class App {
390
477
  this.sessionStorage.removeItem(this.options.session_reset_key);
391
478
  }
392
479
  }
393
- _start(startOpts = {}, resetByWorker = false) {
480
+ checkSessionToken(forceNew) {
481
+ const lsReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
482
+ const needNewSessionID = forceNew || lsReset;
483
+ const sessionToken = this.session.getSessionToken();
484
+ return needNewSessionID || !sessionToken;
485
+ }
486
+ /**
487
+ * start buffering messages without starting the actual session, which gives
488
+ * user 30 seconds to "activate" and record session by calling `start()` on conditional trigger
489
+ * and we will then send buffered batch, so it won't get lost
490
+ * */
491
+ async coldStart(startOpts = {}, conditional) {
492
+ var _a, _b;
493
+ this.singleBuffer = false;
494
+ const second = 1000;
495
+ if (conditional) {
496
+ this.conditionsManager = new ConditionsManager(this, startOpts);
497
+ }
498
+ const isNewSession = this.checkSessionToken(startOpts.forceNew);
499
+ if (conditional) {
500
+ const r = await fetch(this.options.ingestPoint + '/v1/web/start', {
501
+ method: 'POST',
502
+ headers: {
503
+ 'Content-Type': 'application/json',
504
+ },
505
+ body: JSON.stringify(Object.assign(Object.assign({}, this.getTrackerInfo()), { timestamp: now(), doNotRecord: true, bufferDiff: 0, userID: this.session.getInfo().userID, token: undefined, deviceMemory,
506
+ jsHeapSizeLimit, timezone: getTimezone() })),
507
+ });
508
+ const {
509
+ // this token is needed to fetch conditions and flags,
510
+ // but it can't be used to record a session
511
+ token, userBrowser, userCity, userCountry, userDevice, userOS, userState, projectID, } = await r.json();
512
+ this.session.assign({ projectID });
513
+ this.session.setUserInfo({
514
+ userBrowser,
515
+ userCity,
516
+ userCountry,
517
+ userDevice,
518
+ userOS,
519
+ userState,
520
+ });
521
+ const onStartInfo = { sessionToken: token, userUUID: '', sessionID: '' };
522
+ this.startCallbacks.forEach((cb) => cb(onStartInfo));
523
+ await ((_a = this.conditionsManager) === null || _a === void 0 ? void 0 : _a.fetchConditions(projectID, token));
524
+ await this.featureFlags.reloadFlags(token);
525
+ await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
526
+ (_b = this.conditionsManager) === null || _b === void 0 ? void 0 : _b.processFlags(this.featureFlags.flags);
527
+ }
528
+ const cycle = () => {
529
+ this.orderNumber += 1;
530
+ adjustTimeOrigin();
531
+ this.coldStartTs = now();
532
+ if (this.orderNumber % 2 === 0) {
533
+ this.bufferedMessages1.length = 0;
534
+ this.bufferedMessages1.push(Timestamp(this.timestamp()));
535
+ this.bufferedMessages1.push(TabData(this.session.getTabId()));
536
+ }
537
+ else {
538
+ this.bufferedMessages2.length = 0;
539
+ this.bufferedMessages2.push(Timestamp(this.timestamp()));
540
+ this.bufferedMessages2.push(TabData(this.session.getTabId()));
541
+ }
542
+ this.stop(false);
543
+ this.activityState = ActivityState.ColdStart;
544
+ if (startOpts.sessionHash) {
545
+ this.session.applySessionHash(startOpts.sessionHash);
546
+ }
547
+ if (startOpts.forceNew) {
548
+ this.session.reset();
549
+ }
550
+ this.session.assign({
551
+ userID: startOpts.userID,
552
+ metadata: startOpts.metadata,
553
+ });
554
+ if (!isNewSession) {
555
+ this.debug.log('continuing session on new tab', this.session.getTabId());
556
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
557
+ this.send(TabChange(this.session.getTabId()));
558
+ }
559
+ this.observer.observe();
560
+ this.ticker.start();
561
+ };
562
+ this.coldInterval = setInterval(() => {
563
+ cycle();
564
+ }, 30 * second);
565
+ cycle();
566
+ }
567
+ /**
568
+ * Starts offline session recording
569
+ * @param {Object} startOpts - options for session start, same as .start()
570
+ * @param {Function} onSessionSent - callback that will be called once session is fully sent
571
+ * */
572
+ offlineRecording(startOpts = {}, onSessionSent) {
573
+ this.onSessionSent = onSessionSent;
574
+ this.singleBuffer = true;
575
+ const isNewSession = this.checkSessionToken(startOpts.forceNew);
576
+ adjustTimeOrigin();
577
+ this.coldStartTs = now();
578
+ const saverBuffer = this.localStorage.getItem(bufferStorageKey);
579
+ if (saverBuffer) {
580
+ const data = JSON.parse(saverBuffer);
581
+ this.bufferedMessages1 = Array.isArray(data) ? data : this.bufferedMessages1;
582
+ this.localStorage.removeItem(bufferStorageKey);
583
+ }
584
+ this.bufferedMessages1.push(Timestamp(this.timestamp()));
585
+ this.bufferedMessages1.push(TabData(this.session.getTabId()));
586
+ this.activityState = ActivityState.ColdStart;
587
+ if (startOpts.sessionHash) {
588
+ this.session.applySessionHash(startOpts.sessionHash);
589
+ }
590
+ if (startOpts.forceNew) {
591
+ this.session.reset();
592
+ }
593
+ this.session.assign({
594
+ userID: startOpts.userID,
595
+ metadata: startOpts.metadata,
596
+ });
597
+ const onStartInfo = { sessionToken: '', userUUID: '', sessionID: '' };
598
+ this.startCallbacks.forEach((cb) => cb(onStartInfo));
599
+ if (!isNewSession) {
600
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
601
+ this.send(TabChange(this.session.getTabId()));
602
+ }
603
+ this.observer.observe();
604
+ this.ticker.start();
605
+ return {
606
+ saveBuffer: this.saveBuffer,
607
+ getBuffer: this.getBuffer,
608
+ setBuffer: this.setBuffer,
609
+ };
610
+ }
611
+ /**
612
+ * Saves the captured messages in localStorage (or whatever is used in its place)
613
+ *
614
+ * Then when this.offlineRecording is called, it will preload this messages and clear the storage item
615
+ *
616
+ * Keeping the size of local storage reasonable is up to the end users of this library
617
+ * */
618
+ saveBuffer() {
619
+ this.localStorage.setItem(bufferStorageKey, JSON.stringify(this.bufferedMessages1));
620
+ }
621
+ /**
622
+ * @returns buffer with stored messages for offline recording
623
+ * */
624
+ getBuffer() {
625
+ return this.bufferedMessages1;
626
+ }
627
+ /**
628
+ * Used to set a buffer with messages array
629
+ * */
630
+ setBuffer(buffer) {
631
+ this.bufferedMessages1 = buffer;
632
+ }
633
+ /**
634
+ * Uploads the stored session buffer to backend
635
+ * @returns promise that resolves once messages are loaded, it has to be awaited
636
+ * so the session can be uploaded properly
637
+ * @resolve - if messages were loaded in service worker successfully
638
+ * @reject {string} - error message
639
+ * */
640
+ async uploadOfflineRecording() {
641
+ var _a, _b;
642
+ this.stop(false);
643
+ const timestamp = now();
644
+ (_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage({
645
+ type: 'start',
646
+ pageNo: this.session.incPageNo(),
647
+ ingestPoint: this.options.ingestPoint,
648
+ timestamp: this.coldStartTs,
649
+ url: document.URL,
650
+ connAttemptCount: this.options.connAttemptCount,
651
+ connAttemptGap: this.options.connAttemptGap,
652
+ tabId: this.session.getTabId(),
653
+ });
654
+ const r = await fetch(this.options.ingestPoint + '/v1/web/start', {
655
+ method: 'POST',
656
+ headers: {
657
+ 'Content-Type': 'application/json',
658
+ },
659
+ body: JSON.stringify(Object.assign(Object.assign({}, this.getTrackerInfo()), { timestamp: timestamp, doNotRecord: false, bufferDiff: timestamp - this.coldStartTs, userID: this.session.getInfo().userID, token: undefined, deviceMemory,
660
+ jsHeapSizeLimit, timezone: getTimezone() })),
661
+ });
662
+ const { token, userBrowser, userCity, userCountry, userDevice, userOS, userState, beaconSizeLimit, projectID, } = await r.json();
663
+ (_b = this.worker) === null || _b === void 0 ? void 0 : _b.postMessage({
664
+ type: 'auth',
665
+ token,
666
+ beaconSizeLimit,
667
+ });
668
+ this.session.assign({ projectID });
669
+ this.session.setUserInfo({
670
+ userBrowser,
671
+ userCity,
672
+ userCountry,
673
+ userDevice,
674
+ userOS,
675
+ userState,
676
+ });
677
+ while (this.bufferedMessages1.length > 0) {
678
+ await this.flushBuffer(this.bufferedMessages1);
679
+ }
680
+ this.postToWorker([['q_end']]);
681
+ this.clearBuffers();
682
+ }
683
+ _start(startOpts = {}, resetByWorker = false, conditionName) {
684
+ const isColdStart = this.activityState === ActivityState.ColdStart;
685
+ if (isColdStart && this.coldInterval) {
686
+ clearInterval(this.coldInterval);
687
+ }
394
688
  if (!this.worker) {
395
- return Promise.resolve(UnsuccessfulStart('No worker found: perhaps, CSP is not set.'));
689
+ const reason = 'No worker found: perhaps, CSP is not set.';
690
+ this.signalError(reason, []);
691
+ return Promise.resolve(UnsuccessfulStart(reason));
396
692
  }
397
- if (this.activityState !== ActivityState.NotActive) {
398
- return Promise.resolve(UnsuccessfulStart('OpenReplay: trying to call `start()` on the instance that has been started already.'));
693
+ if (this.activityState === ActivityState.Active ||
694
+ this.activityState === ActivityState.Starting) {
695
+ const reason = 'OpenReplay: trying to call `start()` on the instance that has been started already.';
696
+ return Promise.resolve(UnsuccessfulStart(reason));
399
697
  }
400
698
  this.activityState = ActivityState.Starting;
401
- adjustTimeOrigin();
699
+ if (!isColdStart) {
700
+ adjustTimeOrigin();
701
+ }
402
702
  if (startOpts.sessionHash) {
403
703
  this.session.applySessionHash(startOpts.sessionHash);
404
704
  }
@@ -416,26 +716,24 @@ export default class App {
416
716
  type: 'start',
417
717
  pageNo: this.session.incPageNo(),
418
718
  ingestPoint: this.options.ingestPoint,
419
- timestamp,
719
+ timestamp: isColdStart ? this.coldStartTs : timestamp,
420
720
  url: document.URL,
421
721
  connAttemptCount: this.options.connAttemptCount,
422
722
  connAttemptGap: this.options.connAttemptGap,
423
723
  tabId: this.session.getTabId(),
424
724
  });
425
- const lsReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
426
- this.sessionStorage.removeItem(this.options.session_reset_key);
427
- const needNewSessionID = startOpts.forceNew || lsReset || resetByWorker;
428
725
  const sessionToken = this.session.getSessionToken();
429
- const isNewSession = needNewSessionID || !sessionToken;
430
- this.debug.log('OpenReplay: starting session; need new session id?', needNewSessionID, 'session token: ', sessionToken);
726
+ const isNewSession = this.checkSessionToken(startOpts.forceNew);
727
+ this.sessionStorage.removeItem(this.options.session_reset_key);
728
+ this.debug.log('OpenReplay: starting session; need new session id?', isNewSession, 'session token: ', sessionToken);
431
729
  return window
432
730
  .fetch(this.options.ingestPoint + '/v1/web/start', {
433
731
  method: 'POST',
434
732
  headers: {
435
733
  'Content-Type': 'application/json',
436
734
  },
437
- body: JSON.stringify(Object.assign(Object.assign({}, this.getTrackerInfo()), { timestamp, userID: this.session.getInfo().userID, token: isNewSession ? undefined : sessionToken, deviceMemory,
438
- jsHeapSizeLimit, timezone: getTimezone() })),
735
+ body: JSON.stringify(Object.assign(Object.assign({}, this.getTrackerInfo()), { timestamp, doNotRecord: false, bufferDiff: timestamp - this.coldStartTs, userID: this.session.getInfo().userID, token: isNewSession ? undefined : sessionToken, deviceMemory,
736
+ jsHeapSizeLimit, timezone: getTimezone(), condition: conditionName })),
439
737
  })
440
738
  .then((r) => {
441
739
  if (r.status === 200) {
@@ -449,13 +747,17 @@ export default class App {
449
747
  : Promise.reject(`Server error: ${r.status}. ${text}`));
450
748
  }
451
749
  })
452
- .then((r) => {
750
+ .then(async (r) => {
453
751
  var _a;
454
752
  if (!this.worker) {
455
- return Promise.reject('no worker found after start request (this might not happen)');
753
+ const reason = 'no worker found after start request (this might not happen)';
754
+ this.signalError(reason, []);
755
+ return Promise.reject(reason);
456
756
  }
457
757
  if (this.activityState === ActivityState.NotActive) {
458
- return Promise.reject('Tracker stopped during authorization');
758
+ const reason = 'Tracker stopped during authorization';
759
+ this.signalError(reason, []);
760
+ return Promise.reject(reason);
459
761
  }
460
762
  const { token, userUUID, projectID, beaconSizeLimit, compressionThreshold, // how big the batch should be before we decide to compress it
461
763
  delay, // derived from token
@@ -468,7 +770,9 @@ export default class App {
468
770
  typeof sessionID !== 'string' ||
469
771
  typeof delay !== 'number' ||
470
772
  (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
471
- return Promise.reject(`Incorrect server response: ${JSON.stringify(r)}`);
773
+ const reason = `Incorrect server response: ${JSON.stringify(r)}`;
774
+ this.signalError(reason, []);
775
+ return Promise.reject(reason);
472
776
  }
473
777
  this.delay = delay;
474
778
  this.session.setSessionToken(token);
@@ -485,6 +789,11 @@ export default class App {
485
789
  timestamp: startTimestamp || timestamp,
486
790
  projectID,
487
791
  });
792
+ this.worker.postMessage({
793
+ type: 'auth',
794
+ token,
795
+ beaconSizeLimit,
796
+ });
488
797
  if (!isNewSession && token === sessionToken) {
489
798
  this.debug.log('continuing session on new tab', this.session.getTabId());
490
799
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
@@ -493,13 +802,14 @@ export default class App {
493
802
  // (Re)send Metadata for the case of a new session
494
803
  Object.entries(this.session.getInfo().metadata).forEach(([key, value]) => this.send(Metadata(key, value)));
495
804
  this.localStorage.setItem(this.options.local_uuid_key, userUUID);
496
- this.worker.postMessage({
497
- type: 'auth',
498
- token,
499
- beaconSizeLimit,
500
- });
501
805
  this.compressionThreshold = compressionThreshold;
502
806
  const onStartInfo = { sessionToken: token, userUUID, sessionID };
807
+ // TODO: start as early as possible (before receiving the token)
808
+ /** after start */
809
+ this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
810
+ void this.featureFlags.reloadFlags();
811
+ await this.tagWatcher.fetchTags(this.options.ingestPoint, token);
812
+ this.activityState = ActivityState.Active;
503
813
  if (canvasEnabled) {
504
814
  this.canvasRecorder =
505
815
  (_a = this.canvasRecorder) !== null && _a !== void 0 ? _a : new CanvasRecorder(this, {
@@ -509,12 +819,22 @@ export default class App {
509
819
  });
510
820
  this.canvasRecorder.startTracking();
511
821
  }
512
- // TODO: start as early as possible (before receiving the token)
513
- this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
514
- this.observer.observe();
515
- this.ticker.start();
516
- this.activityState = ActivityState.Active;
517
- this.notify.log('OpenReplay tracking started.');
822
+ /** --------------- COLD START BUFFER ------------------*/
823
+ if (isColdStart) {
824
+ const biggestBuffer = this.bufferedMessages1.length > this.bufferedMessages2.length
825
+ ? this.bufferedMessages1
826
+ : this.bufferedMessages2;
827
+ while (biggestBuffer.length > 0) {
828
+ await this.flushBuffer(biggestBuffer);
829
+ }
830
+ this.clearBuffers();
831
+ this.commit();
832
+ /** --------------- COLD START BUFFER ------------------*/
833
+ }
834
+ else {
835
+ this.observer.observe();
836
+ this.ticker.start();
837
+ }
518
838
  // get rid of onStart ?
519
839
  if (typeof this.options.onStart === 'function') {
520
840
  this.options.onStart(onStartInfo);
@@ -555,10 +875,11 @@ export default class App {
555
875
  this.stop();
556
876
  this.session.reset();
557
877
  if (reason === CANCELED) {
878
+ this.signalError(CANCELED, []);
558
879
  return UnsuccessfulStart(CANCELED);
559
880
  }
560
- this.notify.log('OpenReplay was unable to start. ', reason);
561
881
  this._debug('session_start', reason);
882
+ this.signalError(START_ERROR, []);
562
883
  return UnsuccessfulStart(START_ERROR);
563
884
  });
564
885
  }
@@ -603,6 +924,27 @@ export default class App {
603
924
  getTabId() {
604
925
  return this.session.getTabId();
605
926
  }
927
+ clearBuffers() {
928
+ this.bufferedMessages1.length = 0;
929
+ this.bufferedMessages2.length = 0;
930
+ }
931
+ /**
932
+ * Creates a named hook that expects event name, data string and msg direction (up/down),
933
+ * it will skip any message bigger than 5 mb or event name bigger than 255 symbols
934
+ * @returns {(msgType: string, data: string, dir: "up" | "down") => void}
935
+ * */
936
+ trackWs(channelName) {
937
+ const channel = channelName;
938
+ return (msgType, data, dir = 'down') => {
939
+ if (typeof msgType !== 'string' ||
940
+ typeof data !== 'string' ||
941
+ data.length > 5 * 1024 * 1024 ||
942
+ msgType.length > 255) {
943
+ return;
944
+ }
945
+ this.send(WSChannel('websocket', channel, data, this.timestamp(), dir, msgType));
946
+ };
947
+ }
606
948
  stop(stopWorker = true) {
607
949
  var _a;
608
950
  if (this.activityState !== ActivityState.NotActive) {
@@ -613,7 +955,8 @@ export default class App {
613
955
  this.nodes.clear();
614
956
  this.ticker.stop();
615
957
  this.stopCallbacks.forEach((cb) => cb());
616
- this.notify.log('OpenReplay tracking stopped.');
958
+ this.debug.log('OpenReplay tracking stopped.');
959
+ this.tagWatcher.clear();
617
960
  if (this.worker && stopWorker) {
618
961
  this.worker.postMessage('stop');
619
962
  }