@formo/analytics 1.30.1 → 1.32.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.
@@ -3,6 +3,7 @@ import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
4
  private options?;
5
5
  private compiledPathPattern?;
6
+ private excludedQueryParams;
6
7
  constructor(options?: Options);
7
8
  /**
8
9
  * Validate an address for both EVM and Solana chains.
@@ -16,9 +17,37 @@ declare class EventFactory implements IEventFactory {
16
17
  private getLocation;
17
18
  private getLanguage;
18
19
  private getLibraryVersion;
20
+ private isExcludedQueryParam;
21
+ /**
22
+ * Strip excluded (sensitive) query parameters from a URL in place. Only the
23
+ * query string is touched; the path and hash/fragment are left as-is.
24
+ */
25
+ private redactQueryParams;
26
+ /**
27
+ * Return the given absolute URL with excluded query parameters removed. The
28
+ * input is returned unchanged when it is empty or cannot be parsed (e.g. an
29
+ * empty referrer).
30
+ */
31
+ private redactUrl;
19
32
  private extractUTMParameters;
20
33
  private extractClickIdParameters;
21
34
  private extractReferralParameter;
35
+ /**
36
+ * Returns the document referrer with same-host referrers filtered out.
37
+ * Internal navigation populates `document.referrer` with the previous page
38
+ * on the same site, which is not an attribution signal — treating it as
39
+ * "external" would otherwise let an internal URL become the session's
40
+ * first-touch referrer after a direct landing.
41
+ */
42
+ private getExternalReferrer;
43
+ /**
44
+ * Apply the current query-param denylist to a previously-persisted traffic
45
+ * source object. Traffic-source keys (utm_*, click ids, ref) are themselves
46
+ * query-parameter names, so an excluded key's stored value is dropped; the
47
+ * referrer is a URL and is re-redacted. Guards against a stored value
48
+ * outliving the config (or SDK version) under which it was first captured.
49
+ */
50
+ private redactStoredTrafficSources;
22
51
  private getTrafficSources;
23
52
  private getScreen;
24
53
  private generateContext;
@@ -46,6 +46,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
46
46
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
47
  }
48
48
  };
49
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
50
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
51
+ if (ar || !(i in from)) {
52
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
53
+ ar[i] = from[i];
54
+ }
55
+ }
56
+ return to.concat(ar || Array.prototype.slice.call(from));
57
+ };
49
58
  var __importDefault = (this && this.__importDefault) || function (mod) {
50
59
  return (mod && mod.__esModule) ? mod : { "default": mod };
51
60
  };
