@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.
- package/dist/cjs/src/event/EventFactory.d.ts +29 -0
- package/dist/cjs/src/event/EventFactory.js +134 -17
- package/dist/cjs/src/event/constants.d.ts +13 -1
- package/dist/cjs/src/event/constants.js +18 -1
- package/dist/cjs/src/types/base.d.ts +14 -0
- package/dist/esm/src/event/EventFactory.d.ts +29 -0
- package/dist/esm/src/event/EventFactory.js +135 -18
- package/dist/esm/src/event/constants.d.ts +13 -1
- package/dist/esm/src/event/constants.js +17 -1
- package/dist/esm/src/types/base.d.ts +14 -0
- package/dist/index.umd.min.js +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
133
|
-
|
|
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) || "",
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 ((
|
|
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
|
-
|
|
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:
|
|
127
|
-
|
|
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) || "",
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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 ((
|
|
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
|
-
|
|
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
|
}
|