@v-tilt/browser 1.0.10 → 1.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/array.js +1 -1
  3. package/dist/array.js.map +1 -1
  4. package/dist/array.no-external.js +1 -1
  5. package/dist/array.no-external.js.map +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/main.js +1 -1
  8. package/dist/main.js.map +1 -1
  9. package/dist/module.d.ts +57 -4
  10. package/dist/module.js +1 -1
  11. package/dist/module.js.map +1 -1
  12. package/dist/module.no-external.d.ts +57 -4
  13. package/dist/module.no-external.js +1 -1
  14. package/dist/module.no-external.js.map +1 -1
  15. package/dist/rate-limiter.d.ts +52 -0
  16. package/dist/request-queue.d.ts +78 -0
  17. package/dist/request.d.ts +54 -0
  18. package/dist/retry-queue.d.ts +64 -0
  19. package/dist/types.d.ts +1 -0
  20. package/dist/user-manager.d.ts +21 -0
  21. package/dist/utils/event-utils.d.ts +35 -17
  22. package/dist/utils/index.d.ts +21 -0
  23. package/dist/utils/request-utils.d.ts +17 -0
  24. package/dist/vtilt.d.ts +40 -8
  25. package/lib/constants.d.ts +1 -0
  26. package/lib/constants.js +2 -1
  27. package/lib/rate-limiter.d.ts +52 -0
  28. package/lib/rate-limiter.js +80 -0
  29. package/lib/request-queue.d.ts +78 -0
  30. package/lib/request-queue.js +156 -0
  31. package/lib/request.d.ts +54 -0
  32. package/lib/request.js +265 -0
  33. package/lib/retry-queue.d.ts +64 -0
  34. package/lib/retry-queue.js +182 -0
  35. package/lib/types.d.ts +1 -0
  36. package/lib/user-manager.d.ts +21 -0
  37. package/lib/user-manager.js +66 -0
  38. package/lib/utils/event-utils.d.ts +35 -17
  39. package/lib/utils/event-utils.js +247 -118
  40. package/lib/utils/index.d.ts +21 -0
  41. package/lib/utils/index.js +58 -0
  42. package/lib/utils/request-utils.d.ts +17 -0
  43. package/lib/utils/request-utils.js +80 -0
  44. package/lib/vtilt.d.ts +40 -8
  45. package/lib/vtilt.js +161 -11
  46. package/package.json +61 -61
package/lib/vtilt.d.ts CHANGED
@@ -1,20 +1,21 @@
1
1
  import { VTiltConfig, EventPayload } from "./types";
2
2
  import { HistoryAutocapture } from "./extensions/history-autocapture";
