@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
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Retry Queue - Exponential Backoff (PostHog-style)
4
+ *
5
+ * Retries failed requests with jittered exponential backoff.
6
+ * Detects online/offline status and pauses retries when offline.
7
+ *
8
+ * Features:
9
+ * - Exponential backoff: 3s, 6s, 12s, 24s... up to 30 minutes
10
+ * - Jitter: +/- 50% to prevent thundering herd
11
+ * - Online/offline detection
12
+ * - Max 10 retries before giving up
13
+ * - Uses sendBeacon on page unload for final attempt
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.RetryQueue = void 0;
17
+ exports.pickNextRetryDelay = pickNextRetryDelay;
18
+ const utils_1 = require("./utils");
19
+ const globals_1 = require("./utils/globals");
20
+ const THIRTY_MINUTES = 30 * 60 * 1000;
21
+ const MAX_RETRIES = 10;
22
+ /**
23
+ * Generates a jittered exponential backoff delay in milliseconds
24
+ *
25
+ * Base value is 3 seconds, doubled with each retry up to 30 minutes max.
26
+ * Each value has +/- 50% jitter.
27
+ *
28
+ * @param retriesPerformedSoFar - Number of retries already attempted
29
+ * @returns Delay in milliseconds
30
+ */
31
+ function pickNextRetryDelay(retriesPerformedSoFar) {
32
+ const rawBackoffTime = 3000 * 2 ** retriesPerformedSoFar;
33
+ const minBackoff = rawBackoffTime / 2;
34
+ const cappedBackoffTime = Math.min(THIRTY_MINUTES, rawBackoffTime);
35
+ const jitterFraction = Math.random() - 0.5; // Random between -0.5 and 0.5
36
+ const jitter = jitterFraction * (cappedBackoffTime - minBackoff);
37
+ return Math.ceil(cappedBackoffTime + jitter);
38
+ }
39
+ class RetryQueue {
40
+ constructor(config) {
41
+ this._isPolling = false;
42
+ this._pollIntervalMs = 3000;
43
+ this._queue = [];
44
+ this._areWeOnline = true;
45
+ this._sendRequest = config.sendRequest;
46
+ this._sendBeacon = config.sendBeacon;
47
+ // Set up online/offline detection
48
+ if (globals_1.window && typeof globals_1.navigator !== "undefined" && "onLine" in globals_1.navigator) {
49
+ this._areWeOnline = globals_1.navigator.onLine;
50
+ (0, utils_1.addEventListener)(globals_1.window, "online", () => {
51
+ this._areWeOnline = true;
52
+ this._flush();
53
+ });
54
+ (0, utils_1.addEventListener)(globals_1.window, "offline", () => {
55
+ this._areWeOnline = false;
56
+ });
57
+ }
58
+ }
59
+ /**
60
+ * Get current queue length
61
+ */
62
+ get length() {
63
+ return this._queue.length;
64
+ }
65
+ /**
66
+ * Enqueue a failed request for retry
67
+ */
68
+ enqueue(request, retriesPerformedSoFar = 0) {
69
+ // Don't retry if we've exceeded max retries
70
+ if (retriesPerformedSoFar >= MAX_RETRIES) {
71
+ console.warn(`VTilt: Request failed after ${MAX_RETRIES} retries, giving up`);
72
+ return;
73
+ }
74
+ const msToNextRetry = pickNextRetryDelay(retriesPerformedSoFar);
75
+ const retryAt = Date.now() + msToNextRetry;
76
+ this._queue.push({
77
+ retryAt,
78
+ request,
79
+ retriesPerformedSoFar: retriesPerformedSoFar + 1,
80
+ });
81
+ let logMessage = `VTilt: Enqueued failed request for retry in ${Math.round(msToNextRetry / 1000)}s`;
82
+ if (!this._areWeOnline) {
83
+ logMessage += " (Browser is offline)";
84
+ }
85
+ console.warn(logMessage);
86
+ // Start polling if not already
87
+ if (!this._isPolling) {
88
+ this._isPolling = true;
89
+ this._poll();
90
+ }
91
+ }
92
+ /**
93
+ * Attempt to send a request with retry on failure
94
+ */
95
+ async retriableRequest(request) {
96
+ try {
97
+ const response = await this._sendRequest(request);
98
+ // Retry on server errors (5xx) or network errors (0)
99
+ // Don't retry on client errors (4xx)
100
+ if (response.statusCode !== 200 &&
101
+ (response.statusCode < 400 || response.statusCode >= 500)) {
102
+ this.enqueue(request, 0);
103
+ }
104
+ }
105
+ catch (_a) {
106
+ // Network error - enqueue for retry
107
+ this.enqueue(request, 0);
108
+ }
109
+ }
110
+ /**
111
+ * Start polling for retries
112
+ */
113
+ _poll() {
114
+ if (this._poller) {
115
+ clearTimeout(this._poller);
116
+ }
117
+ this._poller = setTimeout(() => {
118
+ if (this._areWeOnline && this._queue.length > 0) {
119
+ this._flush();
120
+ }
121
+ // Continue polling if there are items in queue
122
+ if (this._queue.length > 0) {
123
+ this._poll();
124
+ }
125
+ else {
126
+ this._isPolling = false;
127
+ }
128
+ }, this._pollIntervalMs);
129
+ }
130
+ /**
131
+ * Flush ready items from the queue
132
+ */
133
+ _flush() {
134
+ const now = Date.now();
135
+ const notReady = [];
136
+ const ready = [];
137
+ this._queue.forEach((item) => {
138
+ if (item.retryAt < now) {
139
+ ready.push(item);
140
+ }
141
+ else {
142
+ notReady.push(item);
143
+ }
144
+ });
145
+ this._queue = notReady;
146
+ // Retry ready items
147
+ ready.forEach(async ({ request, retriesPerformedSoFar }) => {
148
+ try {
149
+ const response = await this._sendRequest(request);
150
+ // If still failing, re-enqueue
151
+ if (response.statusCode !== 200 &&
152
+ (response.statusCode < 400 || response.statusCode >= 500)) {
153
+ this.enqueue(request, retriesPerformedSoFar);
154
+ }
155
+ }
156
+ catch (_a) {
157
+ // Network error - re-enqueue
158
+ this.enqueue(request, retriesPerformedSoFar);
159
+ }
160
+ });
161
+ }
162
+ /**
163
+ * Flush all queued requests using sendBeacon on page unload
164
+ */
165
+ unload() {
166
+ if (this._poller) {
167
+ clearTimeout(this._poller);
168
+ this._poller = undefined;
169
+ }
170
+ // Attempt final send of all queued requests
171
+ this._queue.forEach(({ request }) => {
172
+ try {
173
+ this._sendBeacon(request);
174
+ }
175
+ catch (e) {
176
+ console.error("VTilt: Failed to send beacon on unload", e);
177
+ }
178
+ });
179
+ this._queue = [];
180
+ }
181
+ }
182
+ exports.RetryQueue = RetryQueue;
package/lib/types.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface VTiltConfig {
13
13
  globalAttributes?: Record<string, string>;
14
14
  persistence?: "localStorage" | "cookie";
15
15
  crossSubdomainCookie?: boolean;
16
+ disable_compression?: boolean;
16
17
  }
