@affiliateo/web 4.0.0 → 4.2.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/README.md +238 -238
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +183 -11
- 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/README.md
CHANGED
|
@@ -1,238 +1,238 @@
|
|
|
1
|
-
# @affiliateo/web
|
|
2
|
-
|
|
3
|
-
Affiliateo web SDK. track affiliate visits and pass attribution metadata to your Stripe checkout.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @affiliateo/web
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
1. Connect your Stripe account on the Affiliateo dashboard
|
|
14
|
-
2. Wrap your app in `<AffiliateoProvider>` with your campaign ID
|
|
15
|
-
3. At checkout time, call `getAttribution()` and pass the result to Stripe metadata
|
|
16
|
-
|
|
17
|
-
### Step 1. wrap your app
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
// app/layout.tsx (Next.js App Router) or _app.tsx (Pages Router)
|
|
21
|
-
import { AffiliateoProvider } from '@affiliateo/web';
|
|
22
|
-
|
|
23
|
-
export default function RootLayout({ children }) {
|
|
24
|
-
return (
|
|
25
|
-
<AffiliateoProvider campaignId="YOUR_CAMPAIGN_ID">
|
|
26
|
-
{children}
|
|
27
|
-
</AffiliateoProvider>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The provider:
|
|
33
|
-
- Reads `?ref=` from the URL when a visitor arrives
|
|
34
|
-
- Stores attribution in `localStorage` so it survives navigation
|
|
35
|
-
- Sends pageview events to Affiliateo for click tracking
|
|
36
|
-
|
|
37
|
-
### Step 2. in your checkout button (client-side)
|
|
38
|
-
|
|
39
|
-
```tsx
|
|
40
|
-
'use client'
|
|
41
|
-
import { getAttribution } from '@affiliateo/web';
|
|
42
|
-
|
|
43
|
-
async function handleCheckout() {
|
|
44
|
-
const attribution = getAttribution();
|
|
45
|
-
|
|
46
|
-
const res = await fetch('/api/checkout', {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
body: JSON.stringify({ attribution }),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const { url } = await res.json();
|
|
52
|
-
window.location.href = url;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`getAttribution()` reads `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_campaign_id` from `localStorage` and returns them as an object, ready to pass to Stripe.
|
|
57
|
-
|
|
58
|
-
### Step 3. in your checkout API route (server-side)
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
// app/api/checkout/route.ts
|
|
62
|
-
export async function POST(req) {
|
|
63
|
-
const { attribution } = await req.json();
|
|
64
|
-
|
|
65
|
-
const session = await stripe.checkout.sessions.create({
|
|
66
|
-
line_items: [{ price: 'price_xxx', quantity: 1 }],
|
|
67
|
-
mode: 'payment',
|
|
68
|
-
success_url: 'https://yoursite.com/success',
|
|
69
|
-
cancel_url: 'https://yoursite.com/cancel',
|
|
70
|
-
metadata: attribution,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return Response.json({ url: session.url });
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
That's it. Affiliateo's webhook will automatically read the metadata and credit the right affiliate when the payment succeeds.
|
|
78
|
-
|
|
79
|
-
## How it works
|
|
80
|
-
|
|
81
|
-
1. Visitor clicks affiliate link → `yoursite.com?ref=pinkproton`
|
|
82
|
-
2. `<AffiliateoProvider>` reads `?ref=pinkproton` from the URL and saves it in `localStorage`
|
|
83
|
-
3. As the visitor browses, attribution stays in their browser
|
|
84
|
-
4. At checkout, `getAttribution()` reads localStorage and returns the attribution object
|
|
85
|
-
5. Your server passes it as Stripe metadata when creating the checkout session
|
|
86
|
-
6. When Stripe fires the `checkout.session.completed` webhook, Affiliateo reads the metadata and credits the affiliate
|
|
87
|
-
|
|
88
|
-
## Why `localStorage` and not cookies?
|
|
89
|
-
|
|
90
|
-
- **Not blocked by Safari ITP or most privacy tools**
|
|
91
|
-
- **No CSRF concerns**
|
|
92
|
-
|
|
93
|
-
## GDPR / ePrivacy. merchant responsibility
|
|
94
|
-
|
|
95
|
-
The SDK writes persistent identifiers (`affiliateo_visitor_id`, `affiliateo_ref`, `affiliateo_campaign_id`) to the visitor's `localStorage` and sends pageview events that include the visitor's IP, user agent, and approximate geolocation to Affiliateo's servers. Under EU ePrivacy (Article 5(3)) and GDPR, persistent identifiers stored on a visitor's device require prior consent the same way cookies do.
|
|
96
|
-
|
|
97
|
-
If you serve visitors in the EU/EEA, UK, or any jurisdiction with equivalent rules, **you are responsible for obtaining consent before this script loads**, the same way you would for Google Analytics, Meta Pixel, or any other analytics tool. Typical approaches:
|
|
98
|
-
|
|
99
|
-
- Gate the `<AffiliateoProvider>` (or the `<script src="…/t.js">` tag) behind your cookie banner's "accept" callback so it only loads after consent.
|
|
100
|
-
- Document Affiliateo in your privacy policy as a processor (see our [Data Processing Agreement](https://affiliateo.com/dpa)) and list the categories of data collected.
|
|
101
|
-
|
|
102
|
-
Affiliateo acts as your data processor for visitor analytics. You are the controller and decide the lawful basis for processing your visitors' data.
|
|
103
|
-
|
|
104
|
-
## Alternative: vanilla HTML (no React)
|
|
105
|
-
|
|
106
|
-
If you're not using React, use the tracking script directly instead of the provider:
|
|
107
|
-
|
|
108
|
-
```html
|
|
109
|
-
<script defer
|
|
110
|
-
data-campaign="YOUR_CAMPAIGN_ID"
|
|
111
|
-
src="https://affiliateo.com/t.js"
|
|
112
|
-
></script>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Then in your checkout flow (vanilla JS):
|
|
116
|
-
|
|
117
|
-
```js
|
|
118
|
-
const attribution = {
|
|
119
|
-
affiliateo_ref: localStorage.getItem('affiliateo_ref'),
|
|
120
|
-
affiliateo_visitor_id: localStorage.getItem('affiliateo_visitor_id'),
|
|
121
|
-
affiliateo_campaign_id: localStorage.getItem('affiliateo_campaign_id'),
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Send to your server with the rest of your checkout payload
|
|
125
|
-
fetch('/api/checkout', {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
body: JSON.stringify({ attribution }),
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Track conversion goals
|
|
132
|
-
|
|
133
|
-
Pageviews auto-track. For specific moments that matter to your funnel
|
|
134
|
-
(`signup_completed`, `trial_started`, `initiate_checkout`, etc.), use
|
|
135
|
-
`track()` or HTML data attributes.
|
|
136
|
-
|
|
137
|
-
### From React
|
|
138
|
-
|
|
139
|
-
```tsx
|
|
140
|
-
'use client'
|
|
141
|
-
import { useEffect } from 'react'
|
|
142
|
-
import { useAffiliateRef } from '@affiliateo/web'
|
|
143
|
-
|
|
144
|
-
export default function SignupSuccess() {
|
|
145
|
-
const { track } = useAffiliateRef()
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
track('signup_completed', { plan: 'free' })
|
|
148
|
-
}, [])
|
|
149
|
-
return <div>Welcome!</div>
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### HTML data attributes (zero JS, works everywhere)
|
|
154
|
-
|
|
155
|
-
```html
|
|
156
|
-
<button data-affiliateo-goal="initiate_checkout"
|
|
157
|
-
data-affiliateo-goal-price="49"
|
|
158
|
-
data-affiliateo-goal-plan="pro">
|
|
159
|
-
Subscribe
|
|
160
|
-
</button>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
The SDK auto-fires the goal on click. Optional `data-affiliateo-goal-*`
|
|
164
|
-
attributes become metadata (kebab-case → snake_case, up to 10 params).
|
|
165
|
-
|
|
166
|
-
### Scroll-depth tracking
|
|
167
|
-
|
|
168
|
-
```html
|
|
169
|
-
<section data-affiliateo-scroll="pricing_viewed">
|
|
170
|
-
...your pricing content...
|
|
171
|
-
</section>
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Fires the first time any pixel of the section overlaps the viewport.
|
|
175
|
-
Maximum permissiveness — if the user scrolled enough to see even a sliver,
|
|
176
|
-
it counts. Earlier versions required 50% visibility, which was unreachable
|
|
177
|
-
for tall sections (a pricing page with comparison tables silently dropped
|
|
178
|
-
out of every funnel). Great for landing pages where you want to know
|
|
179
|
-
which sections convert.
|
|
180
|
-
|
|
181
|
-
### Goal naming rules
|
|
182
|
-
|
|
183
|
-
Lowercase letters, digits, underscores, hyphens, colons. Max 64 chars.
|
|
184
|
-
Metadata bounded to 4KB JSON.
|
|
185
|
-
|
|
186
|
-
## Identify signed-in users (cross-device stitching)
|
|
187
|
-
|
|
188
|
-
Without `identify()`, the same person browsing on phone and buying on
|
|
189
|
-
laptop counts as two visitors. Call it once after sign-in:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
'use client'
|
|
193
|
-
import { useEffect } from 'react'
|
|
194
|
-
import { useAffiliateRef } from '@affiliateo/web'
|
|
195
|
-
import { useUser } from './your-auth'
|
|
196
|
-
|
|
197
|
-
export default function AuthBoundary({ children }) {
|
|
198
|
-
const { identify } = useAffiliateRef()
|
|
199
|
-
const user = useUser()
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (user) identify(user.id)
|
|
202
|
-
}, [user])
|
|
203
|
-
return <>{children}</>
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Idempotent. safe to fire on every page load while a user is signed in.
|
|
208
|
-
**user_id only.** The SDK does not accept, collect, or transmit email
|
|
209
|
-
or any other PII.
|
|
210
|
-
|
|
211
|
-
## API
|
|
212
|
-
|
|
213
|
-
### `<AffiliateoProvider campaignId="..." apiUrl?="...">`
|
|
214
|
-
|
|
215
|
-
React provider. Wrap your app once. Reads `?ref=` from URL, persists to localStorage, sends pageview events, sets up data-attribute + scroll listeners.
|
|
216
|
-
|
|
217
|
-
Props:
|
|
218
|
-
- `campaignId` (required): your Affiliateo campaign ID
|
|
219
|
-
- `apiUrl` (optional): defaults to `https://affiliateo.com`
|
|
220
|
-
|
|
221
|
-
### `useAffiliateRef()`
|
|
222
|
-
|
|
223
|
-
React hook that returns `{ refCode, visitorId, track, identify }`.
|
|
224
|
-
|
|
225
|
-
- `track(eventName, metadata?)`. fire a custom goal
|
|
226
|
-
- `identify(userId)`. link this visitor to a merchant user_id (no PII)
|
|
227
|
-
|
|
228
|
-
### `getAttribution()`
|
|
229
|
-
|
|
230
|
-
Client-side function. Returns an object with `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_campaign_id` from localStorage. Pass directly as Stripe `metadata`.
|
|
231
|
-
|
|
232
|
-
## Framework support
|
|
233
|
-
|
|
234
|
-
Works with any JavaScript framework or vanilla JS:
|
|
235
|
-
- **Next.js** (App Router & Pages Router)
|
|
236
|
-
- **Remix** / **React Router**
|
|
237
|
-
- **Vite + React**
|
|
238
|
-
- **Vanilla JS/HTML** (use the `<script>` tag instead)
|
|
1
|
+
# @affiliateo/web
|
|
2
|
+
|
|
3
|
+
Affiliateo web SDK. track affiliate visits and pass attribution metadata to your Stripe checkout.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @affiliateo/web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
1. Connect your Stripe account on the Affiliateo dashboard
|
|
14
|
+
2. Wrap your app in `<AffiliateoProvider>` with your campaign ID
|
|
15
|
+
3. At checkout time, call `getAttribution()` and pass the result to Stripe metadata
|
|
16
|
+
|
|
17
|
+
### Step 1. wrap your app
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx (Next.js App Router) or _app.tsx (Pages Router)
|
|
21
|
+
import { AffiliateoProvider } from '@affiliateo/web';
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }) {
|
|
24
|
+
return (
|
|
25
|
+
<AffiliateoProvider campaignId="YOUR_CAMPAIGN_ID">
|
|
26
|
+
{children}
|
|
27
|
+
</AffiliateoProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The provider:
|
|
33
|
+
- Reads `?ref=` from the URL when a visitor arrives
|
|
34
|
+
- Stores attribution in `localStorage` so it survives navigation
|
|
35
|
+
- Sends pageview events to Affiliateo for click tracking
|
|
36
|
+
|
|
37
|
+
### Step 2. in your checkout button (client-side)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
'use client'
|
|
41
|
+
import { getAttribution } from '@affiliateo/web';
|
|
42
|
+
|
|
43
|
+
async function handleCheckout() {
|
|
44
|
+
const attribution = getAttribution();
|
|
45
|
+
|
|
46
|
+
const res = await fetch('/api/checkout', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: JSON.stringify({ attribution }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { url } = await res.json();
|
|
52
|
+
window.location.href = url;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`getAttribution()` reads `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_campaign_id` from `localStorage` and returns them as an object, ready to pass to Stripe.
|
|
57
|
+
|
|
58
|
+
### Step 3. in your checkout API route (server-side)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// app/api/checkout/route.ts
|
|
62
|
+
export async function POST(req) {
|
|
63
|
+
const { attribution } = await req.json();
|
|
64
|
+
|
|
65
|
+
const session = await stripe.checkout.sessions.create({
|
|
66
|
+
line_items: [{ price: 'price_xxx', quantity: 1 }],
|
|
67
|
+
mode: 'payment',
|
|
68
|
+
success_url: 'https://yoursite.com/success',
|
|
69
|
+
cancel_url: 'https://yoursite.com/cancel',
|
|
70
|
+
metadata: attribution,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return Response.json({ url: session.url });
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it. Affiliateo's webhook will automatically read the metadata and credit the right affiliate when the payment succeeds.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
1. Visitor clicks affiliate link → `yoursite.com?ref=pinkproton`
|
|
82
|
+
2. `<AffiliateoProvider>` reads `?ref=pinkproton` from the URL and saves it in `localStorage`
|
|
83
|
+
3. As the visitor browses, attribution stays in their browser
|
|
84
|
+
4. At checkout, `getAttribution()` reads localStorage and returns the attribution object
|
|
85
|
+
5. Your server passes it as Stripe metadata when creating the checkout session
|
|
86
|
+
6. When Stripe fires the `checkout.session.completed` webhook, Affiliateo reads the metadata and credits the affiliate
|
|
87
|
+
|
|
88
|
+
## Why `localStorage` and not cookies?
|
|
89
|
+
|
|
90
|
+
- **Not blocked by Safari ITP or most privacy tools**
|
|
91
|
+
- **No CSRF concerns**
|
|
92
|
+
|
|
93
|
+
## GDPR / ePrivacy. merchant responsibility
|
|
94
|
+
|
|
95
|
+
The SDK writes persistent identifiers (`affiliateo_visitor_id`, `affiliateo_ref`, `affiliateo_campaign_id`) to the visitor's `localStorage` and sends pageview events that include the visitor's IP, user agent, and approximate geolocation to Affiliateo's servers. Under EU ePrivacy (Article 5(3)) and GDPR, persistent identifiers stored on a visitor's device require prior consent the same way cookies do.
|
|
96
|
+
|
|
97
|
+
If you serve visitors in the EU/EEA, UK, or any jurisdiction with equivalent rules, **you are responsible for obtaining consent before this script loads**, the same way you would for Google Analytics, Meta Pixel, or any other analytics tool. Typical approaches:
|
|
98
|
+
|
|
99
|
+
- Gate the `<AffiliateoProvider>` (or the `<script src="…/t.js">` tag) behind your cookie banner's "accept" callback so it only loads after consent.
|
|
100
|
+
- Document Affiliateo in your privacy policy as a processor (see our [Data Processing Agreement](https://affiliateo.com/dpa)) and list the categories of data collected.
|
|
101
|
+
|
|
102
|
+
Affiliateo acts as your data processor for visitor analytics. You are the controller and decide the lawful basis for processing your visitors' data.
|
|
103
|
+
|
|
104
|
+
## Alternative: vanilla HTML (no React)
|
|
105
|
+
|
|
106
|
+
If you're not using React, use the tracking script directly instead of the provider:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<script defer
|
|
110
|
+
data-campaign="YOUR_CAMPAIGN_ID"
|
|
111
|
+
src="https://affiliateo.com/t.js"
|
|
112
|
+
></script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then in your checkout flow (vanilla JS):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
const attribution = {
|
|
119
|
+
affiliateo_ref: localStorage.getItem('affiliateo_ref'),
|
|
120
|
+
affiliateo_visitor_id: localStorage.getItem('affiliateo_visitor_id'),
|
|
121
|
+
affiliateo_campaign_id: localStorage.getItem('affiliateo_campaign_id'),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Send to your server with the rest of your checkout payload
|
|
125
|
+
fetch('/api/checkout', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
body: JSON.stringify({ attribution }),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Track conversion goals
|
|
132
|
+
|
|
133
|
+
Pageviews auto-track. For specific moments that matter to your funnel
|
|
134
|
+
(`signup_completed`, `trial_started`, `initiate_checkout`, etc.), use
|
|
135
|
+
`track()` or HTML data attributes.
|
|
136
|
+
|
|
137
|
+
### From React
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
'use client'
|
|
141
|
+
import { useEffect } from 'react'
|
|
142
|
+
import { useAffiliateRef } from '@affiliateo/web'
|
|
143
|
+
|
|
144
|
+
export default function SignupSuccess() {
|
|
145
|
+
const { track } = useAffiliateRef()
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
track('signup_completed', { plan: 'free' })
|
|
148
|
+
}, [])
|
|
149
|
+
return <div>Welcome!</div>
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### HTML data attributes (zero JS, works everywhere)
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<button data-affiliateo-goal="initiate_checkout"
|
|
157
|
+
data-affiliateo-goal-price="49"
|
|
158
|
+
data-affiliateo-goal-plan="pro">
|
|
159
|
+
Subscribe
|
|
160
|
+
</button>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The SDK auto-fires the goal on click. Optional `data-affiliateo-goal-*`
|
|
164
|
+
attributes become metadata (kebab-case → snake_case, up to 10 params).
|
|
165
|
+
|
|
166
|
+
### Scroll-depth tracking
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<section data-affiliateo-scroll="pricing_viewed">
|
|
170
|
+
...your pricing content...
|
|
171
|
+
</section>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Fires the first time any pixel of the section overlaps the viewport.
|
|
175
|
+
Maximum permissiveness — if the user scrolled enough to see even a sliver,
|
|
176
|
+
it counts. Earlier versions required 50% visibility, which was unreachable
|
|
177
|
+
for tall sections (a pricing page with comparison tables silently dropped
|
|
178
|
+
out of every funnel). Great for landing pages where you want to know
|
|
179
|
+
which sections convert.
|
|
180
|
+
|
|
181
|
+
### Goal naming rules
|
|
182
|
+
|
|
183
|
+
Lowercase letters, digits, underscores, hyphens, colons. Max 64 chars.
|
|
184
|
+
Metadata bounded to 4KB JSON.
|
|
185
|
+
|
|
186
|
+
## Identify signed-in users (cross-device stitching)
|
|
187
|
+
|
|
188
|
+
Without `identify()`, the same person browsing on phone and buying on
|
|
189
|
+
laptop counts as two visitors. Call it once after sign-in:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
'use client'
|
|
193
|
+
import { useEffect } from 'react'
|
|
194
|
+
import { useAffiliateRef } from '@affiliateo/web'
|
|
195
|
+
import { useUser } from './your-auth'
|
|
196
|
+
|
|
197
|
+
export default function AuthBoundary({ children }) {
|
|
198
|
+
const { identify } = useAffiliateRef()
|
|
199
|
+
const user = useUser()
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (user) identify(user.id)
|
|
202
|
+
}, [user])
|
|
203
|
+
return <>{children}</>
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Idempotent. safe to fire on every page load while a user is signed in.
|
|
208
|
+
**user_id only.** The SDK does not accept, collect, or transmit email
|
|
209
|
+
or any other PII.
|
|
210
|
+
|
|
211
|
+
## API
|
|
212
|
+
|
|
213
|
+
### `<AffiliateoProvider campaignId="..." apiUrl?="...">`
|
|
214
|
+
|
|
215
|
+
React provider. Wrap your app once. Reads `?ref=` from URL, persists to localStorage, sends pageview events, sets up data-attribute + scroll listeners.
|
|
216
|
+
|
|
217
|
+
Props:
|
|
218
|
+
- `campaignId` (required): your Affiliateo campaign ID
|
|
219
|
+
- `apiUrl` (optional): defaults to `https://affiliateo.com`
|
|
220
|
+
|
|
221
|
+
### `useAffiliateRef()`
|
|
222
|
+
|
|
223
|
+
React hook that returns `{ refCode, visitorId, track, identify }`.
|
|
224
|
+
|
|
225
|
+
- `track(eventName, metadata?)`. fire a custom goal
|
|
226
|
+
- `identify(userId)`. link this visitor to a merchant user_id (no PII)
|
|
227
|
+
|
|
228
|
+
### `getAttribution()`
|
|
229
|
+
|
|
230
|
+
Client-side function. Returns an object with `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_campaign_id` from localStorage. Pass directly as Stripe `metadata`.
|
|
231
|
+
|
|
232
|
+
## Framework support
|
|
233
|
+
|
|
234
|
+
Works with any JavaScript framework or vanilla JS:
|
|
235
|
+
- **Next.js** (App Router & Pages Router)
|
|
236
|
+
- **Remix** / **React Router**
|
|
237
|
+
- **Vite + React**
|
|
238
|
+
- **Vanilla JS/HTML** (use the `<script>` tag instead)
|
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
|
@@ -13,6 +13,23 @@ const LS_CAMPAIGN_ID = 'affiliateo_campaign_id';
|
|
|
13
13
|
// page reloads. removed by optIn(). Checked at init time so a previously
|
|
14
14
|
// opted-out visitor never re-fires auto-pageviews after a refresh.
|
|
15
15
|
const LS_OPT_OUT = 'affiliateo_opt_out';
|
|
16
|
+
// Client-owned session id (Datafast-style). The BROWSER owns the session,
|
|
17
|
+
// not the server. One id is reused for every event within a sliding
|
|
18
|
+
// 10-minute activity window, then a fresh one is minted. Because the id is
|
|
19
|
+
// generated here and sent on each event, a burst of pageviews (a redirect
|
|
20
|
+
// bounce, multiple tabs) all carry the SAME id, so the dashboard collapses
|
|
21
|
+
// them into a single visit. Server-side minting was racy: simultaneous
|
|
22
|
+
// events each invented a different id. 10 min matches the server session
|
|
23
|
+
// window and the live-presence window so everything ages out on one clock.
|
|
24
|
+
const LS_SESSION = 'affiliateo_session';
|
|
25
|
+
const SESSION_WINDOW_MS = 10 * 60 * 1000;
|
|
26
|
+
// Same-URL pageview throttle, persisted in sessionStorage so it survives
|
|
27
|
+
// hard navigations / redirect bounces (the old in-memory throttle reset on
|
|
28
|
+
// every fresh page context, which let a 3-hop bounce fire 3 pageviews).
|
|
29
|
+
// Different URLs are never throttled against each other. Mirrors Datafast's
|
|
30
|
+
// 60s same-URL suppression.
|
|
31
|
+
const SS_PV_THROTTLE = 'affiliateo_pv_throttle';
|
|
32
|
+
const PV_THROTTLE_MS = 60 * 1000;
|
|
16
33
|
// localStorage helpers
|
|
17
34
|
function lsGet(key) {
|
|
18
35
|
try {
|
|
@@ -34,6 +51,24 @@ function lsRemove(key) {
|
|
|
34
51
|
}
|
|
35
52
|
catch { }
|
|
36
53
|
}
|
|
54
|
+
// sessionStorage helpers. Used by the pageview throttle so its state lives
|
|
55
|
+
// for the life of the tab (and survives hard navigations within it) but
|
|
56
|
+
// resets when the tab closes, which is exactly the scope we want for
|
|
57
|
+
// "don't re-fire the same URL within a minute."
|
|
58
|
+
function ssGet(key) {
|
|
59
|
+
try {
|
|
60
|
+
return sessionStorage.getItem(key);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function ssSet(key, value) {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(key, value);
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
37
72
|
// True when the visitor has opted out via optOut(). Single source of truth
|
|
38
73
|
// read by every outbound call so the flag works even across tabs (each tab's
|
|
39
74
|
// next call re-reads localStorage).
|
|
@@ -50,6 +85,34 @@ function uuid() {
|
|
|
50
85
|
return v.toString(16);
|
|
51
86
|
});
|
|
52
87
|
}
|
|
88
|
+
// Get-or-create the current session id with a sliding 10-minute window.
|
|
89
|
+
// Stored in localStorage so every tab and every page load in the same
|
|
90
|
+
// browser shares one session. Each call slides the window forward (the same
|
|
91
|
+
// way Datafast refreshes its session cookie on each event), so an actively
|
|
92
|
+
// browsing visitor keeps one id and a returning visitor past the window
|
|
93
|
+
// gets a fresh one. Returns a UUID string the server stamps onto the event.
|
|
94
|
+
function getSessionId() {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
try {
|
|
97
|
+
const raw = lsGet(LS_SESSION);
|
|
98
|
+
if (raw) {
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (parsed &&
|
|
101
|
+
typeof parsed.id === 'string' &&
|
|
102
|
+
typeof parsed.lastActive === 'number' &&
|
|
103
|
+
now - parsed.lastActive < SESSION_WINDOW_MS) {
|
|
104
|
+
lsSet(LS_SESSION, JSON.stringify({ id: parsed.id, lastActive: now }));
|
|
105
|
+
return parsed.id;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Corrupt entry or storage blocked. Fall through and mint a fresh one.
|
|
111
|
+
}
|
|
112
|
+
const id = uuid();
|
|
113
|
+
lsSet(LS_SESSION, JSON.stringify({ id, lastActive: now }));
|
|
114
|
+
return id;
|
|
115
|
+
}
|
|
53
116
|
const asyncNoop = async () => { };
|
|
54
117
|
const syncNoop = () => { };
|
|
55
118
|
const AffiliateoContext = createContext({
|
|
@@ -102,7 +165,26 @@ export function getAttribution() {
|
|
|
102
165
|
// mount (host has the call sites) but we can shout in dev so the
|
|
103
166
|
// mistake is visible.
|
|
104
167
|
let providerMountCount = 0;
|
|
105
|
-
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, debug, children, }) {
|
|
168
|
+
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }) {
|
|
169
|
+
// Normalize autoCapturePageviews. Three shapes:
|
|
170
|
+
// - undefined / true / object → auto-capture ON (with object overrides)
|
|
171
|
+
// - false → auto-capture OFF entirely (host must call trackPageview)
|
|
172
|
+
// Defaults match Datafast: captureInitialPageview true, trackHashChanges
|
|
173
|
+
// false, debounceMs 100.
|
|
174
|
+
const autoCfg = autoCapturePageviews === false
|
|
175
|
+
? { enabled: false, captureInitialPageview: false, trackHashChanges: false, debounceMs: 100 }
|
|
176
|
+
: {
|
|
177
|
+
enabled: true,
|
|
178
|
+
captureInitialPageview: typeof autoCapturePageviews === 'object'
|
|
179
|
+
? autoCapturePageviews?.captureInitialPageview ?? true
|
|
180
|
+
: true,
|
|
181
|
+
trackHashChanges: typeof autoCapturePageviews === 'object'
|
|
182
|
+
? autoCapturePageviews?.trackHashChanges ?? false
|
|
183
|
+
: false,
|
|
184
|
+
debounceMs: typeof autoCapturePageviews === 'object'
|
|
185
|
+
? autoCapturePageviews?.debounceMs ?? 100
|
|
186
|
+
: 100,
|
|
187
|
+
};
|
|
106
188
|
const [state, setState] = useState({
|
|
107
189
|
refCode: null,
|
|
108
190
|
visitorId: null,
|
|
@@ -172,6 +254,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
172
254
|
type: 'custom',
|
|
173
255
|
campaign_id: cid,
|
|
174
256
|
visitor_id: vid,
|
|
257
|
+
session_id: getSessionId(),
|
|
175
258
|
event_name: name,
|
|
176
259
|
};
|
|
177
260
|
if (metadata && typeof metadata === 'object')
|
|
@@ -247,6 +330,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
247
330
|
type: 'pageview',
|
|
248
331
|
campaign_id: cid,
|
|
249
332
|
visitor_id: vid,
|
|
333
|
+
session_id: getSessionId(),
|
|
250
334
|
url,
|
|
251
335
|
path: pagePath,
|
|
252
336
|
hostname: window.location.hostname,
|
|
@@ -301,6 +385,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
301
385
|
const newVid = uuid();
|
|
302
386
|
lsSet(LS_VISITOR_ID, newVid);
|
|
303
387
|
lsRemove(LS_REF);
|
|
388
|
+
lsRemove(LS_SESSION);
|
|
304
389
|
visitorIdRef.current = newVid;
|
|
305
390
|
setState((prev) => ({ ...prev, visitorId: newVid, refCode: null }));
|
|
306
391
|
}
|
|
@@ -391,6 +476,24 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
391
476
|
});
|
|
392
477
|
return;
|
|
393
478
|
}
|
|
479
|
+
// File-protocol guard. Pages served via file:// (someone double-clicking
|
|
480
|
+
// a built HTML file rather than running a server) almost certainly aren't
|
|
481
|
+
// real merchant traffic. Block by default; opt in via allowFileProtocol.
|
|
482
|
+
if (!allowFileProtocol && window.location.protocol === 'file:') {
|
|
483
|
+
log('blocked: file:// protocol (set allowFileProtocol={true} to enable)');
|
|
484
|
+
setState({
|
|
485
|
+
refCode: null,
|
|
486
|
+
visitorId: null,
|
|
487
|
+
track: asyncNoop,
|
|
488
|
+
identify: asyncNoop,
|
|
489
|
+
trackPageview: asyncNoop,
|
|
490
|
+
reset: asyncNoop,
|
|
491
|
+
optOut: syncNoop,
|
|
492
|
+
optIn: syncNoop,
|
|
493
|
+
flush: asyncNoop,
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
394
497
|
// Iframe guard. Default behavior is "if we're inside an iframe, don't
|
|
395
498
|
// track" so scrapers, preview tools, and archive crawlers embedding
|
|
396
499
|
// the merchant's site don't count as pageviews. Cross-origin iframe
|
|
@@ -433,7 +536,10 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
433
536
|
// auto-fired pageview below; only session_end stays on sendBeacon
|
|
434
537
|
// because tab-close urgency beats queue-retry reliability.
|
|
435
538
|
if (queueRef.current === null) {
|
|
436
|
-
queueRef.current = new EventQueue(
|
|
539
|
+
queueRef.current = new EventQueue({
|
|
540
|
+
flushIntervalMs: flushInterval,
|
|
541
|
+
maxQueueSize: maxQueueSize,
|
|
542
|
+
});
|
|
437
543
|
}
|
|
438
544
|
const queue = queueRef.current;
|
|
439
545
|
// Opt-out short-circuit. A visitor who previously called optOut()
|
|
@@ -517,19 +623,42 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
517
623
|
track, identify, trackPageview, reset, optOut, optIn, flush,
|
|
518
624
|
});
|
|
519
625
|
log('init', { campaign: campaignIdRef.current, visitor: vid, ref: ref || null });
|
|
520
|
-
//
|
|
626
|
+
// lastSendTime drives the visibility-refire heuristic below (refire a
|
|
627
|
+
// pageview when a tab returns after the session window). Duplicate
|
|
628
|
+
// suppression itself is the persisted same-URL throttle inside
|
|
629
|
+
// sendPageview, which survives hard navigations.
|
|
521
630
|
let lastSendTime = 0;
|
|
522
631
|
function sendPageview() {
|
|
523
632
|
const now = Date.now();
|
|
524
|
-
|
|
525
|
-
|
|
633
|
+
const href = window.location.href;
|
|
634
|
+
// Same-URL throttle, persisted in sessionStorage so a redirect bounce
|
|
635
|
+
// or hard navigation that lands back on the same URL within the window
|
|
636
|
+
// does not re-fire. Different URLs are never throttled against each
|
|
637
|
+
// other, so a genuine multi-page visit still records every page.
|
|
638
|
+
const prevRaw = ssGet(SS_PV_THROTTLE);
|
|
639
|
+
if (prevRaw) {
|
|
640
|
+
try {
|
|
641
|
+
const prev = JSON.parse(prevRaw);
|
|
642
|
+
if (prev &&
|
|
643
|
+
prev.url === href &&
|
|
644
|
+
typeof prev.time === 'number' &&
|
|
645
|
+
now - prev.time < PV_THROTTLE_MS) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Corrupt entry. Fall through and fire.
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
ssSet(SS_PV_THROTTLE, JSON.stringify({ url: href, time: now }));
|
|
526
654
|
lastSendTime = now;
|
|
527
655
|
ended = false;
|
|
528
656
|
queue.enqueue(endpoint, {
|
|
529
657
|
type: 'pageview',
|
|
530
658
|
campaign_id: campaignIdRef.current,
|
|
531
659
|
visitor_id: vid,
|
|
532
|
-
|
|
660
|
+
session_id: getSessionId(),
|
|
661
|
+
url: href,
|
|
533
662
|
path: window.location.pathname,
|
|
534
663
|
hostname: window.location.hostname,
|
|
535
664
|
referrer: document.referrer || '',
|
|
@@ -607,23 +736,60 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
607
736
|
}
|
|
608
737
|
}
|
|
609
738
|
}
|
|
610
|
-
// Send pageview on mount
|
|
611
|
-
|
|
739
|
+
// Send pageview on mount. captureInitialPageview === false skips this
|
|
740
|
+
// for cookie-consent flows where the host wants to defer until the
|
|
741
|
+
// user accepts (then call trackPageview() manually).
|
|
742
|
+
if (!sentRef.current && autoCfg.captureInitialPageview && autoCfg.enabled) {
|
|
612
743
|
sentRef.current = true;
|
|
613
744
|
sendPageview();
|
|
614
745
|
}
|
|
615
746
|
// Detect user leaving
|
|
616
747
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
617
748
|
window.addEventListener('pagehide', sendEnd);
|
|
618
|
-
// SPA support: detect URL changes
|
|
749
|
+
// SPA support: detect URL changes. We track BOTH pathname (modern
|
|
750
|
+
// pushState-based routers: React Router v6, Next.js App Router, Vue
|
|
751
|
+
// Router 4) AND hash (legacy hash routing: Vue Router with hash mode,
|
|
752
|
+
// React Router HashRouter, old Angular). Hash tracking is opt-in via
|
|
753
|
+
// autoCapturePageviews.trackHashChanges because modern SPAs never use
|
|
754
|
+
// it and firing on every #anchor click would inflate pageview counts.
|
|
755
|
+
//
|
|
756
|
+
// Debouncing collapses back-to-back navigation events into one. Some
|
|
757
|
+
// frameworks (Next.js, certain Vue routers) fire pushState then
|
|
758
|
+
// replaceState in the same tick to canonicalize the URL; without
|
|
759
|
+
// debouncing that's two pageviews per real navigation. Default 100ms
|
|
760
|
+
// matches Datafast; set debounceMs to 0 to disable.
|
|
619
761
|
let lastPath = window.location.pathname;
|
|
762
|
+
let lastHash = window.location.hash;
|
|
763
|
+
let pendingPageviewTimer = null;
|
|
620
764
|
function onUrlChange() {
|
|
621
|
-
|
|
622
|
-
|
|
765
|
+
const pathChanged = window.location.pathname !== lastPath;
|
|
766
|
+
const hashChanged = autoCfg.trackHashChanges && window.location.hash !== lastHash;
|
|
767
|
+
if (!pathChanged && !hashChanged)
|
|
768
|
+
return;
|
|
769
|
+
lastPath = window.location.pathname;
|
|
770
|
+
lastHash = window.location.hash;
|
|
771
|
+
if (!autoCfg.enabled)
|
|
772
|
+
return;
|
|
773
|
+
// Debounce: if a pending pageview is already scheduled, the later
|
|
774
|
+
// urlchange wins (the URL the browser ends up on after both calls
|
|
775
|
+
// is what we want to report).
|
|
776
|
+
if (pendingPageviewTimer !== null) {
|
|
777
|
+
clearTimeout(pendingPageviewTimer);
|
|
778
|
+
}
|
|
779
|
+
if (autoCfg.debounceMs > 0) {
|
|
780
|
+
pendingPageviewTimer = setTimeout(() => {
|
|
781
|
+
pendingPageviewTimer = null;
|
|
782
|
+
sendPageview();
|
|
783
|
+
}, autoCfg.debounceMs);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
623
786
|
sendPageview();
|
|
624
787
|
}
|
|
625
788
|
}
|
|
626
789
|
window.addEventListener('popstate', onUrlChange);
|
|
790
|
+
if (autoCfg.trackHashChanges) {
|
|
791
|
+
window.addEventListener('hashchange', onUrlChange);
|
|
792
|
+
}
|
|
627
793
|
const origPushState = history.pushState;
|
|
628
794
|
const origReplaceState = history.replaceState;
|
|
629
795
|
history.pushState = function (...args) {
|
|
@@ -711,6 +877,12 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
711
877
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
712
878
|
window.removeEventListener('pagehide', sendEnd);
|
|
713
879
|
window.removeEventListener('popstate', onUrlChange);
|
|
880
|
+
if (autoCfg.trackHashChanges) {
|
|
881
|
+
window.removeEventListener('hashchange', onUrlChange);
|
|
882
|
+
}
|
|
883
|
+
if (pendingPageviewTimer !== null) {
|
|
884
|
+
clearTimeout(pendingPageviewTimer);
|
|
885
|
+
}
|
|
714
886
|
history.pushState = origPushState;
|
|
715
887
|
history.replaceState = origReplaceState;
|
|
716
888
|
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) */
|