@affiliateo/web 4.3.0 → 4.4.1
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/bot-detect.d.ts +3 -3
- package/dist/bot-detect.js +3 -3
- package/dist/provider.js +11 -11
- package/dist/queue.d.ts +2 -2
- package/dist/queue.js +2 -2
- 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/bot-detect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client-side bot detection. Mirrors
|
|
2
|
+
* Client-side bot detection. Mirrors PostHog / Plausible's
|
|
3
3
|
* approach: catch the obvious automation tools at SDK init so they
|
|
4
4
|
* never fire a single network request.
|
|
5
5
|
*
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* layer 3 (kicks in on the request), then layer 2 (server checks before
|
|
21
21
|
* writing). If any one layer rejects, the event drops.
|
|
22
22
|
*
|
|
23
|
-
* Patterns sourced from
|
|
24
|
-
* inspectable). We include
|
|
23
|
+
* Patterns sourced from widely-used analytics SDKs (publicly
|
|
24
|
+
* inspectable). We include a comprehensive list because it represents years
|
|
25
25
|
* of accumulated bot-shape knowledge that's expensive to reproduce.
|
|
26
26
|
*/
|
|
27
27
|
/**
|
package/dist/bot-detect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client-side bot detection. Mirrors
|
|
2
|
+
* Client-side bot detection. Mirrors PostHog / Plausible's
|
|
3
3
|
* approach: catch the obvious automation tools at SDK init so they
|
|
4
4
|
* never fire a single network request.
|
|
5
5
|
*
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* layer 3 (kicks in on the request), then layer 2 (server checks before
|
|
21
21
|
* writing). If any one layer rejects, the event drops.
|
|
22
22
|
*
|
|
23
|
-
* Patterns sourced from
|
|
24
|
-
* inspectable). We include
|
|
23
|
+
* Patterns sourced from widely-used analytics SDKs (publicly
|
|
24
|
+
* inspectable). We include a comprehensive list because it represents years
|
|
25
25
|
* of accumulated bot-shape knowledge that's expensive to reproduce.
|
|
26
26
|
*/
|
|
27
27
|
// Selenium/WebDriver/headless flags set on window or navigator. Modern
|
package/dist/provider.js
CHANGED
|
@@ -8,12 +8,12 @@ const DEFAULT_API_URL = 'https://affiliateo.com';
|
|
|
8
8
|
const LS_VISITOR_ID = 'affiliateo_visitor_id';
|
|
9
9
|
const LS_REF = 'affiliateo_ref';
|
|
10
10
|
const LS_CAMPAIGN_ID = 'affiliateo_campaign_id';
|
|
11
|
-
// Persistent opt-out flag
|
|
11
|
+
// Persistent opt-out flag (affiliateo_opt_out) —
|
|
12
12
|
// when set to "true" the SDK silently noops every outbound call. Survives
|
|
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
|
|
16
|
+
// Client-owned session id. The BROWSER owns the session,
|
|
17
17
|
// not the server. One id is reused for every event within a sliding
|
|
18
18
|
// 10-minute activity window, then a fresh one is minted. Because the id is
|
|
19
19
|
// generated here and sent on each event, a burst of pageviews (a redirect
|
|
@@ -26,7 +26,7 @@ const SESSION_WINDOW_MS = 10 * 60 * 1000;
|
|
|
26
26
|
// Same-URL pageview throttle, persisted in sessionStorage so it survives
|
|
27
27
|
// hard navigations / redirect bounces (the old in-memory throttle reset on
|
|
28
28
|
// every fresh page context, which let a 3-hop bounce fire 3 pageviews).
|
|
29
|
-
// Different URLs are never throttled against each other
|
|
29
|
+
// Different URLs are never throttled against each other, with a
|
|
30
30
|
// 60s same-URL suppression.
|
|
31
31
|
const SS_PV_THROTTLE = 'affiliateo_pv_throttle';
|
|
32
32
|
const PV_THROTTLE_MS = 60 * 1000;
|
|
@@ -88,7 +88,7 @@ function uuid() {
|
|
|
88
88
|
// Get-or-create the current session id with a sliding 10-minute window.
|
|
89
89
|
// Stored in localStorage so every tab and every page load in the same
|
|
90
90
|
// browser shares one session. Each call slides the window forward (the same
|
|
91
|
-
// way
|
|
91
|
+
// way a session cookie is refreshed on each event), so an actively
|
|
92
92
|
// browsing visitor keeps one id and a returning visitor past the window
|
|
93
93
|
// gets a fresh one. Returns a UUID string the server stamps onto the event.
|
|
94
94
|
function getSessionId() {
|
|
@@ -169,7 +169,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
169
169
|
// Normalize autoCapturePageviews. Three shapes:
|
|
170
170
|
// - undefined / true / object → auto-capture ON (with object overrides)
|
|
171
171
|
// - false → auto-capture OFF entirely (host must call trackPageview)
|
|
172
|
-
//
|
|
172
|
+
// Sensible defaults: captureInitialPageview true, trackHashChanges
|
|
173
173
|
// false, debounceMs 100.
|
|
174
174
|
const autoCfg = autoCapturePageviews === false
|
|
175
175
|
? { enabled: false, captureInitialPageview: false, trackHashChanges: false, debounceMs: 100 }
|
|
@@ -299,7 +299,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
299
299
|
}
|
|
300
300
|
catch { /* swallow */ }
|
|
301
301
|
}, [apiBase, log]);
|
|
302
|
-
// Manual pageview
|
|
302
|
+
// Manual pageview via trackPageview(path?). Same payload
|
|
303
303
|
// shape the auto-fired pageview uses (see sendPageview inside the route
|
|
304
304
|
// effect below) so server-side routing / dedup / IP+UA enrichment behave
|
|
305
305
|
// identically for both code paths.
|
|
@@ -370,13 +370,13 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
370
370
|
// affiliate ref so the next pageview is a brand-new anonymous visitor.
|
|
371
371
|
// The campaign_id stays (it's a property of the site, not the visitor).
|
|
372
372
|
//
|
|
373
|
-
//
|
|
373
|
+
// reset() semantics for non-cookieless mode: new
|
|
374
374
|
// visitor, no event fired, caller handles "tell the server about it"
|
|
375
375
|
// by triggering whatever pageview / navigation comes next.
|
|
376
376
|
//
|
|
377
377
|
// Returns a Promise so callers can `await reset()` before navigating
|
|
378
378
|
// (the localStorage writes are sync but the Promise lets the API stay
|
|
379
|
-
//
|
|
379
|
+
// an async signature, future-proofing for
|
|
380
380
|
// server-minted IDs in cookieless mode).
|
|
381
381
|
const reset = useCallback(async () => {
|
|
382
382
|
try {
|
|
@@ -766,8 +766,8 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
766
766
|
// Debouncing collapses back-to-back navigation events into one. Some
|
|
767
767
|
// frameworks (Next.js, certain Vue routers) fire pushState then
|
|
768
768
|
// replaceState in the same tick to canonicalize the URL; without
|
|
769
|
-
// debouncing that's two pageviews per real navigation. Default 100ms
|
|
770
|
-
//
|
|
769
|
+
// debouncing that's two pageviews per real navigation. Default 100ms;
|
|
770
|
+
// set debounceMs to 0 to disable.
|
|
771
771
|
let lastPath = window.location.pathname;
|
|
772
772
|
let lastHash = window.location.hash;
|
|
773
773
|
let pendingPageviewTimer = null;
|
|
@@ -812,7 +812,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
812
812
|
};
|
|
813
813
|
// HTML-attribute goal scanning (`data-affiliateo-goal`,
|
|
814
814
|
// `data-affiliateo-scroll`) and the IntersectionObserver that drove
|
|
815
|
-
// them were removed in 3.0.0 to
|
|
815
|
+
// them were removed in 3.0.0 to streamline the npm SDK ergonomics:
|
|
816
816
|
// SDK does pageview + track() + identify(); merchants fire goals
|
|
817
817
|
// themselves via onClick={() => track('signup_clicked')} or by
|
|
818
818
|
// wiring their own IntersectionObserver and calling track() from it.
|
package/dist/queue.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* networks (train wifi, subway transitions, dropping 4G) merchants saw
|
|
10
10
|
* ~10-15% fewer events than they should have.
|
|
11
11
|
*
|
|
12
|
-
* This queue mirrors the pattern PostHog / Segment / Amplitude
|
|
12
|
+
* This queue mirrors the pattern PostHog / Segment / Amplitude
|
|
13
13
|
* all converged on: in-memory queue, localStorage persistence so events
|
|
14
14
|
* survive page reload, periodic auto-flush, retry on failure up to N
|
|
15
15
|
* times, drop after N tries to bound memory.
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* - MAX_QUEUE_SIZE hard cap so a long network outage can't fill
|
|
38
38
|
* localStorage and break the host app. Events
|
|
39
39
|
* beyond the cap are dropped FIFO (oldest first).
|
|
40
|
-
* - FLUSH_INTERVAL_MS = 5_000
|
|
40
|
+
* - FLUSH_INTERVAL_MS = 5_000 a sensible web SDK default
|
|
41
41
|
* - SIZE_FLUSH_THRESHOLD = 10 triggers a flush when queue grows
|
|
42
42
|
* past this even before the timer fires
|
|
43
43
|
*/
|
package/dist/queue.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* networks (train wifi, subway transitions, dropping 4G) merchants saw
|
|
10
10
|
* ~10-15% fewer events than they should have.
|
|
11
11
|
*
|
|
12
|
-
* This queue mirrors the pattern PostHog / Segment / Amplitude
|
|
12
|
+
* This queue mirrors the pattern PostHog / Segment / Amplitude
|
|
13
13
|
* all converged on: in-memory queue, localStorage persistence so events
|
|
14
14
|
* survive page reload, periodic auto-flush, retry on failure up to N
|
|
15
15
|
* times, drop after N tries to bound memory.
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* - MAX_QUEUE_SIZE hard cap so a long network outage can't fill
|
|
38
38
|
* localStorage and break the host app. Events
|
|
39
39
|
* beyond the cap are dropped FIFO (oldest first).
|
|
40
|
-
* - FLUSH_INTERVAL_MS = 5_000
|
|
40
|
+
* - FLUSH_INTERVAL_MS = 5_000 a sensible web SDK default
|
|
41
41
|
* - SIZE_FLUSH_THRESHOLD = 10 triggers a flush when queue grows
|
|
42
42
|
* past this even before the timer fires
|
|
43
43
|
*/
|