@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/LICENSE +21 -0
- package/dist/array.js +1 -1
- package/dist/array.js.map +1 -1
- package/dist/array.no-external.js +1 -1
- package/dist/array.no-external.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.d.ts +55 -3
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +55 -3
- package/dist/module.no-external.js +1 -1
- package/dist/module.no-external.js.map +1 -1
- package/dist/rate-limiter.d.ts +52 -0
- package/dist/request-queue.d.ts +78 -0
- package/dist/request.d.ts +54 -0
- package/dist/retry-queue.d.ts +64 -0
- package/dist/types.d.ts +1 -0
- package/dist/vtilt.d.ts +38 -7
- package/lib/rate-limiter.d.ts +52 -0
- package/lib/rate-limiter.js +80 -0
- package/lib/request-queue.d.ts +78 -0
- package/lib/request-queue.js +156 -0
- package/lib/request.d.ts +54 -0
- package/lib/request.js +265 -0
- package/lib/retry-queue.d.ts +64 -0
- package/lib/retry-queue.js +182 -0
- package/lib/types.d.ts +1 -0
- package/lib/utils/event-utils.js +88 -82
- package/lib/utils/index.js +2 -2
- package/lib/utils/request-utils.js +21 -19
- package/lib/vtilt.d.ts +38 -7
- package/lib/vtilt.js +143 -15
- package/package.json +13 -13
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 = []; //
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 $
|
|
245
|
-
//
|
|
246
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
+
}
|