@v-tilt/browser 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/array.js +2 -0
- package/dist/array.js.map +1 -0
- package/dist/array.no-external.js +2 -0
- package/dist/array.no-external.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/constants.d.ts +10 -0
- package/dist/entrypoints/array.d.ts +1 -0
- package/dist/entrypoints/array.no-external.d.ts +1 -0
- package/dist/entrypoints/main.cjs.d.ts +4 -0
- package/dist/entrypoints/module.es.d.ts +3 -0
- package/dist/entrypoints/module.no-external.es.d.ts +4 -0
- package/dist/geolocation.d.ts +5 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -0
- package/dist/module.d.ts +214 -0
- package/dist/module.js +2 -0
- package/dist/module.js.map +1 -0
- package/dist/module.no-external.d.ts +214 -0
- package/dist/module.no-external.js +2 -0
- package/dist/module.no-external.js.map +1 -0
- package/dist/session.d.ts +26 -0
- package/dist/tracking.d.ts +112 -0
- package/dist/types.d.ts +67 -0
- package/dist/user-manager.d.ts +152 -0
- package/dist/utils/globals.d.ts +4 -0
- package/dist/utils/index.d.ts +30 -0
- package/dist/utils.d.ts +21 -0
- package/dist/vtilt.d.ts +178 -0
- package/dist/web-vitals.d.ts +11 -0
- package/lib/config.d.ts +17 -0
- package/lib/config.js +76 -0
- package/lib/constants.d.ts +10 -0
- package/lib/constants.js +463 -0
- package/lib/entrypoints/array.d.ts +1 -0
- package/lib/entrypoints/array.js +3 -0
- package/lib/entrypoints/array.no-external.d.ts +1 -0
- package/lib/entrypoints/array.no-external.js +4 -0
- package/lib/entrypoints/main.cjs.d.ts +4 -0
- package/lib/entrypoints/main.cjs.js +29 -0
- package/lib/entrypoints/module.es.d.ts +3 -0
- package/lib/entrypoints/module.es.js +22 -0
- package/lib/entrypoints/module.no-external.es.d.ts +4 -0
- package/lib/entrypoints/module.no-external.es.js +23 -0
- package/lib/geolocation.d.ts +5 -0
- package/lib/geolocation.js +26 -0
- package/lib/session.d.ts +26 -0
- package/lib/session.js +115 -0
- package/lib/tracking.d.ts +112 -0
- package/lib/tracking.js +326 -0
- package/lib/types.d.ts +67 -0
- package/lib/types.js +2 -0
- package/lib/user-manager.d.ts +152 -0
- package/lib/user-manager.js +565 -0
- package/lib/utils/globals.d.ts +4 -0
- package/lib/utils/globals.js +5 -0
- package/lib/utils/index.d.ts +30 -0
- package/lib/utils/index.js +89 -0
- package/lib/utils.d.ts +21 -0
- package/lib/utils.js +57 -0
- package/lib/vtilt.d.ts +178 -0
- package/lib/vtilt.js +403 -0
- package/lib/web-vitals.d.ts +11 -0
- package/lib/web-vitals.js +67 -0
- package/package.json +61 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserManager = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
class UserManager {
|
|
7
|
+
constructor(storageMethod = "localStorage", domain) {
|
|
8
|
+
this._cachedPersonProperties = null; // PostHog behavior: cache for deduplication
|
|
9
|
+
this.storageMethod = storageMethod;
|
|
10
|
+
this.domain = domain;
|
|
11
|
+
this.userIdentity = this.loadUserIdentity();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get current user identity
|
|
15
|
+
*/
|
|
16
|
+
getUserIdentity() {
|
|
17
|
+
return { ...this.userIdentity };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get current distinct ID (identified user ID)
|
|
21
|
+
*/
|
|
22
|
+
getDistinctId() {
|
|
23
|
+
return this.userIdentity.distinct_id;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get current anonymous ID
|
|
27
|
+
*/
|
|
28
|
+
getAnonymousId() {
|
|
29
|
+
if (!this.userIdentity.anonymous_id) {
|
|
30
|
+
// Regenerate if somehow undefined
|
|
31
|
+
this.userIdentity.anonymous_id = this.generateAnonymousId();
|
|
32
|
+
this.saveUserIdentity();
|
|
33
|
+
}
|
|
34
|
+
return this.userIdentity.anonymous_id;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get current user properties
|
|
38
|
+
*/
|
|
39
|
+
getUserProperties() {
|
|
40
|
+
return { ...this.userIdentity.properties };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Identify a user with distinct ID and properties
|
|
44
|
+
* Copied from PostHog's identify method implementation
|
|
45
|
+
*/
|
|
46
|
+
identify(newDistinctId, userPropertiesToSet, userPropertiesToSetOnce) {
|
|
47
|
+
// PostHog validation: Convert number to string
|
|
48
|
+
if (typeof newDistinctId === "number") {
|
|
49
|
+
newDistinctId = newDistinctId.toString();
|
|
50
|
+
console.warn("The first argument to vTilt.identify was a number, but it should be a string. It has been converted to a string.");
|
|
51
|
+
}
|
|
52
|
+
// PostHog validation: Check if distinct_id is provided
|
|
53
|
+
if (!newDistinctId) {
|
|
54
|
+
console.error("Unique user id has not been set in vTilt.identify");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// PostHog validation: Check for hardcoded strings
|
|
58
|
+
if (this.isDistinctIdStringLike(newDistinctId)) {
|
|
59
|
+
console.error(`The string "${newDistinctId}" was set in vTilt.identify which indicates an error. This ID should be unique to the user and not a hardcoded string.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// PostHog validation: Check for cookieless sentinel value
|
|
63
|
+
if (newDistinctId === "COOKIELESS_SENTINEL_VALUE") {
|
|
64
|
+
console.error(`The string "${newDistinctId}" was set in vTilt.identify which indicates an error. This ID is only used as a sentinel value.`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const previousDistinctId = this.userIdentity.distinct_id;
|
|
68
|
+
const anonymousId = this.userIdentity.anonymous_id;
|
|
69
|
+
const deviceId = this.userIdentity.device_id;
|
|
70
|
+
// PostHog behavior: Register user ID
|
|
71
|
+
this.userIdentity.distinct_id = newDistinctId;
|
|
72
|
+
// PostHog behavior: Handle device ID if not already set
|
|
73
|
+
if (!this.userIdentity.device_id) {
|
|
74
|
+
// The persisted distinct id might not actually be a device id at all
|
|
75
|
+
// it might be a distinct id of the user from before
|
|
76
|
+
const device_id = previousDistinctId || this.userIdentity.anonymous_id;
|
|
77
|
+
this.userIdentity.device_id = device_id;
|
|
78
|
+
this.userIdentity.properties = {
|
|
79
|
+
...this.userIdentity.properties,
|
|
80
|
+
$had_persisted_distinct_id: true,
|
|
81
|
+
$device_id: device_id,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// PostHog behavior: Clear alias if distinct_id is changing
|
|
85
|
+
if (newDistinctId !== previousDistinctId) {
|
|
86
|
+
// Clear any stored alias (we don't have alias support yet, but keeping PostHog structure)
|
|
87
|
+
this.userIdentity.distinct_id = newDistinctId;
|
|
88
|
+
}
|
|
89
|
+
// PostHog behavior: Check if user was anonymous
|
|
90
|
+
const isKnownAnonymous = this.userIdentity.user_state === "anonymous";
|
|
91
|
+
// PostHog behavior: Send $identify event only when distinct_id is changing AND user was anonymous
|
|
92
|
+
// - logic on the server will determine whether or not to do anything with it.
|
|
93
|
+
if (newDistinctId !== previousDistinctId && isKnownAnonymous) {
|
|
94
|
+
// Update user state to identified
|
|
95
|
+
this.userIdentity.user_state = "identified";
|
|
96
|
+
// Handle $set properties (update existing)
|
|
97
|
+
if (userPropertiesToSet) {
|
|
98
|
+
this.userIdentity.properties = {
|
|
99
|
+
...this.userIdentity.properties,
|
|
100
|
+
...userPropertiesToSet,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Handle $set_once properties (preserve first value)
|
|
104
|
+
if (userPropertiesToSetOnce) {
|
|
105
|
+
Object.keys(userPropertiesToSetOnce).forEach((key) => {
|
|
106
|
+
if (!(key in this.userIdentity.properties)) {
|
|
107
|
+
this.userIdentity.properties[key] = userPropertiesToSetOnce[key];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Save to storage
|
|
112
|
+
this.saveUserIdentity();
|
|
113
|
+
// PostHog behavior: Send $identify event for session merging
|
|
114
|
+
this.sendIdentifyEvent(newDistinctId, anonymousId, deviceId, {
|
|
115
|
+
$set: userPropertiesToSet || {},
|
|
116
|
+
$set_once: userPropertiesToSetOnce || {},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (userPropertiesToSet || userPropertiesToSetOnce) {
|
|
120
|
+
// PostHog behavior: If distinct_id is not changing but we have properties to set
|
|
121
|
+
// Update user state if not already identified
|
|
122
|
+
if (this.userIdentity.user_state === "anonymous") {
|
|
123
|
+
this.userIdentity.user_state = "identified";
|
|
124
|
+
}
|
|
125
|
+
// Handle $set properties (update existing)
|
|
126
|
+
if (userPropertiesToSet) {
|
|
127
|
+
this.userIdentity.properties = {
|
|
128
|
+
...this.userIdentity.properties,
|
|
129
|
+
...userPropertiesToSet,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Handle $set_once properties (preserve first value)
|
|
133
|
+
if (userPropertiesToSetOnce) {
|
|
134
|
+
Object.keys(userPropertiesToSetOnce).forEach((key) => {
|
|
135
|
+
if (!(key in this.userIdentity.properties)) {
|
|
136
|
+
this.userIdentity.properties[key] = userPropertiesToSetOnce[key];
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Save to storage
|
|
141
|
+
this.saveUserIdentity();
|
|
142
|
+
// PostHog behavior: Send $set event to notify server of property updates
|
|
143
|
+
// This allows Tinybird to track property changes when identify() is called with properties
|
|
144
|
+
// but no $identify event is sent (user was already identified)
|
|
145
|
+
this.sendSetEvent(userPropertiesToSet || {}, userPropertiesToSetOnce || {});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// If distinct_id is changing but user was already identified, just update the distinct_id
|
|
149
|
+
if (newDistinctId !== previousDistinctId) {
|
|
150
|
+
this.userIdentity.user_state = "identified";
|
|
151
|
+
this.saveUserIdentity();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Set user properties without changing distinct ID (PostHog-style)
|
|
157
|
+
* Sets properties on the person profile associated with the current distinct_id
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```js
|
|
161
|
+
* // Set properties that can be updated
|
|
162
|
+
* vt.setUserProperties({ name: 'John Doe', email: 'john@example.com' })
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```js
|
|
167
|
+
* // Set properties with $set and $set_once operations
|
|
168
|
+
* vt.setUserProperties(
|
|
169
|
+
* { name: 'John Doe', last_login: new Date().toISOString() }, // $set properties
|
|
170
|
+
* { first_login: new Date().toISOString() } // $set_once properties
|
|
171
|
+
* )
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* @param userPropertiesToSet Optional: Properties to set (can be updated)
|
|
175
|
+
* @param userPropertiesToSetOnce Optional: Properties to set once (preserves first value)
|
|
176
|
+
*/
|
|
177
|
+
setUserProperties(userPropertiesToSet, userPropertiesToSetOnce) {
|
|
178
|
+
// PostHog behavior: Return early if no properties provided
|
|
179
|
+
if (!userPropertiesToSet && !userPropertiesToSetOnce) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// PostHog behavior: Create hash for deduplication
|
|
183
|
+
const distinctId = this.userIdentity.distinct_id || this.userIdentity.anonymous_id;
|
|
184
|
+
const hash = this.getPersonPropertiesHash(distinctId, userPropertiesToSet, userPropertiesToSetOnce);
|
|
185
|
+
// PostHog behavior: Skip if exactly this $set call has been sent before
|
|
186
|
+
if (this._cachedPersonProperties === hash) {
|
|
187
|
+
console.info("VTilt: A duplicate setUserProperties call was made with the same properties. It has been ignored.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// PostHog behavior: Handle $set properties (update existing)
|
|
191
|
+
if (userPropertiesToSet) {
|
|
192
|
+
this.userIdentity.properties = {
|
|
193
|
+
...this.userIdentity.properties,
|
|
194
|
+
...userPropertiesToSet,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// PostHog behavior: Handle $set_once properties (preserve first value)
|
|
198
|
+
if (userPropertiesToSetOnce) {
|
|
199
|
+
Object.keys(userPropertiesToSetOnce).forEach((key) => {
|
|
200
|
+
if (!(key in this.userIdentity.properties)) {
|
|
201
|
+
this.userIdentity.properties[key] = userPropertiesToSetOnce[key];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Save to storage
|
|
206
|
+
this.saveUserIdentity();
|
|
207
|
+
// PostHog behavior: Send $set event to notify server of property updates
|
|
208
|
+
this.sendSetEvent(userPropertiesToSet || {}, userPropertiesToSetOnce || {});
|
|
209
|
+
// PostHog behavior: Cache the hash to prevent duplicate sends
|
|
210
|
+
this._cachedPersonProperties = hash;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get hash for person properties (PostHog behavior)
|
|
214
|
+
* Used for deduplication of identical setUserProperties calls
|
|
215
|
+
*/
|
|
216
|
+
getPersonPropertiesHash(distinctId, userPropertiesToSet, userPropertiesToSetOnce) {
|
|
217
|
+
// PostHog behavior: Create deterministic hash from distinct_id and properties
|
|
218
|
+
return JSON.stringify({
|
|
219
|
+
distinct_id: distinctId,
|
|
220
|
+
userPropertiesToSet,
|
|
221
|
+
userPropertiesToSetOnce,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Send $set event for property updates (PostHog behavior)
|
|
226
|
+
* This notifies the server when user properties are updated
|
|
227
|
+
*/
|
|
228
|
+
sendSetEvent(userPropertiesToSet, userPropertiesToSetOnce) {
|
|
229
|
+
// Emit a custom event that TrackingManager can listen to
|
|
230
|
+
const setEvent = new CustomEvent("vtilt:set", {
|
|
231
|
+
detail: {
|
|
232
|
+
$set: userPropertiesToSet || {},
|
|
233
|
+
$set_once: userPropertiesToSetOnce || {},
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
window.dispatchEvent(setEvent);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Reset user identity (logout)
|
|
240
|
+
* PostHog behavior: Generates new anonymous ID, clears user data, optionally resets device ID
|
|
241
|
+
*
|
|
242
|
+
* @param reset_device_id - If true, also resets device_id. Default: false (preserves device_id)
|
|
243
|
+
*/
|
|
244
|
+
reset(reset_device_id) {
|
|
245
|
+
// PostHog behavior: Generate completely new anonymous ID (don't revert to original)
|
|
246
|
+
const newAnonymousId = this.generateAnonymousId();
|
|
247
|
+
const device_id = this.userIdentity.device_id;
|
|
248
|
+
// Generate new device ID if reset_device_id is true
|
|
249
|
+
const newDeviceId = reset_device_id ? this.generateDeviceId() : device_id;
|
|
250
|
+
this.userIdentity = {
|
|
251
|
+
distinct_id: null,
|
|
252
|
+
anonymous_id: newAnonymousId,
|
|
253
|
+
device_id: newDeviceId,
|
|
254
|
+
properties: {},
|
|
255
|
+
user_state: "anonymous",
|
|
256
|
+
};
|
|
257
|
+
// PostHog behavior: Clear cached person properties on reset
|
|
258
|
+
this._cachedPersonProperties = null;
|
|
259
|
+
this.saveUserIdentity();
|
|
260
|
+
// PostHog behavior: Set $last_posthog_reset property
|
|
261
|
+
// Store it in properties, it will be sent on the next event
|
|
262
|
+
this.userIdentity.properties = {
|
|
263
|
+
...this.userIdentity.properties,
|
|
264
|
+
$last_posthog_reset: new Date().toISOString(),
|
|
265
|
+
};
|
|
266
|
+
this.saveUserIdentity();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get the effective ID for event tracking
|
|
270
|
+
*/
|
|
271
|
+
getEffectiveId() {
|
|
272
|
+
return this.userIdentity.distinct_id || this.getAnonymousId();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get current device ID
|
|
276
|
+
*/
|
|
277
|
+
getDeviceId() {
|
|
278
|
+
return this.userIdentity.device_id;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get current user state
|
|
282
|
+
*/
|
|
283
|
+
getUserState() {
|
|
284
|
+
return this.userIdentity.user_state;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Create an alias to link two distinct IDs
|
|
288
|
+
* PostHog behavior: If original is not provided, uses current distinct_id
|
|
289
|
+
* If alias matches original, just calls identify instead
|
|
290
|
+
*
|
|
291
|
+
* @param alias - A unique identifier that you want to use for this user in the future
|
|
292
|
+
* @param original - The current identifier being used for this user (optional, defaults to current distinct_id)
|
|
293
|
+
*/
|
|
294
|
+
createAlias(alias, original) {
|
|
295
|
+
if (!this.isValidDistinctId(alias)) {
|
|
296
|
+
console.warn("Invalid alias provided");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// PostHog behavior: If original is not provided, use current distinct_id
|
|
300
|
+
if (original === undefined) {
|
|
301
|
+
original = this.getDistinctId() || this.getAnonymousId();
|
|
302
|
+
}
|
|
303
|
+
if (!this.isValidDistinctId(original)) {
|
|
304
|
+
console.warn("Invalid original distinct ID");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// PostHog behavior: If alias matches original, just call identify instead
|
|
308
|
+
if (alias === original) {
|
|
309
|
+
console.warn("alias matches current distinct_id - calling identify instead");
|
|
310
|
+
this.identify(alias);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// { distinct_id: alias, original: original }
|
|
314
|
+
const aliasEvent = {
|
|
315
|
+
distinct_id: alias,
|
|
316
|
+
original,
|
|
317
|
+
};
|
|
318
|
+
// Emit alias event
|
|
319
|
+
const customEvent = new CustomEvent("vtilt:alias", {
|
|
320
|
+
detail: aliasEvent,
|
|
321
|
+
});
|
|
322
|
+
window.dispatchEvent(customEvent);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Validate distinct ID to prevent hardcoded strings
|
|
326
|
+
* Copied from PostHog's validation logic
|
|
327
|
+
*/
|
|
328
|
+
isValidDistinctId(distinctId) {
|
|
329
|
+
if (!distinctId || typeof distinctId !== "string") {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
// PostHog's validation patterns
|
|
333
|
+
const invalidPatterns = [
|
|
334
|
+
"null",
|
|
335
|
+
"undefined",
|
|
336
|
+
"false",
|
|
337
|
+
"true",
|
|
338
|
+
"anonymous",
|
|
339
|
+
"anon",
|
|
340
|
+
"user",
|
|
341
|
+
"test",
|
|
342
|
+
"guest",
|
|
343
|
+
"visitor",
|
|
344
|
+
"unknown",
|
|
345
|
+
"none",
|
|
346
|
+
];
|
|
347
|
+
const lower = distinctId.toLowerCase().trim();
|
|
348
|
+
if (invalidPatterns.includes(lower)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
// Check for empty or whitespace-only strings
|
|
352
|
+
if (distinctId.trim().length === 0) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Check if distinct ID is string-like (hardcoded string)
|
|
359
|
+
* Copied from PostHog's isDistinctIdStringLike function
|
|
360
|
+
*/
|
|
361
|
+
isDistinctIdStringLike(distinctId) {
|
|
362
|
+
if (!distinctId || typeof distinctId !== "string") {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
// PostHog's hardcoded string patterns
|
|
366
|
+
const hardcodedPatterns = [
|
|
367
|
+
"null",
|
|
368
|
+
"undefined",
|
|
369
|
+
"false",
|
|
370
|
+
"true",
|
|
371
|
+
"anonymous",
|
|
372
|
+
"anon",
|
|
373
|
+
"user",
|
|
374
|
+
"test",
|
|
375
|
+
"guest",
|
|
376
|
+
"visitor",
|
|
377
|
+
"unknown",
|
|
378
|
+
"none",
|
|
379
|
+
"demo",
|
|
380
|
+
"example",
|
|
381
|
+
"sample",
|
|
382
|
+
"placeholder",
|
|
383
|
+
];
|
|
384
|
+
const lower = distinctId.toLowerCase().trim();
|
|
385
|
+
return hardcodedPatterns.includes(lower);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Load user identity from storage
|
|
389
|
+
*/
|
|
390
|
+
loadUserIdentity() {
|
|
391
|
+
const anonymousId = this.getStoredValue(constants_1.ANONYMOUS_ID_KEY) || this.generateAnonymousId();
|
|
392
|
+
const distinctId = this.getStoredValue(constants_1.DISTINCT_ID_KEY) || null;
|
|
393
|
+
const deviceId = this.getStoredValue(constants_1.DEVICE_ID_KEY) || this.generateDeviceId();
|
|
394
|
+
const properties = this.getStoredUserProperties();
|
|
395
|
+
const userState = this.getStoredValue(constants_1.USER_STATE_KEY) ||
|
|
396
|
+
"anonymous";
|
|
397
|
+
// Ensure anonymous_id is never undefined
|
|
398
|
+
const safeAnonymousId = anonymousId || this.generateAnonymousId();
|
|
399
|
+
const identity = {
|
|
400
|
+
distinct_id: distinctId,
|
|
401
|
+
anonymous_id: safeAnonymousId,
|
|
402
|
+
device_id: deviceId,
|
|
403
|
+
properties: properties,
|
|
404
|
+
user_state: userState,
|
|
405
|
+
};
|
|
406
|
+
return identity;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Save user identity to storage
|
|
410
|
+
*/
|
|
411
|
+
saveUserIdentity() {
|
|
412
|
+
this.setStoredValue(constants_1.ANONYMOUS_ID_KEY, this.userIdentity.anonymous_id);
|
|
413
|
+
this.setStoredValue(constants_1.DEVICE_ID_KEY, this.userIdentity.device_id);
|
|
414
|
+
this.setStoredValue(constants_1.USER_STATE_KEY, this.userIdentity.user_state);
|
|
415
|
+
if (this.userIdentity.distinct_id) {
|
|
416
|
+
this.setStoredValue(constants_1.DISTINCT_ID_KEY, this.userIdentity.distinct_id);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
this.removeStoredValue(constants_1.DISTINCT_ID_KEY);
|
|
420
|
+
}
|
|
421
|
+
this.setStoredUserProperties(this.userIdentity.properties);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Generate a new anonymous ID
|
|
425
|
+
*/
|
|
426
|
+
generateAnonymousId() {
|
|
427
|
+
return `anon_${(0, utils_1.uuidv4)()}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Generate a new device ID
|
|
431
|
+
*/
|
|
432
|
+
generateDeviceId() {
|
|
433
|
+
return `device_${(0, utils_1.uuidv4)()}`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Send identify event for session merging
|
|
437
|
+
*/
|
|
438
|
+
sendIdentifyEvent(distinctId, anonymousId, deviceId, propertyOperations) {
|
|
439
|
+
// This will be handled by the TrackingManager
|
|
440
|
+
// We'll emit a custom event that the TrackingManager can listen to
|
|
441
|
+
const identifyEvent = new CustomEvent("vtilt:identify", {
|
|
442
|
+
detail: {
|
|
443
|
+
distinct_id: distinctId,
|
|
444
|
+
anonymous_id: anonymousId,
|
|
445
|
+
device_id: deviceId,
|
|
446
|
+
properties: {
|
|
447
|
+
$anon_distinct_id: anonymousId,
|
|
448
|
+
$device_id: deviceId,
|
|
449
|
+
...propertyOperations,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
window.dispatchEvent(identifyEvent);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get stored value from storage
|
|
457
|
+
*/
|
|
458
|
+
getStoredValue(key) {
|
|
459
|
+
try {
|
|
460
|
+
if (this.storageMethod === constants_1.STORAGE_METHODS.localStorage) {
|
|
461
|
+
return localStorage.getItem(key);
|
|
462
|
+
}
|
|
463
|
+
else if (this.storageMethod === constants_1.STORAGE_METHODS.sessionStorage) {
|
|
464
|
+
return sessionStorage.getItem(key);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
return this.getCookieValue(key);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
// If storage access fails, return null
|
|
472
|
+
console.warn("Failed to access storage:", error);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Set stored value in storage
|
|
478
|
+
*/
|
|
479
|
+
setStoredValue(key, value) {
|
|
480
|
+
try {
|
|
481
|
+
if (this.storageMethod === constants_1.STORAGE_METHODS.localStorage) {
|
|
482
|
+
localStorage.setItem(key, value);
|
|
483
|
+
}
|
|
484
|
+
else if (this.storageMethod === constants_1.STORAGE_METHODS.sessionStorage) {
|
|
485
|
+
sessionStorage.setItem(key, value);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
this.setCookieValue(key, value);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
// If storage access fails, log warning but don't throw
|
|
493
|
+
console.warn("Failed to save to storage:", error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Remove stored value from storage
|
|
498
|
+
*/
|
|
499
|
+
removeStoredValue(key) {
|
|
500
|
+
if (this.storageMethod === constants_1.STORAGE_METHODS.localStorage) {
|
|
501
|
+
localStorage.removeItem(key);
|
|
502
|
+
}
|
|
503
|
+
else if (this.storageMethod === constants_1.STORAGE_METHODS.sessionStorage) {
|
|
504
|
+
sessionStorage.removeItem(key);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
this.removeCookieValue(key);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get user properties from storage
|
|
512
|
+
*/
|
|
513
|
+
getStoredUserProperties() {
|
|
514
|
+
const stored = this.getStoredValue(constants_1.USER_PROPERTIES_KEY);
|
|
515
|
+
if (!stored) {
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
return JSON.parse(stored);
|
|
520
|
+
}
|
|
521
|
+
catch (_a) {
|
|
522
|
+
return {};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Set user properties in storage
|
|
527
|
+
*/
|
|
528
|
+
setStoredUserProperties(properties) {
|
|
529
|
+
this.setStoredValue(constants_1.USER_PROPERTIES_KEY, JSON.stringify(properties));
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get cookie value
|
|
533
|
+
*/
|
|
534
|
+
getCookieValue(name) {
|
|
535
|
+
const cookies = document.cookie.split(";");
|
|
536
|
+
for (const cookie of cookies) {
|
|
537
|
+
const [key, value] = cookie.trim().split("=");
|
|
538
|
+
if (key === name) {
|
|
539
|
+
return decodeURIComponent(value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Set cookie value
|
|
546
|
+
*/
|
|
547
|
+
setCookieValue(name, value) {
|
|
548
|
+
let cookieValue = `${name}=${encodeURIComponent(value)}; Max-Age=31536000; path=/; secure; SameSite=Lax`;
|
|
549
|
+
if (this.domain) {
|
|
550
|
+
cookieValue += `; domain=${this.domain}`;
|
|
551
|
+
}
|
|
552
|
+
document.cookie = cookieValue;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Remove cookie value
|
|
556
|
+
*/
|
|
557
|
+
removeCookieValue(name) {
|
|
558
|
+
let cookieValue = `${name}=; Max-Age=0; path=/`;
|
|
559
|
+
if (this.domain) {
|
|
560
|
+
cookieValue += `; domain=${this.domain}`;
|
|
561
|
+
}
|
|
562
|
+
document.cookie = cookieValue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
exports.UserManager = UserManager;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EventPayload } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Generate uuid to identify the session. Random, not data-derived
|
|
4
|
+
*/
|
|
5
|
+
export declare function uuidv4(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Validate user agent string
|
|
8
|
+
*/
|
|
9
|
+
export declare function isValidUserAgent(userAgent: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Validate payload string
|
|
12
|
+
*/
|
|
13
|
+
export declare function isValidPayload(payloadStr: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Try to mask PPI and potential sensible attributes
|
|
16
|
+
*/
|
|
17
|
+
export declare function maskSuspiciousAttributes(payload: EventPayload): string;
|
|
18
|
+
/**
|
|
19
|
+
* Check if current environment is a test environment
|
|
20
|
+
*/
|
|
21
|
+
export declare function isTestEnvironment(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Iterate over an array or object
|
|
24
|
+
*/
|
|
25
|
+
export declare function each<T>(obj: T[] | Record<string, T> | null | undefined, iterator: (value: T, key: string | number) => void, thisArg?: any): void;
|
|
26
|
+
/**
|
|
27
|
+
* Use this instead of element.addEventListener to avoid eslint errors
|
|
28
|
+
* This properly implements the default options for passive event listeners
|
|
29
|
+
*/
|
|
30
|
+
export declare function addEventListener(element: Window | Document | Element | undefined, event: string, callback: EventListener, options?: AddEventListenerOptions): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uuidv4 = uuidv4;
|
|
4
|
+
exports.isValidUserAgent = isValidUserAgent;
|
|
5
|
+
exports.isValidPayload = isValidPayload;
|
|
6
|
+
exports.maskSuspiciousAttributes = maskSuspiciousAttributes;
|
|
7
|
+
exports.isTestEnvironment = isTestEnvironment;
|
|
8
|
+
exports.each = each;
|
|
9
|
+
exports.addEventListener = addEventListener;
|
|
10
|
+
const constants_1 = require("../constants");
|
|
11
|
+
/**
|
|
12
|
+
* Generate uuid to identify the session. Random, not data-derived
|
|
13
|
+
*/
|
|
14
|
+
function uuidv4() {
|
|
15
|
+
return `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`.replace(/[018]/g, (c) => (+c ^
|
|
16
|
+
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate user agent string
|
|
20
|
+
*/
|
|
21
|
+
function isValidUserAgent(userAgent) {
|
|
22
|
+
// empty is fine
|
|
23
|
+
if (!userAgent || typeof userAgent !== "string") {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (userAgent.length > 500) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate payload string
|
|
33
|
+
*/
|
|
34
|
+
function isValidPayload(payloadStr) {
|
|
35
|
+
if (!payloadStr || typeof payloadStr !== "string") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (payloadStr.length < 2 || payloadStr.length > 10240) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Try to mask PPI and potential sensible attributes
|
|
45
|
+
*/
|
|
46
|
+
function maskSuspiciousAttributes(payload) {
|
|
47
|
+
// Deep copy
|
|
48
|
+
let _payload = JSON.stringify(payload);
|
|
49
|
+
constants_1.ATTRIBUTES_TO_MASK.forEach((attr) => {
|
|
50
|
+
_payload = _payload.replace(new RegExp(`("${attr}"):(".+?"|\\d+)`, "gmi"), '$1:"********"');
|
|
51
|
+
});
|
|
52
|
+
return _payload;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if current environment is a test environment
|
|
56
|
+
*/
|
|
57
|
+
function isTestEnvironment() {
|
|
58
|
+
return !!("__nightmare" in window ||
|
|
59
|
+
window.navigator.webdriver ||
|
|
60
|
+
"Cypress" in window);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Iterate over an array or object
|
|
64
|
+
*/
|
|
65
|
+
function each(obj, iterator, thisArg) {
|
|
66
|
+
if (!obj) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(obj)) {
|
|
70
|
+
obj.forEach((value, index) => {
|
|
71
|
+
iterator.call(thisArg, value, index);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
for (const key in obj) {
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
77
|
+
iterator.call(thisArg, obj[key], key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Use this instead of element.addEventListener to avoid eslint errors
|
|
84
|
+
* This properly implements the default options for passive event listeners
|
|
85
|
+
*/
|
|
86
|
+
function addEventListener(element, event, callback, options) {
|
|
87
|
+
const { capture = false, passive = true } = options !== null && options !== void 0 ? options : {};
|
|
88
|
+
element === null || element === void 0 ? void 0 : element.addEventListener(event, callback, { capture, passive });
|
|
89
|
+
}
|