@@ -67,7 +76,7 @@ var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
67
76
  var EventFactory = /** @class */ (function () {
68
77
  function EventFactory(options) {
69
78
  var _this = this;
70
- var _a;
79
+ var _a, _b;
71
80
  this.extractUTMParameters = function (url) {
72
81
  var result = {
73
82
  utm_campaign: "",
@@ -127,17 +136,46 @@ var EventFactory = /** @class */ (function () {
127
136
  }
128
137
  return "";
129
138
  };
139
+ /**
140
+ * Returns the document referrer with same-host referrers filtered out.
141
+ * Internal navigation populates `document.referrer` with the previous page
142
+ * on the same site, which is not an attribution signal — treating it as
143
+ * "external" would otherwise let an internal URL become the session's
144
+ * first-touch referrer after a direct landing.
145
+ */
146
+ this.getExternalReferrer = function () {
147
+ var _a;
148
+ var ref = document.referrer;
149
+ if (!ref)
150
+ return "";
151
+ try {
152
+ var currentHost = (_a = globalThis.location) === null || _a === void 0 ? void 0 : _a.hostname;
153
+ if (currentHost && new URL(ref).hostname === currentHost)
154
+ return "";
155
+ }
156
+ catch (_b) { }
157
+ return _this.redactUrl(ref);
158
+ };
130
159
  this.getTrafficSources = function (url) {
131
160
  var urlObj = new URL(url);
132
- var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: document.referrer });
133
- var storedTrafficSources = (0, storage_1.session)().get(constants_1.SESSION_TRAFFIC_SOURCE_KEY) || {};
161
+ var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() });
162
+ // Sticky traffic sources may have been persisted by an older SDK version or
163
+ // a looser config, before the current excludeQueryParams was in effect.
164
+ // Honor the current denylist on the way out so excluded values can never
165
+ // resurface from session storage (or get re-persisted below).
166
+ var storedTrafficSources = _this.redactStoredTrafficSources((0, storage_1.session)().get(constants_1.SESSION_TRAFFIC_SOURCE_KEY) || {});
134
167
  var mergedClickIds = {};
135
168
  for (var _i = 0, CLICK_ID_PARAMS_2 = constants_2.CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
136
169
  var p = CLICK_ID_PARAMS_2[_i];
137
170
  mergedClickIds[p] =
138
171
  contextTrafficSources[p] || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources[p]) || "";
139
172
  }
140
- var finalTrafficSources = __assign({ ref: contextTrafficSources.ref || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.ref) || "", referrer: contextTrafficSources.referrer || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.referrer) || "", utm_campaign: contextTrafficSources.utm_campaign ||
173
+ var finalTrafficSources = __assign({ ref: contextTrafficSources.ref || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.ref) || "",
174
+ // Referrer is sticky (first-touch wins). Same-host referrers are already
175
+ // stripped by getExternalReferrer; the stored-first OR keeps the entry
176
+ // referrer pinned even if a later pageview reports a different external
177
+ // referrer (e.g. cross-domain return from an outbound click).
178
+ referrer: (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.referrer) || contextTrafficSources.referrer || "", utm_campaign: contextTrafficSources.utm_campaign ||
141
179
  (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.utm_campaign) ||
142
180
  "", utm_content: contextTrafficSources.utm_content ||
143
181
  (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.utm_content) ||
@@ -166,8 +204,17 @@ var EventFactory = /** @class */ (function () {
166
204
  this.getPageProperties = function (properties) {
167
205
  // Create a copy to avoid mutating the original properties object
168
206
  var pageProps = __assign({}, properties);
207
+ // Parse the current URL once and strip any excluded (sensitive) query
208
+ // params up front, so nothing sensitive is forwarded via url, query, or the
209
+ // per-param explosion below. The hash/fragment is intentionally untouched.
210
+ var urlObj = null;
211
+ try {
212
+ urlObj = new URL(globalThis.location.href);
213
+ _this.redactQueryParams(urlObj);
214
+ }
215
+ catch (_a) { }
169
216
  if ((0, validators_1.isUndefined)(pageProps.url)) {
170
- pageProps.url = new URL(globalThis.location.href).href;
217
+ pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
171
218
  }
172
219
  if ((0, validators_1.isUndefined)(pageProps.path)) {
173
220
  pageProps.path = globalThis.location.pathname;
@@ -177,18 +224,22 @@ var EventFactory = /** @class */ (function () {
177
224
  }
178
225
  // Add query string without the '?' prefix
179
226
  if ((0, validators_1.isUndefined)(pageProps.query)) {
180
- pageProps.query = globalThis.location.search.slice(1);
227
+ pageProps.query = urlObj
228
+ ? urlObj.search.slice(1)
229
+ : globalThis.location.search.slice(1);
181
230
  }
182
231
  // Parse query parameters and add as individual properties (don't overwrite existing)
183
- // Skip fields that are already captured in context or are semantic event properties
232
+ // Skip fields that are already captured in context or are semantic event properties.
233
+ // Excluded params were already removed from urlObj above.
184
234
  try {
185
- var urlObj = new URL(globalThis.location.href);
186
- urlObj.searchParams.forEach(function (value, key) {
187
- // Only add if the property doesn't already exist and is not excluded
188
- if ((0, validators_1.isUndefined)(pageProps[key]) && !constants_2.PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
189
- pageProps[key] = value;
190
- }
191
- });
235
+ if (urlObj) {
236
+ urlObj.searchParams.forEach(function (value, key) {
237
+ // Only add if the property doesn't already exist and is not excluded
238
+ if ((0, validators_1.isUndefined)(pageProps[key]) && !constants_2.PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
239
+ pageProps[key] = value;
240
+ }
241
+ });
242
+ }
192
243
  }
193
244
  catch (error) {
194
245
  logger_1.logger.error("Error parsing query parameters for page properties:", error);
@@ -196,8 +247,13 @@ var EventFactory = /** @class */ (function () {
196
247
  return pageProps;
197
248
  };
198
249
  this.options = options;
250
+ var tracking = options === null || options === void 0 ? void 0 : options.tracking;
251
+ var configuredExcludes = typeof tracking === "object" ? (_a = tracking.excludeQueryParams) !== null && _a !== void 0 ? _a : [] : [];
252
+ this.excludedQueryParams = new Set(__spreadArray(__spreadArray([], constants_2.DEFAULT_EXCLUDED_QUERY_PARAMS, true), configuredExcludes, true).map(function (key) {
253
+ return key.toLowerCase();
254
+ }));
199
255
  // Compile regex pattern once for better performance
200
- if ((_a = options === null || options === void 0 ? void 0 : options.referral) === null || _a === void 0 ? void 0 : _a.pathPattern) {
256
+ if ((_b = options === null || options === void 0 ? void 0 : options.referral) === null || _b === void 0 ? void 0 : _b.pathPattern) {
201
257
  try {
202
258
  this.compiledPathPattern = new RegExp(options.referral.pathPattern);
203
259
  }
@@ -259,6 +315,66 @@ var EventFactory = /** @class */ (function () {
259
315
  EventFactory.prototype.getLibraryVersion = function () {
260
316
  return version_1.version;
261
317
  };
318
+ EventFactory.prototype.isExcludedQueryParam = function (key) {
319
+ return this.excludedQueryParams.has(key.toLowerCase());
320
+ };
321
+ /**
322
+ * Strip excluded (sensitive) query parameters from a URL in place. Only the
323
+ * query string is touched; the path and hash/fragment are left as-is.
324
+ */
325
+ EventFactory.prototype.redactQueryParams = function (url) {
326
+ var _this = this;
327
+ // Collect first, then delete: mutating searchParams while iterating is
328
+ // unsafe, and deleting a key removes all of its values at once.
329
+ var keysToDelete = new Set();
330
+ url.searchParams.forEach(function (_value, key) {
331
+ if (_this.isExcludedQueryParam(key)) {
332
+ keysToDelete.add(key);
333
+ }
334
+ });
335
+ keysToDelete.forEach(function (key) { return url.searchParams.delete(key); });
336
+ };
337
+ /**
338
+ * Return the given absolute URL with excluded query parameters removed. The
339
+ * input is returned unchanged when it is empty or cannot be parsed (e.g. an
340
+ * empty referrer).
341
+ */
342
+ EventFactory.prototype.redactUrl = function (href) {
343
+ if (!href)
344
+ return href;
345
+ try {
346
+ var url = new URL(href);
347
+ this.redactQueryParams(url);
348
+ return url.href;
349
+ }
350
+ catch (_a) {
351
+ return href;
352
+ }
353
+ };
354
+ /**
355
+ * Apply the current query-param denylist to a previously-persisted traffic
356
+ * source object. Traffic-source keys (utm_*, click ids, ref) are themselves
357
+ * query-parameter names, so an excluded key's stored value is dropped; the
358
+ * referrer is a URL and is re-redacted. Guards against a stored value
359
+ * outliving the config (or SDK version) under which it was first captured.
360
+ */
361
+ EventFactory.prototype.redactStoredTrafficSources = function (stored) {
362
+ var result = {};
363
+ for (var _i = 0, _a = Object.keys(stored); _i < _a.length; _i++) {
364
+ var key = _a[_i];
365
+ var value = stored[key];
366
+ if (key === "referrer") {
367
+ result[key] = this.redactUrl(value || "");
368
+ }
369
+ else if (this.isExcludedQueryParam(key)) {
370
+ result[key] = "";
371
+ }
372
+ else {
373
+ result[key] = value;
374
+ }
375
+ }
376
+ return result;
377
+ };
262
378
  // Get screen dimensions and pixel density
263
379
  // Returns safe defaults if any error occurs to ensure event creation continues
264
380
  EventFactory.prototype.getScreen = function () {
@@ -287,7 +403,7 @@ var EventFactory = /** @class */ (function () {
287
403
  // Contextual fields that are automatically collected and populated by the Formo SDK
288
404
  EventFactory.prototype.generateContext = function (context) {
289
405
  return __awaiter(this, void 0, void 0, function () {
290
- var browserName, language, timezone, location, library_version, defaultContext, mergedContext;
406
+ var browserName, language, timezone, location, library_version, redactedHref, defaultContext, mergedContext;
291
407
  return __generator(this, function (_a) {
292
408
  switch (_a.label) {
293
409
  case 0: return [4 /*yield*/, (0, browsers_1.detectBrowser)()];
@@ -297,7 +413,8 @@ var EventFactory = /** @class */ (function () {
297
413
  timezone = this.getTimezone();
298
414
  location = this.getLocation();
299
415
  library_version = this.getLibraryVersion();
300
- defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
416
+ redactedHref = this.redactUrl(globalThis.location.href);
417
+ defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(redactedHref)), { page_title: document.title, page_url: redactedHref, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
301
418
  mergedContext = (0, mergeDeepRight_1.default)(defaultContext, context || {});
302
419
  return [2 /*return*/, mergedContext];
303
420
  }
@@ -6,6 +6,18 @@ declare const VERSION = "0";
6
6
  * in src/types/events.ts derived from this array.
7
7
  */
8
8
  declare const CLICK_ID_PARAMS: readonly ["gclid", "gad_source", "fbclid", "msclkid", "twclid", "li_fat_id", "rdt_cid", "ttclid"];
9
+ /**
10
+ * Query parameters that are ALWAYS stripped from forwarded and stored URLs,
11
+ * regardless of consumer configuration, because they carry high-sensitivity
12
+ * secrets that must never reach Formo:
13
+ * - privy_oauth_code: Privy OAuth authorization code
14
+ * - privy_oauth_state: Privy OAuth CSRF state token
15
+ * - privy_oauth_provider: Privy OAuth provider identifier
16
+ *
17
+ * Consumers can extend the denylist via `tracking.excludeQueryParams` but
18
+ * cannot remove these built-ins. Matched case-insensitively.
19
+ */
20
+ declare const DEFAULT_EXCLUDED_QUERY_PARAMS: readonly ["privy_oauth_code", "privy_oauth_state", "privy_oauth_provider"];
9
21
  /**
10
22
  * Fields that should be excluded from page event properties parsing
11
23
  * These are either:
@@ -13,5 +25,5 @@ declare const CLICK_ID_PARAMS: readonly ["gclid", "gad_source", "fbclid", "msclk
13
25
  * - Semantic event properties that should not be overridden by URL params
14
26
  */
15
27
  declare const PAGE_PROPERTIES_EXCLUDED_FIELDS: Set<string>;
16
- export { CHANNEL, VERSION, CLICK_ID_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS };
28
+ export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
17
29
  //# sourceMappingURL=constants.d.ts.map
@@ -9,7 +9,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
9
9
  return to.concat(ar || Array.prototype.slice.call(from));
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.PAGE_PROPERTIES_EXCLUDED_FIELDS = exports.CLICK_ID_PARAMS = exports.VERSION = exports.CHANNEL = void 0;
12
+ exports.PAGE_PROPERTIES_EXCLUDED_FIELDS = exports.DEFAULT_EXCLUDED_QUERY_PARAMS = exports.CLICK_ID_PARAMS = exports.VERSION = exports.CHANNEL = void 0;
13
13
  var CHANNEL = "web";
14
14
  exports.CHANNEL = CHANNEL;
15
15
  var VERSION = "0";
@@ -30,6 +30,23 @@ var CLICK_ID_PARAMS = [
30
30
  "ttclid", // TikTok Ads
31
31
  ];
32
32
  exports.CLICK_ID_PARAMS = CLICK_ID_PARAMS;
33
+ /**
34
+ * Query parameters that are ALWAYS stripped from forwarded and stored URLs,
35
+ * regardless of consumer configuration, because they carry high-sensitivity
36
+ * secrets that must never reach Formo:
37
+ * - privy_oauth_code: Privy OAuth authorization code
38
+ * - privy_oauth_state: Privy OAuth CSRF state token
39
+ * - privy_oauth_provider: Privy OAuth provider identifier
40
+ *
41
+ * Consumers can extend the denylist via `tracking.excludeQueryParams` but
42
+ * cannot remove these built-ins. Matched case-insensitively.
43
+ */
44
+ var DEFAULT_EXCLUDED_QUERY_PARAMS = [
45
+ "privy_oauth_code",
46
+ "privy_oauth_state",
47
+ "privy_oauth_provider",
48
+ ];
49
+ exports.DEFAULT_EXCLUDED_QUERY_PARAMS = DEFAULT_EXCLUDED_QUERY_PARAMS;
33
50
  /**
34
51
  * Fields that should be excluded from page event properties parsing
35
52
  * These are either:
@@ -72,6 +72,20 @@ export interface TrackingOptions {
72
72
  excludeHosts?: string[];
73
73
  excludePaths?: string[];
74
74
  excludeChains?: ChainID[];
75
+ /**
76
+ * Additional query parameter names to strip from forwarded and stored URLs,
77
+ * on top of a built-in always-on denylist (currently `privy_oauth_code`,
78
+ * `privy_oauth_state`, and `privy_oauth_provider`) that cannot be disabled.
79
+ * Matched case-insensitively. Excluded params are stripped from the captured
80
+ * page URL, query string, per-parameter page properties, and referrer before
81
+ * any event is sent. The URL hash/fragment is intentionally left untouched.
82
+ *
83
+ * Mirrors Mixpanel's `property_blacklist` and PostHog's `property_denylist`,
84
+ * scoped here to URL query parameters.
85
+ *
86
+ * @example ["token", "access_token", "email", "signature"]
87
+ */
88
+ excludeQueryParams?: string[];
75
89
  }
76
90
  /**
77
91
  * Configuration options for controlling wallet event autocapture
@@ -3,6 +3,7 @@ import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
4
  private options?;
5
5
  private compiledPathPattern?;
6
+ private excludedQueryParams;
6
7
  constructor(options?: Options);
7
8
  /**
8
9
  * Validate an address for both EVM and Solana chains.
@@ -16,9 +17,37 @@ declare class EventFactory implements IEventFactory {
16
17
  private getLocation;
17
18
  private getLanguage;
18
19
  private getLibraryVersion;
20
+ private isExcludedQueryParam;
21
+ /**
22
+ * Strip excluded (sensitive) query parameters from a URL in place. Only the
23
+ * query string is touched; the path and hash/fragment are left as-is.
24
+ */
25
+ private redactQueryParams;
26
+ /**
27
+ * Return the given absolute URL with excluded query parameters removed. The
28
+ * input is returned unchanged when it is empty or cannot be parsed (e.g. an
29
+ * empty referrer).
30
+ */
31
+ private redactUrl;
19
32
  private extractUTMParameters;
20
33
  private extractClickIdParameters;
21
34
  private extractReferralParameter;
35
+ /**
36
+ * Returns the document referrer with same-host referrers filtered out.
37
+ * Internal navigation populates `document.referrer` with the previous page
38
+ * on the same site, which is not an attribution signal — treating it as
39
+ * "external" would otherwise let an internal URL become the session's
40
+ * first-touch referrer after a direct landing.
41
+ */
42
+ private getExternalReferrer;
43
+ /**
44
+ * Apply the current query-param denylist to a previously-persisted traffic
45
+ * source object. Traffic-source keys (utm_*, click ids, ref) are themselves
46
+ * query-parameter names, so an excluded key's stored value is dropped; the
47
+ * referrer is a URL and is re-redacted. Guards against a stored value
48
+ * outliving the config (or SDK version) under which it was first captured.
49
+ */
50
+ private redactStoredTrafficSources;
22
51
  private getTrafficSources;
23
52
  private getScreen;
24
53
  private generateContext;
@@ -45,6 +45,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
45
45
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
46
46
  }
47
47
  };
48
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
49
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
50
+ if (ar || !(i in from)) {
51
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
52
+ ar[i] = from[i];
53
+ }
54
+ }
55
+ return to.concat(ar || Array.prototype.slice.call(from));
56
+ };
48
57
  import { COUNTRY_LIST, LOCAL_ANONYMOUS_ID_KEY, SESSION_TRAFFIC_SOURCE_KEY, } from "../constants";
49
58
  import { toSnakeCase } from "../utils";
50
59
  import { validateAddress } from "../utils/address";
@@ -54,14 +63,14 @@ import { logger } from "../logger";
54
63
  import mergeDeepRight from "../ramda/mergeDeepRight";
55
64
  import { session } from "../storage";
56
65
  import { version } from "../version";
57
- import { CHANNEL, CLICK_ID_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
66
+ import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
58
67
  import { generateAnonymousId } from "./utils";
59
68
  import { detectBrowser } from "../browser/browsers";
60
69
  var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
61
70
  var EventFactory = /** @class */ (function () {
62
71
  function EventFactory(options) {
63
72
  var _this = this;
64
- var _a;
73
+ var _a, _b;
65
74
  this.extractUTMParameters = function (url) {
66
75
  var result = {
67
76
  utm_campaign: "",
@@ -121,17 +130,46 @@ var EventFactory = /** @class */ (function () {
121
130
  }
122
131
  return "";
123
132
  };
133
+ /**
134
+ * Returns the document referrer with same-host referrers filtered out.
135
+ * Internal navigation populates `document.referrer` with the previous page
136
+ * on the same site, which is not an attribution signal — treating it as
137
+ * "external" would otherwise let an internal URL become the session's
138
+ * first-touch referrer after a direct landing.
139
+ */
140
+ this.getExternalReferrer = function () {
141
+ var _a;
142
+ var ref = document.referrer;
143
+ if (!ref)
144
+ return "";
145
+ try {
146
+ var currentHost = (_a = globalThis.location) === null || _a === void 0 ? void 0 : _a.hostname;
147
+ if (currentHost && new URL(ref).hostname === currentHost)
148
+ return "";
149
+ }
150
+ catch (_b) { }
151
+ return _this.redactUrl(ref);
152
+ };
124
153
  this.getTrafficSources = function (url) {
125
154
  var urlObj = new URL(url);
126
- var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: document.referrer });
127
- var storedTrafficSources = session().get(SESSION_TRAFFIC_SOURCE_KEY) || {};
155
+ var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() });
156
+ // Sticky traffic sources may have been persisted by an older SDK version or
157
+ // a looser config, before the current excludeQueryParams was in effect.
158
+ // Honor the current denylist on the way out so excluded values can never
159
+ // resurface from session storage (or get re-persisted below).
160
+ var storedTrafficSources = _this.redactStoredTrafficSources(session().get(SESSION_TRAFFIC_SOURCE_KEY) || {});
128
161
  var mergedClickIds = {};
129
162
  for (var _i = 0, CLICK_ID_PARAMS_2 = CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
130
163
  var p = CLICK_ID_PARAMS_2[_i];
131
164
  mergedClickIds[p] =
132
165
  contextTrafficSources[p] || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources[p]) || "";
133
166
  }
134
- var finalTrafficSources = __assign({ ref: contextTrafficSources.ref || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.ref) || "", referrer: contextTrafficSources.referrer || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.referrer) || "", utm_campaign: contextTrafficSources.utm_campaign ||
167
+ var finalTrafficSources = __assign({ ref: contextTrafficSources.ref || (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.ref) || "",
168
+ // Referrer is sticky (first-touch wins). Same-host referrers are already
169
+ // stripped by getExternalReferrer; the stored-first OR keeps the entry
170
+ // referrer pinned even if a later pageview reports a different external
171
+ // referrer (e.g. cross-domain return from an outbound click).
172
+ referrer: (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.referrer) || contextTrafficSources.referrer || "", utm_campaign: contextTrafficSources.utm_campaign ||
135
173
  (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.utm_campaign) ||
136
174
  "", utm_content: contextTrafficSources.utm_content ||
137
175
  (storedTrafficSources === null || storedTrafficSources === void 0 ? void 0 : storedTrafficSources.utm_content) ||
@@ -160,8 +198,17 @@ var EventFactory = /** @class */ (function () {
160
198
  this.getPageProperties = function (properties) {
161
199
  // Create a copy to avoid mutating the original properties object
162
200
  var pageProps = __assign({}, properties);
201
+ // Parse the current URL once and strip any excluded (sensitive) query
202
+ // params up front, so nothing sensitive is forwarded via url, query, or the
203
+ // per-param explosion below. The hash/fragment is intentionally untouched.
204
+ var urlObj = null;
205
+ try {
206
+ urlObj = new URL(globalThis.location.href);
207
+ _this.redactQueryParams(urlObj);
208
+ }
209
+ catch (_a) { }
163
210
  if (isUndefined(pageProps.url)) {
164
- pageProps.url = new URL(globalThis.location.href).href;
211
+ pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
165
212
  }
166
213
  if (isUndefined(pageProps.path)) {
167
214
  pageProps.path = globalThis.location.pathname;
@@ -171,18 +218,22 @@ var EventFactory = /** @class */ (function () {
171
218
  }
172
219
  // Add query string without the '?' prefix
173
220
  if (isUndefined(pageProps.query)) {
174
- pageProps.query = globalThis.location.search.slice(1);
221
+ pageProps.query = urlObj
222
+ ? urlObj.search.slice(1)
223
+ : globalThis.location.search.slice(1);
175
224
  }
176
225
  // Parse query parameters and add as individual properties (don't overwrite existing)
177
- // Skip fields that are already captured in context or are semantic event properties
226
+ // Skip fields that are already captured in context or are semantic event properties.
227
+ // Excluded params were already removed from urlObj above.
178
228
  try {
179
- var urlObj = new URL(globalThis.location.href);
180
- urlObj.searchParams.forEach(function (value, key) {
181
- // Only add if the property doesn't already exist and is not excluded
182
- if (isUndefined(pageProps[key]) && !PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
183
- pageProps[key] = value;
184
- }
185
- });
229
+ if (urlObj) {
230
+ urlObj.searchParams.forEach(function (value, key) {
231
+ // Only add if the property doesn't already exist and is not excluded
232
+ if (isUndefined(pageProps[key]) && !PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
233
+ pageProps[key] = value;
234
+ }
235
+ });
236
+ }
186
237
  }
187
238
  catch (error) {
188
239
  logger.error("Error parsing query parameters for page properties:", error);
@@ -190,8 +241,13 @@ var EventFactory = /** @class */ (function () {
190
241
  return pageProps;
191
242
  };
192
243
  this.options = options;
244
+ var tracking = options === null || options === void 0 ? void 0 : options.tracking;
245
+ var configuredExcludes = typeof tracking === "object" ? (_a = tracking.excludeQueryParams) !== null && _a !== void 0 ? _a : [] : [];
246
+ this.excludedQueryParams = new Set(__spreadArray(__spreadArray([], DEFAULT_EXCLUDED_QUERY_PARAMS, true), configuredExcludes, true).map(function (key) {
247
+ return key.toLowerCase();
248
+ }));
193
249
  // Compile regex pattern once for better performance
194
- if ((_a = options === null || options === void 0 ? void 0 : options.referral) === null || _a === void 0 ? void 0 : _a.pathPattern) {
250
+ if ((_b = options === null || options === void 0 ? void 0 : options.referral) === null || _b === void 0 ? void 0 : _b.pathPattern) {
195
251
  try {
196
252
  this.compiledPathPattern = new RegExp(options.referral.pathPattern);
197
253
  }
@@ -253,6 +309,66 @@ var EventFactory = /** @class */ (function () {
253
309
  EventFactory.prototype.getLibraryVersion = function () {
254
310
  return version;
255
311
  };
312
+ EventFactory.prototype.isExcludedQueryParam = function (key) {
313
+ return this.excludedQueryParams.has(key.toLowerCase());
314
+ };
315
+ /**
316
+ * Strip excluded (sensitive) query parameters from a URL in place. Only the
317
+ * query string is touched; the path and hash/fragment are left as-is.
318
+ */
319
+ EventFactory.prototype.redactQueryParams = function (url) {
320
+ var _this = this;
321
+ // Collect first, then delete: mutating searchParams while iterating is
322
+ // unsafe, and deleting a key removes all of its values at once.
323
+ var keysToDelete = new Set();
324
+ url.searchParams.forEach(function (_value, key) {
325
+ if (_this.isExcludedQueryParam(key)) {
326
+ keysToDelete.add(key);
327
+ }
328
+ });
329
+ keysToDelete.forEach(function (key) { return url.searchParams.delete(key); });
330
+ };
331
+ /**
332
+ * Return the given absolute URL with excluded query parameters removed. The
333
+ * input is returned unchanged when it is empty or cannot be parsed (e.g. an
334
+ * empty referrer).
335
+ */
336
+ EventFactory.prototype.redactUrl = function (href) {
337
+ if (!href)
338
+ return href;
339
+ try {
340
+ var url = new URL(href);
341
+ this.redactQueryParams(url);
342
+ return url.href;
343
+ }
344
+ catch (_a) {
345
+ return href;
346
+ }
347
+ };
348
+ /**
349
+ * Apply the current query-param denylist to a previously-persisted traffic
350
+ * source object. Traffic-source keys (utm_*, click ids, ref) are themselves
351
+ * query-parameter names, so an excluded key's stored value is dropped; the
352
+ * referrer is a URL and is re-redacted. Guards against a stored value
353
+ * outliving the config (or SDK version) under which it was first captured.
354
+ */
355
+ EventFactory.prototype.redactStoredTrafficSources = function (stored) {
356
+ var result = {};
357
+ for (var _i = 0, _a = Object.keys(stored); _i < _a.length; _i++) {
358
+ var key = _a[_i];
359
+ var value = stored[key];
360
+ if (key === "referrer") {
361
+ result[key] = this.redactUrl(value || "");
362
+ }
363
+ else if (this.isExcludedQueryParam(key)) {
364
+ result[key] = "";
365
+ }
366
+ else {
367
+ result[key] = value;
368
+ }
369
+ }
370
+ return result;
371
+ };
256
372
  // Get screen dimensions and pixel density
257
373
  // Returns safe defaults if any error occurs to ensure event creation continues
258
374
  EventFactory.prototype.getScreen = function () {
@@ -281,7 +397,7 @@ var EventFactory = /** @class */ (function () {
281
397
  // Contextual fields that are automatically collected and populated by the Formo SDK
282
398
  EventFactory.prototype.generateContext = function (context) {
283
399
  return __awaiter(this, void 0, void 0, function () {
284
- var browserName, language, timezone, location, library_version, defaultContext, mergedContext;
400
+ var browserName, language, timezone, location, library_version, redactedHref, defaultContext, mergedContext;
285
401
  return __generator(this, function (_a) {
286
402
  switch (_a.label) {
287
403
  case 0: return [4 /*yield*/, detectBrowser()];
@@ -291,7 +407,8 @@ var EventFactory = /** @class */ (function () {
291
407
  timezone = this.getTimezone();
292
408
  location = this.getLocation();
293
409
  library_version = this.getLibraryVersion();
294
- defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(globalThis.location.href)), { page_title: document.title, page_url: globalThis.location.href, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
410
+ redactedHref = this.redactUrl(globalThis.location.href);
411
+ defaultContext = __assign(__assign(__assign({ user_agent: globalThis.navigator.userAgent, locale: language, timezone: timezone, location: location }, this.getTrafficSources(redactedHref)), { page_title: document.title, page_url: redactedHref, library_name: "Formo Web SDK", library_version: library_version, browser: browserName }), this.getScreen());
295
412
  mergedContext = mergeDeepRight(defaultContext, context || {});
296
413
  return [2 /*return*/, mergedContext];
297
414
  }