@v-tilt/browser 1.0.11 → 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.
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.
@@ -99,10 +125,30 @@ class VTilt {
99
125
  // Initialize history autocapture
100
126
  this.historyAutocapture = new history_autocapture_1.HistoryAutocapture(this);
101
127
  this.historyAutocapture.startIfEnabled();
128
+ // Set up page unload handler to flush queued events
129
+ this._setupUnloadHandler();
102
130
  // Capture initial pageview (with visibility check)
103
131
  this._captureInitialPageview();
104
132
  return this;
105
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
+ }
106
152
  /**
107
153
  * Returns a string representation of the instance name
108
154
  * Used for debugging and logging
@@ -177,6 +223,7 @@ class VTilt {
177
223
  /**
178
224
  * Send HTTP request
179
225
  * This is the central entry point for all tracking requests
226
+ * Events are batched and sent every 3 seconds for better performance
180
227
  */
181
228
  sendRequest(url, event, shouldEnqueue) {
182
229
  // Validate configuration (only warns once per instance)
@@ -193,10 +240,69 @@ class VTilt {
193
240
  return;
194
241
  }
195
242
  }
196
- const request = new XMLHttpRequest();
197
- request.open("POST", url, true);
198
- request.setRequestHeader("Content-Type", "application/json");
199
- 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
+ });
200
306
  }
201
307
  /**
202
308
  * Send a queued request (called after DOM is loaded)
@@ -213,8 +319,9 @@ class VTilt {
213
319
  *
214
320
  * @param name - Event name
215
321
  * @param payload - Event payload
322
+ * @param options - Optional capture options
216
323
  */
217
- capture(name, payload) {
324
+ capture(name, payload, options) {
218
325
  this.sessionManager.setSessionId();
219
326
  // Only send events in browser environment (not SSR)
220
327
  if (!globals_1.navigator || !globals_1.navigator.userAgent) {
@@ -223,6 +330,12 @@ class VTilt {
223
330
  if (!(0, utils_1.isValidUserAgent)(globals_1.navigator.userAgent)) {
224
331
  return;
225
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
+ }
226
339
  const url = this.buildUrl();
227
340
  // Add properties to all events
228
341
  // This includes: $current_url, $host, $pathname, $referrer, $referring_domain, $browser, $os, $device, $timezone, etc.
@@ -241,14 +354,16 @@ class VTilt {
241
354
  // This allows linking events with different distinct_ids that share the same anonymous_id
242
355
  // This is especially important for handling race conditions when $identify events arrive
243
356
  const anonymousId = this.userManager.getAnonymousId();
244
- // Build $set and $set_once from initial props
245
- // Initial props are added to $set_once (only first time, preserves first values)
246
- // Regular event properties that match EVENT_TO_PERSON_PROPERTIES will be added to $set by server
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.
247
361
  const setOnce = {};
248
- if (Object.keys(initialProps).length > 0) {
362
+ // Only include initial props if we haven't sent them yet this page load
363
+ if (!this._setOncePropertiesSent && Object.keys(initialProps).length > 0) {
249
364
  Object.assign(setOnce, initialProps);
250
365
  }
251
- // Merge with user-provided $set_once if present
366
+ // Always merge with user-provided $set_once if present
252
367
  if (payload.$set_once) {
253
368
  Object.assign(setOnce, payload.$set_once);
254
369
  }
@@ -260,12 +375,16 @@ class VTilt {
260
375
  // Always include $anon_distinct_id for identity linking (even for identified users)
261
376
  // This allows the server to merge identities proactively when events arrive out of order
262
377
  ...(anonymousId ? { $anon_distinct_id: anonymousId } : {}),
263
- // Add $set_once with initial props
378
+ // Add $set_once with initial props (only if there are properties to set)
264
379
  ...(Object.keys(setOnce).length > 0 ? { $set_once: setOnce } : {}),
265
380
  // Merge user-provided $set if present
266
381
  ...(payload.$set ? { $set: payload.$set } : {}),
267
382
  ...payload, // User-provided payload (can override base and person properties)
268
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
+ }
269
388
  // Add title only to $pageview events
270
389
  if (name === "$pageview" && globals_1.document) {
271
390
  enrichedPayload.title = globals_1.document.title;
@@ -313,6 +432,13 @@ class VTilt {
313
432
  };
314
433
  this.sendRequest(url, trackingEvent, true);
315
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
+ }
316
442
  /**
317
443
  * Track a custom event (alias for capture)
318
444
  */
@@ -614,14 +740,16 @@ class VTilt {
614
740
  });
615
741
  }
616
742
  /**
617
- * Called when DOM is loaded - processes queued requests
743
+ * Called when DOM is loaded - processes queued requests and enables batching
618
744
  */
619
745
  _dom_loaded() {
620
- // Process all queued requests
746
+ // Enable the request queue for batched sending
747
+ this.requestQueue.enable();
748
+ // Process all pre-init queued requests
621
749
  this.__request_queue.forEach((item) => {
622
750
  this._send_retriable_request(item);
623
751
  });
624
- // Clear the queue
752
+ // Clear the legacy queue
625
753
  this.__request_queue = [];
626
754
  }
627
755
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v-tilt/browser",
3
- "version": "1.0.11",
3
+ "version": "1.1.0",
4
4
  "description": "vTilt browser tracking library",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/module.js",
@@ -12,15 +12,6 @@
12
12
  "publishConfig": {
13
13
  "access": "public"
14
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
15
  "keywords": [
25
16
  "analytics",
26
17
  "tracking",
@@ -38,16 +29,17 @@
38
29
  "@rollup/plugin-terser": "^0.4.4",
39
30
  "@rollup/plugin-typescript": "^11.1.6",
40
31
  "@types/node": "^20.10.5",
41
- "@v-tilt/eslint-config": "workspace:*",
42
32
  "eslint": "^9.0.0",
43
33
  "rimraf": "^5.0.5",
44
34
  "rollup": "^4.9.1",
45
35
  "rollup-plugin-dts": "^6.2.3",
46
36
  "rollup-plugin-terser": "^7.0.2",
47
37
  "rollup-plugin-visualizer": "^6.0.3",
48
- "typescript": "^5.3.3"
38
+ "typescript": "^5.3.3",
39
+ "@v-tilt/eslint-config": "1.0.0"
49
40
  },
50
41
  "dependencies": {
42
+ "fflate": "^0.8.2",
51
43
  "web-vitals": "^3.5.0"
52
44
  },
53
45
  "peerDependencies": {
@@ -57,5 +49,13 @@
57
49
  "web-vitals": {
58
50
  "optional": true
59
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
60
  }
61
- }
61
+ }