@formo/analytics 1.12.0-alpha.3 → 1.13.0-alpha.1

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 (67) hide show
  1. package/.env.example +1 -0
  2. package/CONTRIBUTING.md +93 -0
  3. package/README.md +4 -163
  4. package/dist/cjs/src/FormoAnalytics.d.ts +45 -69
  5. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  6. package/dist/cjs/src/FormoAnalytics.js +378 -404
  7. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  8. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +2 -2
  9. package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -1
  10. package/dist/cjs/src/FormoAnalyticsProvider.js +120 -29
  11. package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -1
  12. package/dist/cjs/src/constants/config.d.ts +2 -2
  13. package/dist/cjs/src/constants/config.d.ts.map +1 -1
  14. package/dist/cjs/src/constants/config.js +3 -3
  15. package/dist/cjs/src/constants/config.js.map +1 -1
  16. package/dist/cjs/src/constants/events.d.ts +3 -1
  17. package/dist/cjs/src/constants/events.d.ts.map +1 -1
  18. package/dist/cjs/src/constants/events.js +2 -0
  19. package/dist/cjs/src/constants/events.js.map +1 -1
  20. package/dist/cjs/src/types/base.d.ts +10 -2
  21. package/dist/cjs/src/types/base.d.ts.map +1 -1
  22. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  23. package/dist/esm/src/FormoAnalytics.d.ts +45 -69
  24. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  25. package/dist/esm/src/FormoAnalytics.js +381 -407
  26. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  27. package/dist/esm/src/FormoAnalyticsProvider.d.ts +2 -2
  28. package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -1
  29. package/dist/esm/src/FormoAnalyticsProvider.js +120 -29
  30. package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -1
  31. package/dist/esm/src/constants/config.d.ts +2 -2
  32. package/dist/esm/src/constants/config.d.ts.map +1 -1
  33. package/dist/esm/src/constants/config.js +2 -2
  34. package/dist/esm/src/constants/config.js.map +1 -1
  35. package/dist/esm/src/constants/events.d.ts +3 -1
  36. package/dist/esm/src/constants/events.d.ts.map +1 -1
  37. package/dist/esm/src/constants/events.js +2 -0
  38. package/dist/esm/src/constants/events.js.map +1 -1
  39. package/dist/esm/src/types/base.d.ts +10 -2
  40. package/dist/esm/src/types/base.d.ts.map +1 -1
  41. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  42. package/dist/index.umd.min.js +1 -1
  43. package/dist/index.umd.min.js.map +1 -1
  44. package/package.json +2 -3
  45. package/src/FormoAnalytics.ts +293 -448
  46. package/src/FormoAnalyticsProvider.tsx +60 -30
  47. package/src/constants/config.ts +2 -2
  48. package/src/constants/events.ts +2 -0
  49. package/src/types/base.ts +16 -2
  50. package/dist/cjs/src/utils/index.d.ts +0 -2
  51. package/dist/cjs/src/utils/index.d.ts.map +0 -1
  52. package/dist/cjs/src/utils/index.js +0 -18
  53. package/dist/cjs/src/utils/index.js.map +0 -1
  54. package/dist/cjs/src/utils/isNotEmptyObject.d.ts +0 -2
  55. package/dist/cjs/src/utils/isNotEmptyObject.d.ts.map +0 -1
  56. package/dist/cjs/src/utils/isNotEmptyObject.js +0 -9
  57. package/dist/cjs/src/utils/isNotEmptyObject.js.map +0 -1
  58. package/dist/esm/src/utils/index.d.ts +0 -2
  59. package/dist/esm/src/utils/index.d.ts.map +0 -1
  60. package/dist/esm/src/utils/index.js +0 -2
  61. package/dist/esm/src/utils/index.js.map +0 -1
  62. package/dist/esm/src/utils/isNotEmptyObject.d.ts +0 -2
  63. package/dist/esm/src/utils/isNotEmptyObject.d.ts.map +0 -1
  64. package/dist/esm/src/utils/isNotEmptyObject.js +0 -6
  65. package/dist/esm/src/utils/isNotEmptyObject.js.map +0 -1
  66. package/src/utils/index.ts +0 -1
  67. package/src/utils/isNotEmptyObject.ts +0 -5
@@ -53,256 +53,168 @@ Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.FormoAnalytics = void 0;
54
54
  var axios_1 = __importDefault(require("axios"));
55
55
  var constants_1 = require("./constants");
56
- var utils_1 = require("./utils");
57
56
  var highlight_run_1 = require("highlight.run");
