@formo/analytics 1.31.0 → 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,6 +17,18 @@ 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;
@@ -27,6 +40,14 @@ declare class EventFactory implements IEventFactory {
27
40
  * first-touch referrer after a direct landing.
28
41
  */
29
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;
30
51
  private getTrafficSources;
31
52
  private getScreen;
32
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: "",
@@ -145,12 +154,16 @@ var EventFactory = /** @class */ (function () {
145
154
  return "";
146
155
  }
147
156
  catch (_b) { }
148
- return ref;
157
+ return _this.redactUrl(ref);
149
158
  };
150
159
  this.getTrafficSources = function (url) {
151
160
  var urlObj = new URL(url);
152
161
  var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() });
153
- var storedTrafficSources = (0, storage_1.session)().get(constants_1.SESSION_TRAFFIC_SOURCE_KEY) || {};
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) || {});
154
167
  var mergedClickIds = {};
155
168
  for (var _i = 0, CLICK_ID_PARAMS_2 = constants_2.CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
156
169
  var p = CLICK_ID_PARAMS_2[_i];
@@ -191,8 +204,17 @@ var EventFactory = /** @class */ (function () {
191
204
  this.getPageProperties = function (properties) {
192
205
  // Create a copy to avoid mutating the original properties object
193
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) { }
194
216
  if ((0, validators_1.isUndefined)(pageProps.url)) {
195
- pageProps.url = new URL(globalThis.location.href).href;
217
+ pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
196
218
  }
197
219
  if ((0, validators_1.isUndefined)(pageProps.path)) {
198
220
  pageProps.path = globalThis.location.pathname;
@@ -202,18 +224,22 @@ var EventFactory = /** @class */ (function () {
202
224
  }
203
225
  // Add query string without the '?' prefix
204
226
  if ((0, validators_1.isUndefined)(pageProps.query)) {
205
- pageProps.query = globalThis.location.search.slice(1);
227
+ pageProps.query = urlObj
228
+ ? urlObj.search.slice(1)
229
+ : globalThis.location.search.slice(1);
206
230
  }
207
231
  // Parse query parameters and add as individual properties (don't overwrite existing)
208
- // 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.
209
234
  try {
210
- var urlObj = new URL(globalThis.location.href);
211
- urlObj.searchParams.forEach(function (value, key) {
212
- // Only add if the property doesn't already exist and is not excluded
213
- if ((0, validators_1.isUndefined)(pageProps[key]) && !constants_2.PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
214
- pageProps[key] = value;
215
- }
216
- });
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
+ }
217
243
  }
218
244
  catch (error) {
219
245
  logger_1.logger.error("Error parsing query parameters for page properties:", error);
@@ -221,8 +247,13 @@ var EventFactory = /** @class */ (function () {
221
247
  return pageProps;
222
248
  };
223
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
+ }));
224
255
  // Compile regex pattern once for better performance
225
- 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) {
226
257
  try {
227
258
  this.compiledPathPattern = new RegExp(options.referral.pathPattern);
228
259
  }
@@ -284,6 +315,66 @@ var EventFactory = /** @class */ (function () {
284
315
  EventFactory.prototype.getLibraryVersion = function () {
285
316
  return version_1.version;
286
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
+ };
287
378
  // Get screen dimensions and pixel density
288
379
  // Returns safe defaults if any error occurs to ensure event creation continues
289
380
  EventFactory.prototype.getScreen = function () {
@@ -312,7 +403,7 @@ var EventFactory = /** @class */ (function () {
312
403
  // Contextual fields that are automatically collected and populated by the Formo SDK
313
404
  EventFactory.prototype.generateContext = function (context) {
314
405
  return __awaiter(this, void 0, void 0, function () {
315
- var browserName, language, timezone, location, library_version, defaultContext, mergedContext;
406
+ var browserName, language, timezone, location, library_version, redactedHref, defaultContext, mergedContext;
316
407
  return __generator(this, function (_a) {
317
408
  switch (_a.label) {
318
409
  case 0: return [4 /*yield*/, (0, browsers_1.detectBrowser)()];
@@ -322,7 +413,8 @@ var EventFactory = /** @class */ (function () {
322
413
  timezone = this.getTimezone();
323
414
  location = this.getLocation();
324
415
  library_version = this.getLibraryVersion();
325
- 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());
326
418
  mergedContext = (0, mergeDeepRight_1.default)(defaultContext, context || {});
327
419
  return [2 /*return*/, mergedContext];
328
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,6 +17,18 @@ 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;
@@ -27,6 +40,14 @@ declare class EventFactory implements IEventFactory {
27
40
  * first-touch referrer after a direct landing.
28
41
  */
29
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;
30
51
  private getTrafficSources;
31
52
  private getScreen;
32
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: "",
@@ -139,12 +148,16 @@ var EventFactory = /** @class */ (function () {
139
148
  return "";
140
149
  }
141
150
  catch (_b) { }
142
- return ref;
151
+ return _this.redactUrl(ref);
143
152
  };
144
153
  this.getTrafficSources = function (url) {
145
154
  var urlObj = new URL(url);
146
155
  var contextTrafficSources = __assign(__assign(__assign({}, _this.extractUTMParameters(url)), _this.extractClickIdParameters(urlObj)), { ref: _this.extractReferralParameter(urlObj), referrer: _this.getExternalReferrer() });
147
- var storedTrafficSources = session().get(SESSION_TRAFFIC_SOURCE_KEY) || {};
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) || {});
148
161
  var mergedClickIds = {};
149
162
  for (var _i = 0, CLICK_ID_PARAMS_2 = CLICK_ID_PARAMS; _i < CLICK_ID_PARAMS_2.length; _i++) {
150
163
  var p = CLICK_ID_PARAMS_2[_i];
@@ -185,8 +198,17 @@ var EventFactory = /** @class */ (function () {
185
198
  this.getPageProperties = function (properties) {
186
199
  // Create a copy to avoid mutating the original properties object
187
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) { }
188
210
  if (isUndefined(pageProps.url)) {
189
- pageProps.url = new URL(globalThis.location.href).href;
211
+ pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
190
212
  }
191
213
  if (isUndefined(pageProps.path)) {
192
214
  pageProps.path = globalThis.location.pathname;
@@ -196,18 +218,22 @@ var EventFactory = /** @class */ (function () {
196
218
  }
197
219
  // Add query string without the '?' prefix
198
220
  if (isUndefined(pageProps.query)) {
199
- pageProps.query = globalThis.location.search.slice(1);
221
+ pageProps.query = urlObj
222
+ ? urlObj.search.slice(1)
223
+ : globalThis.location.search.slice(1);
200
224
  }
201
225
  // Parse query parameters and add as individual properties (don't overwrite existing)
202
- // 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.
203
228
  try {
204
- var urlObj = new URL(globalThis.location.href);
205
- urlObj.searchParams.forEach(function (value, key) {
206
- // Only add if the property doesn't already exist and is not excluded
207
- if (isUndefined(pageProps[key]) && !PAGE_PROPERTIES_EXCLUDED_FIELDS.has(key)) {
208
- pageProps[key] = value;
209
- }
210
- });
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
+ }
211
237
  }
212
238
  catch (error) {
213
239
  logger.error("Error parsing query parameters for page properties:", error);
@@ -215,8 +241,13 @@ var EventFactory = /** @class */ (function () {
215
241
  return pageProps;
216
242
  };
217
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
+ }));
218
249
  // Compile regex pattern once for better performance
219
- 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) {
220
251
  try {
221
252
  this.compiledPathPattern = new RegExp(options.referral.pathPattern);
222
253
  }
@@ -278,6 +309,66 @@ var EventFactory = /** @class */ (function () {
278
309
  EventFactory.prototype.getLibraryVersion = function () {
279
310
  return version;
280
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
+ };
281
372
  // Get screen dimensions and pixel density
282
373
  // Returns safe defaults if any error occurs to ensure event creation continues
283
374
  EventFactory.prototype.getScreen = function () {
@@ -306,7 +397,7 @@ var EventFactory = /** @class */ (function () {
306
397
  // Contextual fields that are automatically collected and populated by the Formo SDK
307
398
  EventFactory.prototype.generateContext = function (context) {
308
399
  return __awaiter(this, void 0, void 0, function () {
309
- var browserName, language, timezone, location, library_version, defaultContext, mergedContext;
400
+ var browserName, language, timezone, location, library_version, redactedHref, defaultContext, mergedContext;
310
401
  return __generator(this, function (_a) {
311
402
  switch (_a.label) {
312
403
  case 0: return [4 /*yield*/, detectBrowser()];
@@ -316,7 +407,8 @@ var EventFactory = /** @class */ (function () {
316
407
  timezone = this.getTimezone();
317
408
  location = this.getLocation();
318
409
  library_version = this.getLibraryVersion();
319
- 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());
320
412
  mergedContext = mergeDeepRight(defaultContext, context || {});
321
413
  return [2 /*return*/, mergedContext];
322
414
  }
@@ -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
@@ -24,6 +24,22 @@ var CLICK_ID_PARAMS = [
24
24
  "rdt_cid", // Reddit Ads
25
25
  "ttclid", // TikTok Ads
26
26
  ];
27
+ /**
28
+ * Query parameters that are ALWAYS stripped from forwarded and stored URLs,
29
+ * regardless of consumer configuration, because they carry high-sensitivity
30
+ * secrets that must never reach Formo:
31
+ * - privy_oauth_code: Privy OAuth authorization code
32
+ * - privy_oauth_state: Privy OAuth CSRF state token
33
+ * - privy_oauth_provider: Privy OAuth provider identifier
34
+ *
35
+ * Consumers can extend the denylist via `tracking.excludeQueryParams` but
36
+ * cannot remove these built-ins. Matched case-insensitively.
37
+ */
38
+ var DEFAULT_EXCLUDED_QUERY_PARAMS = [
39
+ "privy_oauth_code",
40
+ "privy_oauth_state",
41
+ "privy_oauth_provider",
42
+ ];
27
43
  /**
28
44
  * Fields that should be excluded from page event properties parsing
29
45
  * These are either:
@@ -50,5 +66,5 @@ var PAGE_PROPERTIES_EXCLUDED_FIELDS = new Set(__spreadArray(__spreadArray([
50
66
  'hash',
51
67
  'query',
52
68
  ], false));
53
- export { CHANNEL, VERSION, CLICK_ID_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS };
69
+ export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
54
70
  //# sourceMappingURL=constants.js.map
@@ -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