17
18
  export interface SessionData {
18
19
  value: string;
@@ -159,4 +159,25 @@ export declare class UserManager {
159
159
  * Remove cookie value
160
160
  */
161
161
  private removeCookieValue;
162
+ /**
163
+ * Register a value once (only if not already set)
164
+ * Stores properties in localStorage only if they don't already exist
165
+ */
166
+ private register_once;
167
+ /**
168
+ * Set initial person info
169
+ * Stores referrer and URL info on first visit for generating $initial_* properties
170
+ */
171
+ set_initial_person_info(maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[]): void;
172
+ /**
173
+ * Get initial props
174
+ * Generates $initial_* properties from stored initial person info
175
+ * These are sent with events as $set_once to preserve first values
176
+ */
177
+ get_initial_props(): Record<string, any>;
178
+ /**
179
+ * Update referrer info
180
+ * Stores current referrer information if not already stored
181
+ */
182
+ update_referrer_info(): void;
162
183
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UserManager = void 0;
4
4
  const constants_1 = require("./constants");
5
5
  const utils_1 = require("./utils");
6
+ const event_utils_1 = require("./utils/event-utils");
6
7
  class UserManager {
7
8
  constructor(storageMethod = "localStorage", domain) {
8
9
  this._cachedPersonProperties = null; // Cache for deduplication
@@ -570,5 +571,70 @@ class UserManager {
570
571
  }
571
572
  document.cookie = cookieValue;
572
573
  }
574
+ /**
575
+ * Register a value once (only if not already set)
576
+ * Stores properties in localStorage only if they don't already exist
577
+ */
578
+ register_once(props, defaultValues) {
579
+ const stored = this.getStoredUserProperties();
580
+ let changed = false;
581
+ for (const key in props) {
582
+ if (Object.prototype.hasOwnProperty.call(props, key)) {
583
+ if (!(key in stored)) {
584
+ stored[key] = props[key];
585
+ changed = true;
586
+ }
587
+ }
588
+ }
589
+ if (defaultValues) {
590
+ for (const key in defaultValues) {
591
+ if (Object.prototype.hasOwnProperty.call(defaultValues, key)) {
592
+ if (!(key in stored)) {
593
+ stored[key] = defaultValues[key];
594
+ changed = true;
595
+ }
596
+ }
597
+ }
598
+ }
599
+ if (changed) {
600
+ this.setStoredUserProperties(stored);
601
+ }
602
+ }
603
+ /**
604
+ * Set initial person info
605
+ * Stores referrer and URL info on first visit for generating $initial_* properties
606
+ */
607
+ set_initial_person_info(maskPersonalDataProperties, customPersonalDataProperties) {
608
+ const stored = this.getStoredUserProperties();
609
+ // Check if already set (backwards compatibility check)
610
+ if (stored[constants_1.INITIAL_PERSON_INFO]) {
611
+ return;
612
+ }
613
+ const personInfo = (0, event_utils_1.getPersonInfo)(maskPersonalDataProperties, customPersonalDataProperties);
614
+ this.register_once({
615
+ [constants_1.INITIAL_PERSON_INFO]: personInfo,
616
+ }, undefined);
617
+ }
618
+ /**
619
+ * Get initial props
620
+ * Generates $initial_* properties from stored initial person info
621
+ * These are sent with events as $set_once to preserve first values
622
+ */
623
+ get_initial_props() {
624
+ const stored = this.getStoredUserProperties();
625
+ const initialPersonInfo = stored[constants_1.INITIAL_PERSON_INFO];
626
+ if (!initialPersonInfo) {
627
+ return {};
628
+ }
629
+ return (0, event_utils_1.getInitialPersonPropsFromInfo)(initialPersonInfo);
630
+ }
631
+ /**
632
+ * Update referrer info
633
+ * Stores current referrer information if not already stored
634
+ */
635
+ update_referrer_info() {
636
+ const referrerInfo = (0, event_utils_1.getReferrerInfo)();
637
+ this.register_once(referrerInfo, undefined);
638
+ }
573
639
  }
574
640
  exports.UserManager = UserManager;
@@ -1,34 +1,52 @@
1
1
  /**
2
- * Get browser language
3
- * Returns the browser's language setting (e.g., "en-US")
2
+ * Event utilities
3
+ * Functions for extracting event properties, campaign parameters, and person info
4
4
  */
5
- export declare function getBrowserLanguage(): string | undefined;
5
+ export declare const PERSONAL_DATA_CAMPAIGN_PARAMS: string[];
6
+ export declare const CAMPAIGN_PARAMS: string[];
7
+ export declare const EVENT_TO_PERSON_PROPERTIES: string[];
8
+ export declare const MASKED = "<masked>";
9
+ export declare const COOKIE_CAMPAIGN_PARAMS: string[];
6
10
  /**
7
- * Get browser language prefix
8
- * Returns the language code without region (e.g., "en" from "en-US")
11
+ * Get campaign parameters from URL
12
+ * Extracts UTM and other campaign tracking parameters from current page URL
13
+ * Masks personal data parameters if configured
9
14
  */
15
+ export declare function getCampaignParams(customTrackedParams?: string[], maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[] | undefined): Record<string, string>;
16
+ export declare function getSearchInfo(): Record<string, any>;
17
+ export declare function getBrowserLanguage(): string | undefined;
10
18
  export declare function getBrowserLanguagePrefix(): string | undefined;
19
+ export declare function getReferrer(): string;
20
+ export declare function getReferringDomain(): string;
11
21
  /**
12
- * Get referrer
13
- * Returns document.referrer or '$direct' if no referrer
22
+ * Get referrer information
23
+ * Returns current referrer and referring domain
14
24
  */
15
- export declare function getReferrer(): string;
25
+ export declare function getReferrerInfo(): Record<string, any>;
16
26
  /**
17
- * Get referring domain
18
- * Returns the hostname of the referrer URL or '$direct' if no referrer
27
+ * Get person info for initial storage
28
+ * Extracts referrer and URL info, masks personal data if configured
29
+ * Returns compact format (r: referrer, u: url) for storage efficiency
19
30
  */
20
- export declare function getReferringDomain(): string;
31
+ export declare function getPersonInfo(maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[]): {
32
+ r: string;
33
+ u: string | undefined;
34
+ };
21
35
  /**
22
- * Get timezone
23
- * Returns the timezone (e.g., "America/New_York")
36
+ * Convert person info to person properties
37
+ * Extracts referrer, URL, campaign params, and search info from stored person info
24
38
  */
25
- export declare function getTimezone(): string | undefined;
39
+ export declare function getPersonPropsFromInfo(info: Record<string, any>): Record<string, any>;
26
40
  /**
27
- * Get timezone offset
28
- * Returns the timezone offset in minutes
41
+ * Convert person info to initial person properties
42
+ * Generates $initial_* properties from person info (preserves first values)
29
43
  */
44
+ export declare function getInitialPersonPropsFromInfo(info: Record<string, any>): Record<string, any>;
45
+ export declare function getTimezone(): string | undefined;
30
46
  export declare function getTimezoneOffset(): number | undefined;
31
47
  /**
32
48
  * Get event properties that should be added to all events
49
+ * Returns all event context properties (browser, device, URL, etc.) plus event metadata
50
+ * Note: Only properties in EVENT_TO_PERSON_PROPERTIES are copied to person properties
33
51
  */
34
- export declare function getEventProperties(): Record<string, any>;
52
+ export declare function getEventProperties(maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[]): Record<string, any>;