@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.
- 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/constants.d.ts +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/module.d.ts +57 -4
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/module.no-external.d.ts +57 -4
- 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/user-manager.d.ts +21 -0
- package/dist/utils/event-utils.d.ts +35 -17
- package/dist/utils/index.d.ts +21 -0
- package/dist/utils/request-utils.d.ts +17 -0
- package/dist/vtilt.d.ts +40 -8
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +2 -1
- 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/user-manager.d.ts +21 -0
- package/lib/user-manager.js +66 -0
- package/lib/utils/event-utils.d.ts +35 -17
- package/lib/utils/event-utils.js +247 -118
- package/lib/utils/index.d.ts +21 -0
- package/lib/utils/index.js +58 -0
- package/lib/utils/request-utils.d.ts +17 -0
- package/lib/utils/request-utils.js +80 -0
- package/lib/vtilt.d.ts +40 -8
- package/lib/vtilt.js +161 -11
- 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
package/lib/user-manager.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/user-manager.js
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Event utilities
|
|
3
|
+
* Functions for extracting event properties, campaign parameters, and person info
|
|
4
4
|
*/
|
|
5
|
-
export declare
|
|
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
|
|
8
|
-
*
|
|
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
|
|
22
|
+
* Get referrer information
|
|
23
|
+
* Returns current referrer and referring domain
|
|
14
24
|
*/
|
|
15
|
-
export declare function
|
|
25
|
+
export declare function getReferrerInfo(): Record<string, any>;
|
|
16
26
|
/**
|
|
17
|
-
* Get
|
|
18
|
-
*
|
|
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
|
|
31
|
+
export declare function getPersonInfo(maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[]): {
|
|
32
|
+
r: string;
|
|
33
|
+
u: string | undefined;
|
|
34
|
+
};
|
|
21
35
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
39
|
+
export declare function getPersonPropsFromInfo(info: Record<string, any>): Record<string, any>;
|
|
26
40
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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>;
|