trackhome 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +206 -0
- package/dist/index.js.map +1 -0
- package/dist/t.umd.js +2 -0
- package/dist/t.umd.js.map +1 -0
- package/dist/umd.d.ts +6 -0
- package/dist/umd.d.ts.map +1 -0
- package/dist/umd.js +6 -0
- package/dist/umd.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# trackhome
|
|
2
|
+
|
|
3
|
+
Tiny browser SDK for [trackhome](https://github.com/exreve/trackhome) — a self-hosted, tags-only event/click tracker.
|
|
4
|
+
|
|
5
|
+
- **One-line setup**: `init('https://your-trackhome.example.com')` and you're done.
|
|
6
|
+
- **Auto-collects page views** on init and on every SPA route change (React Router, Next.js, Vue Router — all work without extra config).
|
|
7
|
+
- **Tags-only model**: every event is `{type, tags}`. No metadata, no props, no per-visitor IDs on the wire. Use tags for source, campaign, variant, funnel step — uniformly queryable.
|
|
8
|
+
- **Privacy-friendly**: visitor identity is a server-set HttpOnly cookie. JS never touches it. IP/UA are hashed on the server, never stored raw.
|
|
9
|
+
- **Zero dependencies**, ~1 KB gzipped (UMD), works with or without a bundler.
|
|
10
|
+
- **Auto-enriched server-side**: every event also gets `device:*`, `os:*`, `country:*`, `lang:*` tags derived from request headers, so dashboards break down by these for free.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install trackhome
|
|
16
|
+
# or: pnpm add trackhome / yarn add trackhome
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start (3 ways)
|
|
20
|
+
|
|
21
|
+
### 1. Bundler (webpack, Vite, esbuild, …)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { init } from 'trackhome';
|
|
25
|
+
|
|
26
|
+
// That's it. Fires page_view immediately + on every SPA navigation.
|
|
27
|
+
init('https://trk.example.com');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Build-less `<script>` tag (CDN from your own trackhome server)
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<script src="https://trk.example.com/t.js"></script>
|
|
34
|
+
<script>
|
|
35
|
+
trackhome.init('https://trk.example.com');
|
|
36
|
+
</script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. From a CDN (coming soon — for now, host `/t.js` from your own instance, see option 2)
|
|
40
|
+
|
|
41
|
+
## Custom events
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { track, identify } from 'trackhome';
|
|
45
|
+
|
|
46
|
+
track('signup_click', ['tier:pro', 'cta:header']);
|
|
47
|
+
track('purchase', ['plan:annual']);
|
|
48
|
+
|
|
49
|
+
identify('user_42'); // → identify event with user:user_42 tag
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `init(endpointOrConfig)`
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
init('https://trk.example.com') // shorthand
|
|
58
|
+
|
|
59
|
+
init({ // full config
|
|
60
|
+
endpoint: 'https://trk.example.com',
|
|
61
|
+
tags: ['app:marketing'], // persistent tags on every event
|
|
62
|
+
autoPageView: true, // default true — see below
|
|
63
|
+
batchSize: 20, // max events per flush
|
|
64
|
+
flushIntervalMs: 5_000, // flush cadency in ms
|
|
65
|
+
disabled: false, // kill switch for dev
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Idempotent — calling `init()` twice is a no-op.
|
|
70
|
+
|
|
71
|
+
**`autoPageView` (default: `true`)** — fires a `page_view` event tagged `page:/current/path`:
|
|
72
|
+
1. immediately on init
|
|
73
|
+
2. on `history.pushState` / `replaceState` (catches React Router, Vue Router, Next.js navigation)
|
|
74
|
+
3. on `popstate` (back/forward buttons)
|
|
75
|
+
4. on `hashchange`
|
|
76
|
+
|
|
77
|
+
So for many sites, the one-liner `init(url)` is all you need — the SDK collects traffic without any manual `track()` calls. Pass `autoPageView: false` if you have your own router hook or want to call `page()` manually.
|
|
78
|
+
|
|
79
|
+
### `track(type, tags?)`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
track('signup_click', ['tier:pro']);
|
|
83
|
+
track('page_view'); // tags optional
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`tags` (if provided) are merged with persistent tags (set via `init({tags})` / `setTags` / `addTag`). Duplicates are deduped.
|
|
87
|
+
|
|
88
|
+
### `page(name?, tags?)`
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
page() // → page_view with page:<current-path>
|
|
92
|
+
page('pricing') // → page_view with page:pricing
|
|
93
|
+
page('/blog/post-1') // → page_view with page:/blog/post-1
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Useful when you want to control page-view timing yourself (e.g. after a custom route transition).
|
|
97
|
+
|
|
98
|
+
### `identify(userId, tags?)`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
identify('user_42');
|
|
102
|
+
// → emits { type: 'identify', tags: ['user:user_42', ...persistentTags] }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This is just a tag — it's the same as `track('identify', ['user:user_42'])`. The server doesn't have a separate user table; downstream dashboards can group by `user:*` tags.
|
|
106
|
+
|
|
107
|
+
### `setTags(tags)` / `addTag(tag)`
|
|
108
|
+
|
|
109
|
+
Replace / append to the persistent tag set.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
setTags(['app:marketing', 'variant:b']);
|
|
113
|
+
addTag('flow:onboarding');
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `flush({ useBeacon? })`
|
|
117
|
+
|
|
118
|
+
Manually flush the pending queue. Auto-flushes on:
|
|
119
|
+
- batch full (default 20 events)
|
|
120
|
+
- timer (default every 5s)
|
|
121
|
+
- `visibilitychange` to hidden
|
|
122
|
+
- `beforeunload`
|
|
123
|
+
- `pagehide`
|
|
124
|
+
|
|
125
|
+
You only need to call this yourself in tests or for at-exit guarantees.
|
|
126
|
+
|
|
127
|
+
## Auto-tags (server-side, zero-config)
|
|
128
|
+
|
|
129
|
+
Every event sent to the trackhome server is enriched with tags derived from the HTTP request — **callers never set these**:
|
|
130
|
+
|
|
131
|
+
| Tag | Source | Example |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `device:mobile` \| `device:desktop` \| `device:tablet` | User-Agent regex | `device:mobile` |
|
|
134
|
+
| `os:android` \| `os:ios` \| `os:macos` \| `os:windows` \| `os:linux` \| `os:chromeos` | User-Agent | `os:ios` |
|
|
135
|
+
| `country:xx` | `cf-ipcountry` / `x-vercel-ip-country` / `x-aws-region` (2-letter ISO) | `country:us` |
|
|
136
|
+
| `lang:xx` | `Accept-Language` (primary prefix) | `lang:en` |
|
|
137
|
+
|
|
138
|
+
This means a dashboard can break down traffic by device, OS, country, language **for free** — no extra client config. Caller-supplied tags of the same name win on collision.
|
|
139
|
+
|
|
140
|
+
## Server
|
|
141
|
+
|
|
142
|
+
This package is just the SDK. The server is at [github.com/exreve/trackhome](https://github.com/exreve/trackhome) — a single Docker image with Fastify + ClickHouse that serves the dashboard, the API, and (optionally) this SDK at `/t.js`. One-command deploy.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trackhome — tiny browser SDK for the trackhome analytics server.
|
|
3
|
+
*
|
|
4
|
+
* Simplest setup (auto-collects page views + clicks):
|
|
5
|
+
*
|
|
6
|
+
* import { init } from 'trackhome';
|
|
7
|
+
* init('https://trk.example.com');
|
|
8
|
+
*
|
|
9
|
+
* That's it. The SDK fires a page_view immediately, hooks SPA navigation,
|
|
10
|
+
* and flushes in the background. For custom events:
|
|
11
|
+
*
|
|
12
|
+
* import { track } from 'trackhome';
|
|
13
|
+
* track('signup_click', ['tier:pro']);
|
|
14
|
+
*
|
|
15
|
+
* Tags-only model: every event is {type, tags}. The visitor is identified
|
|
16
|
+
* by a server-set HttpOnly cookie (__tv), minted automatically on the first
|
|
17
|
+
* /ev call. JS never touches the cookie or any visitor id.
|
|
18
|
+
*
|
|
19
|
+
* Persistent tags: set via init({tags}) or setTags()/addTag(); they are
|
|
20
|
+
* auto-merged into every subsequent event. Per-event tags (2nd arg to track)
|
|
21
|
+
* layer on top.
|
|
22
|
+
*/
|
|
23
|
+
export interface TrackerConfig {
|
|
24
|
+
/** Base URL of your trackhome instance. Required. */
|
|
25
|
+
endpoint: string;
|
|
26
|
+
/** Tags applied to every event from this init. */
|
|
27
|
+
tags?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Auto-fire `page_view` events on init + on SPA route changes
|
|
30
|
+
* (popstate / hashchange / pushState / replaceState). Default: true.
|
|
31
|
+
*/
|
|
32
|
+
autoPageView?: boolean;
|
|
33
|
+
/** Max events per batch before a forced flush. Default: 20. */
|
|
34
|
+
batchSize?: number;
|
|
35
|
+
/** Max time (ms) between flushes. Default: 5000. */
|
|
36
|
+
flushIntervalMs?: number;
|
|
37
|
+
/** Disable SDK entirely (e.g. in dev). Default: false. */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Initialize the SDK. Two accepted shapes:
|
|
42
|
+
*
|
|
43
|
+
* init('https://trk.example.com') // shorthand: just the URL
|
|
44
|
+
* init({ endpoint: 'https://trk.example.com' }) // full config
|
|
45
|
+
*
|
|
46
|
+
* Idempotent — calling init() twice is a no-op.
|
|
47
|
+
*
|
|
48
|
+
* With `autoPageView` (default: true), the SDK:
|
|
49
|
+
* - fires a `page_view` event immediately for the current URL
|
|
50
|
+
* - hooks history.pushState / replaceState + popstate + hashchange
|
|
51
|
+
* - fires a `page_view` tagged `path:<current-path>` on every navigation
|
|
52
|
+
*
|
|
53
|
+
* That means many sites don't need to call track() at all for basic traffic
|
|
54
|
+
* analytics — init() alone is enough.
|
|
55
|
+
*/
|
|
56
|
+
export declare function init(userCfgOrUrl: string | TrackerConfig): void;
|
|
57
|
+
/** Replace the persistent tag set. */
|
|
58
|
+
export declare function setTags(tags: string[]): void;
|
|
59
|
+
/** Add a tag to the persistent set. */
|
|
60
|
+
export declare function addTag(tag: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Track an event. `tags` (optional) are merged with persistent tags.
|
|
63
|
+
*
|
|
64
|
+
* track('signup_click', ['tier:pro', 'cta:header']);
|
|
65
|
+
* track('page_view'); // tags optional
|
|
66
|
+
*/
|
|
67
|
+
export declare function track(type: string, tags?: string[]): void;
|
|
68
|
+
/**
|
|
69
|
+
* Shortcut: track('page_view', ['page:'+path]).
|
|
70
|
+
*
|
|
71
|
+
* With no args, uses the current path from window.location — useful for
|
|
72
|
+
* manual SPA hookups when autoPageView isn't right for your router.
|
|
73
|
+
*
|
|
74
|
+
* page() // → page_view with page:/current/path
|
|
75
|
+
* page('pricing') // → page_view with page:pricing
|
|
76
|
+
* page('/blog/post-1') // → page_view with page:/blog/post-1
|
|
77
|
+
*/
|
|
78
|
+
export declare function page(name?: string, tags?: string[]): void;
|
|
79
|
+
/** Shortcut: track('identify', ['user:'+userId]). */
|
|
80
|
+
export declare function identify(userId: string, tags?: string[]): void;
|
|
81
|
+
/** Manually flush the pending queue. Mostly useful in tests. */
|
|
82
|
+
export declare function flush(opts?: {
|
|
83
|
+
useBeacon?: boolean;
|
|
84
|
+
}): Promise<void>;
|
|
85
|
+
/** Expose a global for UMD <script> users. */
|
|
86
|
+
export declare const Tracker: {
|
|
87
|
+
init: typeof init;
|
|
88
|
+
track: typeof track;
|
|
89
|
+
page: typeof page;
|
|
90
|
+
identify: typeof identify;
|
|
91
|
+
setTags: typeof setTags;
|
|
92
|
+
addTag: typeof addTag;
|
|
93
|
+
flush: typeof flush;
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CA4C/D;AAED,sCAAsC;AACtC,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAG5C;AAED,uCAAuC;AACvC,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAGxC;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,IAAI,CAG7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,IAAI,CAG7D;AAED,qDAAqD;AACrD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,IAAI,CAElE;AAED,gEAAgE;AAChE,wBAAsB,KAAK,CAAC,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB7E;AAsDD,8CAA8C;AAC9C,eAAO,MAAM,OAAO;;;;;;;;CAA0D,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trackhome — tiny browser SDK for the trackhome analytics server.
|
|
3
|
+
*
|
|
4
|
+
* Simplest setup (auto-collects page views + clicks):
|
|
5
|
+
*
|
|
6
|
+
* import { init } from 'trackhome';
|
|
7
|
+
* init('https://trk.example.com');
|
|
8
|
+
*
|
|
9
|
+
* That's it. The SDK fires a page_view immediately, hooks SPA navigation,
|
|
10
|
+
* and flushes in the background. For custom events:
|
|
11
|
+
*
|
|
12
|
+
* import { track } from 'trackhome';
|
|
13
|
+
* track('signup_click', ['tier:pro']);
|
|
14
|
+
*
|
|
15
|
+
* Tags-only model: every event is {type, tags}. The visitor is identified
|
|
16
|
+
* by a server-set HttpOnly cookie (__tv), minted automatically on the first
|
|
17
|
+
* /ev call. JS never touches the cookie or any visitor id.
|
|
18
|
+
*
|
|
19
|
+
* Persistent tags: set via init({tags}) or setTags()/addTag(); they are
|
|
20
|
+
* auto-merged into every subsequent event. Per-event tags (2nd arg to track)
|
|
21
|
+
* layer on top.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULTS = {
|
|
24
|
+
batchSize: 20,
|
|
25
|
+
flushIntervalMs: 5000,
|
|
26
|
+
};
|
|
27
|
+
let cfg;
|
|
28
|
+
let queue = [];
|
|
29
|
+
let autoPageViewInstalled = false;
|
|
30
|
+
const isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the SDK. Two accepted shapes:
|
|
33
|
+
*
|
|
34
|
+
* init('https://trk.example.com') // shorthand: just the URL
|
|
35
|
+
* init({ endpoint: 'https://trk.example.com' }) // full config
|
|
36
|
+
*
|
|
37
|
+
* Idempotent — calling init() twice is a no-op.
|
|
38
|
+
*
|
|
39
|
+
* With `autoPageView` (default: true), the SDK:
|
|
40
|
+
* - fires a `page_view` event immediately for the current URL
|
|
41
|
+
* - hooks history.pushState / replaceState + popstate + hashchange
|
|
42
|
+
* - fires a `page_view` tagged `path:<current-path>` on every navigation
|
|
43
|
+
*
|
|
44
|
+
* That means many sites don't need to call track() at all for basic traffic
|
|
45
|
+
* analytics — init() alone is enough.
|
|
46
|
+
*/
|
|
47
|
+
export function init(userCfgOrUrl) {
|
|
48
|
+
if (!isBrowser)
|
|
49
|
+
return;
|
|
50
|
+
if (cfg)
|
|
51
|
+
return; // idempotent
|
|
52
|
+
const userCfg = typeof userCfgOrUrl === 'string' ? { endpoint: userCfgOrUrl } : userCfgOrUrl;
|
|
53
|
+
if (userCfg.disabled)
|
|
54
|
+
return;
|
|
55
|
+
if (!userCfg.endpoint) {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn('[trackhome] init: endpoint is required');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const c = {
|
|
61
|
+
endpoint: userCfg.endpoint.replace(/\/$/, ''),
|
|
62
|
+
autoPageView: userCfg.autoPageView ?? true,
|
|
63
|
+
batchSize: userCfg.batchSize ?? DEFAULTS.batchSize,
|
|
64
|
+
flushIntervalMs: userCfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,
|
|
65
|
+
tags: dedupe(userCfg.tags ?? []),
|
|
66
|
+
};
|
|
67
|
+
cfg = c;
|
|
68
|
+
// Background flush timer lives for the page lifetime. setInterval keeps the
|
|
69
|
+
// reference; we don't need to hold onto it.
|
|
70
|
+
setInterval(() => {
|
|
71
|
+
void flush().catch(() => { });
|
|
72
|
+
}, c.flushIntervalMs);
|
|
73
|
+
window.addEventListener('visibilitychange', () => {
|
|
74
|
+
if (document.visibilityState === 'hidden')
|
|
75
|
+
void flush({ useBeacon: true }).catch(() => { });
|
|
76
|
+
});
|
|
77
|
+
window.addEventListener('beforeunload', () => {
|
|
78
|
+
void flush({ useBeacon: true }).catch(() => { });
|
|
79
|
+
});
|
|
80
|
+
window.addEventListener('pagehide', () => {
|
|
81
|
+
void flush({ useBeacon: true }).catch(() => { });
|
|
82
|
+
});
|
|
83
|
+
if (c.autoPageView) {
|
|
84
|
+
installAutoPageView();
|
|
85
|
+
// Fire the initial page_view for the entry URL.
|
|
86
|
+
page();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Replace the persistent tag set. */
|
|
90
|
+
export function setTags(tags) {
|
|
91
|
+
if (!cfg)
|
|
92
|
+
return;
|
|
93
|
+
cfg.tags = dedupe(tags);
|
|
94
|
+
}
|
|
95
|
+
/** Add a tag to the persistent set. */
|
|
96
|
+
export function addTag(tag) {
|
|
97
|
+
if (!cfg)
|
|
98
|
+
return;
|
|
99
|
+
if (!cfg.tags.includes(tag))
|
|
100
|
+
cfg.tags = [...cfg.tags, tag];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Track an event. `tags` (optional) are merged with persistent tags.
|
|
104
|
+
*
|
|
105
|
+
* track('signup_click', ['tier:pro', 'cta:header']);
|
|
106
|
+
* track('page_view'); // tags optional
|
|
107
|
+
*/
|
|
108
|
+
export function track(type, tags = []) {
|
|
109
|
+
if (!cfg || !type)
|
|
110
|
+
return;
|
|
111
|
+
enqueue(type, tags);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Shortcut: track('page_view', ['page:'+path]).
|
|
115
|
+
*
|
|
116
|
+
* With no args, uses the current path from window.location — useful for
|
|
117
|
+
* manual SPA hookups when autoPageView isn't right for your router.
|
|
118
|
+
*
|
|
119
|
+
* page() // → page_view with page:/current/path
|
|
120
|
+
* page('pricing') // → page_view with page:pricing
|
|
121
|
+
* page('/blog/post-1') // → page_view with page:/blog/post-1
|
|
122
|
+
*/
|
|
123
|
+
export function page(name, tags = []) {
|
|
124
|
+
const path = name ?? (typeof location !== 'undefined' ? location.pathname : '');
|
|
125
|
+
track('page_view', [path ? `page:${path}` : '', ...tags].filter(Boolean));
|
|
126
|
+
}
|
|
127
|
+
/** Shortcut: track('identify', ['user:'+userId]). */
|
|
128
|
+
export function identify(userId, tags = []) {
|
|
129
|
+
track('identify', [`user:${userId}`, ...tags]);
|
|
130
|
+
}
|
|
131
|
+
/** Manually flush the pending queue. Mostly useful in tests. */
|
|
132
|
+
export async function flush(opts = {}) {
|
|
133
|
+
if (!cfg || queue.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const batch = queue.splice(0, queue.length);
|
|
136
|
+
const payload = JSON.stringify(batch);
|
|
137
|
+
try {
|
|
138
|
+
if (opts.useBeacon && navigator.sendBeacon) {
|
|
139
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
140
|
+
const ok = navigator.sendBeacon(`${cfg.endpoint}/ev`, blob);
|
|
141
|
+
if (ok)
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await fetch(`${cfg.endpoint}/ev`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'content-type': 'application/json' },
|
|
147
|
+
credentials: 'include',
|
|
148
|
+
keepalive: true,
|
|
149
|
+
body: payload,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
queue.unshift(...batch);
|
|
154
|
+
if (cfg && !opts.useBeacon) {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.warn('[trackhome] flush failed', err);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ============================================================
|
|
161
|
+
// Internal: queue + SPA page_view hook
|
|
162
|
+
// ============================================================
|
|
163
|
+
function enqueue(type, tags) {
|
|
164
|
+
queue.push({ type, tags: dedupe([...(cfg?.tags ?? []), ...tags]) });
|
|
165
|
+
if (queue.length >= (cfg?.batchSize ?? DEFAULTS.batchSize)) {
|
|
166
|
+
void flush().catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function dedupe(tags) {
|
|
170
|
+
return Array.from(new Set(tags.filter((t) => typeof t === 'string' && t.length > 0)));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Hook SPA navigation so autoPageView fires on pushState/replaceState too.
|
|
174
|
+
* Most frameworks (React Router, Vue Router, Next.js) use the History API,
|
|
175
|
+
* so this catches their navigations without framework-specific glue.
|
|
176
|
+
*/
|
|
177
|
+
function installAutoPageView() {
|
|
178
|
+
if (autoPageViewInstalled || typeof history === 'undefined')
|
|
179
|
+
return;
|
|
180
|
+
autoPageViewInstalled = true;
|
|
181
|
+
let lastPath = typeof location !== 'undefined' ? location.pathname : '';
|
|
182
|
+
const onRouteChange = () => {
|
|
183
|
+
if (!cfg?.autoPageView)
|
|
184
|
+
return;
|
|
185
|
+
const path = typeof location !== 'undefined' ? location.pathname : '';
|
|
186
|
+
if (path === lastPath)
|
|
187
|
+
return;
|
|
188
|
+
lastPath = path;
|
|
189
|
+
page();
|
|
190
|
+
};
|
|
191
|
+
// Wrap pushState + replaceState so we get a callback after the URL changes.
|
|
192
|
+
for (const method of ['pushState', 'replaceState']) {
|
|
193
|
+
const original = history[method];
|
|
194
|
+
history[method] = function (...args) {
|
|
195
|
+
const ret = original.apply(this, args);
|
|
196
|
+
// Defer to let the browser update location before we read it.
|
|
197
|
+
setTimeout(onRouteChange, 0);
|
|
198
|
+
return ret;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
window.addEventListener('popstate', onRouteChange);
|
|
202
|
+
window.addEventListener('hashchange', onRouteChange);
|
|
203
|
+
}
|
|
204
|
+
/** Expose a global for UMD <script> users. */
|
|
205
|
+
export const Tracker = { init, track, page, identify, setTags, addTag, flush };
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/t.umd.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(a,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(a=typeof globalThis<"u"?globalThis:a||self,o(a.Tracker={}))})(this,function(a){"use strict";const o={batchSize:20,flushIntervalMs:5e3};let n,c=[],l=!1;const w=typeof window<"u"&&typeof navigator<"u";function h(e){if(!w||n)return;const t=typeof e=="string"?{endpoint:e}:e;if(t.disabled)return;if(!t.endpoint){console.warn("[trackhome] init: endpoint is required");return}const i={endpoint:t.endpoint.replace(/\/$/,""),autoPageView:t.autoPageView??!0,batchSize:t.batchSize??o.batchSize,flushIntervalMs:t.flushIntervalMs??o.flushIntervalMs,tags:f(t.tags??[])};n=i,setInterval(()=>{s().catch(()=>{})},i.flushIntervalMs),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&s({useBeacon:!0}).catch(()=>{})}),window.addEventListener("beforeunload",()=>{s({useBeacon:!0}).catch(()=>{})}),window.addEventListener("pagehide",()=>{s({useBeacon:!0}).catch(()=>{})}),i.autoPageView&&(S(),r())}function p(e){n&&(n.tags=f(e))}function g(e){n&&(n.tags.includes(e)||(n.tags=[...n.tags,e]))}function u(e,t=[]){!n||!e||b(e,t)}function r(e,t=[]){const i=e??(typeof location<"u"?location.pathname:"");u("page_view",[i?`page:${i}`:"",...t].filter(Boolean))}function y(e,t=[]){u("identify",[`user:${e}`,...t])}async function s(e={}){if(!n||c.length===0)return;const t=c.splice(0,c.length),i=JSON.stringify(t);try{if(e.useBeacon&&navigator.sendBeacon){const d=new Blob([i],{type:"application/json"});if(navigator.sendBeacon(`${n.endpoint}/ev`,d))return}await fetch(`${n.endpoint}/ev`,{method:"POST",headers:{"content-type":"application/json"},credentials:"include",keepalive:!0,body:i})}catch(d){c.unshift(...t),n&&!e.useBeacon&&console.warn("[trackhome] flush failed",d)}}function b(e,t){c.push({type:e,tags:f([...(n==null?void 0:n.tags)??[],...t])}),c.length>=((n==null?void 0:n.batchSize)??o.batchSize)&&s().catch(()=>{})}function f(e){return Array.from(new Set(e.filter(t=>typeof t=="string"&&t.length>0)))}function S(){if(l||typeof history>"u")return;l=!0;let e=typeof location<"u"?location.pathname:"";const t=()=>{if(!(n!=null&&n.autoPageView))return;const i=typeof location<"u"?location.pathname:"";i!==e&&(e=i,r())};for(const i of["pushState","replaceState"]){const d=history[i];history[i]=function(...v){const T=d.apply(this,v);return setTimeout(t,0),T}}window.addEventListener("popstate",t),window.addEventListener("hashchange",t)}const m={init:h,track:u,page:r,identify:y,setTags:p,addTag:g,flush:s};a.Tracker=m,a.addTag=g,a.flush=s,a.identify=y,a.init=h,a.page=r,a.setTags=p,a.track=u,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})});
|
|
2
|
+
//# sourceMappingURL=t.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"t.umd.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * trackhome — tiny browser SDK for the trackhome analytics server.\n *\n * Simplest setup (auto-collects page views + clicks):\n *\n * import { init } from 'trackhome';\n * init('https://trk.example.com');\n *\n * That's it. The SDK fires a page_view immediately, hooks SPA navigation,\n * and flushes in the background. For custom events:\n *\n * import { track } from 'trackhome';\n * track('signup_click', ['tier:pro']);\n *\n * Tags-only model: every event is {type, tags}. The visitor is identified\n * by a server-set HttpOnly cookie (__tv), minted automatically on the first\n * /ev call. JS never touches the cookie or any visitor id.\n *\n * Persistent tags: set via init({tags}) or setTags()/addTag(); they are\n * auto-merged into every subsequent event. Per-event tags (2nd arg to track)\n * layer on top.\n */\n\nexport interface TrackerConfig {\n /** Base URL of your trackhome instance. Required. */\n endpoint: string;\n /** Tags applied to every event from this init. */\n tags?: string[];\n /**\n * Auto-fire `page_view` events on init + on SPA route changes\n * (popstate / hashchange / pushState / replaceState). Default: true.\n */\n autoPageView?: boolean;\n /** Max events per batch before a forced flush. Default: 20. */\n batchSize?: number;\n /** Max time (ms) between flushes. Default: 5000. */\n flushIntervalMs?: number;\n /** Disable SDK entirely (e.g. in dev). Default: false. */\n disabled?: boolean;\n}\n\ninterface PendingEvent {\n type: string;\n tags: string[];\n ts?: string;\n}\n\nconst DEFAULTS = {\n batchSize: 20,\n flushIntervalMs: 5_000,\n} as const;\n\nlet cfg:\n | (Omit<TrackerConfig, 'tags' | 'disabled'> & { tags: string[] })\n | undefined;\nlet queue: PendingEvent[] = [];\nlet autoPageViewInstalled = false;\n\nconst isBrowser = typeof window !== 'undefined' && typeof navigator !== 'undefined';\n\n/**\n * Initialize the SDK. Two accepted shapes:\n *\n * init('https://trk.example.com') // shorthand: just the URL\n * init({ endpoint: 'https://trk.example.com' }) // full config\n *\n * Idempotent — calling init() twice is a no-op.\n *\n * With `autoPageView` (default: true), the SDK:\n * - fires a `page_view` event immediately for the current URL\n * - hooks history.pushState / replaceState + popstate + hashchange\n * - fires a `page_view` tagged `path:<current-path>` on every navigation\n *\n * That means many sites don't need to call track() at all for basic traffic\n * analytics — init() alone is enough.\n */\nexport function init(userCfgOrUrl: string | TrackerConfig): void {\n if (!isBrowser) return;\n if (cfg) return; // idempotent\n\n const userCfg: TrackerConfig =\n typeof userCfgOrUrl === 'string' ? { endpoint: userCfgOrUrl } : userCfgOrUrl;\n\n if (userCfg.disabled) return;\n if (!userCfg.endpoint) {\n // eslint-disable-next-line no-console\n console.warn('[trackhome] init: endpoint is required');\n return;\n }\n\n const c: Omit<TrackerConfig, 'tags' | 'disabled'> & { tags: string[] } = {\n endpoint: userCfg.endpoint.replace(/\\/$/, ''),\n autoPageView: userCfg.autoPageView ?? true,\n batchSize: userCfg.batchSize ?? DEFAULTS.batchSize,\n flushIntervalMs: userCfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,\n tags: dedupe(userCfg.tags ?? []),\n };\n cfg = c;\n\n // Background flush timer lives for the page lifetime. setInterval keeps the\n // reference; we don't need to hold onto it.\n setInterval(() => {\n void flush().catch(() => {});\n }, c.flushIntervalMs);\n\n window.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') void flush({ useBeacon: true }).catch(() => {});\n });\n window.addEventListener('beforeunload', () => {\n void flush({ useBeacon: true }).catch(() => {});\n });\n window.addEventListener('pagehide', () => {\n void flush({ useBeacon: true }).catch(() => {});\n });\n\n if (c.autoPageView) {\n installAutoPageView();\n // Fire the initial page_view for the entry URL.\n page();\n }\n}\n\n/** Replace the persistent tag set. */\nexport function setTags(tags: string[]): void {\n if (!cfg) return;\n cfg.tags = dedupe(tags);\n}\n\n/** Add a tag to the persistent set. */\nexport function addTag(tag: string): void {\n if (!cfg) return;\n if (!cfg.tags.includes(tag)) cfg.tags = [...cfg.tags, tag];\n}\n\n/**\n * Track an event. `tags` (optional) are merged with persistent tags.\n *\n * track('signup_click', ['tier:pro', 'cta:header']);\n * track('page_view'); // tags optional\n */\nexport function track(type: string, tags: string[] = []): void {\n if (!cfg || !type) return;\n enqueue(type, tags);\n}\n\n/**\n * Shortcut: track('page_view', ['page:'+path]).\n *\n * With no args, uses the current path from window.location — useful for\n * manual SPA hookups when autoPageView isn't right for your router.\n *\n * page() // → page_view with page:/current/path\n * page('pricing') // → page_view with page:pricing\n * page('/blog/post-1') // → page_view with page:/blog/post-1\n */\nexport function page(name?: string, tags: string[] = []): void {\n const path = name ?? (typeof location !== 'undefined' ? location.pathname : '');\n track('page_view', [path ? `page:${path}` : '', ...tags].filter(Boolean));\n}\n\n/** Shortcut: track('identify', ['user:'+userId]). */\nexport function identify(userId: string, tags: string[] = []): void {\n track('identify', [`user:${userId}`, ...tags]);\n}\n\n/** Manually flush the pending queue. Mostly useful in tests. */\nexport async function flush(opts: { useBeacon?: boolean } = {}): Promise<void> {\n if (!cfg || queue.length === 0) return;\n const batch = queue.splice(0, queue.length);\n const payload = JSON.stringify(batch);\n try {\n if (opts.useBeacon && navigator.sendBeacon) {\n const blob = new Blob([payload], { type: 'application/json' });\n const ok = navigator.sendBeacon(`${cfg.endpoint}/ev`, blob);\n if (ok) return;\n }\n await fetch(`${cfg.endpoint}/ev`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n credentials: 'include',\n keepalive: true,\n body: payload,\n });\n } catch (err) {\n queue.unshift(...batch);\n if (cfg && !opts.useBeacon) {\n // eslint-disable-next-line no-console\n console.warn('[trackhome] flush failed', err);\n }\n }\n}\n\n// ============================================================\n// Internal: queue + SPA page_view hook\n// ============================================================\n\nfunction enqueue(type: string, tags: string[]): void {\n queue.push({ type, tags: dedupe([...(cfg?.tags ?? []), ...tags]) });\n if (queue.length >= (cfg?.batchSize ?? DEFAULTS.batchSize)) {\n void flush().catch(() => {});\n }\n}\n\nfunction dedupe(tags: string[]): string[] {\n return Array.from(new Set(tags.filter((t) => typeof t === 'string' && t.length > 0)));\n}\n\n/**\n * Hook SPA navigation so autoPageView fires on pushState/replaceState too.\n * Most frameworks (React Router, Vue Router, Next.js) use the History API,\n * so this catches their navigations without framework-specific glue.\n */\nfunction installAutoPageView(): void {\n if (autoPageViewInstalled || typeof history === 'undefined') return;\n autoPageViewInstalled = true;\n\n let lastPath = typeof location !== 'undefined' ? location.pathname : '';\n\n const onRouteChange = () => {\n if (!cfg?.autoPageView) return;\n const path = typeof location !== 'undefined' ? location.pathname : '';\n if (path === lastPath) return;\n lastPath = path;\n page();\n };\n\n // Wrap pushState + replaceState so we get a callback after the URL changes.\n for (const method of ['pushState', 'replaceState'] as const) {\n const original = history[method] as (\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ) => void;\n history[method] = function (...args: Parameters<typeof original>) {\n const ret = original.apply(this, args);\n // Defer to let the browser update location before we read it.\n setTimeout(onRouteChange, 0);\n return ret;\n };\n }\n window.addEventListener('popstate', onRouteChange);\n window.addEventListener('hashchange', onRouteChange);\n}\n\n/** Expose a global for UMD <script> users. */\nexport const Tracker = { init, track, page, identify, setTags, addTag, flush };\n"],"names":["DEFAULTS","cfg","queue","autoPageViewInstalled","isBrowser","init","userCfgOrUrl","userCfg","c","dedupe","flush","installAutoPageView","page","setTags","tags","addTag","tag","track","type","enqueue","name","path","identify","userId","opts","batch","payload","blob","err","lastPath","onRouteChange","method","original","args","ret","Tracker"],"mappings":"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"}
|
package/dist/umd.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"umd.d.ts","sourceRoot":"","sources":["../src/umd.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC"}
|
package/dist/umd.js
ADDED
package/dist/umd.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"umd.js","sourceRoot":"","sources":["../src/umd.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trackhome",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny browser SDK for trackhome — a self-hosted, tags-only event/click tracker.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"analytics",
|
|
7
|
+
"tracking",
|
|
8
|
+
"events",
|
|
9
|
+
"click-tracking",
|
|
10
|
+
"self-hosted",
|
|
11
|
+
"privacy",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/exreve/trackhome",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/exreve/trackhome.git",
|
|
18
|
+
"directory": "packages/sdk"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "exreve",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"browser": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/t.umd.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./umd": {
|
|
35
|
+
"browser": "./dist/t.umd.js",
|
|
36
|
+
"default": "./dist/t.umd.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": ["dist", "README.md"],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json && vite build",
|
|
43
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
44
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
45
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
+
"prepublishOnly": "pnpm run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"typescript": "^5.5.0",
|
|
50
|
+
"vite": "^5.3.0"
|
|
51
|
+
}
|
|
52
|
+
}
|