@affiliateo/web 4.0.0 → 4.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/dist/provider.d.ts +1 -1
- package/dist/provider.js +90 -7
- package/dist/queue.d.ts +9 -1
- package/dist/queue.js +11 -6
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
package/dist/provider.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export declare function useAffiliateRef(): AffiliateoContextValue;
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export declare function getAttribution(): Record<string, string>;
|
|
21
|
-
export declare function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, debug, children, }: AffiliateoConfig & {
|
|
21
|
+
export declare function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }: AffiliateoConfig & {
|
|
22
22
|
children: React.ReactNode;
|
|
23
23
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/provider.js
CHANGED
|
@@ -102,7 +102,26 @@ export function getAttribution() {
|
|
|
102
102
|
// mount (host has the call sites) but we can shout in dev so the
|
|
103
103
|
// mistake is visible.
|
|
104
104
|
let providerMountCount = 0;
|
|
105
|
-
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, debug, children, }) {
|
|
105
|
+
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }) {
|
|
106
|
+
// Normalize autoCapturePageviews. Three shapes:
|
|
107
|
+
// - undefined / true / object → auto-capture ON (with object overrides)
|
|
108
|
+
// - false → auto-capture OFF entirely (host must call trackPageview)
|
|
109
|
+
// Defaults match Datafast: captureInitialPageview true, trackHashChanges
|
|
110
|
+
// false, debounceMs 100.
|
|
111
|
+
const autoCfg = autoCapturePageviews === false
|
|
112
|
+
? { enabled: false, captureInitialPageview: false, trackHashChanges: false, debounceMs: 100 }
|
|
113
|
+
: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
captureInitialPageview: typeof autoCapturePageviews === 'object'
|
|
116
|
+
? autoCapturePageviews?.captureInitialPageview ?? true
|
|
117
|
+
: true,
|
|
118
|
+
trackHashChanges: typeof autoCapturePageviews === 'object'
|
|
119
|
+
? autoCapturePageviews?.trackHashChanges ?? false
|
|
120
|
+
: false,
|
|
121
|
+
debounceMs: typeof autoCapturePageviews === 'object'
|
|
122
|
+
? autoCapturePageviews?.debounceMs ?? 100
|
|
123
|
+
: 100,
|
|
124
|
+
};
|
|
106
125
|
const [state, setState] = useState({
|
|
107
126
|
refCode: null,
|
|
108
127
|
visitorId: null,
|
|
@@ -391,6 +410,24 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
391
410
|
});
|
|
392
411
|
return;
|
|
393
412
|
}
|
|
413
|
+
// File-protocol guard. Pages served via file:// (someone double-clicking
|
|
414
|
+
// a built HTML file rather than running a server) almost certainly aren't
|
|
415
|
+
// real merchant traffic. Block by default; opt in via allowFileProtocol.
|
|
416
|
+
if (!allowFileProtocol && window.location.protocol === 'file:') {
|
|
417
|
+
log('blocked: file:// protocol (set allowFileProtocol={true} to enable)');
|
|
418
|
+
setState({
|
|
419
|
+
refCode: null,
|
|
420
|
+
visitorId: null,
|
|
421
|
+
track: asyncNoop,
|
|
422
|
+
identify: asyncNoop,
|
|
423
|
+
trackPageview: asyncNoop,
|
|
424
|
+
reset: asyncNoop,
|
|
425
|
+
optOut: syncNoop,
|
|
426
|
+
optIn: syncNoop,
|
|
427
|
+
flush: asyncNoop,
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
394
431
|
// Iframe guard. Default behavior is "if we're inside an iframe, don't
|
|
395
432
|
// track" so scrapers, preview tools, and archive crawlers embedding
|
|
396
433
|
// the merchant's site don't count as pageviews. Cross-origin iframe
|
|
@@ -433,7 +470,10 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
433
470
|
// auto-fired pageview below; only session_end stays on sendBeacon
|
|
434
471
|
// because tab-close urgency beats queue-retry reliability.
|
|
435
472
|
if (queueRef.current === null) {
|
|
436
|
-
queueRef.current = new EventQueue(
|
|
473
|
+
queueRef.current = new EventQueue({
|
|
474
|
+
flushIntervalMs: flushInterval,
|
|
475
|
+
maxQueueSize: maxQueueSize,
|
|
476
|
+
});
|
|
437
477
|
}
|
|
438
478
|
const queue = queueRef.current;
|
|
439
479
|
// Opt-out short-circuit. A visitor who previously called optOut()
|
|
@@ -607,23 +647,60 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
607
647
|
}
|
|
608
648
|
}
|
|
609
649
|
}
|
|
610
|
-
// Send pageview on mount
|
|
611
|
-
|
|
650
|
+
// Send pageview on mount. captureInitialPageview === false skips this
|
|
651
|
+
// for cookie-consent flows where the host wants to defer until the
|
|
652
|
+
// user accepts (then call trackPageview() manually).
|
|
653
|
+
if (!sentRef.current && autoCfg.captureInitialPageview && autoCfg.enabled) {
|
|
612
654
|
sentRef.current = true;
|
|
613
655
|
sendPageview();
|
|
614
656
|
}
|
|
615
657
|
// Detect user leaving
|
|
616
658
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
617
659
|
window.addEventListener('pagehide', sendEnd);
|
|
618
|
-
// SPA support: detect URL changes
|
|
660
|
+
// SPA support: detect URL changes. We track BOTH pathname (modern
|
|
661
|
+
// pushState-based routers: React Router v6, Next.js App Router, Vue
|
|
662
|
+
// Router 4) AND hash (legacy hash routing: Vue Router with hash mode,
|
|
663
|
+
// React Router HashRouter, old Angular). Hash tracking is opt-in via
|
|
664
|
+
// autoCapturePageviews.trackHashChanges because modern SPAs never use
|
|
665
|
+
// it and firing on every #anchor click would inflate pageview counts.
|
|
666
|
+
//
|
|
667
|
+
// Debouncing collapses back-to-back navigation events into one. Some
|
|
668
|
+
// frameworks (Next.js, certain Vue routers) fire pushState then
|
|
669
|
+
// replaceState in the same tick to canonicalize the URL; without
|
|
670
|
+
// debouncing that's two pageviews per real navigation. Default 100ms
|
|
671
|
+
// matches Datafast; set debounceMs to 0 to disable.
|
|
619
672
|
let lastPath = window.location.pathname;
|
|
673
|
+
let lastHash = window.location.hash;
|
|
674
|
+
let pendingPageviewTimer = null;
|
|
620
675
|
function onUrlChange() {
|
|
621
|
-
|
|
622
|
-
|
|
676
|
+
const pathChanged = window.location.pathname !== lastPath;
|
|
677
|
+
const hashChanged = autoCfg.trackHashChanges && window.location.hash !== lastHash;
|
|
678
|
+
if (!pathChanged && !hashChanged)
|
|
679
|
+
return;
|
|
680
|
+
lastPath = window.location.pathname;
|
|
681
|
+
lastHash = window.location.hash;
|
|
682
|
+
if (!autoCfg.enabled)
|
|
683
|
+
return;
|
|
684
|
+
// Debounce: if a pending pageview is already scheduled, the later
|
|
685
|
+
// urlchange wins (the URL the browser ends up on after both calls
|
|
686
|
+
// is what we want to report).
|
|
687
|
+
if (pendingPageviewTimer !== null) {
|
|
688
|
+
clearTimeout(pendingPageviewTimer);
|
|
689
|
+
}
|
|
690
|
+
if (autoCfg.debounceMs > 0) {
|
|
691
|
+
pendingPageviewTimer = setTimeout(() => {
|
|
692
|
+
pendingPageviewTimer = null;
|
|
693
|
+
sendPageview();
|
|
694
|
+
}, autoCfg.debounceMs);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
623
697
|
sendPageview();
|
|
624
698
|
}
|
|
625
699
|
}
|
|
626
700
|
window.addEventListener('popstate', onUrlChange);
|
|
701
|
+
if (autoCfg.trackHashChanges) {
|
|
702
|
+
window.addEventListener('hashchange', onUrlChange);
|
|
703
|
+
}
|
|
627
704
|
const origPushState = history.pushState;
|
|
628
705
|
const origReplaceState = history.replaceState;
|
|
629
706
|
history.pushState = function (...args) {
|
|
@@ -711,6 +788,12 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
711
788
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
712
789
|
window.removeEventListener('pagehide', sendEnd);
|
|
713
790
|
window.removeEventListener('popstate', onUrlChange);
|
|
791
|
+
if (autoCfg.trackHashChanges) {
|
|
792
|
+
window.removeEventListener('hashchange', onUrlChange);
|
|
793
|
+
}
|
|
794
|
+
if (pendingPageviewTimer !== null) {
|
|
795
|
+
clearTimeout(pendingPageviewTimer);
|
|
796
|
+
}
|
|
714
797
|
history.pushState = origPushState;
|
|
715
798
|
history.replaceState = origReplaceState;
|
|
716
799
|
if (allowedHosts.length > 0) {
|
package/dist/queue.d.ts
CHANGED
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
* - SIZE_FLUSH_THRESHOLD = 10 triggers a flush when queue grows
|
|
42
42
|
* past this even before the timer fires
|
|
43
43
|
*/
|
|
44
|
+
export interface EventQueueOptions {
|
|
45
|
+
/** Override the auto-flush cadence. Defaults to 5000ms. */
|
|
46
|
+
flushIntervalMs?: number;
|
|
47
|
+
/** Override the hard cap on queued events. Defaults to 100. */
|
|
48
|
+
maxQueueSize?: number;
|
|
49
|
+
}
|
|
44
50
|
export interface QueuedEvent {
|
|
45
51
|
/** Stable id used to identify the event across retries. UUID v4
|
|
46
52
|
* generated client-side; collisions are astronomically unlikely so
|
|
@@ -63,7 +69,9 @@ export declare class EventQueue {
|
|
|
63
69
|
private flushTimer;
|
|
64
70
|
private isFlushing;
|
|
65
71
|
private shuttingDown;
|
|
66
|
-
|
|
72
|
+
private readonly flushIntervalMs;
|
|
73
|
+
private readonly maxQueueSize;
|
|
74
|
+
constructor(opts?: EventQueueOptions);
|
|
67
75
|
/** Add an event. Persists synchronously to localStorage so a tab
|
|
68
76
|
* close right after enqueue doesn't lose the event from memory
|
|
69
77
|
* before it's safely on disk. */
|
package/dist/queue.js
CHANGED
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
*/
|
|
44
44
|
const STORAGE_KEY = 'affiliateo_event_queue';
|
|
45
45
|
const MAX_RETRIES = 3;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
47
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 5000;
|
|
48
48
|
const SIZE_FLUSH_THRESHOLD = 10;
|
|
49
49
|
function lsGet(key) {
|
|
50
50
|
try {
|
|
@@ -76,11 +76,16 @@ function uuid() {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
export class EventQueue {
|
|
79
|
-
constructor() {
|
|
79
|
+
constructor(opts = {}) {
|
|
80
80
|
this.queue = [];
|
|
81
81
|
this.flushTimer = null;
|
|
82
82
|
this.isFlushing = false;
|
|
83
83
|
this.shuttingDown = false;
|
|
84
|
+
// Bounds: refuse pathological values silently. Minimum 1s flush so we
|
|
85
|
+
// don't hammer the network; minimum 10 events so the queue is useful;
|
|
86
|
+
// maximum 1000 events so a misconfigured app can't fill localStorage.
|
|
87
|
+
this.flushIntervalMs = Math.max(1000, opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
|
|
88
|
+
this.maxQueueSize = Math.max(10, Math.min(1000, opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE));
|
|
84
89
|
// Hydrate from localStorage so events from a prior page load survive
|
|
85
90
|
// a refresh / navigation. Guards against corrupt JSON or wrong-shape
|
|
86
91
|
// entries by resetting to empty if anything looks off — better to
|
|
@@ -125,8 +130,8 @@ export class EventQueue {
|
|
|
125
130
|
// over budget the oldest events are likely already stale (the
|
|
126
131
|
// visitor moved on, the page they were on isn't current) so
|
|
127
132
|
// dropping them preserves more value than dropping the new ones.
|
|
128
|
-
if (this.queue.length >
|
|
129
|
-
this.queue.splice(0, this.queue.length -
|
|
133
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
134
|
+
this.queue.splice(0, this.queue.length - this.maxQueueSize);
|
|
130
135
|
}
|
|
131
136
|
this.persist();
|
|
132
137
|
// Burst protection: when many events fire in quick succession
|
|
@@ -261,6 +266,6 @@ export class EventQueue {
|
|
|
261
266
|
return;
|
|
262
267
|
this.flushTimer = setInterval(() => {
|
|
263
268
|
void this.flush();
|
|
264
|
-
},
|
|
269
|
+
}, this.flushIntervalMs);
|
|
265
270
|
}
|
|
266
271
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -57,6 +57,39 @@ export interface AffiliateoConfig {
|
|
|
57
57
|
* Example:
|
|
58
58
|
* <AffiliateoProvider campaignId="..." debug> */
|
|
59
59
|
debug?: boolean;
|
|
60
|
+
/** Allow tracking from pages served via the `file://` protocol (someone
|
|
61
|
+
* double-clicking a built HTML file instead of running a server).
|
|
62
|
+
* Defaults to false. Almost never useful. set true only if you're
|
|
63
|
+
* intentionally testing an offline-first build. */
|
|
64
|
+
allowFileProtocol?: boolean;
|
|
65
|
+
/** How often the event queue tries to flush, in milliseconds. Defaults
|
|
66
|
+
* to 5000 (every 5 seconds). Lower for chattier delivery (more
|
|
67
|
+
* requests, fresher data); raise for a quieter queue (fewer requests,
|
|
68
|
+
* bigger batches). */
|
|
69
|
+
flushInterval?: number;
|
|
70
|
+
/** Maximum number of events the queue holds before dropping the oldest
|
|
71
|
+
* FIFO. Defaults to 100. Acts as a backstop against a long network
|
|
72
|
+
* outage filling localStorage. */
|
|
73
|
+
maxQueueSize?: number;
|
|
74
|
+
/** Pageview auto-capture behavior. Pass `false` to disable entirely
|
|
75
|
+
* (you'll call `trackPageview()` yourself). Pass an object to fine-tune.
|
|
76
|
+
* Defaults to `true` (auto-capture everything). */
|
|
77
|
+
autoCapturePageviews?: boolean | {
|
|
78
|
+
/** Fire the initial pageview on mount. Defaults to true. Set false
|
|
79
|
+
* for cookie-consent flows where you want to defer until the user
|
|
80
|
+
* accepts. */
|
|
81
|
+
captureInitialPageview?: boolean;
|
|
82
|
+
/** Fire pageviews on URL hash changes (e.g. `/#/about` → `/#/pricing`
|
|
83
|
+
* in older Vue / Angular / React Router HashRouter setups). Defaults
|
|
84
|
+
* to false. modern SPAs use pushState (already tracked). Turn on
|
|
85
|
+
* only for legacy hash routing. */
|
|
86
|
+
trackHashChanges?: boolean;
|
|
87
|
+
/** Debounce window for back-to-back pushState / replaceState calls,
|
|
88
|
+
* in milliseconds. Some frameworks (Next.js, some Vue routers) fire
|
|
89
|
+
* both within one tick, producing two pageviews per navigation
|
|
90
|
+
* without debouncing. Defaults to 100ms. Set 0 to disable. */
|
|
91
|
+
debounceMs?: number;
|
|
92
|
+
};
|
|
60
93
|
}
|
|
61
94
|
export interface AffiliateoContextValue {
|
|
62
95
|
/** The affiliate ref code from the URL (if present) */
|