@indiebob/tracker 0.3.0 → 0.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/CHANGELOG.md +32 -0
- package/README.md +112 -508
- package/dist/index.cjs +187 -94
- package/dist/index.d.mts +36 -7
- package/dist/index.d.ts +36 -7
- package/dist/index.global.js +2 -2
- package/dist/index.js +187 -94
- package/dist/server.d.mts +17 -2
- package/dist/server.d.ts +17 -2
- package/dist/server.js +15 -2
- package/dist/server.mjs +15 -2
- package/dist/vue.cjs +187 -94
- package/dist/vue.global.js +2 -2
- package/dist/vue.js +187 -94
- package/package.json +2 -2
- package/dist/chunk-4IK5PET7.js +0 -2
- package/dist/chunk-DZOEO647.js +0 -2
- package/dist/chunk-XKQYO25W.js +0 -2
package/README.md
CHANGED
|
@@ -1,574 +1,178 @@
|
|
|
1
1
|
# @indiebob/tracker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The analytics script for [IndieBob](https://www.indiebob.com): page views, traffic sources, events,
|
|
4
|
+
signups and revenue. About 5.7 KB gzipped, no dependencies, no cookies. By default it stores nothing
|
|
5
|
+
in the visitor's browser.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# or
|
|
10
|
-
pnpm add @indiebob/tracker
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Or via script tag (CDN) — one line, nothing else to write:
|
|
9
|
+
**Script tag** — one line before `</head>`. It counts every page, including navigation in a
|
|
10
|
+
single-page app.
|
|
14
11
|
|
|
15
12
|
```html
|
|
16
|
-
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js" data-project="
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
The tag initialises itself from `data-project` and tracks this page plus every
|
|
20
|
-
SPA navigation. See [Script tag](#vanilla-js-script-tag) for the options.
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
Three lines to start tracking:
|
|
25
|
-
|
|
26
|
-
```js
|
|
27
|
-
import { IndieBob } from '@indiebob/tracker'
|
|
28
|
-
|
|
29
|
-
const bob = IndieBob.init({ projectId: 'proj_your16charId' })
|
|
30
|
-
bob.page()
|
|
13
|
+
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js" data-project="proj_…" defer></script>
|
|
31
14
|
```
|
|
32
15
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
endpoint: 'https://indiebob.com/api/collect', // Optional. Default shown.
|
|
41
|
-
debug: false, // Optional. Logs events to console when true.
|
|
42
|
-
respectDnt: true, // Optional. Honor browser Do Not Track. Default: true.
|
|
43
|
-
respectGpc: true, // Optional. Honor Global Privacy Control. Default: true.
|
|
44
|
-
autoDetect: true, // Optional. Auto-detect signups, logins, checkouts, etc. Default: true.
|
|
45
|
-
hashMode: false, // Optional. Treat the URL hash as part of the path (/#/route routers). Default: false.
|
|
46
|
-
})
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
| Option | Type | Default | Description |
|
|
50
|
-
|---|---|---|---|
|
|
51
|
-
| `projectId` | `string` | **required** | Your SDK project ID from the dashboard |
|
|
52
|
-
| `endpoint` | `string` | `'https://indiebob.com/api/collect'` | Override for self-hosted instances |
|
|
53
|
-
| `debug` | `boolean` | `false` | Log all events to browser console |
|
|
54
|
-
| `respectDnt` | `boolean` | `true` | Honor the browser's Do Not Track header |
|
|
55
|
-
| `respectGpc` | `boolean` | `true` | Honor Global Privacy Control signal (legally binding in CA/GDPR) |
|
|
56
|
-
| `autoDetect` | `boolean \| object` | `true` | Auto-detect user intent from URL patterns, form submissions, outbound clicks, file downloads, and data attributes. See [Auto-Detection](#auto-detection). |
|
|
57
|
-
| `hashMode` | `boolean` | `false` | Count a hash-only change as a new page and report the hash in the path. For hash routers (`/#/settings`). |
|
|
58
|
-
|
|
59
|
-
## API Reference
|
|
60
|
-
|
|
61
|
-
### `IndieBob.init(config)`
|
|
16
|
+
| Attribute | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `data-project` | Your project id (IndieBob › your project › Connect). Required. |
|
|
19
|
+
| `data-persistence="local"` | Full attribution (see below). Only with the visitor's consent. |
|
|
20
|
+
| `data-hash-mode="true"` | Your routes live after a `#` (`/#/pricing`). |
|
|
21
|
+
| `data-auto-page="false"` | Start the tracker but count pages yourself. |
|
|
22
|
+
| `data-api="…"` | Send to another endpoint. |
|
|
62
23
|
|
|
63
|
-
|
|
24
|
+
**npm**
|
|
64
25
|
|
|
65
|
-
```
|
|
66
|
-
|
|
26
|
+
```sh
|
|
27
|
+
npm install @indiebob/tracker
|
|
67
28
|
```
|
|
68
29
|
|
|
69
|
-
Calling `init()` again with the same `projectId` returns the instance that is
|
|
70
|
-
already running (the first config wins), so nothing is ever counted twice —
|
|
71
|
-
for example a `data-project` script tag plus your own `init()` call.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
### `bob.page(overrides?)`
|
|
76
|
-
|
|
77
|
-
Track a page view. Call this on every page load and SPA navigation.
|
|
78
|
-
|
|
79
30
|
```ts
|
|
80
|
-
|
|
81
|
-
bob.page({ path: '/pricing' }) // Override path
|
|
82
|
-
bob.page({ title: 'Pricing Page' }) // Override title
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**Auto-detected:** URL path, page title, referrer, UTM parameters, device type.
|
|
86
|
-
|
|
87
|
-
A `page()` for the path you are already on (same pathname + query) is skipped,
|
|
88
|
-
so a framework hook that fires twice, or a hash-only change, never counts a
|
|
89
|
-
second view. A page restored from the back/forward cache counts again. A page
|
|
90
|
-
that is prerendered or opened in a background tab is counted when it is first
|
|
91
|
-
shown, not before.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
### `bob.track(event, properties?)`
|
|
96
|
-
|
|
97
|
-
Track a custom event.
|
|
31
|
+
import { IndieBob } from '@indiebob/tracker'
|
|
98
32
|
|
|
99
|
-
|
|
100
|
-
bob.
|
|
101
|
-
bob.track('trial_activated', { plan: 'pro' })
|
|
102
|
-
bob.track('feature_used', { feature: 'csv_export', count: 3 })
|
|
103
|
-
bob.track('checkout_completed', { revenue: 29.00, currency: 'USD', plan: 'pro-monthly' })
|
|
33
|
+
const bob = IndieBob.init({ projectId: 'proj_…' })
|
|
34
|
+
bob.page()
|
|
104
35
|
```
|
|
105
36
|
|
|
106
|
-
|
|
37
|
+
## Privacy mode and full attribution
|
|
107
38
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
39
|
+
| | Privacy mode (default) | Full attribution |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Stored in the visitor's browser | nothing | a visitor id, a session id, the first and last UTM, the first referrer |
|
|
42
|
+
| A visitor is recognised | for one day | across days |
|
|
43
|
+
| A signup is credited to a visit on an earlier day | no | yes |
|
|
44
|
+
| Needs the visitor's consent in the EU | no | yes |
|
|
111
45
|
|
|
112
|
-
|
|
46
|
+
In privacy mode IndieBob's server identifies a visitor by hashing a random daily salt together with
|
|
47
|
+
the project, the IP address and the user agent. Each day's salt is deleted, and the IP address is
|
|
48
|
+
not stored.
|
|
113
49
|
|
|
114
|
-
|
|
50
|
+
Full attribution stores an id on the visitor's device, which needs consent in the EU (ePrivacy
|
|
51
|
+
Directive, Art. 5(3); localStorage counts the same as a cookie). Switch it on from your consent
|
|
52
|
+
banner:
|
|
115
53
|
|
|
116
54
|
```ts
|
|
117
|
-
bob.
|
|
118
|
-
|
|
119
|
-
name: 'Jane Smith',
|
|
120
|
-
plan: 'pro',
|
|
121
|
-
createdAt: '2026-01-15',
|
|
122
|
-
})
|
|
55
|
+
bob.setPersistence('local') // the visitor accepted
|
|
56
|
+
bob.setPersistence('none') // consent withdrawn: removes everything the tracker stored
|
|
123
57
|
```
|
|
124
58
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
**`traits`** — plain object, optional. Can include `email` for attribution linking.
|
|
128
|
-
|
|
129
|
-
After `identify()`, all subsequent events are linked to this userId until the session ends.
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
### `bob.revenue(data)`
|
|
59
|
+
If consent is already known when the page loads, pass `persistence: 'local'` to `init()` or put
|
|
60
|
+
`data-persistence="local"` on the tag.
|
|
134
61
|
|
|
135
|
-
|
|
62
|
+
## Configuration
|
|
136
63
|
|
|
137
64
|
```ts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
65
|
+
IndieBob.init({
|
|
66
|
+
projectId: 'proj_…', // required
|
|
67
|
+
persistence: 'none', // 'none' (privacy mode) | 'local' (full attribution)
|
|
68
|
+
autoDetect: true, // automatic events, below; false turns them off
|
|
69
|
+
hashMode: false, // routes after a #
|
|
70
|
+
respectDnt: true, // send nothing when Do Not Track is on
|
|
71
|
+
respectGpc: true, // send nothing when Global Privacy Control is on
|
|
72
|
+
endpoint: 'https://www.indiebob.com/api/collect',
|
|
73
|
+
debug: false, // log every event to the console
|
|
143
74
|
})
|
|
144
75
|
```
|
|
145
76
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
### Privacy Controls
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
bob.optOut() // Stop all tracking. Clears all stored identifiers.
|
|
152
|
-
bob.optIn() // Re-enable tracking.
|
|
153
|
-
bob.isOptedOut() // Returns boolean.
|
|
154
|
-
bob.isIgnored() // True if this browser excluded itself with ?ib_ignore=1.
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Exclude your own visits (`ib_ignore`)
|
|
77
|
+
A misspelled or unknown option logs a warning instead of being silently ignored.
|
|
158
78
|
|
|
159
|
-
|
|
160
|
-
(`https://yoursite.com/?ib_ignore=1`). That browser stops being counted — on
|
|
161
|
-
every page, from then on — and the console says
|
|
162
|
-
`IndieBob: this browser is excluded from analytics (ib_ignore)` on each page
|
|
163
|
-
load so you can see it working. Open any page with `?ib_ignore=0` to be counted
|
|
164
|
-
again. It is stored in `localStorage` as `ib_ignore`, so it is per browser and
|
|
165
|
-
per site. Your identifiers are kept (unlike `optOut()`).
|
|
79
|
+
## API
|
|
166
80
|
|
|
167
|
-
|
|
168
|
-
Puppeteer, headless test runners) send nothing at all.
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
### Debug / Testing
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
bob.getAnonymousId() // Returns the current anonymous ID
|
|
176
|
-
bob.getSessionId() // Returns the current session ID
|
|
177
|
-
bob.getConfig() // Returns a read-only copy of the config
|
|
178
|
-
bob.flush() // Force-send all queued events immediately
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## Auto-Detection
|
|
184
|
-
|
|
185
|
-
The SDK automatically fires semantic events out of the box — no manual `bob.track()` calls needed for common user journeys. This powers IndieBob's attribution funnels (visit → signup → checkout → paid).
|
|
186
|
-
|
|
187
|
-
### URL Intent Detection
|
|
188
|
-
|
|
189
|
-
When `bob.page()` is called, the SDK checks the URL path against known patterns (multilingual: EN, ES, FR, DE, PT, IT, NL) and fires the corresponding event:
|
|
190
|
-
|
|
191
|
-
| Event | Triggered By |
|
|
81
|
+
| Call | |
|
|
192
82
|
|---|---|
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
Clicks on links pointing to external domains automatically fire `outbound_click` with `{ url, hostname }` properties.
|
|
219
|
-
|
|
220
|
-
### File Download Tracking
|
|
221
|
-
|
|
222
|
-
A click on a link whose path ends in a file extension fires `file_download`
|
|
223
|
-
with `{ url, path, ext }` — on your own host or any other (a GitHub release,
|
|
224
|
-
a CDN). `url` is the link without its query string or hash, so a signed
|
|
225
|
-
download token never reaches your analytics. A download on another host fires
|
|
226
|
-
only `file_download`, not `outbound_click` as well.
|
|
227
|
-
|
|
228
|
-
Extensions: `pdf zip dmg exe msi pkg apk ipa deb rpm appimage gz tgz bz2 xz
|
|
229
|
-
tar 7z rar csv xls xlsx doc docx ppt pptx txt epub mp3 wav mp4 mov iso`.
|
|
230
|
-
|
|
231
|
-
```html
|
|
232
|
-
<a href="/downloads/MyApp-2.1.dmg">Download for Mac</a>
|
|
233
|
-
<!-- → track('file_download', { url: 'https://yoursite.com/downloads/MyApp-2.1.dmg',
|
|
234
|
-
path: '/downloads/MyApp-2.1.dmg', ext: 'dmg', auto: true }) -->
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Turn it off with `autoDetect: { fileDownloads: false }`.
|
|
238
|
-
|
|
239
|
-
### Engagement (time on page and scroll depth)
|
|
240
|
-
|
|
241
|
-
After `page()`, the SDK measures how long the page was **visible and focused**
|
|
242
|
-
(a background tab or another focused window does not count) and how far down
|
|
243
|
-
it was read (the deepest point seen, as a percentage of the page height). It
|
|
244
|
-
sends this as an `engagement` event — never a pageview — with the context of
|
|
245
|
-
the page it describes:
|
|
246
|
-
|
|
247
|
-
- when the tab is hidden or the page is closed (by beacon, like every last event);
|
|
248
|
-
- right before an SPA navigation, for the page being left.
|
|
249
|
-
|
|
250
|
-
It is sent only when there is something new: the scroll depth grew, or at
|
|
251
|
-
least 3 seconds of engaged time accumulated since the last one. The counters
|
|
252
|
-
start again for every page. Nothing to set up.
|
|
253
|
-
|
|
254
|
-
### Data Attribute Tracking
|
|
255
|
-
|
|
256
|
-
Add `data-ib-event` to any element for zero-JS custom event tracking:
|
|
257
|
-
|
|
258
|
-
```html
|
|
259
|
-
<button data-ib-event="add_to_cart" data-ib-plan="pro" data-ib-price="29">
|
|
260
|
-
Add to Cart
|
|
261
|
-
</button>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
This fires `track('add_to_cart', { plan: 'pro', price: '29' })` on click. All `data-ib-*` attributes (except `data-ib-event`) become event properties.
|
|
265
|
-
|
|
266
|
-
### Customizing Auto-Detection
|
|
83
|
+
| `IndieBob.init(config)` | Starts the tracker and returns it. Called again for the same project, it returns the running one. |
|
|
84
|
+
| `bob.page(overrides?)` | Counts a page view. The same path twice in a row counts once. `overrides`: `{ path, title, referrer }`. |
|
|
85
|
+
| `bob.track(name, properties?)` | A named event: `bob.track('csv_exported', { rows: 1240 })`. |
|
|
86
|
+
| `bob.identify(userId, traits?, { isNew? })` | After signup or login. Links the visit to your user id and sends `signup` or `login`. Pass `isNew: true` after a registration and `false` on a login — in privacy mode it is required for the `signup`/`login` event. Use your own id, never an email address, as `userId`. |
|
|
87
|
+
| `bob.revenue({ amount, currency, plan, type })` | A payment, if Stripe is not connected to IndieBob. `type`: `new`, `renewal`, `upgrade`, `downgrade`. |
|
|
88
|
+
| `bob.setPersistence('none' \| 'local')` | Switches mode, above. |
|
|
89
|
+
| `bob.enableAutoPage()` | Counts a page on every history change (the script tag does this itself). |
|
|
90
|
+
| `bob.optOut()`, `bob.optIn()`, `bob.isOptedOut()` | The visitor's own opt-out; `optOut()` removes everything stored. |
|
|
91
|
+
| `bob.isIgnored()` | Whether this browser is excluded with `?ib_ignore=1`. |
|
|
92
|
+
| `bob.flush()` | Sends queued events now. |
|
|
93
|
+
|
|
94
|
+
Events are sent in batches every 5 seconds and when the page is hidden or closed.
|
|
95
|
+
|
|
96
|
+
## Collected automatically
|
|
97
|
+
|
|
98
|
+
| Event | When |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `page` | Every page view, including a page restored with the Back button. |
|
|
101
|
+
| `engagement` | When a page is left or hidden: how long it was visible and focused, and how far it was scrolled. |
|
|
102
|
+
| `outbound_click` | A link to another site is clicked. |
|
|
103
|
+
| `file_download` | A link to a file (pdf, zip, dmg, exe, apk, …) is clicked. |
|
|
104
|
+
| `signup_start`, `login_view`, `pricing_view`, `checkout_view`, `success_view`, `onboarding_view`, `contact_view` | The page's path matches `/signup`, `/login`, `/pricing`, … or their Spanish, French, German, Portuguese, Italian or Dutch words. |
|
|
105
|
+
| `signup_submit`, `login_submit`, `checkout_submit` | A form is submitted on one of those pages. |
|
|
106
|
+
| any name | An element with `data-ib-event="name"` is clicked; its other `data-ib-*` attributes become properties. |
|
|
267
107
|
|
|
268
|
-
|
|
108
|
+
Turn parts off or replace the path patterns:
|
|
269
109
|
|
|
270
110
|
```ts
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
autoDetect: {
|
|
274
|
-
// Custom URL patterns — strings or RegExp (override defaults)
|
|
275
|
-
signupPaths: ['/my-signup-page'], // string: matched via path.includes()
|
|
276
|
-
checkoutPaths: [/\/buy|\/purchase/i], // RegExp: matched via .test()
|
|
277
|
-
|
|
278
|
-
// Disable specific features
|
|
279
|
-
outboundClicks: false,
|
|
280
|
-
fileDownloads: false,
|
|
281
|
-
formSubmit: false,
|
|
282
|
-
dataAttributes: false,
|
|
283
|
-
},
|
|
284
|
-
})
|
|
111
|
+
autoDetect: { fileDownloads: false, outboundClicks: false, formSubmit: false, dataAttributes: false,
|
|
112
|
+
signupPaths: [/\/start-trial/] }
|
|
285
113
|
```
|
|
286
114
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
| Option | Type | Default | Description |
|
|
290
|
-
|---|---|---|---|
|
|
291
|
-
| `signupPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for signup pages |
|
|
292
|
-
| `loginPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for login pages |
|
|
293
|
-
| `pricingPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for pricing pages |
|
|
294
|
-
| `checkoutPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for checkout/cart pages |
|
|
295
|
-
| `successPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for success/thank-you pages |
|
|
296
|
-
| `onboardingPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for onboarding pages |
|
|
297
|
-
| `contactPaths` | `(string \| RegExp)[]` | built-in i18n | URL patterns for contact pages |
|
|
298
|
-
| `dataAttributes` | `boolean` | `true` | Track clicks on `data-ib-event` elements |
|
|
299
|
-
| `formSubmit` | `boolean` | `true` | Track form submissions on intent pages |
|
|
300
|
-
| `outboundClicks` | `boolean` | `true` | Track clicks on external links |
|
|
301
|
-
| `fileDownloads` | `boolean` | `true` | Track clicks on links to files as `file_download` |
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
## Framework Integrations
|
|
306
|
-
|
|
307
|
-
### Vue / Nuxt
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
// main.ts or plugins/tracker.client.ts
|
|
311
|
-
import { IndieBobPlugin } from '@indiebob/tracker/vue'
|
|
312
|
-
|
|
313
|
-
app.use(IndieBobPlugin, { projectId: 'proj_abc123def456ghij' })
|
|
314
|
-
```
|
|
115
|
+
## What is sent
|
|
315
116
|
|
|
316
|
-
|
|
117
|
+
- The page's path and URL, keeping only the `utm_*` and `ref` query parameters. Anything else in a
|
|
118
|
+
query string — sign-in tokens, reset codes, email addresses, ad click ids — is removed before it
|
|
119
|
+
leaves the page, and IndieBob's server removes it again from older versions of the tracker.
|
|
120
|
+
- The page title; the referrer's origin and path; the screen size class; the browser's language and
|
|
121
|
+
time zone; your event's properties.
|
|
122
|
+
- IndieBob adds the country, region and city (looked up from the IP address, which is not stored),
|
|
123
|
+
the browser, the operating system and the device type.
|
|
317
124
|
|
|
318
|
-
|
|
319
|
-
|
|
125
|
+
Nothing is sent from a browser with Do Not Track or Global Privacy Control on, after `optOut()`,
|
|
126
|
+
from automated browsers, or from your own browser once you open your site with `?ib_ignore=1`
|
|
127
|
+
(`?ib_ignore=0` undoes it).
|
|
320
128
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
```
|
|
129
|
+
IndieBob processes this data for you, in the EU (Frankfurt). A data processing agreement is
|
|
130
|
+
available from info@indiebob.com.
|
|
324
131
|
|
|
325
|
-
|
|
132
|
+
## Vue and Nuxt
|
|
326
133
|
|
|
327
134
|
```ts
|
|
135
|
+
// plugins/indiebob.client.ts (Nuxt) — or app.use(...) after the router in a Vue app
|
|
328
136
|
import { IndieBobPlugin } from '@indiebob/tracker/vue'
|
|
329
137
|
|
|
330
138
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
331
|
-
nuxtApp.vueApp.use(IndieBobPlugin, {
|
|
332
|
-
projectId: useRuntimeConfig().public.trackerProjectId,
|
|
333
|
-
})
|
|
139
|
+
nuxtApp.vueApp.use(IndieBobPlugin, { projectId: 'proj_…' })
|
|
334
140
|
})
|
|
335
141
|
```
|
|
336
142
|
|
|
337
|
-
Page views
|
|
338
|
-
|
|
339
|
-
### React
|
|
143
|
+
Page views follow vue-router. In a component: `const bob = useTracker()` from
|
|
144
|
+
`@indiebob/tracker/vue`.
|
|
340
145
|
|
|
341
|
-
|
|
342
|
-
import { IndieBob } from '@indiebob/tracker'
|
|
343
|
-
import { useEffect } from 'react'
|
|
146
|
+
## React
|
|
344
147
|
|
|
345
|
-
|
|
346
|
-
const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
|
|
148
|
+
There is no React package; call `page()` when the route changes:
|
|
347
149
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}, [pathname])
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Vanilla JS (Script Tag)
|
|
355
|
-
|
|
356
|
-
One tag, before `</head>` on every page:
|
|
357
|
-
|
|
358
|
-
```html
|
|
359
|
-
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js" data-project="proj_abc123def456ghij" defer></script>
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
It initialises itself and tracks the page plus every SPA navigation
|
|
363
|
-
(`pushState` / back / forward) — no `init()` or `page()` call. Attributes:
|
|
364
|
-
|
|
365
|
-
| Attribute | Meaning |
|
|
366
|
-
|---|---|
|
|
367
|
-
| `data-project` | Your project ID. Without it the tag does nothing on its own. |
|
|
368
|
-
| `data-api` | Ingestion endpoint override (self-hosted). |
|
|
369
|
-
| `data-auto-page="false"` | Initialise only; you call `page()` yourself. |
|
|
370
|
-
| `data-hash-mode="true"` | Same as the `hashMode` option. |
|
|
371
|
-
|
|
372
|
-
To call `track()` / `identify()` from your own script, ask for the running
|
|
373
|
-
instance — `init()` with the same project ID returns it. With `defer`, the
|
|
374
|
-
global exists once the document has been parsed:
|
|
375
|
-
|
|
376
|
-
```html
|
|
377
|
-
<script>
|
|
378
|
-
document.addEventListener('DOMContentLoaded', function () {
|
|
379
|
-
var bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
|
|
380
|
-
bob.track('newsletter_signup')
|
|
381
|
-
})
|
|
382
|
-
</script>
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
The classic install keeps working unchanged:
|
|
386
|
-
|
|
387
|
-
```html
|
|
388
|
-
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
|
|
389
|
-
<script>
|
|
390
|
-
const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
|
|
391
|
-
bob.page()
|
|
392
|
-
</script>
|
|
150
|
+
```tsx
|
|
151
|
+
const pathname = usePathname()
|
|
152
|
+
useEffect(() => { bob.page() }, [pathname])
|
|
393
153
|
```
|
|
394
154
|
|
|
395
|
-
|
|
396
|
-
call is there — a deferred script runs after it), nothing is counted twice:
|
|
397
|
-
`init()` returns the running instance and the second `page()` for the same
|
|
398
|
-
path is skipped.
|
|
155
|
+
## Server
|
|
399
156
|
|
|
400
|
-
|
|
157
|
+
For events with no browser — a webhook, a job:
|
|
401
158
|
|
|
402
159
|
```ts
|
|
403
160
|
import { IndieBob } from '@indiebob/tracker/server'
|
|
404
161
|
|
|
405
|
-
const bob = IndieBob.initServer({ projectId: '
|
|
406
|
-
bob.track('
|
|
162
|
+
const bob = IndieBob.initServer({ projectId: 'proj_…' })
|
|
163
|
+
await bob.track('trial_started', { plan: 'pro' }, 'user_123')
|
|
164
|
+
await bob.identify('user_123', { plan: 'pro' })
|
|
407
165
|
```
|
|
408
166
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
## How It Works
|
|
412
|
-
|
|
413
|
-
### Anonymous ID
|
|
414
|
-
|
|
415
|
-
A random 21-character ID is generated on first visit and stored in `localStorage`. This persists across sessions so you can track a visitor's journey before they sign up.
|
|
416
|
-
|
|
417
|
-
- Stored as `_ib_anon` in localStorage
|
|
418
|
-
- Falls back to sessionStorage if localStorage is blocked
|
|
419
|
-
- Never uses cookies — no consent banner required
|
|
420
|
-
- Cleared when `optOut()` is called
|
|
421
|
-
|
|
422
|
-
### Session ID
|
|
423
|
-
|
|
424
|
-
A session ID is generated per browser tab/window using `crypto.randomUUID()`. Sessions expire after 30 minutes of inactivity or when the tab closes.
|
|
425
|
-
|
|
426
|
-
### UTM Attribution
|
|
427
|
-
|
|
428
|
-
UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) are captured from the URL automatically on `init()`.
|
|
429
|
-
|
|
430
|
-
- **First-touch** attribution stored in `_ib_first_utm` — never overwritten
|
|
431
|
-
- **Last-touch** attribution in `_ib_last_utm` — updated each time UTMs are present in the URL
|
|
432
|
-
- Enables "which campaign drove this paying customer?" — the core attribution question
|
|
433
|
-
|
|
434
|
-
### Event IDs
|
|
435
|
-
|
|
436
|
-
Every event carries a random, url-safe `id` (21 characters). It stays the same
|
|
437
|
-
when a batch is retried or replayed from the offline queue, so the server
|
|
438
|
-
stores each event once.
|
|
439
|
-
|
|
440
|
-
### Event Batching
|
|
441
|
-
|
|
442
|
-
Events are batched for efficiency:
|
|
443
|
-
- Sent when 10 events accumulate
|
|
444
|
-
- Sent every 5 seconds regardless
|
|
445
|
-
- Sent immediately when the page becomes hidden (tab switch, close)
|
|
446
|
-
- Queued in localStorage when offline; flushed when back online
|
|
447
|
-
|
|
448
|
-
### Privacy by Default
|
|
449
|
-
|
|
450
|
-
The SDK respects:
|
|
451
|
-
- **Do Not Track** (`navigator.doNotTrack === '1'`): no events sent, `respectDnt: true` by default
|
|
452
|
-
- **Global Privacy Control** (`navigator.globalPrivacyControl`): legally binding under CCPA in California, `respectGpc: true` by default
|
|
453
|
-
- Neither cookies nor fingerprinting is used at any point
|
|
454
|
-
- IP addresses are never stored by the server — only the country code derived at ingestion time
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
## What Is and Isn't Collected
|
|
459
|
-
|
|
460
|
-
### Automatically collected (on `page()`)
|
|
461
|
-
|
|
462
|
-
| Data | What | Stored As |
|
|
463
|
-
|---|---|---|
|
|
464
|
-
| Page path | `/pricing`, `/blog/post-1` | String |
|
|
465
|
-
| Page title | `document.title` | String |
|
|
466
|
-
| Referrer | `document.referrer` | String |
|
|
467
|
-
| UTM params | From URL query string | Object |
|
|
468
|
-
| Screen size | `window.innerWidth/Height` | Numbers |
|
|
469
|
-
| Device type | Derived from screen size | `'desktop'` \| `'mobile'` \| `'tablet'` |
|
|
470
|
-
| Engaged time | Visible-and-focused milliseconds on the page (`engagement` event) | Number |
|
|
471
|
-
| Scroll depth | Deepest point of the page seen, % of its height (`engagement` event) | Number |
|
|
472
|
-
| Browser locale | `navigator.language` | String |
|
|
473
|
-
| Timezone | `Intl.DateTimeFormat().resolvedOptions().timeZone` | String |
|
|
474
|
-
| Country | Derived server-side from IP | String (e.g. `'US'`) |
|
|
475
|
-
|
|
476
|
-
### Auto-detected events (when `autoDetect` is enabled)
|
|
477
|
-
|
|
478
|
-
| Event | Trigger |
|
|
479
|
-
|---|---|
|
|
480
|
-
| `signup_start` | User visits a signup/register page |
|
|
481
|
-
| `signup_submit` | User submits a form on a signup page |
|
|
482
|
-
| `signup` | First `identify()` call (new user) |
|
|
483
|
-
| `login_view` | User visits a login page |
|
|
484
|
-
| `login_submit` | User submits a form on a login page |
|
|
485
|
-
| `login` | Subsequent `identify()` call (returning user) |
|
|
486
|
-
| `pricing_view` | User visits a pricing page |
|
|
487
|
-
| `checkout_view` | User visits a checkout/cart page |
|
|
488
|
-
| `checkout_submit` | User submits a form on a checkout page |
|
|
489
|
-
| `success_view` | User visits a success/thank-you page |
|
|
490
|
-
| `onboarding_view` | User visits an onboarding page |
|
|
491
|
-
| `contact_view` | User visits a contact page |
|
|
492
|
-
| `outbound_click` | User clicks an external link |
|
|
493
|
-
| `file_download` | User clicks a link to a file (pdf, zip, dmg, …) |
|
|
494
|
-
| Custom via `data-ib-event` | User clicks an element with the attribute |
|
|
495
|
-
|
|
496
|
-
### NOT collected
|
|
497
|
-
|
|
498
|
-
- IP addresses (never stored, used only for country lookup then discarded)
|
|
499
|
-
- Browser fingerprint
|
|
500
|
-
- Cookies
|
|
501
|
-
- Personal data (name, email, etc.) unless you pass it to `identify()`
|
|
502
|
-
- Cross-site tracking data
|
|
503
|
-
|
|
504
|
-
---
|
|
505
|
-
|
|
506
|
-
## Privacy & Compliance
|
|
507
|
-
|
|
508
|
-
### GDPR (EU)
|
|
509
|
-
|
|
510
|
-
The SDK does not use cookies and does not collect personal data by default. The `anonymousId` is a random identifier with no link to personal identity. Under GDPR, anonymous analytics may not require a consent banner — but consult your legal team.
|
|
511
|
-
|
|
512
|
-
When a user calls `optOut()`, all stored keys are cleared: `_ib_anon`, `_ib_session`, `_ib_first_utm`, `_ib_last_utm`, `_ib_referrer`, `_ib_queue`.
|
|
513
|
-
|
|
514
|
-
`ib_ignore` (the site owner's own self-exclusion, see above) is the one key the
|
|
515
|
-
SDK writes that is not an identifier; it is removed with `?ib_ignore=0`.
|
|
516
|
-
|
|
517
|
-
### CCPA (California)
|
|
518
|
-
|
|
519
|
-
Global Privacy Control (GPC) is automatically honored. If a user has GPC enabled in their browser, the SDK sends no events. This is the legally correct behavior under CCPA as amended by CPRA.
|
|
520
|
-
|
|
521
|
-
### Cookie Law (EU ePrivacy Directive)
|
|
522
|
-
|
|
523
|
-
No cookies are set. The localStorage identifiers are not cookies and fall outside the scope of the Cookie Law.
|
|
524
|
-
|
|
525
|
-
---
|
|
526
|
-
|
|
527
|
-
## Self-Hosting
|
|
528
|
-
|
|
529
|
-
Point the SDK at your own IndieBob instance:
|
|
530
|
-
|
|
531
|
-
```ts
|
|
532
|
-
const bob = IndieBob.init({
|
|
533
|
-
projectId: 'proj_abc123def456ghij',
|
|
534
|
-
endpoint: 'https://your-indiebob.example.com/api/collect',
|
|
535
|
-
})
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
---
|
|
539
|
-
|
|
540
|
-
## Versioning & Backwards Compatibility
|
|
541
|
-
|
|
542
|
-
This SDK follows [Semantic Versioning](https://semver.org/).
|
|
543
|
-
|
|
544
|
-
**1.x guarantee**: All public methods listed in this README are stable. We will never:
|
|
545
|
-
- Remove a method in a 1.x release
|
|
546
|
-
- Change the behavior of an existing method without a deprecation period
|
|
547
|
-
- Break the wire format for events that include `v: 1`
|
|
548
|
-
|
|
549
|
-
**Upgrade path**: `npm update @indiebob/tracker` — no code changes required within 1.x.
|
|
550
|
-
|
|
551
|
-
**Deprecation process**: Methods marked `@deprecated` in JSDoc will log a warning in `debug: true` mode. They remain functional until the next major version.
|
|
552
|
-
|
|
553
|
-
---
|
|
554
|
-
|
|
555
|
-
## Bundle Size
|
|
556
|
-
|
|
557
|
-
| Bundle | Size (gzip -9) |
|
|
558
|
-
|---|---|
|
|
559
|
-
| Browser IIFE (`indiebob.min.js`) | < 5KB (5,076 bytes in 0.3.0) |
|
|
560
|
-
| ESM (`index.js`) | ~5KB (5,135 bytes in 0.3.0; your bundler minifies and tree-shakes it further) |
|
|
561
|
-
| Server (Node.js) | No limit |
|
|
562
|
-
|
|
563
|
-
Zero runtime dependencies.
|
|
564
|
-
|
|
565
|
-
## Browser Support
|
|
167
|
+
## Versions
|
|
566
168
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
169
|
+
`/tracker/latest/indiebob.min.js` is always the newest release. To stay on one version, load
|
|
170
|
+
`/tracker/v0.4.1/indiebob.min.js`; a versioned file never changes. The npm package follows
|
|
171
|
+
[semver](https://semver.org/); while it is 0.x, a minor release (0.3 → 0.4) can change behaviour, so
|
|
172
|
+
read [CHANGELOG.md](./CHANGELOG.md) before upgrading.
|
|
570
173
|
|
|
571
|
-
|
|
174
|
+
Browsers: the script tag needs Chrome 80, Firefox 74 or Safari 13.1 or newer; the npm build targets
|
|
175
|
+
ES2018.
|
|
572
176
|
|
|
573
177
|
## License
|
|
574
178
|
|