@indiebob/tracker 0.2.3 → 0.3.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/CHANGELOG.md +80 -0
- package/README.md +125 -6
- package/dist/index.cjs +880 -2
- package/dist/index.d.mts +69 -11
- package/dist/index.d.ts +69 -11
- package/dist/index.global.js +3 -3
- package/dist/index.js +878 -2
- package/dist/server.d.mts +8 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +5 -7
- package/dist/server.mjs +5 -7
- package/dist/vue.cjs +904 -2
- package/dist/vue.global.js +3 -3
- package/dist/vue.js +900 -2
- package/package.json +11 -9
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,86 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.3.0] — 2026-11-01
|
|
10
|
+
|
|
11
|
+
The npm module bundles (`index.js`/`.cjs`, `vue.js`/`.cjs`) are no longer
|
|
12
|
+
minified — your bundler minifies them. A minified bundle's comma-joined
|
|
13
|
+
declarations slipped past Nuxt's auto-import scanner and broke a Nuxt build.
|
|
14
|
+
The script-tag bundle (`indiebob.min.js`) stays minified, under 5 KB.
|
|
15
|
+
|
|
16
|
+
Needs the matching `/api/collect` (it must accept `type: 'engagement'` and
|
|
17
|
+
the new top-level `id` and `engagement` fields) **before** this version is
|
|
18
|
+
served: the 0.2 server rejects a whole batch that contains an engagement event.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Self-initialising script tag.** `<script src=".../indiebob.min.js"
|
|
22
|
+
data-project="proj_…" defer></script>` initialises itself and tracks the
|
|
23
|
+
page plus every SPA navigation, with no `init()` / `page()` call. Optional
|
|
24
|
+
`data-api` (endpoint), `data-auto-page="false"` (initialise only),
|
|
25
|
+
`data-hash-mode="true"`. A tag without `data-project` behaves exactly as
|
|
26
|
+
before.
|
|
27
|
+
- **Event id.** Every event carries a top-level `id` (21 url-safe
|
|
28
|
+
characters), unchanged across retries and offline replays, so the server
|
|
29
|
+
can store each event once. Server-side events (`/server`) carry one too.
|
|
30
|
+
- **`engagement` event.** Visible-and-focused milliseconds and the deepest
|
|
31
|
+
scroll depth (% of the page) for the page it describes, with that page's
|
|
32
|
+
context. Sent on tab hide, pagehide and right before an SPA navigation,
|
|
33
|
+
only when the scroll grew or ≥ 3 s accumulated since the last one; never a
|
|
34
|
+
pageview.
|
|
35
|
+
- **Automatic file downloads.** A click on a link to a file (pdf, zip, dmg,
|
|
36
|
+
exe, msi, pkg, apk, ipa, deb, rpm, appimage, archives, office files, txt,
|
|
37
|
+
epub, mp3/wav/mp4/mov, iso) fires `file_download` with
|
|
38
|
+
`{ url, path, ext, auto: true }`, on any host; `url` drops the query and
|
|
39
|
+
hash. An outbound download fires only `file_download`. Off with
|
|
40
|
+
`autoDetect: { fileDownloads: false }`.
|
|
41
|
+
- **Owner self-exclude.** `?ib_ignore=1` stops this browser being counted
|
|
42
|
+
(stored as `localStorage.ib_ignore`, with one console line per page load);
|
|
43
|
+
`?ib_ignore=0` undoes it. `bob.isIgnored()` reports it.
|
|
44
|
+
- **`hashMode`** option for `/#/route` routers.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- A `page()` for the path already counted (same pathname + query; a
|
|
48
|
+
hash-only change unless `hashMode`) is skipped.
|
|
49
|
+
- A back/forward-cache restore (`pageshow` with `persisted`) counts as a
|
|
50
|
+
pageview.
|
|
51
|
+
- A page that is prerendered or opened in a background tab counts its first
|
|
52
|
+
pageview when it is first shown.
|
|
53
|
+
- `navigator.webdriver` browsers send nothing.
|
|
54
|
+
- `init()` with a `projectId` that is already running returns that instance
|
|
55
|
+
(the first config wins) instead of starting a second tracker.
|
|
56
|
+
- Builds: the script-tag bundle targets ES2020 (Chrome 80, Firefox 74,
|
|
57
|
+
Safari 13.1) and the npm ESM/CJS build ES2018 (was ES2015 for both) — the
|
|
58
|
+
price of staying under 5 KB gzipped with the features above. Private
|
|
59
|
+
members are shortened at build time.
|
|
60
|
+
- The plain-http fallback for the session id (no `crypto.randomUUID` outside
|
|
61
|
+
a secure context) now uses the SDK's crypto id generator instead of
|
|
62
|
+
`Math.random`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## [0.2.4] — 2026-09-08
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- **`identify(userId, traits, { isNew })` — tell us, don't make us guess.**
|
|
70
|
+
Without the hint, signup-vs-login is decided by a localStorage flag the SDK
|
|
71
|
+
wrote itself, and that flag cannot exist in a browser that has never called
|
|
72
|
+
`identify()`. So on a product that already has users, the day you install
|
|
73
|
+
`identify()` **every returning user files a phantom `signup`** and the top of
|
|
74
|
+
your funnel is wrong from then on. Your server knows whether it just created
|
|
75
|
+
the user row — pass `isNew` and the guess is skipped.
|
|
76
|
+
|
|
77
|
+
Additive and fully backwards compatible: two-argument calls behave exactly as
|
|
78
|
+
before. The emitted event now carries `properties.auto`, which is `false`
|
|
79
|
+
when the verdict came from your server and `true` when it was inferred, so a
|
|
80
|
+
funnel that looks wrong later can be traced to a guess.
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
bob.identify('user_123', { plan: 'pro' }) // inferred
|
|
84
|
+
bob.identify('user_123', { plan: 'pro' }, { isNew: true }) // server truth
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
9
89
|
## [0.2.1] — 2026-08-24
|
|
10
90
|
|
|
11
91
|
### Fixed
|
package/README.md
CHANGED
|
@@ -10,12 +10,15 @@ npm install @indiebob/tracker
|
|
|
10
10
|
pnpm add @indiebob/tracker
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Or via script tag (CDN):
|
|
13
|
+
Or via script tag (CDN) — one line, nothing else to write:
|
|
14
14
|
|
|
15
15
|
```html
|
|
16
|
-
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
|
|
16
|
+
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js" data-project="proj_your16charId" defer></script>
|
|
17
17
|
```
|
|
18
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
|
+
|
|
19
22
|
## Quick Start
|
|
20
23
|
|
|
21
24
|
Three lines to start tracking:
|
|
@@ -39,6 +42,7 @@ const bob = IndieBob.init({
|
|
|
39
42
|
respectDnt: true, // Optional. Honor browser Do Not Track. Default: true.
|
|
40
43
|
respectGpc: true, // Optional. Honor Global Privacy Control. Default: true.
|
|
41
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.
|
|
42
46
|
})
|
|
43
47
|
```
|
|
44
48
|
|
|
@@ -49,7 +53,8 @@ const bob = IndieBob.init({
|
|
|
49
53
|
| `debug` | `boolean` | `false` | Log all events to browser console |
|
|
50
54
|
| `respectDnt` | `boolean` | `true` | Honor the browser's Do Not Track header |
|
|
51
55
|
| `respectGpc` | `boolean` | `true` | Honor Global Privacy Control signal (legally binding in CA/GDPR) |
|
|
52
|
-
| `autoDetect` | `boolean \| object` | `true` | Auto-detect user intent from URL patterns, form submissions, outbound clicks, and data attributes. See [Auto-Detection](#auto-detection). |
|
|
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`). |
|
|
53
58
|
|
|
54
59
|
## API Reference
|
|
55
60
|
|
|
@@ -61,6 +66,10 @@ Creates and returns a tracker instance. Call once at app startup.
|
|
|
61
66
|
const bob = IndieBob.init({ projectId: 'proj_abc123def456ghij' })
|
|
62
67
|
```
|
|
63
68
|
|
|
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
|
+
|
|
64
73
|
---
|
|
65
74
|
|
|
66
75
|
### `bob.page(overrides?)`
|
|
@@ -75,6 +84,12 @@ bob.page({ title: 'Pricing Page' }) // Override title
|
|
|
75
84
|
|
|
76
85
|
**Auto-detected:** URL path, page title, referrer, UTM parameters, device type.
|
|
77
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
|
+
|
|
78
93
|
---
|
|
79
94
|
|
|
80
95
|
### `bob.track(event, properties?)`
|
|
@@ -136,8 +151,22 @@ bob.revenue({
|
|
|
136
151
|
bob.optOut() // Stop all tracking. Clears all stored identifiers.
|
|
137
152
|
bob.optIn() // Re-enable tracking.
|
|
138
153
|
bob.isOptedOut() // Returns boolean.
|
|
154
|
+
bob.isIgnored() // True if this browser excluded itself with ?ib_ignore=1.
|
|
139
155
|
```
|
|
140
156
|
|
|
157
|
+
### Exclude your own visits (`ib_ignore`)
|
|
158
|
+
|
|
159
|
+
Open your site once with `?ib_ignore=1` on the URL
|
|
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()`).
|
|
166
|
+
|
|
167
|
+
Browsers driven by automation (`navigator.webdriver` — Playwright, Selenium,
|
|
168
|
+
Puppeteer, headless test runners) send nothing at all.
|
|
169
|
+
|
|
141
170
|
---
|
|
142
171
|
|
|
143
172
|
### Debug / Testing
|
|
@@ -188,6 +217,40 @@ On signup, login, and checkout pages, form submissions are automatically tracked
|
|
|
188
217
|
|
|
189
218
|
Clicks on links pointing to external domains automatically fire `outbound_click` with `{ url, hostname }` properties.
|
|
190
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
|
+
|
|
191
254
|
### Data Attribute Tracking
|
|
192
255
|
|
|
193
256
|
Add `data-ib-event` to any element for zero-JS custom event tracking:
|
|
@@ -214,6 +277,7 @@ const bob = IndieBob.init({
|
|
|
214
277
|
|
|
215
278
|
// Disable specific features
|
|
216
279
|
outboundClicks: false,
|
|
280
|
+
fileDownloads: false,
|
|
217
281
|
formSubmit: false,
|
|
218
282
|
dataAttributes: false,
|
|
219
283
|
},
|
|
@@ -234,6 +298,7 @@ Set `autoDetect: false` to disable all auto-detection entirely.
|
|
|
234
298
|
| `dataAttributes` | `boolean` | `true` | Track clicks on `data-ib-event` elements |
|
|
235
299
|
| `formSubmit` | `boolean` | `true` | Track form submissions on intent pages |
|
|
236
300
|
| `outboundClicks` | `boolean` | `true` | Track clicks on external links |
|
|
301
|
+
| `fileDownloads` | `boolean` | `true` | Track clicks on links to files as `file_download` |
|
|
237
302
|
|
|
238
303
|
---
|
|
239
304
|
|
|
@@ -288,6 +353,37 @@ useEffect(() => {
|
|
|
288
353
|
|
|
289
354
|
### Vanilla JS (Script Tag)
|
|
290
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
|
+
|
|
291
387
|
```html
|
|
292
388
|
<script src="https://assets.indiebob.com/tracker/latest/indiebob.min.js"></script>
|
|
293
389
|
<script>
|
|
@@ -296,6 +392,11 @@ useEffect(() => {
|
|
|
296
392
|
</script>
|
|
297
393
|
```
|
|
298
394
|
|
|
395
|
+
If you add `data-project` to that same tag (leave out `defer` while the inline
|
|
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.
|
|
399
|
+
|
|
299
400
|
### Server-Side (Node.js)
|
|
300
401
|
|
|
301
402
|
```ts
|
|
@@ -330,6 +431,12 @@ UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_con
|
|
|
330
431
|
- **Last-touch** attribution in `_ib_last_utm` — updated each time UTMs are present in the URL
|
|
331
432
|
- Enables "which campaign drove this paying customer?" — the core attribution question
|
|
332
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
|
+
|
|
333
440
|
### Event Batching
|
|
334
441
|
|
|
335
442
|
Events are batched for efficiency:
|
|
@@ -360,6 +467,8 @@ The SDK respects:
|
|
|
360
467
|
| UTM params | From URL query string | Object |
|
|
361
468
|
| Screen size | `window.innerWidth/Height` | Numbers |
|
|
362
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 |
|
|
363
472
|
| Browser locale | `navigator.language` | String |
|
|
364
473
|
| Timezone | `Intl.DateTimeFormat().resolvedOptions().timeZone` | String |
|
|
365
474
|
| Country | Derived server-side from IP | String (e.g. `'US'`) |
|
|
@@ -381,6 +490,7 @@ The SDK respects:
|
|
|
381
490
|
| `onboarding_view` | User visits an onboarding page |
|
|
382
491
|
| `contact_view` | User visits a contact page |
|
|
383
492
|
| `outbound_click` | User clicks an external link |
|
|
493
|
+
| `file_download` | User clicks a link to a file (pdf, zip, dmg, …) |
|
|
384
494
|
| Custom via `data-ib-event` | User clicks an element with the attribute |
|
|
385
495
|
|
|
386
496
|
### NOT collected
|
|
@@ -401,6 +511,9 @@ The SDK does not use cookies and does not collect personal data by default. The
|
|
|
401
511
|
|
|
402
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`.
|
|
403
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
|
+
|
|
404
517
|
### CCPA (California)
|
|
405
518
|
|
|
406
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.
|
|
@@ -441,14 +554,20 @@ This SDK follows [Semantic Versioning](https://semver.org/).
|
|
|
441
554
|
|
|
442
555
|
## Bundle Size
|
|
443
556
|
|
|
444
|
-
| Bundle | Size (gzip) |
|
|
557
|
+
| Bundle | Size (gzip -9) |
|
|
445
558
|
|---|---|
|
|
446
|
-
| Browser IIFE (`
|
|
447
|
-
| ESM tree-
|
|
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) |
|
|
448
561
|
| Server (Node.js) | No limit |
|
|
449
562
|
|
|
450
563
|
Zero runtime dependencies.
|
|
451
564
|
|
|
565
|
+
## Browser Support
|
|
566
|
+
|
|
567
|
+
The script-tag bundle targets ES2020: Chrome/Edge 80+, Firefox 74+, Safari
|
|
568
|
+
13.1+ (iOS 13.4+). The npm build (ESM/CJS) targets ES2018 and is transpiled
|
|
569
|
+
further by your bundler if you need older browsers.
|
|
570
|
+
|
|
452
571
|
---
|
|
453
572
|
|
|
454
573
|
## License
|