@formo/analytics 1.22.0 → 1.24.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.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## Installation
19
19
 
20
- The Formo Web SDK is a Javascript library that allows you to track and analyze user interactions on your dapp.
20
+ The Formo Web SDK is a Javascript library that allows you to track user event data from your website and app.
21
21
 
22
22
  You can install Formo on:
23
23
  - [Websites](https://docs.formo.so/install#website)
@@ -36,7 +36,80 @@ Learn how Formo handles [onchain attribution](https://docs.formo.so/data/attribu
36
36
 
37
37
  Join the [Formo community Slack channel](https://formo.so/slack) for help and questions.
38
38
 
39
+ ## Development
40
+
41
+ ### Building the SDK
42
+
43
+ ```bash
44
+ pnpm install
45
+ pnpm build
46
+ ```
47
+
48
+ ### Running Tests
49
+
50
+ ```bash
51
+ pnpm test
52
+ ```
53
+
54
+ ### Publishing a New Release
55
+
56
+ This project uses **OIDC Trusted Publishing** for secure, automated npm releases. No manual token management required!
57
+
58
+ 1. **(Optional) Preview release notes**:
59
+ ```bash
60
+ pnpm preview-release
61
+ ```
62
+ This shows what the release notes will look like based on commits since the last tag.
63
+
64
+ 2. **Update the version** in `package.json`:
65
+ ```bash
66
+ npm version patch # For bug fixes (1.24.0 → 1.24.1)
67
+ npm version minor # For new features (1.24.0 → 1.25.0)
68
+ npm version major # For breaking changes (1.24.0 → 2.0.0)
69
+ ```
70
+
71
+ > **Note**: `npm version` automatically:
72
+ > - Updates `package.json` with the new version
73
+ > - Updates `src/version.ts` with the new version (via the `version` script)
74
+ > - Creates a git commit with the changes
75
+ > - Creates a version tag (e.g., `v1.24.1`)
76
+
77
+ 3. **Push the commit and tag**:
78
+ ```bash
79
+ git push --follow-tags
80
+ # or separately:
81
+ # git push && git push --tags
82
+ ```
83
+
84
+ > **Important**: You must push both the commit AND the tag. Use `--follow-tags` to push both in one command.
85
+
86
+ 4. **Automatic workflow execution**:
87
+ - GitHub Actions workflow triggers on the `v*` tag
88
+ - Builds and tests the package
89
+ - Publishes to npm using OIDC (no tokens needed!)
90
+ - Creates a GitHub release with:
91
+ - Changelog from git commits
92
+ - Installation instructions
93
+ - CDN usage examples
94
+ - SRI hash for secure CDN usage
95
+
96
+ #### What Gets Published
97
+
98
+ - **npm**: `@formo/analytics@<version>`
99
+ - **GitHub Release**: Tagged release with changelog and SRI hash
100
+ - **CDN**: `https://cdn.formo.so/analytics@<version>`
101
+ - **Provenance**: Automatically generated cryptographic attestations
102
+
103
+ #### Security Features
104
+
105
+ ✅ **OIDC Trusted Publishing** - No long-lived tokens
106
+ ✅ **Automatic Provenance** - Cryptographic proof of build authenticity
107
+ ✅ **SRI Hash** - Subresource integrity for CDN usage
108
+ ✅ **Secure by Default** - Short-lived, workflow-specific credentials
109
+
110
+ Learn more: [npm Trusted Publishing Documentation](https://docs.npmjs.com/trusted-publishers)
111
+
39
112
  ## Contributing
40
113
 
41
- [Contributions](https://github.com/getformo/sdk/blob/main/CONTRIBUTING.md) are welcome! Feel free to open fixes and feature suggestions.
114
+ Contributions are welcome! Feel free to open fixes and feature suggestions.
42
115
 
@@ -121,12 +121,12 @@ var FormoAnalytics = /** @class */ (function () {
121
121
  enabledLevels: ((_b = options.logger) === null || _b === void 0 ? void 0 : _b.levels) || [],
122
122
  });
123
123
  this.eventManager = new lib_1.EventManager(new lib_1.EventQueue(this.config.writeKey, {
124
- url: constants_1.EVENTS_API_URL,
124
+ apiHost: options.apiHost || constants_1.EVENTS_API_HOST,
125
125
  flushAt: options.flushAt,
126
126
  retryCount: options.retryCount,
127
127
  maxQueueSize: options.maxQueueSize,
128
128
  flushInterval: options.flushInterval,
129
- }));
129
+ }), options);
130
130
  // Check consent status on initialization
131
131
  if (this.hasOptedOutTracking()) {
132
132
  lib_1.logger.info("User has previously opted out of tracking");
@@ -1,6 +1,5 @@
1
- export declare const EVENTS_API_HOST = "https://events.formo.so";
2
- export declare const EVENTS_API_URL = "https://events.formo.so/v0/raw_events";
3
- export declare const USER_API_URL = "https://events.formo.so/user";
1
+ export declare const EVENTS_API_ORIGIN = "https://events.formo.so";
2
+ export declare const EVENTS_API_HOST = "https://events.formo.so/v0/raw_events";
4
3
  export declare const EVENTS_API_REQUEST_HEADER: (writeKey: string) => {
5
4
  "Content-Type": string;
6
5
  Authorization: string;
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.COUNTRY_LIST = exports.EVENTS_API_REQUEST_HEADER = exports.USER_API_URL = exports.EVENTS_API_URL = exports.EVENTS_API_HOST = void 0;
4
- exports.EVENTS_API_HOST = "https://events.formo.so";
5
- exports.EVENTS_API_URL = "".concat(exports.EVENTS_API_HOST, "/v0/raw_events");
6
- exports.USER_API_URL = "".concat(exports.EVENTS_API_HOST, "/user");
3
+ exports.COUNTRY_LIST = exports.EVENTS_API_REQUEST_HEADER = exports.EVENTS_API_HOST = exports.EVENTS_API_ORIGIN = void 0;
4
+ exports.EVENTS_API_ORIGIN = "https://events.formo.so";
5
+ exports.EVENTS_API_HOST = "".concat(exports.EVENTS_API_ORIGIN, "/v0/raw_events");
7
6
  var EVENTS_API_REQUEST_HEADER = function (writeKey) { return ({
8
7
  "Content-Type": "application/json",
9
8
  Authorization: "Basic ".concat(writeKey),
@@ -1,6 +1,9 @@
1
- import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, SignatureStatus, TransactionStatus } from "../../types";
1
+ import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, Options, SignatureStatus, TransactionStatus } from "../../types";
2
2
  import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
+ private options?;
5
+ private compiledPathPattern?;
6
+ constructor(options?: Options);
4
7
  private getTimezone;
5
8
  private getLocation;
6
9
  private getLanguage;
@@ -8,6 +11,7 @@ declare class EventFactory implements IEventFactory {
8
11
  private extractUTMParameters;
9
12
  private extractReferralParameter;
10
13
  private getTrafficSources;
14
+ private getScreen;
11
15
  private generateContext;
12
16
  /**
13
17
  * Add any missing default page properties using values from options and defaults
@@ -59,13 +59,14 @@ var validators_1 = require("../../validators");
59
59
  var logger_1 = require("../logger");
60
60
  var mergeDeepRight_1 = __importDefault(require("../ramda/mergeDeepRight"));
61
61
  var storage_1 = require("../storage");
62
- var version_1 = require("../version");
62
+ var version_1 = require("../../version");
63
63
  var constants_2 = require("./constants");
64
64
  var utils_2 = require("./utils");
65
65
  var browsers_1 = require("../browser/browsers");
66
66
  var EventFactory = /** @class */ (function () {
67
- function EventFactory() {
67
+ function EventFactory(options) {
68
68
  var _this = this;
69
+ var _a;
69
70
  this.extractUTMParameters = function (url) {
70
71
  var result = {
71
72
  utm_campaign: "",
@@ -87,14 +88,33 @@ var EventFactory = /** @class */ (function () {
87
88
  return result;
88
89
  };
89
90
  this.extractReferralParameter = function (urlObj) {
90
- var _a;
91
- var referralParams = ["ref", "referral", "refcode"];
91
+ var _a, _b, _c;
92
+ // Strategy: Check query params first, then check path pattern if configured
93
+ // Query params logic:
94
+ // - If no referral config exists → use defaults
95
+ // - If referral config exists but queryParams is undefined → use defaults
96
+ // - If referral config exists with queryParams → use those
97
+ var defaultParams = ["ref", "referral", "refcode"];
98
+ var referralParams = !((_a = _this.options) === null || _a === void 0 ? void 0 : _a.referral)
99
+ ? defaultParams // No referral config at all → use defaults
100
+ : ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : defaultParams); // Has config → use queryParams or defaults
101
+ // Check query parameters (if any configured)
92
102
  for (var _i = 0, referralParams_1 = referralParams; _i < referralParams_1.length; _i++) {
93
103
  var param = referralParams_1[_i];
94
- var value = (_a = urlObj.searchParams.get(param)) === null || _a === void 0 ? void 0 : _a.trim();
104
+ var value = (_c = urlObj.searchParams.get(param)) === null || _c === void 0 ? void 0 : _c.trim();
95
105
  if (value)
96
106
  return value;
97
107
  }
108
+ // Check URL path pattern if configured
109
+ if (_this.compiledPathPattern) {
110
+ var pathname = urlObj.pathname;
111
+ var match = pathname.match(_this.compiledPathPattern);
112
+ if (match && match[1]) {
113
+ var referralCode = match[1].trim();
114
+ if (referralCode)
115
+ return referralCode;
116
+ }
117
+ }
98
118
  return "";
99
119
  };
100
120
  this.getTrafficSources = function (url) {
@@ -148,6 +168,16 @@ var EventFactory = /** @class */ (function () {
148
168
  }
149
169
  return pageProps;
150
170
  };
171
+ this.options = options;
172
+ // Compile regex pattern once for better performance
173
+ if ((_a = options === null || options === void 0 ? void 0 : options.referral) === null || _a === void 0 ? void 0 : _a.pathPattern) {
174
+ try {
175
+ this.compiledPathPattern = new RegExp(options.referral.pathPattern);
176
+ }
177
+ catch (error) {
178
+ logger_1.logger.warn("Invalid referral path pattern: ".concat(options.referral.pathPattern, ". Error: ").concat(error));
179
+ }
180
+ }
151
181
  }
152
182
  EventFactory.prototype.getTimezone = function () {
153
183
  try {
@@ -184,6 +214,31 @@ var EventFactory = /** @class */ (function () {
184
214
  EventFactory.prototype.getLibraryVersion = function () {
185
215
  return version_1.version;
186
216
  };
217
+ // Get screen dimensions and pixel density
218
+ // Returns safe defaults if any error occurs to ensure event creation continues
219
+ EventFactory.prototype.getScreen = function () {
220
+ var _a, _b;
221
+ var safeDefaults = {
222
+ screen_width: 0,
223
+ screen_height: 0,
224
+ screen_density: 1,
225
+ viewport_width: 0,
226
+ viewport_height: 0,
227
+ };
228
+ try {
229
+ return {
230
+ screen_width: ((_a = globalThis.screen) === null || _a === void 0 ? void 0 : _a.width) || 0,
231
+ screen_height: ((_b = globalThis.screen) === null || _b === void 0 ? void 0 : _b.height) || 0,
232
+ screen_density: globalThis.devicePixelRatio || 1,
233
+ viewport_width: globalThis.innerWidth || 0,
234
+ viewport_height: globalThis.innerHeight || 0,
235
+ };
236
+ }
237
+ catch (error) {
238
+ logger_1.logger.error("Error resolving screen properties:", error);
239
+ return safeDefaults;
240
+ }
241
+ };
187
242
  // Contextual fields that are automatically collected and populated by the Formo SDK
188
243
  EventFactory.prototype.generateContext = function (context) {
189
244
  return __awaiter(this, void 0, void 0, function () {
@@ -199,7 +254,7 @@ var EventFactory = /** @class */ (function () {
199
254
  timezone = this.getTimezone();
200
255
  location = this.getLocation();
201
256
  library_version = this.getLibraryVersion();
202
- defaultContext = __assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_path: path, page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName });
257
+ defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_path: path, page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
203
258
  mergedContext = (0, mergeDeepRight_1.default)(defaultContext, context || {});
204
259
  return [2 /*return*/, mergedContext];
205
260
  }
@@ -1,4 +1,4 @@
1
- import { Address, APIEvent } from "../../types";
1
+ import { Address, APIEvent, Options } from "../../types";
2
2
  import { IEventQueue } from "../queue";
3
3
  import { IEventFactory, IEventManager } from "./type";
4
4
  /**
@@ -10,8 +10,9 @@ declare class EventManager implements IEventManager {
10
10
  /**
11
11
  *
12
12
  * @param eventQueue Event queue instance
13
+ * @param options Optional configuration (referral parsing, etc.)
13
14
  */
14
- constructor(eventQueue: IEventQueue);
15
+ constructor(eventQueue: IEventQueue, options?: Options);
15
16
  /**
16
17
  * Consumes a new incoming event
17
18
  * @param event Incoming event data
@@ -58,10 +58,11 @@ var EventManager = /** @class */ (function () {
58
58
  /**
59
59
  *
60
60
  * @param eventQueue Event queue instance
61
+ * @param options Optional configuration (referral parsing, etc.)
61
62
  */
62
- function EventManager(eventQueue) {
63
+ function EventManager(eventQueue, options) {
63
64
  this.eventQueue = eventQueue;
64
- this.eventFactory = new EventFactory_1.EventFactory();
65
+ this.eventFactory = new EventFactory_1.EventFactory(options);
65
66
  }
66
67
  /**
67
68
  * Consumes a new incoming event
@@ -1,7 +1,7 @@
1
1
  import { IFormoEvent } from "../../types";
2
2
  import { IEventQueue } from "./type";
3
3
  type Options = {
4
- url: string;
4
+ apiHost: string;
5
5
  flushAt?: number;
6
6
  flushInterval?: number;
7
7
  host?: string;
@@ -11,7 +11,7 @@ type Options = {
11
11
  };
12
12
  export declare class EventQueue implements IEventQueue {
13
13
  private writeKey;
14
- private url;
14
+ private apiHost;
15
15
  private queue;
16
16
  private timer;
17
17
  private flushAt;
@@ -127,7 +127,7 @@ var EventQueue = /** @class */ (function () {
127
127
  options = options || {};
128
128
  this.queue = [];
129
129
  this.writeKey = writeKey;
130
- this.url = options.url;
130
+ this.apiHost = options.apiHost;
131
131
  this.retryCount = (0, utils_1.clampNumber)(options.retryCount || DEFAULT_RETRY, MAX_RETRY, MIN_RETRY);
132
132
  this.flushAt = (0, utils_1.clampNumber)(options.flushAt || DEFAULT_FLUSH_AT, MAX_FLUSH_AT, MIN_FLUSH_AT);
133
133
  this.maxQueueSize = (0, utils_1.clampNumber)(options.maxQueueSize || DEFAULT_QUEUE_SIZE, MAX_QUEUE_SIZE, MIN_QUEUE_SIZE);
@@ -245,7 +245,7 @@ var EventQueue = /** @class */ (function () {
245
245
  });
246
246
  callback(err, data);
247
247
  };
248
- return [2 /*return*/, (this.pendingFlush = (0, fetch_1.default)("".concat(this.url), {
248
+ return [2 /*return*/, (this.pendingFlush = (0, fetch_1.default)("".concat(this.apiHost), {
249
249
  headers: (0, constants_1.EVENTS_API_REQUEST_HEADER)(this.writeKey),
250
250
  method: "POST",
251
251
  body: JSON.stringify(data),
@@ -94,6 +94,24 @@ export interface AutocaptureOptions {
94
94
  */
95
95
  chain?: boolean;
96
96
  }
97
+ /**
98
+ * Configuration options for referral parameter parsing
99
+ */
100
+ export interface ReferralOptions {
101
+ /**
102
+ * Custom query parameter names to check for referral codes
103
+ * @default ["ref", "referral", "refcode"]
104
+ * @example ["via", "referrer", "source"] - will check ?via=CODE, ?referrer=CODE, ?source=CODE
105
+ */
106
+ queryParams?: string[];
107
+ /**
108
+ * URL path pattern to extract referral code from
109
+ * Should be a regex string that matches the path segment containing the referral code
110
+ * The first capture group will be used as the referral code
111
+ * @example "/r/([^/]+)" - will extract "01K17FKB" from "https://glider.fi/r/01K17FKB"
112
+ */
113
+ pathPattern?: string;
114
+ }
97
115
  export interface Options {
98
116
  provider?: EIP1193Provider;
99
117
  tracking?: boolean | TrackingOptions;
@@ -105,6 +123,15 @@ export interface Options {
105
123
  * @default true
106
124
  */
107
125
  autocapture?: boolean | AutocaptureOptions;
126
+ /**
127
+ * Custom API host for sending events through your own domain to bypass ad blockers
128
+ * - If not provided, events are sent directly to events.formo.so
129
+ * - When provided, events are sent to your custom endpoint which should forward them to Formo
130
+ * - Example: 'https://your-host-url.com/ingest' or '/api/analytics'
131
+ *
132
+ * See https://docs.formo.so/sdks/web#proxy for setup instructions
133
+ */
134
+ apiHost?: string;
108
135
  flushAt?: number;
109
136
  flushInterval?: number;
110
137
  retryCount?: number;
@@ -113,6 +140,12 @@ export interface Options {
113
140
  enabled?: boolean;
114
141
  levels?: LogLevel[];
115
142
  };
143
+ /**
144
+ * Configuration for referral parameter parsing from URLs
145
+ * Allows customizing how referral codes are detected from query parameters and URL paths
146
+ * @example { queryParams: ["via"], pathPattern: "/r/([^/]+)" }
147
+ */
148
+ referral?: ReferralOptions;
116
149
  ready?: (formo: IFormoAnalytics) => void;
117
150
  }
118
151
  export interface FormoAnalyticsProviderProps {
@@ -0,0 +1,2 @@
1
+ export declare const version = "1.24.0";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.version = void 0;
4
+ // This file is auto-generated by scripts/update-version.js during npm version
5
+ // Do not edit manually - it will be overwritten
6
+ exports.version = '1.24.0';
7
+ //# sourceMappingURL=version.js.map
@@ -55,7 +55,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
55
55
  return to.concat(ar || Array.prototype.slice.call(from));
56
56
  };
57
57
  import { createStore } from "mipd";
58
- import { EVENTS_API_URL, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_CURRENT_URL_KEY, SESSION_USER_ID_KEY, SESSION_WALLET_DETECTED_KEY, SESSION_WALLET_IDENTIFIED_KEY, DEFAULT_PROVIDER_ICON, CONSENT_OPT_OUT_KEY, } from "./constants";
58
+ import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_CURRENT_URL_KEY, SESSION_USER_ID_KEY, SESSION_WALLET_DETECTED_KEY, SESSION_WALLET_IDENTIFIED_KEY, DEFAULT_PROVIDER_ICON, CONSENT_OPT_OUT_KEY, } from "./constants";
59
59
  import { cookie, EventManager, EventQueue, initStorageManager, logger, Logger, setConsentFlag, getConsentFlag, removeConsentFlag, } from "./lib";
60
60
  import { SignatureStatus, TransactionStatus, WRAPPED_REQUEST_SYMBOL, WRAPPED_REQUEST_REF_SYMBOL, } from "./types";
61
61
  import { toChecksumAddress } from "./utils";
@@ -118,12 +118,12 @@ var FormoAnalytics = /** @class */ (function () {
118
118
  enabledLevels: ((_b = options.logger) === null || _b === void 0 ? void 0 : _b.levels) || [],
119
119
  });
120
120
  this.eventManager = new EventManager(new EventQueue(this.config.writeKey, {
121
- url: EVENTS_API_URL,
121
+ apiHost: options.apiHost || EVENTS_API_HOST,
122
122
  flushAt: options.flushAt,
123
123
  retryCount: options.retryCount,
124
124
  maxQueueSize: options.maxQueueSize,
125
125
  flushInterval: options.flushInterval,
126
- }));
126
+ }), options);
127
127
  // Check consent status on initialization
128
128
  if (this.hasOptedOutTracking()) {
129
129
  logger.info("User has previously opted out of tracking");
@@ -1,6 +1,5 @@
1
- export declare const EVENTS_API_HOST = "https://events.formo.so";
2
- export declare const EVENTS_API_URL = "https://events.formo.so/v0/raw_events";
3
- export declare const USER_API_URL = "https://events.formo.so/user";
1
+ export declare const EVENTS_API_ORIGIN = "https://events.formo.so";
2
+ export declare const EVENTS_API_HOST = "https://events.formo.so/v0/raw_events";
4
3
  export declare const EVENTS_API_REQUEST_HEADER: (writeKey: string) => {
5
4
  "Content-Type": string;
6
5
  Authorization: string;
@@ -1,6 +1,5 @@
1
- export var EVENTS_API_HOST = "https://events.formo.so";
2
- export var EVENTS_API_URL = "".concat(EVENTS_API_HOST, "/v0/raw_events");
3
- export var USER_API_URL = "".concat(EVENTS_API_HOST, "/user");
1
+ export var EVENTS_API_ORIGIN = "https://events.formo.so";
2
+ export var EVENTS_API_HOST = "".concat(EVENTS_API_ORIGIN, "/v0/raw_events");
4
3
  export var EVENTS_API_REQUEST_HEADER = function (writeKey) { return ({
5
4
  "Content-Type": "application/json",
6
5
  Authorization: "Basic ".concat(writeKey),
@@ -1,6 +1,9 @@
1
- import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, SignatureStatus, TransactionStatus } from "../../types";
1
+ import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, Options, SignatureStatus, TransactionStatus } from "../../types";
2
2
  import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
+ private options?;
5
+ private compiledPathPattern?;
6
+ constructor(options?: Options);
4
7
  private getTimezone;
5
8
  private getLocation;
6
9
  private getLanguage;
@@ -8,6 +11,7 @@ declare class EventFactory implements IEventFactory {
8
11
  private extractUTMParameters;
9
12
  private extractReferralParameter;
10
13
  private getTrafficSources;
14
+ private getScreen;
11
15
  private generateContext;
12
16
  /**
13
17
  * Add any missing default page properties using values from options and defaults
@@ -53,13 +53,14 @@ import { isUndefined } from "../../validators";
53
53
  import { logger } from "../logger";
54
54
  import mergeDeepRight from "../ramda/mergeDeepRight";
55
55
  import { session } from "../storage";
56
- import { version } from "../version";
56
+ import { version } from "../../version";
57
57
  import { CHANNEL, VERSION } from "./constants";
58
58
  import { generateAnonymousId } from "./utils";
59
59
  import { detectBrowser } from "../browser/browsers";
60
60
  var EventFactory = /** @class */ (function () {
61
- function EventFactory() {
61
+ function EventFactory(options) {
62
62
  var _this = this;
63
+ var _a;
63
64
  this.extractUTMParameters = function (url) {
64
65
  var result = {
65
66
  utm_campaign: "",
@@ -81,14 +82,33 @@ var EventFactory = /** @class */ (function () {
81
82
  return result;
82
83
  };
83
84
  this.extractReferralParameter = function (urlObj) {
84
- var _a;
85
- var referralParams = ["ref", "referral", "refcode"];
85
+ var _a, _b, _c;
86
+ // Strategy: Check query params first, then check path pattern if configured
87
+ // Query params logic:
88
+ // - If no referral config exists → use defaults
89
+ // - If referral config exists but queryParams is undefined → use defaults
90
+ // - If referral config exists with queryParams → use those
91
+ var defaultParams = ["ref", "referral", "refcode"];
92
+ var referralParams = !((_a = _this.options) === null || _a === void 0 ? void 0 : _a.referral)
93
+ ? defaultParams // No referral config at all → use defaults
94
+ : ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : defaultParams); // Has config → use queryParams or defaults
95
+ // Check query parameters (if any configured)
86
96
  for (var _i = 0, referralParams_1 = referralParams; _i < referralParams_1.length; _i++) {
87
97
  var param = referralParams_1[_i];
88
- var value = (_a = urlObj.searchParams.get(param)) === null || _a === void 0 ? void 0 : _a.trim();
98
+ var value = (_c = urlObj.searchParams.get(param)) === null || _c === void 0 ? void 0 : _c.trim();
89
99
  if (value)
90
100
  return value;
91
101
  }
102
+ // Check URL path pattern if configured
103
+ if (_this.compiledPathPattern) {
104
+ var pathname = urlObj.pathname;
105
+ var match = pathname.match(_this.compiledPathPattern);
106
+ if (match && match[1]) {
107
+ var referralCode = match[1].trim();
108
+ if (referralCode)
109
+ return referralCode;
110
+ }
111
+ }
92
112
  return "";
93
113
  };
94
114
  this.getTrafficSources = function (url) {
@@ -142,6 +162,16 @@ var EventFactory = /** @class */ (function () {
142
162
  }
143
163
  return pageProps;
144
164
  };
165
+ this.options = options;
166
+ // Compile regex pattern once for better performance
167
+ if ((_a = options === null || options === void 0 ? void 0 : options.referral) === null || _a === void 0 ? void 0 : _a.pathPattern) {
168
+ try {
169
+ this.compiledPathPattern = new RegExp(options.referral.pathPattern);
170
+ }
171
+ catch (error) {
172
+ logger.warn("Invalid referral path pattern: ".concat(options.referral.pathPattern, ". Error: ").concat(error));
173
+ }
174
+ }
145
175
  }
146
176
  EventFactory.prototype.getTimezone = function () {
147
177
  try {
@@ -178,6 +208,31 @@ var EventFactory = /** @class */ (function () {
178
208
  EventFactory.prototype.getLibraryVersion = function () {
179
209
  return version;
180
210
  };
211
+ // Get screen dimensions and pixel density
212
+ // Returns safe defaults if any error occurs to ensure event creation continues
213
+ EventFactory.prototype.getScreen = function () {
214
+ var _a, _b;
215
+ var safeDefaults = {
216
+ screen_width: 0,
217
+ screen_height: 0,
218
+ screen_density: 1,
219
+ viewport_width: 0,
220
+ viewport_height: 0,
221
+ };
222
+ try {
223
+ return {
224
+ screen_width: ((_a = globalThis.screen) === null || _a === void 0 ? void 0 : _a.width) || 0,
225
+ screen_height: ((_b = globalThis.screen) === null || _b === void 0 ? void 0 : _b.height) || 0,
226
+ screen_density: globalThis.devicePixelRatio || 1,
227
+ viewport_width: globalThis.innerWidth || 0,
228
+ viewport_height: globalThis.innerHeight || 0,
229
+ };
230
+ }
231
+ catch (error) {
232
+ logger.error("Error resolving screen properties:", error);
233
+ return safeDefaults;
234
+ }
235
+ };
181
236
  // Contextual fields that are automatically collected and populated by the Formo SDK
182
237
  EventFactory.prototype.generateContext = function (context) {
183
238
  return __awaiter(this, void 0, void 0, function () {
@@ -193,7 +248,7 @@ var EventFactory = /** @class */ (function () {
193
248
  timezone = this.getTimezone();
194
249
  location = this.getLocation();
195
250
  library_version = this.getLibraryVersion();
196
- defaultContext = __assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_path: path, page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName });
251
+ defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_path: path, page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
197
252
  mergedContext = mergeDeepRight(defaultContext, context || {});
198
253
  return [2 /*return*/, mergedContext];
199
254
  }
@@ -1,4 +1,4 @@
1
- import { Address, APIEvent } from "../../types";
1
+ import { Address, APIEvent, Options } from "../../types";
2
2
  import { IEventQueue } from "../queue";
3
3
  import { IEventFactory, IEventManager } from "./type";
4
4
  /**
@@ -10,8 +10,9 @@ declare class EventManager implements IEventManager {
10
10
  /**
11
11
  *
12
12
  * @param eventQueue Event queue instance
13
+ * @param options Optional configuration (referral parsing, etc.)
13
14
  */
14
- constructor(eventQueue: IEventQueue);
15
+ constructor(eventQueue: IEventQueue, options?: Options);
15
16
  /**
16
17
  * Consumes a new incoming event
17
18
  * @param event Incoming event data
@@ -55,10 +55,11 @@ var EventManager = /** @class */ (function () {
55
55
  /**
56
56
  *
57
57
  * @param eventQueue Event queue instance
58
+ * @param options Optional configuration (referral parsing, etc.)
58
59
  */
59
- function EventManager(eventQueue) {
60
+ function EventManager(eventQueue, options) {
60
61
  this.eventQueue = eventQueue;
61
- this.eventFactory = new EventFactory();
62
+ this.eventFactory = new EventFactory(options);
62
63
  }
63
64
  /**
64
65
  * Consumes a new incoming event
@@ -1,7 +1,7 @@
1
1
  import { IFormoEvent } from "../../types";
2
2
  import { IEventQueue } from "./type";
3
3
  type Options = {
4
- url: string;
4
+ apiHost: string;
5
5
  flushAt?: number;
6
6
  flushInterval?: number;
7
7
  host?: string;
@@ -11,7 +11,7 @@ type Options = {
11
11
  };
12
12
  export declare class EventQueue implements IEventQueue {
13
13
  private writeKey;
14
- private url;
14
+ private apiHost;
15
15
  private queue;
16
16
  private timer;
17
17
  private flushAt;
@@ -121,7 +121,7 @@ var EventQueue = /** @class */ (function () {
121
121
  options = options || {};
122
122
  this.queue = [];
123
123
  this.writeKey = writeKey;
124
- this.url = options.url;
124
+ this.apiHost = options.apiHost;
125
125
  this.retryCount = clampNumber(options.retryCount || DEFAULT_RETRY, MAX_RETRY, MIN_RETRY);
126
126
  this.flushAt = clampNumber(options.flushAt || DEFAULT_FLUSH_AT, MAX_FLUSH_AT, MIN_FLUSH_AT);
127
127
  this.maxQueueSize = clampNumber(options.maxQueueSize || DEFAULT_QUEUE_SIZE, MAX_QUEUE_SIZE, MIN_QUEUE_SIZE);
@@ -239,7 +239,7 @@ var EventQueue = /** @class */ (function () {
239
239
  });
240
240
  callback(err, data);
241
241
  };
242
- return [2 /*return*/, (this.pendingFlush = fetch("".concat(this.url), {
242
+ return [2 /*return*/, (this.pendingFlush = fetch("".concat(this.apiHost), {
243
243
  headers: EVENTS_API_REQUEST_HEADER(this.writeKey),
244
244
  method: "POST",
245
245
  body: JSON.stringify(data),