58
57
  var FormoAnalytics = /** @class */ (function () {
59
- function FormoAnalytics(apiKey, projectId) {
58
+ function FormoAnalytics(apiKey, options) {
59
+ if (options === void 0) { options = {}; }
60
60
  var _a;
61
61
  this.apiKey = apiKey;
62
- this.projectId = projectId;
63
- this._registeredProviderListeners = {};
64
- this.sessionKey = 'walletAddress';
65
- this.sessionIdKey = constants_1.SESSION_STORAGE_ID_KEY;
66
- this.timezoneToCountry = constants_1.COUNTRY_LIST;
62
+ this.options = options;
63
+ this._providerListeners = {};
67
64
  this.config = {
68
- token: this.apiKey,
65
+ apiKey: apiKey,
69
66
  };
70
- var provider = (window === null || window === void 0 ? void 0 : window.ethereum) || ((_a = window.web3) === null || _a === void 0 ? void 0 : _a.currentProvider);
67
+ var provider = (window === null || window === void 0 ? void 0 : window.ethereum) || ((_a = window.web3) === null || _a === void 0 ? void 0 : _a.currentProvider) || (options === null || options === void 0 ? void 0 : options.provider);
71
68
  if (provider) {
72
69
  this.trackProvider(provider);
73
70
  }
74
71
  }
75
- FormoAnalytics.init = function (apiKey, projectId) {
72
+ FormoAnalytics.init = function (apiKey, options) {
76
73
  return __awaiter(this, void 0, void 0, function () {
77
- var config, instance;
78
74
  return __generator(this, function (_a) {
79
- config = {
80
- token: apiKey,
81
- };
82
- instance = new FormoAnalytics(apiKey, projectId);
83
- instance.config = config;
84
- return [2 /*return*/, instance];
75
+ // May be needed for delayed loading
76
+ // https://github.com/segmentio/analytics-next/tree/master/packages/browser#lazy--delayed-loading
77
+ return [2 /*return*/, new FormoAnalytics(apiKey, options)];
85
78
  });
86
79
  });
87
80
  };
88
- Object.defineProperty(FormoAnalytics.prototype, "provider", {
89
- get: function () {
90
- return this._provider;
91
- },
92
- enumerable: false,
93
- configurable: true
94
- });
95
- FormoAnalytics.prototype.identifyUser = function (userData) {
96
- this.trackEvent(constants_1.Event.IDENTIFY, userData);
97
- };
98
- FormoAnalytics.prototype.getSessionId = function () {
99
- var existingSessionId = this.getCookieValue(this.sessionIdKey);
100
- if (existingSessionId) {
101
- return existingSessionId;
102
- }
103
- var newSessionId = this.generateSessionId();
104
- return newSessionId;
105
- };
106
- // Function to set the session cookie
107
- FormoAnalytics.prototype.setSessionCookie = function (domain) {
108
- var sessionId = this.getSessionId();
109
- var cookieValue = "".concat(this.sessionIdKey, "=").concat(sessionId, "; Max-Age=1800; path=/; secure");
110
- if (domain) {
111
- cookieValue += "; domain=".concat(domain);
112
- }
113
- document.cookie = cookieValue;
114
- };
115
- // Function to generate a new session ID
116
- FormoAnalytics.prototype.generateSessionId = function () {
117
- return crypto.randomUUID();
118
- };
119
- // Function to get a cookie value by name
120
- FormoAnalytics.prototype.getCookieValue = function (name) {
121
- var cookies = document.cookie.split(';').reduce(function (acc, cookie) {
122
- var _a = cookie.split('='), key = _a[0], value = _a[1];
123
- acc[key.trim()] = value;
124
- return acc;
125
- }, {});
126
- return cookies[name];
81
+ /*
82
+ Public SDK functions
83
+ */
84
+ FormoAnalytics.prototype.connect = function (_a) {
85
+ return __awaiter(this, arguments, void 0, function (_b) {
86
+ var chainId = _b.chainId, address = _b.address;
87
+ return __generator(this, function (_c) {
88
+ switch (_c.label) {
89
+ case 0:
90
+ if (!chainId) {
91
+ throw new Error("FormoAnalytics::connect: chain ID cannot be empty");
92
+ }
93
+ if (!address) {
94
+ throw new Error("FormoAnalytics::connect: address cannot be empty");
95
+ }
96
+ this.currentChainId = chainId;
97
+ this.currentConnectedAddress = address;
98
+ return [4 /*yield*/, this.trackEvent(constants_1.Event.CONNECT, {
99
+ chain_id: chainId,
100
+ address: address,
101
+ })];
102
+ case 1:
103
+ _c.sent();
104
+ return [2 /*return*/];
105
+ }
106
+ });
107
+ });
127
108
  };
128
- // Function to send tracking data
129
- FormoAnalytics.prototype.trackEvent = function (action, payload) {
109
+ FormoAnalytics.prototype.disconnect = function (params) {
130
110
  return __awaiter(this, void 0, void 0, function () {
131
- var maxRetries, attempt, apiUrl, address, requestData, sendRequest;
132
- var _this = this;
111
+ var address, chainId;
133
112
  return __generator(this, function (_a) {
134
113
  switch (_a.label) {
135
114
  case 0:
136
- maxRetries = 3;
137
- attempt = 0;
138
- this.setSessionCookie(this.config.domain);
139
- apiUrl = this.buildApiUrl();
140
- return [4 /*yield*/, this.getCurrentWallet()];
115
+ address = (params === null || params === void 0 ? void 0 : params.address) || this.currentConnectedAddress;
116
+ chainId = (params === null || params === void 0 ? void 0 : params.chainId) || this.currentChainId;
117
+ return [4 /*yield*/, this.handleDisconnect(chainId, address)];
141
118
  case 1:
142
- address = _a.sent();
143
- requestData = {
144
- project_id: this.projectId,
145
- address: address,
146
- session_id: this.getSessionId(),
147
- timestamp: new Date().toISOString(),
148
- action: action,
149
- version: '1',
150
- payload: (0, utils_1.isNotEmpty)(payload) ? this.maskSensitiveData(payload) : payload,
151
- };
152
- sendRequest = function () { return __awaiter(_this, void 0, void 0, function () {
153
- var response, error_1, retryDelay;
154
- return __generator(this, function (_a) {
155
- switch (_a.label) {
156
- case 0:
157
- _a.trys.push([0, 2, , 3]);
158
- return [4 /*yield*/, axios_1.default.post(apiUrl, JSON.stringify(requestData), {
159
- headers: {
160
- 'Content-Type': 'application/json',
161
- },
162
- })];
163
- case 1:
164
- response = _a.sent();
165
- if (response.status >= 200 && response.status < 300) {
166
- console.log('Event sent successfully:', action);
167
- }
168
- else {
169
- throw new Error("Failed with status: ".concat(response.status));
170
- }
171
- return [3 /*break*/, 3];
172
- case 2:
173
- error_1 = _a.sent();
174
- attempt++;
175
- if (attempt <= maxRetries) {
176
- retryDelay = Math.pow(2, attempt) * 1000;
177
- console.error("Attempt ".concat(attempt, ": Retrying event \"").concat(action, "\" in ").concat(retryDelay / 1000, " seconds..."));
178
- setTimeout(sendRequest, retryDelay);
179
- }
180
- else {
181
- highlight_run_1.H.consumeError(error_1, "Request data: ".concat(JSON.stringify(requestData)));
182
- console.error("Event \"".concat(action, "\" failed after ").concat(maxRetries, " attempts. Error: ").concat(error_1));
183
- }
184
- return [3 /*break*/, 3];
185
- case 3: return [2 /*return*/];
186
- }
187
- });
188
- }); };
189
- return [4 /*yield*/, sendRequest()];
190
- case 2:
191
119
  _a.sent();
192
120
  return [2 /*return*/];
193
121
  }
194
122
  });
195
123
  });
196
124
  };
197
- // Function to mask sensitive data in the payload
198
- FormoAnalytics.prototype.maskSensitiveData = function (data) {
199
- // Check if data is null or undefined
200
- if (data === null || data === undefined) {
201
- console.warn('Data is null or undefined, returning null');
202
- return null;
203
- }
204
- // Check if data is a string; if so, parse it to an object
205
- if (typeof data === 'string') {
206
- var parsedData = void 0;
207
- try {
208
- parsedData = JSON.parse(data);
209
- }
210
- catch (error) {
211
- console.error('Failed to parse JSON:', error);
212
- return null; // Return null if parsing fails
213
- }
214
- var sensitiveFields = [
215
- 'username',
216
- 'user',
217
- 'user_id',
218
- 'password',
219
- 'email',
220
- 'phone',
221
- ];
222
- // Create a new object to store masked data
223
- var maskedData_1 = __assign({}, parsedData);
224
- // Mask sensitive fields
225
- sensitiveFields.forEach(function (field) {
226
- if (field in maskedData_1) {
227
- maskedData_1[field] = '********'; // Replace value with masked string
125
+ FormoAnalytics.prototype.chain = function (_a) {
126
+ return __awaiter(this, arguments, void 0, function (_b) {
127
+ var chainId = _b.chainId, address = _b.address;
128
+ return __generator(this, function (_c) {
129
+ switch (_c.label) {
130
+ case 0:
131
+ if (!chainId || Number(chainId) === 0) {
132
+ throw new Error("FormoAnalytics::chain: chainId cannot be empty or 0");
133
+ }
134
+ if (!address && !this.currentConnectedAddress) {
135
+ throw new Error("FormoAnalytics::chain: address was empty and no previous address has been recorded. You can either pass an address or call connect() first");
136
+ }
137
+ if (isNaN(Number(chainId))) {
138
+ throw new Error("FormoAnalytics::chain: chainId must be a valid decimal number");
139
+ }
140
+ this.currentChainId = chainId;
141
+ return [4 /*yield*/, this.trackEvent(constants_1.Event.CHAIN_CHANGED, {
142
+ chain_id: chainId,
143
+ address: address || this.currentConnectedAddress,
144
+ })];
145
+ case 1:
146
+ _c.sent();
147
+ return [2 /*return*/];
228
148
  }
229
149
  });
230
- return maskedData_1; // Return the new object with masked fields
231
- }
232
- else if (typeof data === 'object') {
233
- // If data is already an object, handle masking directly
234
- var sensitiveFields = [
235
- 'username',
236
- 'user',
237
- 'user_id',
238
- 'password',
239
- 'email',
240
- 'phone',
241
- ];
242
- var maskedData_2 = __assign({}, data);
243
- // Mask sensitive fields
244
- sensitiveFields.forEach(function (field) {
245
- if (field in maskedData_2) {
246
- maskedData_2[field] = '********'; // Replace value with masked string
150
+ });
151
+ };
152
+ // TODO: allow custom url as input
153
+ FormoAnalytics.prototype.page = function () {
154
+ return __awaiter(this, void 0, void 0, function () {
155
+ return __generator(this, function (_a) {
156
+ switch (_a.label) {
157
+ case 0: return [4 /*yield*/, this.trackPageHit()];
158
+ case 1:
159
+ _a.sent();
160
+ return [2 /*return*/];
247
161
  }
248
162
  });
249
- return maskedData_2; // Return the new object with masked fields
250
- }
251
- return data;
163
+ });
252
164
  };
253
- // Function to track page hits
254
- FormoAnalytics.prototype.trackPageHit = function () {
255
- var _this = this;
256
- if (window.__nightmare || window.navigator.webdriver || window.Cypress)
257
- return;
258
- var location;
259
- var language;
260
- try {
261
- var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
262
- location = this.timezoneToCountry[timezone];
263
- language =
264
- navigator.languages && navigator.languages.length
265
- ? navigator.languages[0]
266
- : navigator.language || 'en';
267
- }
268
- catch (error) {
269
- console.error('Error resolving timezone or language:', error);
270
- }
271
- setTimeout(function () {
272
- var url = new URL(window.location.href);
273
- var params = new URLSearchParams(url.search);
274
- _this.trackEvent(constants_1.Event.PAGE, {
275
- 'user-agent': window.navigator.userAgent,
276
- locale: language,
277
- location: location,
278
- referrer: document.referrer,
279
- pathname: window.location.pathname,
280
- href: window.location.href,
281
- utm_source: params.get('utm_source'),
282
- utm_medium: params.get('utm_medium'),
283
- utm_campaign: params.get('utm_campaign'),
284
- ref: params.get('ref'),
165
+ FormoAnalytics.prototype.track = function (eventName, eventData) {
166
+ return __awaiter(this, void 0, void 0, function () {
167
+ return __generator(this, function (_a) {
168
+ switch (_a.label) {
169
+ case 0: return [4 /*yield*/, this.trackEvent(eventName, eventData)];
170
+ case 1:
171
+ _a.sent();
172
+ return [2 /*return*/];
173
+ }
285
174
  });
286
- }, 300);
175
+ });
287
176
  };
177
+ /*
178
+ SDK tracking and event listener functions
179
+ */
288
180
  FormoAnalytics.prototype.trackProvider = function (provider) {
289
181
  if (provider === this._provider) {
182
+ console.log("Provider already tracked.");
290
183
  return;
291
184
  }
292
185
  this.currentChainId = undefined;
293
- this.currentConnectedAccount = undefined;
186
+ this.currentConnectedAddress = undefined;
294
187
  if (this._provider) {
295
- var eventNames = Object.keys(this._registeredProviderListeners);
188
+ var eventNames = Object.keys(this._providerListeners);
296
189
  for (var _i = 0, eventNames_1 = eventNames; _i < eventNames_1.length; _i++) {
297
190
  var eventName = eventNames_1[_i];
298
- this._provider.removeListener(eventName, this._registeredProviderListeners[eventName]);
299
- delete this._registeredProviderListeners[eventName];
191
+ this._provider.removeListener(eventName, this._providerListeners[eventName]);
192
+ delete this._providerListeners[eventName];
300
193
  }
301
194
  }
195
+ console.log("Tracking new provider:", provider);
302
196
  this._provider = provider;
303
- this.getCurrentWallet();
304
- this.registerAccountsChangedListener();
197
+ this.getAddress();
198
+ this.registerAddressChangedListener();
305
199
  this.registerChainChangedListener();
200
+ // TODO: track signing and transactions
201
+ // https://linear.app/getformo/issue/P-607/sdk-support-signature-and-transaction-events
202
+ };
203
+ FormoAnalytics.prototype.registerAddressChangedListener = function () {
204
+ var _this = this;
205
+ var _a, _b;
206
+ var listener = function () {
207
+ var args = [];
208
+ for (var _i = 0; _i < arguments.length; _i++) {
209
+ args[_i] = arguments[_i];
210
+ }
211
+ return _this.onAddressChanged(args[0]);
212
+ };
213
+ (_a = this._provider) === null || _a === void 0 ? void 0 : _a.on("accountsChanged", listener);
214
+ this._providerListeners["accountsChanged"] = listener;
215
+ var onAddressDisconnected = this.onAddressDisconnected.bind(this);
216
+ (_b = this._provider) === null || _b === void 0 ? void 0 : _b.on("disconnect", onAddressDisconnected);
217
+ this._providerListeners["disconnect"] = onAddressDisconnected;
306
218
  };
307
219
  FormoAnalytics.prototype.registerChainChangedListener = function () {
308
220
  var _this = this;
@@ -314,264 +226,326 @@ var FormoAnalytics = /** @class */ (function () {
314
226
  }
315
227
  return _this.onChainChanged(args[0]);
316
228
  };
317
- (_a = this.provider) === null || _a === void 0 ? void 0 : _a.on('chainChanged', listener);
318
- this._registeredProviderListeners['chainChanged'] = listener;
229
+ (_a = this.provider) === null || _a === void 0 ? void 0 : _a.on("chainChanged", listener);
230
+ this._providerListeners["chainChanged"] = listener;
319
231
  };
320
- FormoAnalytics.prototype.handleAccountDisconnected = function () {
321
- if (!this.currentConnectedAccount) {
322
- return;
323
- }
324
- var disconnectAttributes = {
325
- address: this.currentConnectedAccount,
326
- chainId: this.currentChainId,
327
- };
328
- this.currentChainId = undefined;
329
- this.currentConnectedAccount = undefined;
330
- this.clearWalletAddress();
331
- return this.trackEvent(constants_1.Event.DISCONNECT, disconnectAttributes);
232
+ FormoAnalytics.prototype.onAddressChanged = function (addresses) {
233
+ return __awaiter(this, void 0, void 0, function () {
234
+ return __generator(this, function (_a) {
235
+ if (addresses.length > 0) {
236
+ this.onAddressConnected(addresses[0]);
237
+ }
238
+ else {
239
+ this.onAddressDisconnected();
240
+ }
241
+ return [2 /*return*/];
242
+ });
243
+ });
244
+ };
245
+ FormoAnalytics.prototype.onAddressConnected = function (address) {
246
+ return __awaiter(this, void 0, void 0, function () {
247
+ var _a;
248
+ return __generator(this, function (_b) {
249
+ switch (_b.label) {
250
+ case 0:
251
+ if (address === this.currentConnectedAddress) {
252
+ // We have already reported this address
253
+ return [2 /*return*/];
254
+ }
255
+ else {
256
+ this.currentConnectedAddress = address;
257
+ }
258
+ _a = this;
259
+ return [4 /*yield*/, this.getCurrentChainId()];
260
+ case 1:
261
+ _a.currentChainId = _b.sent();
262
+ this.connect({ chainId: this.currentChainId, address: address });
263
+ return [2 /*return*/];
264
+ }
265
+ });
266
+ });
267
+ };
268
+ FormoAnalytics.prototype.handleDisconnect = function (chainId, address) {
269
+ return __awaiter(this, void 0, void 0, function () {
270
+ var payload;
271
+ return __generator(this, function (_a) {
272
+ switch (_a.label) {
273
+ case 0:
274
+ payload = {
275
+ chain_id: chainId || this.currentChainId,
276
+ address: address || this.currentConnectedAddress,
277
+ };
278
+ this.currentChainId = undefined;
279
+ this.currentConnectedAddress = undefined;
280
+ return [4 /*yield*/, this.trackEvent(constants_1.Event.DISCONNECT, payload)];
281
+ case 1:
282
+ _a.sent();
283
+ return [2 /*return*/];
284
+ }
285
+ });
286
+ });
287
+ };
288
+ FormoAnalytics.prototype.onAddressDisconnected = function () {
289
+ return __awaiter(this, void 0, void 0, function () {
290
+ return __generator(this, function (_a) {
291
+ switch (_a.label) {
292
+ case 0: return [4 /*yield*/, this.handleDisconnect(this.currentChainId, this.currentConnectedAddress)];
293
+ case 1:
294
+ _a.sent();
295
+ return [2 /*return*/];
296
+ }
297
+ });
298
+ });
332
299
  };
333
300
  FormoAnalytics.prototype.onChainChanged = function (chainIdHex) {
334
301
  return __awaiter(this, void 0, void 0, function () {
335
- var res, err_1;
302
+ var address;
336
303
  return __generator(this, function (_a) {
337
304
  switch (_a.label) {
338
305
  case 0:
339
- this.currentChainId = parseInt(chainIdHex).toString();
340
- if (!!this.currentConnectedAccount) return [3 /*break*/, 4];
306
+ this.currentChainId = parseInt(chainIdHex);
307
+ if (!!this.currentConnectedAddress) return [3 /*break*/, 2];
341
308
  if (!this.provider) {
342
- console.error('error', 'FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported');
343
- return [2 /*return*/];
309
+ console.log("FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported");
310
+ return [2 /*return*/, Promise.resolve()];
344
311
  }
345
- _a.label = 1;
312
+ return [4 /*yield*/, this.getAddress()];
346
313
  case 1:
347
- _a.trys.push([1, 3, , 4]);
348
- return [4 /*yield*/, this.provider.request({
349
- method: 'eth_accounts',
350
- })];
314
+ address = _a.sent();
315
+ if (!address) {
316
+ console.log("FormoAnalytics::onChainChanged: Unable to fetch or store connected address");
317
+ return [2 /*return*/, Promise.resolve()];
318
+ }
319
+ this.currentConnectedAddress = address[0];
320
+ _a.label = 2;
351
321
  case 2:
352
- res = _a.sent();
353
- if (!res || res.length === 0) {
354
- console.error('error', 'FormoAnalytics::onChainChanged: unable to get account. eth_accounts returned empty');
355
- return [2 /*return*/];
322
+ // Proceed only if the address exists
323
+ if (this.currentConnectedAddress) {
324
+ return [2 /*return*/, this.chain({
325
+ chainId: this.currentChainId,
326
+ address: this.currentConnectedAddress,
327
+ })];
356
328
  }
357
- this.currentConnectedAccount = res[0];
358
- return [3 /*break*/, 4];
359
- case 3:
360
- err_1 = _a.sent();
361
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
- if (err_1.code !== 4001) {
363
- // 4001: The request is rejected by the user , see https://docs.metamask.io/wallet/reference/provider-api/#errors
364
- console.error('error', "FormoAnalytics::onChainChanged: unable to get account. eth_accounts threw an error", err_1);
365
- return [2 /*return*/];
329
+ else {
330
+ console.log("FormoAnalytics::onChainChanged: currentConnectedAddress is null despite fetch attempt");
366
331
  }
367
- return [3 /*break*/, 4];
368
- case 4: return [2 /*return*/, this.chain({
369
- chainId: this.currentChainId,
370
- account: this.currentConnectedAccount,
371
- })];
332
+ return [2 /*return*/];
372
333
  }
373
334
  });
374
335
  });
375
336
  };
376
- FormoAnalytics.prototype.onAccountsChanged = function (accounts) {
377
- return __awaiter(this, void 0, void 0, function () {
378
- var newAccount;
337
+ // TOFIX: support multiple page hit events
338
+ // TODO: Add event listener and support for SPA and hash-based navigation
339
+ // https://linear.app/getformo/issue/P-800/sdk-support-spa-and-hash-based-routing
340
+ FormoAnalytics.prototype.trackPageHit = function () {
341
+ var _this = this;
342
+ var pathname = window.location.pathname;
343
+ var href = window.location.href;
344
+ setTimeout(function () { return __awaiter(_this, void 0, void 0, function () {
379
345
  return __generator(this, function (_a) {
380
- if (accounts.length > 0) {
381
- newAccount = accounts[0];
382
- if (newAccount !== this.currentConnectedAccount) {
383
- this.handleAccountConnected(newAccount);
384
- }
385
- }
386
- else {
387
- this.handleAccountDisconnected();
388
- }
346
+ this.trackEvent(constants_1.Event.PAGE, {
347
+ pathname: pathname,
348
+ href: href,
349
+ });
389
350
  return [2 /*return*/];
390
351
  });
391
- });
392
- };
393
- FormoAnalytics.prototype.registerAccountsChangedListener = function () {
394
- var _this = this;
395
- var _a, _b;
396
- var listener = function () {
397
- var args = [];
398
- for (var _i = 0; _i < arguments.length; _i++) {
399
- args[_i] = arguments[_i];
400
- }
401
- return _this.onAccountsChanged(args[0]);
402
- };
403
- (_a = this._provider) === null || _a === void 0 ? void 0 : _a.on('accountsChanged', listener);
404
- this._registeredProviderListeners['accountsChanged'] = listener;
405
- var handleAccountDisconnected = this.handleAccountDisconnected.bind(this);
406
- (_b = this._provider) === null || _b === void 0 ? void 0 : _b.on('disconnect', handleAccountDisconnected);
407
- this._registeredProviderListeners['disconnect'] = handleAccountDisconnected;
352
+ }); }, 300);
408
353
  };
409
- FormoAnalytics.prototype.getCurrentChainId = function () {
354
+ // TODO: refactor this with event queue and flushing
355
+ // https://linear.app/getformo/issue/P-835/sdk-refactor-retries-with-event-queue-and-batching
356
+ FormoAnalytics.prototype.trackEvent = function (action, payload) {
410
357
  return __awaiter(this, void 0, void 0, function () {
411
- var chainIdHex;
358
+ var address, requestData, response, error_1;
412
359
  var _a;
413
360
  return __generator(this, function (_b) {
414
361
  switch (_b.label) {
362
+ case 0: return [4 /*yield*/, this.getAddress()];
363
+ case 1:
364
+ address = _b.sent();
365
+ _a = {
366
+ address: address,
367
+ timestamp: new Date().toISOString(),
368
+ action: action,
369
+ version: "1"
370
+ };
371
+ return [4 /*yield*/, this.buildEventPayload(payload)];
372
+ case 2:
373
+ requestData = (_a.payload = _b.sent(),
374
+ _a);
375
+ _b.label = 3;
376
+ case 3:
377
+ _b.trys.push([3, 5, , 6]);
378
+ return [4 /*yield*/, axios_1.default.post(constants_1.EVENTS_API_URL, JSON.stringify(requestData), {
379
+ headers: {
380
+ "Content-Type": "application/json",
381
+ Authorization: "Bearer ".concat(this.config.apiKey),
382
+ },
383
+ })];
384
+ case 4:
385
+ response = _b.sent();
386
+ if (response.status >= 200 && response.status < 300) {
387
+ console.log("Event sent successfully:", action);
388
+ }
389
+ else {
390
+ throw new Error("Failed with status: ".concat(response.status));
391
+ }
392
+ return [3 /*break*/, 6];
393
+ case 5:
394
+ error_1 = _b.sent();
395
+ highlight_run_1.H.consumeError(error_1, "Request data: ".concat(JSON.stringify(requestData)));
396
+ console.error("Event \"".concat(action, "\" failed. Error: ").concat(error_1));
397
+ return [3 /*break*/, 6];
398
+ case 6: return [2 /*return*/];
399
+ }
400
+ });
401
+ });
402
+ };
403
+ Object.defineProperty(FormoAnalytics.prototype, "provider", {
404
+ /*
405
+ Utility functions
406
+ */
407
+ get: function () {
408
+ return this._provider;
409
+ },
410
+ enumerable: false,
411
+ configurable: true
412
+ });
413
+ FormoAnalytics.prototype.getAddress = function () {
414
+ return __awaiter(this, void 0, void 0, function () {
415
+ var accounts, address, err_1;
416
+ return __generator(this, function (_a) {
417
+ switch (_a.label) {
415
418
  case 0:
416
419
  if (!this.provider) {
417
- console.error('FormoAnalytics::getCurrentChainId: provider not set');
420
+ console.log("FormoAnalytics::getAddress: the provider is not set");
421
+ return [2 /*return*/, null];
418
422
  }
419
- return [4 /*yield*/, ((_a = this.provider) === null || _a === void 0 ? void 0 : _a.request({
420
- method: 'eth_chainId',
421
- }))];
423
+ _a.label = 1;
422
424
  case 1:
423
- chainIdHex = _b.sent();
424
- // Because we're connected, the chainId cannot be null
425
- if (!chainIdHex) {
426
- console.error("FormoAnalytics::getCurrentChainId: chainIdHex is: ".concat(chainIdHex));
425
+ _a.trys.push([1, 3, , 4]);
426
+ return [4 /*yield*/, this.getAccounts()];
427
+ case 2:
428
+ accounts = _a.sent();
429
+ if (accounts && accounts.length > 0) {
430
+ address = accounts[0];
431
+ // TODO: how to handle multiple addresses? Should we emit a connect event here? Since the user has not manually connected
432
+ // https://linear.app/getformo/issue/P-691/sdk-detect-multiple-wallets-using-eip6963
433
+ this.onAddressConnected(address);
434
+ return [2 /*return*/, address];
427
435
  }
428
- return [2 /*return*/, parseInt(chainIdHex, 16).toString()];
436
+ return [3 /*break*/, 4];
437
+ case 3:
438
+ err_1 = _a.sent();
439
+ console.log("Failed to fetch accounts from provider:", err_1);
440
+ return [2 /*return*/, null];
441
+ case 4: return [2 /*return*/, null];
429
442
  }
430
443
  });
431
444
  });
432
445
  };
433
- FormoAnalytics.prototype.handleAccountConnected = function (account) {
446
+ FormoAnalytics.prototype.getAccounts = function () {
434
447
  return __awaiter(this, void 0, void 0, function () {
448
+ var res, err_2;
435
449
  var _a;
436
450
  return __generator(this, function (_b) {
437
451
  switch (_b.label) {
438
452
  case 0:
439
- if (account === this.currentConnectedAccount) {
440
- // We have already reported this account
441
- return [2 /*return*/];
453
+ _b.trys.push([0, 2, , 3]);
454
+ return [4 /*yield*/, ((_a = this.provider) === null || _a === void 0 ? void 0 : _a.request({
455
+ method: "eth_accounts",
456
+ }))];
457
+ case 1:
458
+ res = _b.sent();
459
+ if (!res || res.length === 0) {
460
+ console.log("FormoAnalytics::getAccounts: unable to get account. eth_accounts returned empty");
461
+ return [2 /*return*/, null];
442
462
  }
443
- else {
444
- this.currentConnectedAccount = account;
463
+ return [2 /*return*/, res];
464
+ case 2:
465
+ err_2 = _b.sent();
466
+ if (err_2.code !== 4001) {
467
+ console.log("FormoAnalytics::getAccounts: eth_accounts threw an error", err_2);
445
468
  }
446
- _a = this;
447
- return [4 /*yield*/, this.getCurrentChainId()];
448
- case 1:
449
- _a.currentChainId = _b.sent();
450
- this.connect({ account: account, chainId: this.currentChainId });
451
- this.storeWalletAddress(account);
452
- return [2 /*return*/];
469
+ return [2 /*return*/, null];
470
+ case 3: return [2 /*return*/];
453
471
  }
454
472
  });
455
473
  });
456
474
  };
457
- FormoAnalytics.prototype.getCurrentWallet = function () {
475
+ FormoAnalytics.prototype.getCurrentChainId = function () {
458
476
  return __awaiter(this, void 0, void 0, function () {
459
- var sessionData, parsedData, sessionExpiry, currentTime;
460
- return __generator(this, function (_a) {
461
- if (!this.provider) {
462
- console.warn('FormoAnalytics::getCurrentWallet: the provider is not set');
463
- return [2 /*return*/];
464
- }
465
- sessionData = sessionStorage.getItem(this.sessionKey);
466
- if (!sessionData) {
467
- return [2 /*return*/, null];
468
- }
469
- parsedData = JSON.parse(sessionData);
470
- sessionExpiry = 30 * 60 * 1000;
471
- currentTime = Date.now();
472
- if (currentTime - parsedData.timestamp > sessionExpiry) {
473
- console.warn('Session expired. Ignoring wallet address.');
474
- sessionStorage.removeItem(this.sessionKey); // Clear expired session data
475
- return [2 /*return*/, ''];
477
+ var chainIdHex, err_3;
478
+ var _a;
479
+ return __generator(this, function (_b) {
480
+ switch (_b.label) {
481
+ case 0:
482
+ if (!this.provider) {
483
+ console.error("FormoAnalytics::getCurrentChainId: provider not set");
484
+ }
485
+ _b.label = 1;
486
+ case 1:
487
+ _b.trys.push([1, 3, , 4]);
488
+ return [4 /*yield*/, ((_a = this.provider) === null || _a === void 0 ? void 0 : _a.request({
489
+ method: "eth_chainId",
490
+ }))];
491
+ case 2:
492
+ chainIdHex = _b.sent();
493
+ if (!chainIdHex) {
494
+ console.log("FormoAnalytics::fetchChainId: chain id not found");
495
+ return [2 /*return*/, 0];
496
+ }
497
+ return [2 /*return*/, parseInt(chainIdHex, 16)];
498
+ case 3:
499
+ err_3 = _b.sent();
500
+ console.log("FormoAnalytics::fetchChainId: eth_chainId threw an error", err_3);
501
+ return [2 /*return*/, 0];
502
+ case 4: return [2 /*return*/];
476
503
  }
477
- this.handleAccountConnected(parsedData.address);
478
- return [2 /*return*/, parsedData.address || ''];
479
504
  });
480
505
  });
481
506
  };
482
- /**
483
- * Stores the wallet address in session storage when connected.
484
- * @param address - The wallet address to store.
485
- */
486
- FormoAnalytics.prototype.storeWalletAddress = function (address) {
487
- if (!address) {
488
- console.error('No wallet address provided to store.');
489
- return;
507
+ FormoAnalytics.prototype.getLocation = function () {
508
+ try {
509
+ var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
510
+ return constants_1.COUNTRY_LIST[timezone];
490
511
  }
491
- var sessionData = {
492
- address: address,
493
- timestamp: Date.now(),
494
- };
495
- sessionStorage.setItem(this.sessionKey, JSON.stringify(sessionData));
496
- };
497
- /**
498
- * Clears the wallet address from session storage when disconnected.
499
- */
500
- FormoAnalytics.prototype.clearWalletAddress = function () {
501
- sessionStorage.removeItem(this.sessionKey);
502
- };
503
- // Function to build the API URL
504
- FormoAnalytics.prototype.buildApiUrl = function () {
505
- var _a = this.config, host = _a.host, proxy = _a.proxy, token = _a.token, _b = _a.dataSource, dataSource = _b === void 0 ? 'analytics_events' : _b;
506
- if (token) {
507
- if (proxy) {
508
- return "".concat(proxy, "/api/tracking");
509
- }
510
- if (host) {
511
- return "".concat(host.replace(/\/+$/, ''), "/v0/events?name=").concat(dataSource, "&token=").concat(token);
512
- }
513
- return "".concat(constants_1.EVENTS_API, "?name=").concat(dataSource, "&token=").concat(token);
512
+ catch (error) {
513
+ console.error("Error resolving timezone:", error);
514
+ return undefined;
514
515
  }
515
- return 'Error: No token provided';
516
516
  };
517
- FormoAnalytics.prototype.connect = function (_a) {
518
- var account = _a.account, chainId = _a.chainId;
519
- if (!chainId) {
520
- throw new Error('FormoAnalytics::connect: chainId cannot be empty');
521
- }
522
- if (!account) {
523
- throw new Error('FormoAnalytics::connect: account cannot be empty');
517
+ FormoAnalytics.prototype.getLanguage = function () {
518
+ try {
519
+ return ((navigator.languages && navigator.languages.length
520
+ ? navigator.languages[0]
521
+ : navigator.language) || "en");
524
522
  }
525
- this.currentChainId = chainId.toString();
526
- this.currentConnectedAccount = account;
527
- return this.trackEvent(constants_1.Event.CONNECT, {
528
- chainId: chainId,
529
- address: account,
530
- });
531
- };
532
- FormoAnalytics.prototype.disconnect = function (attributes) {
533
- var account = (attributes === null || attributes === void 0 ? void 0 : attributes.account) || this.currentConnectedAccount;
534
- if (!account) {
535
- // We have most likely already reported this disconnection with the automatic
536
- // `disconnect` detection
537
- return;
523
+ catch (error) {
524
+ console.error("Error resolving language:", error);
525
+ return "en";
538
526
  }
539
- var chainId = (attributes === null || attributes === void 0 ? void 0 : attributes.chainId) || this.currentChainId;
540
- var eventAttributes = __assign({ account: account }, (chainId && { chainId: chainId }));
541
- this.currentChainId = undefined;
542
- this.currentConnectedAccount = undefined;
543
- return this.trackEvent(constants_1.Event.DISCONNECT, eventAttributes);
544
527
  };
545
- FormoAnalytics.prototype.chain = function (_a) {
546
- var chainId = _a.chainId, account = _a.account;
547
- if (!chainId || Number(chainId) === 0) {
548
- throw new Error('FormoAnalytics::chain: chainId cannot be empty or 0');
549
- }
550
- if (!account && !this.currentConnectedAccount) {
551
- throw new Error('FormoAnalytics::chain: account was empty and no previous account has been recorded. You can either pass an account or call connect() first');
552
- }
553
- if (isNaN(Number(chainId))) {
554
- throw new Error('FormoAnalytics::chain: chainId must be a valid hex or decimal number');
555
- }
556
- this.currentChainId = chainId.toString();
557
- return this.trackEvent(constants_1.Event.CHAIN_CHANGED, {
558
- chainId: chainId,
559
- account: account || this.currentConnectedAccount,
528
+ // Adds browser properties to the user-supplied payload
529
+ FormoAnalytics.prototype.buildEventPayload = function () {
530
+ return __awaiter(this, arguments, void 0, function (eventSpecificPayload) {
531
+ var url, params, location, language, address;
532
+ if (eventSpecificPayload === void 0) { eventSpecificPayload = {}; }
533
+ return __generator(this, function (_a) {
534
+ switch (_a.label) {
535
+ case 0:
536
+ url = new URL(window.location.href);
537
+ params = new URLSearchParams(url.search);
538
+ location = this.getLocation();
539
+ language = this.getLanguage();
540
+ return [4 /*yield*/, this.getAddress()];
541
+ case 1:
542
+ address = _a.sent();
543
+ // common browser properties
544
+ return [2 /*return*/, __assign({ "user-agent": window.navigator.userAgent, address: address, locale: language, location: location, referrer: document.referrer, utm_source: params.get("utm_source"), utm_medium: params.get("utm_medium"), utm_campaign: params.get("utm_campaign"), ref: params.get("ref") }, eventSpecificPayload)];
545
+ }
546
+ });
560
547
  });
561
548
  };
562
- FormoAnalytics.prototype.init = function (apiKey, projectId) {
563
- var instance = new FormoAnalytics(apiKey, projectId);
564
- return Promise.resolve(instance);
565
- };
566
- FormoAnalytics.prototype.identify = function (userData) {
567
- this.identifyUser(userData);
568
- };
569
- FormoAnalytics.prototype.page = function () {
570
- this.trackPageHit();
571
- };
572
- FormoAnalytics.prototype.track = function (eventName, eventData) {
573
- this.trackEvent(eventName, eventData);
574
- };
575
549
  return FormoAnalytics;
576
550
  }());
577
551
  exports.FormoAnalytics = FormoAnalytics;