@affiliateo/web 2.4.0 → 3.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/bot-detect.d.ts +37 -0
- package/dist/bot-detect.js +138 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +360 -143
- package/dist/queue.d.ts +99 -0
- package/dist/queue.js +266 -0
- package/dist/types.d.ts +74 -0
- package/package.json +34 -34
package/dist/queue.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventQueue: durable best-effort delivery for analytics events.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* Before this module, every track / identify / pageview was fire-and-forget
|
|
6
|
+
* via navigator.sendBeacon. If the visitor's network blinked, the server
|
|
7
|
+
* was briefly overloaded, or any other transient failure hit, the event
|
|
8
|
+
* was lost with no retry, no log, no observability. On flaky mobile
|
|
9
|
+
* networks (train wifi, subway transitions, dropping 4G) merchants saw
|
|
10
|
+
* ~10-15% fewer events than they should have.
|
|
11
|
+
*
|
|
12
|
+
* This queue mirrors the pattern PostHog / Segment / Amplitude / Datafast
|
|
13
|
+
* all converged on: in-memory queue, localStorage persistence so events
|
|
14
|
+
* survive page reload, periodic auto-flush, retry on failure up to N
|
|
15
|
+
* times, drop after N tries to bound memory.
|
|
16
|
+
*
|
|
17
|
+
* Design notes:
|
|
18
|
+
* - Persisted state is JSON in localStorage under STORAGE_KEY. The
|
|
19
|
+
* persisted shape is intentionally simple (id + payload + retries)
|
|
20
|
+
* so a future schema bump doesn't break a visitor who upgraded
|
|
21
|
+
* mid-session and has events from the old shape sitting around.
|
|
22
|
+
* - Failures are silent. The host app should never see an analytics
|
|
23
|
+
* error bubble up. Console-noise during dev is the only signal.
|
|
24
|
+
* - sendBeacon is NOT used here. sendBeacon is great for "fire on
|
|
25
|
+
* unload" but it doesn't return a response we can branch on for
|
|
26
|
+
* retry. fetch + keepalive gives us a status code AND lets the
|
|
27
|
+
* browser keep the request alive across navigation. Trade-off: a
|
|
28
|
+
* queued event fired right before the visitor closes the tab might
|
|
29
|
+
* not land. That's why session_end stays on sendBeacon (see
|
|
30
|
+
* provider.tsx, sendEnd helper).
|
|
31
|
+
* - The flush timer uses setInterval. Stopped on shutdown(). The
|
|
32
|
+
* provider only ever creates one queue per mount, so timer leaks
|
|
33
|
+
* aren't a practical concern.
|
|
34
|
+
*
|
|
35
|
+
* Caps:
|
|
36
|
+
* - MAX_RETRIES = 3 per-event retry ceiling before drop
|
|
37
|
+
* - MAX_QUEUE_SIZE hard cap so a long network outage can't fill
|
|
38
|
+
* localStorage and break the host app. Events
|
|
39
|
+
* beyond the cap are dropped FIFO (oldest first).
|
|
40
|
+
* - FLUSH_INTERVAL_MS = 5_000 matches Datafast's web SDK default
|
|
41
|
+
* - SIZE_FLUSH_THRESHOLD = 10 triggers a flush when queue grows
|
|
42
|
+
* past this even before the timer fires
|
|
43
|
+
*/
|
|
44
|
+
export interface QueuedEvent {
|
|
45
|
+
/** Stable id used to identify the event across retries. UUID v4
|
|
46
|
+
* generated client-side; collisions are astronomically unlikely so
|
|
47
|
+
* we don't bother with a fallback. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** The exact body the server expects. We persist the full payload
|
|
50
|
+
* (not a delta) so a visitor who reloads mid-flush can still recover
|
|
51
|
+
* every queued event without needing to know the original call site. */
|
|
52
|
+
payload: Record<string, unknown>;
|
|
53
|
+
/** Endpoint to POST to. Keyed per-event so a single queue can hold
|
|
54
|
+
* both /api/v1/web/event (track + pageview) and /api/v1/web/identify
|
|
55
|
+
* events without a separate queue per endpoint. */
|
|
56
|
+
endpoint: string;
|
|
57
|
+
/** Retry counter. Incremented on each failed flush. Event is dropped
|
|
58
|
+
* when this hits MAX_RETRIES. */
|
|
59
|
+
retries: number;
|
|
60
|
+
}
|
|
61
|
+
export declare class EventQueue {
|
|
62
|
+
private queue;
|
|
63
|
+
private flushTimer;
|
|
64
|
+
private isFlushing;
|
|
65
|
+
private shuttingDown;
|
|
66
|
+
constructor();
|
|
67
|
+
/** Add an event. Persists synchronously to localStorage so a tab
|
|
68
|
+
* close right after enqueue doesn't lose the event from memory
|
|
69
|
+
* before it's safely on disk. */
|
|
70
|
+
enqueue(endpoint: string, payload: Record<string, unknown>): void;
|
|
71
|
+
/** Try to deliver every queued event. Each event gets one attempt
|
|
72
|
+
* per flush; failures bump retries and stay in the queue for the
|
|
73
|
+
* next flush. Events that hit MAX_RETRIES get dropped.
|
|
74
|
+
*
|
|
75
|
+
* Concurrency: guarded by isFlushing so overlapping calls (the
|
|
76
|
+
* burst path + the timer firing simultaneously) only run one
|
|
77
|
+
* flush at a time. */
|
|
78
|
+
flush(): Promise<void>;
|
|
79
|
+
/** Best-effort sync flush triggered on page unload. Uses sendBeacon
|
|
80
|
+
* for each event so it survives the tab closing. No retry — this is
|
|
81
|
+
* the last chance.
|
|
82
|
+
*
|
|
83
|
+
* Called from the provider's pagehide / visibilitychange=hidden
|
|
84
|
+
* handlers so any events still queued at unload get one last shot.
|
|
85
|
+
* After this call, the in-memory queue is cleared (events that
|
|
86
|
+
* sendBeacon couldn't queue are lost — that's the cost of the
|
|
87
|
+
* guaranteed-fast unload path). */
|
|
88
|
+
flushOnUnload(): void;
|
|
89
|
+
/** Wipe the queue. Called by reset() and optOut(). Removes the
|
|
90
|
+
* localStorage entry entirely so a reload doesn't re-hydrate the
|
|
91
|
+
* wiped events. */
|
|
92
|
+
clear(): void;
|
|
93
|
+
/** Stop the flush timer and try one final flush. Idempotent. */
|
|
94
|
+
shutdown(): void;
|
|
95
|
+
/** Read-only size getter. Used by tests / debug consoles. */
|
|
96
|
+
get size(): number;
|
|
97
|
+
private persist;
|
|
98
|
+
private startFlushTimer;
|
|
99
|
+
}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventQueue: durable best-effort delivery for analytics events.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* Before this module, every track / identify / pageview was fire-and-forget
|
|
6
|
+
* via navigator.sendBeacon. If the visitor's network blinked, the server
|
|
7
|
+
* was briefly overloaded, or any other transient failure hit, the event
|
|
8
|
+
* was lost with no retry, no log, no observability. On flaky mobile
|
|
9
|
+
* networks (train wifi, subway transitions, dropping 4G) merchants saw
|
|
10
|
+
* ~10-15% fewer events than they should have.
|
|
11
|
+
*
|
|
12
|
+
* This queue mirrors the pattern PostHog / Segment / Amplitude / Datafast
|
|
13
|
+
* all converged on: in-memory queue, localStorage persistence so events
|
|
14
|
+
* survive page reload, periodic auto-flush, retry on failure up to N
|
|
15
|
+
* times, drop after N tries to bound memory.
|
|
16
|
+
*
|
|
17
|
+
* Design notes:
|
|
18
|
+
* - Persisted state is JSON in localStorage under STORAGE_KEY. The
|
|
19
|
+
* persisted shape is intentionally simple (id + payload + retries)
|
|
20
|
+
* so a future schema bump doesn't break a visitor who upgraded
|
|
21
|
+
* mid-session and has events from the old shape sitting around.
|
|
22
|
+
* - Failures are silent. The host app should never see an analytics
|
|
23
|
+
* error bubble up. Console-noise during dev is the only signal.
|
|
24
|
+
* - sendBeacon is NOT used here. sendBeacon is great for "fire on
|
|
25
|
+
* unload" but it doesn't return a response we can branch on for
|
|
26
|
+
* retry. fetch + keepalive gives us a status code AND lets the
|
|
27
|
+
* browser keep the request alive across navigation. Trade-off: a
|
|
28
|
+
* queued event fired right before the visitor closes the tab might
|
|
29
|
+
* not land. That's why session_end stays on sendBeacon (see
|
|
30
|
+
* provider.tsx, sendEnd helper).
|
|
31
|
+
* - The flush timer uses setInterval. Stopped on shutdown(). The
|
|
32
|
+
* provider only ever creates one queue per mount, so timer leaks
|
|
33
|
+
* aren't a practical concern.
|
|
34
|
+
*
|
|
35
|
+
* Caps:
|
|
36
|
+
* - MAX_RETRIES = 3 per-event retry ceiling before drop
|
|
37
|
+
* - MAX_QUEUE_SIZE hard cap so a long network outage can't fill
|
|
38
|
+
* localStorage and break the host app. Events
|
|
39
|
+
* beyond the cap are dropped FIFO (oldest first).
|
|
40
|
+
* - FLUSH_INTERVAL_MS = 5_000 matches Datafast's web SDK default
|
|
41
|
+
* - SIZE_FLUSH_THRESHOLD = 10 triggers a flush when queue grows
|
|
42
|
+
* past this even before the timer fires
|
|
43
|
+
*/
|
|
44
|
+
const STORAGE_KEY = 'affiliateo_event_queue';
|
|
45
|
+
const MAX_RETRIES = 3;
|
|
46
|
+
const MAX_QUEUE_SIZE = 100;
|
|
47
|
+
const FLUSH_INTERVAL_MS = 5000;
|
|
48
|
+
const SIZE_FLUSH_THRESHOLD = 10;
|
|
49
|
+
function lsGet(key) {
|
|
50
|
+
try {
|
|
51
|
+
return localStorage.getItem(key);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function lsSet(key, value) {
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(key, value);
|
|
60
|
+
}
|
|
61
|
+
catch { /* quota / private mode */ }
|
|
62
|
+
}
|
|
63
|
+
function lsRemove(key) {
|
|
64
|
+
try {
|
|
65
|
+
localStorage.removeItem(key);
|
|
66
|
+
}
|
|
67
|
+
catch { /* swallow */ }
|
|
68
|
+
}
|
|
69
|
+
function uuid() {
|
|
70
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID)
|
|
71
|
+
return crypto.randomUUID();
|
|
72
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
73
|
+
const r = (Math.random() * 16) | 0;
|
|
74
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
75
|
+
return v.toString(16);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export class EventQueue {
|
|
79
|
+
constructor() {
|
|
80
|
+
this.queue = [];
|
|
81
|
+
this.flushTimer = null;
|
|
82
|
+
this.isFlushing = false;
|
|
83
|
+
this.shuttingDown = false;
|
|
84
|
+
// Hydrate from localStorage so events from a prior page load survive
|
|
85
|
+
// a refresh / navigation. Guards against corrupt JSON or wrong-shape
|
|
86
|
+
// entries by resetting to empty if anything looks off — better to
|
|
87
|
+
// lose a few stuck events than crash the host app on init.
|
|
88
|
+
try {
|
|
89
|
+
const raw = lsGet(STORAGE_KEY);
|
|
90
|
+
if (raw) {
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (Array.isArray(parsed)) {
|
|
93
|
+
this.queue = parsed.filter((e) => !!e && typeof e === 'object'
|
|
94
|
+
&& typeof e.id === 'string'
|
|
95
|
+
&& typeof e.endpoint === 'string'
|
|
96
|
+
&& typeof e.payload === 'object'
|
|
97
|
+
&& typeof e.retries === 'number');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
this.queue = [];
|
|
103
|
+
}
|
|
104
|
+
this.startFlushTimer();
|
|
105
|
+
// Try an immediate flush on construction so events left over from
|
|
106
|
+
// a previous session get a head start (especially useful on the
|
|
107
|
+
// page-load-after-an-outage scenario).
|
|
108
|
+
if (this.queue.length > 0) {
|
|
109
|
+
void this.flush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Add an event. Persists synchronously to localStorage so a tab
|
|
113
|
+
* close right after enqueue doesn't lose the event from memory
|
|
114
|
+
* before it's safely on disk. */
|
|
115
|
+
enqueue(endpoint, payload) {
|
|
116
|
+
if (this.shuttingDown)
|
|
117
|
+
return;
|
|
118
|
+
this.queue.push({
|
|
119
|
+
id: uuid(),
|
|
120
|
+
endpoint,
|
|
121
|
+
payload,
|
|
122
|
+
retries: 0,
|
|
123
|
+
});
|
|
124
|
+
// FIFO drop when over cap. Network outage edge case: if we're way
|
|
125
|
+
// over budget the oldest events are likely already stale (the
|
|
126
|
+
// visitor moved on, the page they were on isn't current) so
|
|
127
|
+
// dropping them preserves more value than dropping the new ones.
|
|
128
|
+
if (this.queue.length > MAX_QUEUE_SIZE) {
|
|
129
|
+
this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
|
|
130
|
+
}
|
|
131
|
+
this.persist();
|
|
132
|
+
// Burst protection: when many events fire in quick succession
|
|
133
|
+
// (auto-pageview + immediate track + identify on app boot), don't
|
|
134
|
+
// wait for the 5s timer — flush right away. The isFlushing guard
|
|
135
|
+
// inside flush() makes overlapping calls safe.
|
|
136
|
+
if (this.queue.length >= SIZE_FLUSH_THRESHOLD) {
|
|
137
|
+
void this.flush();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Try to deliver every queued event. Each event gets one attempt
|
|
141
|
+
* per flush; failures bump retries and stay in the queue for the
|
|
142
|
+
* next flush. Events that hit MAX_RETRIES get dropped.
|
|
143
|
+
*
|
|
144
|
+
* Concurrency: guarded by isFlushing so overlapping calls (the
|
|
145
|
+
* burst path + the timer firing simultaneously) only run one
|
|
146
|
+
* flush at a time. */
|
|
147
|
+
async flush() {
|
|
148
|
+
if (this.isFlushing || this.queue.length === 0)
|
|
149
|
+
return;
|
|
150
|
+
this.isFlushing = true;
|
|
151
|
+
// Snapshot the queue so we iterate a stable list — new enqueue()
|
|
152
|
+
// calls during the flush land in the live queue and get picked up
|
|
153
|
+
// by the next flush rather than this one (avoids infinite loops
|
|
154
|
+
// and reasoning headaches).
|
|
155
|
+
const batch = this.queue.slice();
|
|
156
|
+
for (const event of batch) {
|
|
157
|
+
let ok = false;
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch(event.endpoint, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify(event.payload),
|
|
163
|
+
// keepalive lets the request survive a navigation. paired
|
|
164
|
+
// with the queue so even if this fetch errors, we'll retry.
|
|
165
|
+
keepalive: true,
|
|
166
|
+
});
|
|
167
|
+
// 2xx = success. Anything else (4xx, 5xx) counts as a failure
|
|
168
|
+
// and bumps retries. A 4xx is usually a permanent error
|
|
169
|
+
// (bad payload, dead campaign) so retrying 3 times then
|
|
170
|
+
// dropping is the right move — keeps the queue clean.
|
|
171
|
+
ok = res.ok;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
ok = false;
|
|
175
|
+
}
|
|
176
|
+
if (ok) {
|
|
177
|
+
// Remove from live queue (not the snapshot) by id
|
|
178
|
+
this.queue = this.queue.filter((e) => e.id !== event.id);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// Find the live entry and bump its retry counter. Use id
|
|
182
|
+
// because the index may have shifted if other events were
|
|
183
|
+
// already removed on this pass.
|
|
184
|
+
const live = this.queue.find((e) => e.id === event.id);
|
|
185
|
+
if (live) {
|
|
186
|
+
live.retries++;
|
|
187
|
+
if (live.retries >= MAX_RETRIES) {
|
|
188
|
+
this.queue = this.queue.filter((e) => e.id !== event.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this.persist();
|
|
194
|
+
this.isFlushing = false;
|
|
195
|
+
}
|
|
196
|
+
/** Best-effort sync flush triggered on page unload. Uses sendBeacon
|
|
197
|
+
* for each event so it survives the tab closing. No retry — this is
|
|
198
|
+
* the last chance.
|
|
199
|
+
*
|
|
200
|
+
* Called from the provider's pagehide / visibilitychange=hidden
|
|
201
|
+
* handlers so any events still queued at unload get one last shot.
|
|
202
|
+
* After this call, the in-memory queue is cleared (events that
|
|
203
|
+
* sendBeacon couldn't queue are lost — that's the cost of the
|
|
204
|
+
* guaranteed-fast unload path). */
|
|
205
|
+
flushOnUnload() {
|
|
206
|
+
if (this.queue.length === 0)
|
|
207
|
+
return;
|
|
208
|
+
if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
|
|
209
|
+
// No sendBeacon (very old browser) — try a sync fetch with
|
|
210
|
+
// keepalive as a fallback. Won't actually finish, but it's our
|
|
211
|
+
// last shot.
|
|
212
|
+
for (const event of this.queue) {
|
|
213
|
+
try {
|
|
214
|
+
fetch(event.endpoint, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
217
|
+
body: JSON.stringify(event.payload),
|
|
218
|
+
keepalive: true,
|
|
219
|
+
}).catch(() => { });
|
|
220
|
+
}
|
|
221
|
+
catch { /* swallow */ }
|
|
222
|
+
}
|
|
223
|
+
this.queue = [];
|
|
224
|
+
this.persist();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
for (const event of this.queue) {
|
|
228
|
+
try {
|
|
229
|
+
navigator.sendBeacon(event.endpoint, new Blob([JSON.stringify(event.payload)], { type: 'application/json' }));
|
|
230
|
+
}
|
|
231
|
+
catch { /* swallow */ }
|
|
232
|
+
}
|
|
233
|
+
this.queue = [];
|
|
234
|
+
this.persist();
|
|
235
|
+
}
|
|
236
|
+
/** Wipe the queue. Called by reset() and optOut(). Removes the
|
|
237
|
+
* localStorage entry entirely so a reload doesn't re-hydrate the
|
|
238
|
+
* wiped events. */
|
|
239
|
+
clear() {
|
|
240
|
+
this.queue = [];
|
|
241
|
+
lsRemove(STORAGE_KEY);
|
|
242
|
+
}
|
|
243
|
+
/** Stop the flush timer and try one final flush. Idempotent. */
|
|
244
|
+
shutdown() {
|
|
245
|
+
this.shuttingDown = true;
|
|
246
|
+
if (this.flushTimer !== null) {
|
|
247
|
+
clearInterval(this.flushTimer);
|
|
248
|
+
this.flushTimer = null;
|
|
249
|
+
}
|
|
250
|
+
void this.flush();
|
|
251
|
+
}
|
|
252
|
+
/** Read-only size getter. Used by tests / debug consoles. */
|
|
253
|
+
get size() {
|
|
254
|
+
return this.queue.length;
|
|
255
|
+
}
|
|
256
|
+
persist() {
|
|
257
|
+
lsSet(STORAGE_KEY, JSON.stringify(this.queue));
|
|
258
|
+
}
|
|
259
|
+
startFlushTimer() {
|
|
260
|
+
if (this.flushTimer !== null)
|
|
261
|
+
return;
|
|
262
|
+
this.flushTimer = setInterval(() => {
|
|
263
|
+
void this.flush();
|
|
264
|
+
}, FLUSH_INTERVAL_MS);
|
|
265
|
+
}
|
|
266
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,30 @@ export interface AffiliateoConfig {
|
|
|
2
2
|
campaignId: string;
|
|
3
3
|
/** Base URL for the API. Defaults to https://affiliateo.com */
|
|
4
4
|
apiUrl?: string;
|
|
5
|
+
/** Other domains that are part of the same merchant property and should
|
|
6
|
+
* share visitor identity + affiliate ref code. When a visitor clicks a
|
|
7
|
+
* link from the current page to any hostname in this list, the SDK
|
|
8
|
+
* rewrites the link URL to append `_affiliateo_vid` and `_affiliateo_ref`
|
|
9
|
+
* search params before the browser navigates. The receiving domain's
|
|
10
|
+
* SDK reads those params on landing, uses them as the visitor identity
|
|
11
|
+
* for that domain, and strips them from the URL bar so the user never
|
|
12
|
+
* sees the tracking junk.
|
|
13
|
+
*
|
|
14
|
+
* Without this, every cross-domain hop creates a brand-new anonymous
|
|
15
|
+
* visitor with no affiliate ref (browsers sandbox localStorage per
|
|
16
|
+
* origin), which silently breaks attribution: `?ref=jane` lands on
|
|
17
|
+
* marketing.com → user clicks → app.com → no ref → affiliate gets $0.
|
|
18
|
+
*
|
|
19
|
+
* Exact-match hostnames only (no wildcards). List every subdomain you
|
|
20
|
+
* want to share identity with explicitly. The current domain doesn't
|
|
21
|
+
* need to be listed.
|
|
22
|
+
*
|
|
23
|
+
* Example:
|
|
24
|
+
* <AffiliateoProvider
|
|
25
|
+
* campaignId="..."
|
|
26
|
+
* allowedHostnames={['app.merchant.com', 'docs.merchant.com']}
|
|
27
|
+
* > */
|
|
28
|
+
allowedHostnames?: string[];
|
|
5
29
|
}
|
|
6
30
|
export interface AffiliateoContextValue {
|
|
7
31
|
/** The affiliate ref code from the URL (if present) */
|
|
@@ -34,4 +58,54 @@ export interface AffiliateoContextValue {
|
|
|
34
58
|
* const { identify } = useAffiliateRef()
|
|
35
59
|
* useEffect(() => { if (user) identify(user.id) }, [user]) */
|
|
36
60
|
identify: (userId: string) => Promise<void>;
|
|
61
|
+
/** Manually fire a pageview. The SDK already auto-fires on mount and on
|
|
62
|
+
* SPA route changes; use this only when you want extra control (e.g.
|
|
63
|
+
* tracking modal opens as virtual pageviews, or merchants who disabled
|
|
64
|
+
* auto-pageviews for custom routing).
|
|
65
|
+
*
|
|
66
|
+
* - With no args: tracks the CURRENT page (same as the auto-fired one).
|
|
67
|
+
* - With a path: tracks that path. Useful for virtual pages.
|
|
68
|
+
*
|
|
69
|
+
* Example:
|
|
70
|
+
* const { trackPageview } = useAffiliateRef()
|
|
71
|
+
* function PricingModal({ open }: { open: boolean }) {
|
|
72
|
+
* useEffect(() => { if (open) trackPageview('/pricing-modal') }, [open])
|
|
73
|
+
* } */
|
|
74
|
+
trackPageview: (path?: string) => Promise<void>;
|
|
75
|
+
/** Wipe the visitor identity. Mints a fresh visitor_id and clears the
|
|
76
|
+
* affiliate ref + campaign_id from localStorage so the next pageview
|
|
77
|
+
* is treated as a brand-new anonymous visitor.
|
|
78
|
+
*
|
|
79
|
+
* Use case: call on user logout so a different person sitting down at
|
|
80
|
+
* the same browser doesn't get their actions merged into the previous
|
|
81
|
+
* user's funnel. Without this, shared family computers / kiosks /
|
|
82
|
+
* hot-desks silently pollute attribution.
|
|
83
|
+
*
|
|
84
|
+
* Example:
|
|
85
|
+
* const { reset } = useAffiliateRef()
|
|
86
|
+
* async function handleLogout() {
|
|
87
|
+
* await supabase.auth.signOut()
|
|
88
|
+
* await reset()
|
|
89
|
+
* router.push('/')
|
|
90
|
+
* } */
|
|
91
|
+
reset: () => Promise<void>;
|
|
92
|
+
/** Stop tracking this visitor. Sets a persistent opt-out flag in
|
|
93
|
+
* localStorage and silences all subsequent track / identify / pageview
|
|
94
|
+
* calls until optIn() is called. Survives reloads, doesn't survive a
|
|
95
|
+
* hard browser reset.
|
|
96
|
+
*
|
|
97
|
+
* Use case: GDPR / CCPA cookie banner where the visitor picked
|
|
98
|
+
* "Don't track me." Call optOut() once and the SDK noops everything
|
|
99
|
+
* for that visitor.
|
|
100
|
+
*
|
|
101
|
+
* Example:
|
|
102
|
+
* const { optOut } = useAffiliateRef()
|
|
103
|
+
* <button onClick={() => optOut()}>Don't track me</button> */
|
|
104
|
+
optOut: () => void;
|
|
105
|
+
/** Re-enable tracking after a previous optOut(). Removes the persistent
|
|
106
|
+
* flag. Note: tracking only resumes for events fired AFTER this call,
|
|
107
|
+
* the current pageview won't retroactively send. A full page reload is
|
|
108
|
+
* the cleanest way to bring the SDK back to its normal "auto-fire on
|
|
109
|
+
* mount" state. */
|
|
110
|
+
optIn: () => void;
|
|
37
111
|
}
|
package/package.json
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@affiliateo/web",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Affiliateo web SDK — affiliate attribution and tracking for web apps",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"module": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"require": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
18
|
-
},
|
|
19
|
-
"peerDependencies": {
|
|
20
|
-
"react": ">=18.0.0"
|
|
21
|
-
},
|
|
22
|
-
"peerDependenciesMeta": {
|
|
23
|
-
"react": {
|
|
24
|
-
"optional": true
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/react": "^19.0.0",
|
|
29
|
-
"typescript": "^5.0.0"
|
|
30
|
-
},
|
|
31
|
-
"keywords": ["affiliateo", "affiliate", "attribution", "stripe", "web", "session", "tracking"],
|
|
32
|
-
"license": "MIT",
|
|
33
|
-
"files": ["dist"]
|
|
34
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@affiliateo/web",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Affiliateo web SDK — affiliate attribution and tracking for web apps",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"react": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": ["affiliateo", "affiliate", "attribution", "stripe", "web", "session", "tracking"],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"files": ["dist"]
|
|
34
|
+
}
|