3
- interface QueuedRequest {
4
- url: string;
5
- event: any;
6
- }
3
+ import { type QueuedRequest } from "./request-queue";
7
4
  export declare class VTilt {
8
5
  private configManager;
9
6
  private sessionManager;
10
7
  private userManager;
11
8
  private webVitalsManager;
9
+ private requestQueue;
10
+ private retryQueue;
11
+ private rateLimiter;
12
12
  historyAutocapture?: HistoryAutocapture;
13
13
  __loaded: boolean;
14
14
  private _initialPageviewCaptured;
15
15
  private _visibilityStateListener;
16
16
  __request_queue: QueuedRequest[];
17
17
  private _hasWarnedAboutConfig;
18
+ private _setOncePropertiesSent;
18
19
  constructor(config?: Partial<VTiltConfig>);
19
20
  /**
20
21
  * Initializes a new instance of the VTilt tracking object.
@@ -52,6 +53,11 @@ export declare class VTilt {
52
53
  * This internal method should only be called by `init()`.
53
54
  */
54
55
  private _init;
56
+ /**
57
+ * Set up handler to flush event queue on page unload
58
+ * Uses both beforeunload and pagehide for maximum compatibility
59
+ */
60
+ private _setupUnloadHandler;
55
61
  /**
56
62
  * Returns a string representation of the instance name
57
63
  * Used for debugging and logging
@@ -77,8 +83,26 @@ export declare class VTilt {
77
83
  /**
78
84
  * Send HTTP request
79
85
  * This is the central entry point for all tracking requests
86
+ * Events are batched and sent every 3 seconds for better performance
80
87
  */
81
88
  private sendRequest;
89
+ /**
90
+ * Send a batched request with multiple events
91
+ * Called by RequestQueue when flushing
92
+ * Uses RetryQueue for automatic retry on failure
93
+ */
94
+ private _sendBatchedRequest;
95
+ /**
96
+ * Send HTTP request and return status code
97
+ * Uses GZip compression for payloads > 1KB
98
+ * Used by RetryQueue for retryable requests
99
+ */
100
+ private _sendHttpRequest;
101
+ /**
102
+ * Send request using sendBeacon for reliable delivery on page unload
103
+ * Uses GZip compression for payloads > 1KB
104
+ */
105
+ private _sendBeaconRequest;
82
106
  /**
83
107
  * Send a queued request (called after DOM is loaded)
84
108
  */
@@ -86,13 +110,22 @@ export declare class VTilt {
86
110
  /**
87
111
  * Capture an event
88
112
  * Automatically adds common properties to all events
89
- * ($current_url, $host, $pathname, $referrer, $referring_domain, $browser_language, etc.)
113
+ * ($current_url, $host, $pathname, $referrer, $referring_domain, $browser, $os, $device, $timezone, etc.)
114
+ * Only properties in EVENT_TO_PERSON_PROPERTIES are copied to person properties
90
115
  * Also adds title property for $pageview events only
91
116
  *
92
117
  * @param name - Event name
93
118
  * @param payload - Event payload
119
+ * @param options - Optional capture options
120
+ */
121
+ capture(name: string, payload: EventPayload, options?: {
122
+ skip_client_rate_limiting?: boolean;
123
+ }): void;
124
+ /**
125
+ * Internal capture method that bypasses rate limiting
126
+ * Used for system events like rate limit warnings
94
127
  */
95
- capture(name: string, payload: EventPayload): void;
128
+ private _captureInternal;
96
129
  /**
97
130
  * Track a custom event (alias for capture)
98
131
  */
@@ -213,7 +246,7 @@ export declare class VTilt {
213
246
  */
214
247
  _execute_array(array: any[]): void;
215
248
  /**
216
- * Called when DOM is loaded - processes queued requests
249
+ * Called when DOM is loaded - processes queued requests and enables batching
217
250
  */
218
251
  _dom_loaded(): void;
219
252
  }
@@ -253,4 +286,3 @@ export declare function init_as_module(): VTilt;
253
286
  * ]
254
287
  */
255
288
  export declare function init_from_snippet(): void;
256
- export {};
package/lib/vtilt.js CHANGED
@@ -8,6 +8,10 @@ const session_1 = require("./session");
8
8
  const user_manager_1 = require("./user-manager");
9
9
  const web_vitals_1 = require("./web-vitals");
10
10
  const history_autocapture_1 = require("./extensions/history-autocapture");
11
+ const request_1 = require("./request");
12
+ const request_queue_1 = require("./request-queue");
13
+ const retry_queue_1 = require("./retry-queue");
14
+ const rate_limiter_1 = require("./rate-limiter");
11
15
  const utils_1 = require("./utils");
12
16
  const event_utils_1 = require("./utils/event-utils");
13
17
  const globals_1 = require("./utils/globals");
@@ -18,8 +22,9 @@ class VTilt {
18
22
  this.__loaded = false; // Matches snippet's window.vt.__loaded check
19
23
  this._initialPageviewCaptured = false;
20
24
  this._visibilityStateListener = null;
21
- this.__request_queue = []; // Public for DOM loaded handler
25
+ this.__request_queue = []; // Legacy queue for DOM loaded handler (pre-init)
22
26
  this._hasWarnedAboutConfig = false; // Track if we've already warned about missing config
27
+ this._setOncePropertiesSent = false; // Track if $set_once with initial props has been sent (only send once per page load)
23
28
  this.configManager = new config_1.ConfigManager(config);
24
29
  const fullConfig = this.configManager.getConfig();
25
30
  // Auto-detect domain from location if not provided
@@ -30,6 +35,27 @@ class VTilt {
30
35
  this.sessionManager = new session_1.SessionManager(fullConfig.storage || "cookie", domain);
31
36
  this.userManager = new user_manager_1.UserManager(fullConfig.persistence || "localStorage", domain);
32
37
  this.webVitalsManager = new web_vitals_1.WebVitalsManager(fullConfig, this);
38
+ // Initialize rate limiter to prevent flooding
39
+ // Default: 10 events/second with burst of 100
40
+ this.rateLimiter = new rate_limiter_1.RateLimiter({
41
+ eventsPerSecond: 10,
42
+ eventsBurstLimit: 100,
43
+ captureWarning: (message) => {
44
+ // Send rate limit warning event (bypasses rate limiting)
45
+ this._captureInternal(rate_limiter_1.RATE_LIMIT_WARNING_EVENT, {
46
+ $$client_ingestion_warning_message: message,
47
+ });
48
+ },
49
+ });
50
+ // Initialize retry queue for failed requests
51
+ // Retries with exponential backoff: 3s, 6s, 12s... up to 30 minutes
52
+ this.retryQueue = new retry_queue_1.RetryQueue({
53
+ sendRequest: (req) => this._sendHttpRequest(req),
54
+ sendBeacon: (req) => this._sendBeaconRequest(req),
55
+ });
56
+ // Initialize request queue for event batching
57
+ // Events are batched and sent every 3 seconds (configurable)
58
+ this.requestQueue = new request_queue_1.RequestQueue((req) => this._sendBatchedRequest(req), { flush_interval_ms: 3000 });
33
59
  }
34
60
  /**
35
61
  * Initializes a new instance of the VTilt tracking object.
@@ -93,13 +119,36 @@ class VTilt {
93
119
  name: name,
94
120
  });
95
121
  this.__loaded = true;
122
+ // Set initial person info: stores referrer and URL on first visit
123
+ const fullConfig = this.configManager.getConfig();
124
+ this.userManager.set_initial_person_info(fullConfig.mask_personal_data_properties, fullConfig.custom_personal_data_properties);
96
125
  // Initialize history autocapture
97
126
  this.historyAutocapture = new history_autocapture_1.HistoryAutocapture(this);
98
127
  this.historyAutocapture.startIfEnabled();
128
+ // Set up page unload handler to flush queued events
129
+ this._setupUnloadHandler();
99
130
  // Capture initial pageview (with visibility check)
100
131
  this._captureInitialPageview();
101
132
  return this;
102
133
  }
134
+ /**
135
+ * Set up handler to flush event queue on page unload
136
+ * Uses both beforeunload and pagehide for maximum compatibility
137
+ */
138
+ _setupUnloadHandler() {
139
+ if (!globals_1.window) {
140
+ return;
141
+ }
142
+ const unloadHandler = () => {
143
+ // Flush all queued events using sendBeacon for reliable delivery
144
+ this.requestQueue.unload();
145
+ // Also flush any pending retries
146
+ this.retryQueue.unload();
147
+ };
148
+ // beforeunload is more reliable but pagehide works better on mobile
149
+ (0, utils_1.addEventListener)(globals_1.window, "beforeunload", unloadHandler);
150
+ (0, utils_1.addEventListener)(globals_1.window, "pagehide", unloadHandler);
151
+ }
103
152
  /**
104
153
  * Returns a string representation of the instance name
105
154
  * Used for debugging and logging
@@ -174,6 +223,7 @@ class VTilt {
174
223
  /**
175
224
  * Send HTTP request
176
225
  * This is the central entry point for all tracking requests
226
+ * Events are batched and sent every 3 seconds for better performance
177
227
  */
178
228
  sendRequest(url, event, shouldEnqueue) {
179
229
  // Validate configuration (only warns once per instance)
@@ -190,10 +240,69 @@ class VTilt {
190
240
  return;
191
241
  }
192
242
  }
193
- const request = new XMLHttpRequest();
194
- request.open("POST", url, true);
195
- request.setRequestHeader("Content-Type", "application/json");
196
- request.send(JSON.stringify(event));
243
+ // Enqueue event for batched sending
244
+ this.requestQueue.enqueue({ url, event });
245
+ }
246
+ /**
247
+ * Send a batched request with multiple events
248
+ * Called by RequestQueue when flushing
249
+ * Uses RetryQueue for automatic retry on failure
250
+ */
251
+ _sendBatchedRequest(req) {
252
+ const { transport } = req;
253
+ // Use sendBeacon for reliable delivery on page unload
254
+ if (transport === "sendBeacon") {
255
+ this._sendBeaconRequest(req);
256
+ return;
257
+ }
258
+ // Use retry queue for normal requests (handles failures automatically)
259
+ this.retryQueue.retriableRequest(req);
260
+ }
261
+ /**
262
+ * Send HTTP request and return status code
263
+ * Uses GZip compression for payloads > 1KB
264
+ * Used by RetryQueue for retryable requests
265
+ */
266
+ _sendHttpRequest(req) {
267
+ return new Promise((resolve) => {
268
+ const { url, events } = req;
269
+ // Prepare payload (single event or batch)
270
+ const payload = events.length === 1 ? events[0] : { events };
271
+ // Determine compression
272
+ const compression = this.configManager.getConfig().disable_compression
273
+ ? request_1.Compression.None
274
+ : request_1.Compression.GZipJS;
275
+ (0, request_1.request)({
276
+ url,
277
+ data: payload,
278
+ method: "POST",
279
+ transport: "XHR",
280
+ compression,
281
+ callback: (response) => {
282
+ resolve({ statusCode: response.statusCode });
283
+ },
284
+ });
285
+ });
286
+ }
287
+ /**
288
+ * Send request using sendBeacon for reliable delivery on page unload
289
+ * Uses GZip compression for payloads > 1KB
290
+ */
291
+ _sendBeaconRequest(req) {
292
+ const { url, events } = req;
293
+ // Prepare payload (single event or batch)
294
+ const payload = events.length === 1 ? events[0] : { events };
295
+ // Determine compression
296
+ const compression = this.configManager.getConfig().disable_compression
297
+ ? request_1.Compression.None
298
+ : request_1.Compression.GZipJS;
299
+ (0, request_1.request)({
300
+ url,
301
+ data: payload,
302
+ method: "POST",
303
+ transport: "sendBeacon",
304
+ compression,
305
+ });
197
306
  }
198
307
  /**
199
308
  * Send a queued request (called after DOM is loaded)
@@ -204,13 +313,15 @@ class VTilt {
204
313
  /**
205
314
  * Capture an event
206
315
  * Automatically adds common properties to all events
207
- * ($current_url, $host, $pathname, $referrer, $referring_domain, $browser_language, etc.)
316
+ * ($current_url, $host, $pathname, $referrer, $referring_domain, $browser, $os, $device, $timezone, etc.)
317
+ * Only properties in EVENT_TO_PERSON_PROPERTIES are copied to person properties
208
318
  * Also adds title property for $pageview events only
209
319
  *
210
320
  * @param name - Event name
211
321
  * @param payload - Event payload
322
+ * @param options - Optional capture options
212
323
  */
213
- capture(name, payload) {
324
+ capture(name, payload, options) {
214
325
  this.sessionManager.setSessionId();
215
326
  // Only send events in browser environment (not SSR)
216
327
  if (!globals_1.navigator || !globals_1.navigator.userAgent) {
@@ -219,13 +330,22 @@ class VTilt {
219
330
  if (!(0, utils_1.isValidUserAgent)(globals_1.navigator.userAgent)) {
220
331
  return;
221
332
  }
333
+ // Check rate limiting (unless explicitly skipped)
334
+ if (!(options === null || options === void 0 ? void 0 : options.skip_client_rate_limiting) &&
335
+ !this.rateLimiter.shouldAllowEvent()) {
336
+ // Event is rate limited - drop it silently
337
+ return;
338
+ }
222
339
  const url = this.buildUrl();
223
340
  // Add properties to all events
224
- // This includes: $current_url, $host, $pathname, $referrer, $referring_domain, $browser_language, etc.
341
+ // This includes: $current_url, $host, $pathname, $referrer, $referring_domain, $browser, $os, $device, $timezone, etc.
342
+ // (Only properties in EVENT_TO_PERSON_PROPERTIES are copied to person properties)
225
343
  const eventProperties = (0, event_utils_1.getEventProperties)();
226
344
  // Get person properties (includes $device_id and other user properties)
227
345
  // These are automatically included in all events
228
346
  const personProperties = this.userManager.getUserProperties();
347
+ // Get initial props: $initial_* properties from stored initial person info
348
+ const initialProps = this.userManager.get_initial_props();
229
349
  // Get session and window IDs
230
350
  // Both methods ensure IDs always exist (generate if needed)
231
351
  const session_id = this.sessionManager.getSessionId();
@@ -234,6 +354,19 @@ class VTilt {
234
354
  // This allows linking events with different distinct_ids that share the same anonymous_id
235
355
  // This is especially important for handling race conditions when $identify events arrive
236
356
  const anonymousId = this.userManager.getAnonymousId();
357
+ // Build $set_once from initial props (only on first event per page load)
358
+ // This optimization follows PostHog's pattern: initial props only need to be sent once
359
+ // since $set_once preserves first values. Sending them on every event would be redundant
360
+ // and cause unnecessary server processing for identity creation/updates.
361
+ const setOnce = {};
362
+ // Only include initial props if we haven't sent them yet this page load
363
+ if (!this._setOncePropertiesSent && Object.keys(initialProps).length > 0) {
364
+ Object.assign(setOnce, initialProps);
365
+ }
366
+ // Always merge with user-provided $set_once if present
367
+ if (payload.$set_once) {
368
+ Object.assign(setOnce, payload.$set_once);
369
+ }
237
370
  const enrichedPayload = {
238
371
  ...eventProperties, // Base properties for all events
239
372
  ...personProperties, // Person properties (includes $device_id)
@@ -242,8 +375,16 @@ class VTilt {
242
375
  // Always include $anon_distinct_id for identity linking (even for identified users)
243
376
  // This allows the server to merge identities proactively when events arrive out of order
244
377
  ...(anonymousId ? { $anon_distinct_id: anonymousId } : {}),
378
+ // Add $set_once with initial props (only if there are properties to set)
379
+ ...(Object.keys(setOnce).length > 0 ? { $set_once: setOnce } : {}),
380
+ // Merge user-provided $set if present
381
+ ...(payload.$set ? { $set: payload.$set } : {}),
245
382
  ...payload, // User-provided payload (can override base and person properties)
246
383
  };
384
+ // Mark that $set_once with initial props has been sent (only do this once per page load)
385
+ if (!this._setOncePropertiesSent && Object.keys(initialProps).length > 0) {
386
+ this._setOncePropertiesSent = true;
387
+ }
247
388
  // Add title only to $pageview events
248
389
  if (name === "$pageview" && globals_1.document) {
249
390
  enrichedPayload.title = globals_1.document.title;
@@ -291,6 +432,13 @@ class VTilt {
291
432
  };
292
433
  this.sendRequest(url, trackingEvent, true);
293
434
  }
435
+ /**
436
+ * Internal capture method that bypasses rate limiting
437
+ * Used for system events like rate limit warnings
438
+ */
439
+ _captureInternal(name, payload) {
440
+ this.capture(name, payload, { skip_client_rate_limiting: true });
441
+ }
294
442
  /**
295
443
  * Track a custom event (alias for capture)
296
444
  */
@@ -592,14 +740,16 @@ class VTilt {
592
740
  });
593
741
  }
594
742
  /**
595
- * Called when DOM is loaded - processes queued requests
743
+ * Called when DOM is loaded - processes queued requests and enables batching
596
744
  */
597
745
  _dom_loaded() {
598
- // Process all queued requests
746
+ // Enable the request queue for batched sending
747
+ this.requestQueue.enable();
748
+ // Process all pre-init queued requests
599
749
  this.__request_queue.forEach((item) => {
600
750
  this._send_retriable_request(item);
601
751
  });
602
- // Clear the queue
752
+ // Clear the legacy queue
603
753
  this.__request_queue = [];
604
754
  }
605
755
  }
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@v-tilt/browser",
3
- "version": "1.0.10",
4
- "description": "vTilt browser tracking library",
5
- "main": "dist/main.js",
6
- "module": "dist/module.js",
7
- "types": "dist/module.d.ts",
8
- "files": [
9
- "lib/*",
10
- "dist/*"
11
- ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "scripts": {
16
- "build": "tsc -b && rollup -c",
17
- "dev": "rollup -c -w",
18
- "type-check": "tsc --noEmit",
19
- "lint": "eslint src --ext .ts",
20
- "lint:fix": "eslint src --ext .ts --fix",
21
- "clean": "rimraf lib dist",
22
- "prepublishOnly": "pnpm run build"
23
- },
24
- "keywords": [
25
- "analytics",
26
- "tracking",
27
- "web-vitals",
28
- "performance"
29
- ],
30
- "author": "vTilt",
31
- "license": "MIT",
32
- "devDependencies": {
33
- "@babel/preset-env": "^7.28.3",
34
- "@rollup/plugin-babel": "^6.0.4",
35
- "@rollup/plugin-commonjs": "^25.0.8",
36
- "@rollup/plugin-json": "^6.1.0",
37
- "@rollup/plugin-node-resolve": "^15.3.1",
38
- "@rollup/plugin-terser": "^0.4.4",
39
- "@rollup/plugin-typescript": "^11.1.6",
40
- "@types/node": "^20.10.5",
41
- "@v-tilt/eslint-config": "workspace:*",
42
- "eslint": "^9.0.0",
43
- "rimraf": "^5.0.5",
44
- "rollup": "^4.9.1",
45
- "rollup-plugin-dts": "^6.2.3",
46
- "rollup-plugin-terser": "^7.0.2",
47
- "rollup-plugin-visualizer": "^6.0.3",
48
- "typescript": "^5.3.3"
49
- },
50
- "dependencies": {
51
- "web-vitals": "^3.5.0"
52
- },
53
- "peerDependencies": {
54
- "web-vitals": "^3.0.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "web-vitals": {
58
- "optional": true
59
- }
60
- }
61
- }
1
+ {
2
+ "name": "@v-tilt/browser",
3
+ "version": "1.1.0",
4
+ "description": "vTilt browser tracking library",
5
+ "main": "dist/main.js",
6
+ "module": "dist/module.js",
7
+ "types": "dist/module.d.ts",
8
+ "files": [
9
+ "lib/*",
10
+ "dist/*"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "analytics",
17
+ "tracking",
18
+ "web-vitals",
19
+ "performance"
20
+ ],
21
+ "author": "vTilt",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@babel/preset-env": "^7.28.3",
25
+ "@rollup/plugin-babel": "^6.0.4",
26
+ "@rollup/plugin-commonjs": "^25.0.8",
27
+ "@rollup/plugin-json": "^6.1.0",
28
+ "@rollup/plugin-node-resolve": "^15.3.1",
29
+ "@rollup/plugin-terser": "^0.4.4",
30
+ "@rollup/plugin-typescript": "^11.1.6",
31
+ "@types/node": "^20.10.5",
32
+ "eslint": "^9.0.0",
33
+ "rimraf": "^5.0.5",
34
+ "rollup": "^4.9.1",
35
+ "rollup-plugin-dts": "^6.2.3",
36
+ "rollup-plugin-terser": "^7.0.2",
37
+ "rollup-plugin-visualizer": "^6.0.3",
38
+ "typescript": "^5.3.3",
39
+ "@v-tilt/eslint-config": "1.0.0"
40
+ },
41
+ "dependencies": {
42
+ "fflate": "^0.8.2",
43
+ "web-vitals": "^3.5.0"
44
+ },
45
+ "peerDependencies": {
46
+ "web-vitals": "^3.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "web-vitals": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -b && rollup -c",
55
+ "dev": "rollup -c -w",
56
+ "type-check": "tsc --noEmit",
57
+ "lint": "eslint src --ext .ts",
58
+ "lint:fix": "eslint src --ext .ts --fix",
59
+ "clean": "rimraf lib dist"
60
+ }
61
+ }