@casoon/trackr 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +117 -49
  2. package/dist/chunk-7UN7MXBM.js +141 -0
  3. package/dist/chunk-7UN7MXBM.js.map +1 -0
  4. package/dist/{chunk-ABUPERUQ.js → chunk-AOB662OQ.js} +34 -3
  5. package/dist/chunk-AOB662OQ.js.map +1 -0
  6. package/dist/chunk-PEAZRYH7.js +78 -0
  7. package/dist/chunk-PEAZRYH7.js.map +1 -0
  8. package/dist/client/index.d.ts +1 -1
  9. package/dist/client/index.js +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +3 -2
  12. package/dist/server/index.d.ts +5 -2
  13. package/dist/server/index.js +8 -2
  14. package/dist/server/pixel.d.ts +5 -0
  15. package/dist/server/pixel.js +97 -0
  16. package/dist/server/pixel.js.map +1 -0
  17. package/dist/storage/api.d.ts +1 -1
  18. package/dist/storage/api.js.map +1 -1
  19. package/dist/storage/batch.d.ts +19 -0
  20. package/dist/storage/batch.js +59 -0
  21. package/dist/storage/batch.js.map +1 -0
  22. package/dist/storage/ga4.d.ts +47 -0
  23. package/dist/storage/ga4.js +131 -0
  24. package/dist/storage/ga4.js.map +1 -0
  25. package/dist/storage/multi.d.ts +21 -0
  26. package/dist/storage/multi.js +14 -0
  27. package/dist/storage/multi.js.map +1 -0
  28. package/dist/storage/postgres.d.ts +1 -1
  29. package/dist/storage/postgres.js +3 -1
  30. package/dist/storage/postgres.js.map +1 -1
  31. package/dist/storage/webhook.d.ts +22 -0
  32. package/dist/storage/webhook.js +54 -0
  33. package/dist/storage/webhook.js.map +1 -0
  34. package/dist/{types-EaeYBDKE.d.ts → types-CceMQIhZ.d.ts} +3 -1
  35. package/package.json +26 -2
  36. package/script.js +1 -0
  37. package/dist/chunk-ABUPERUQ.js.map +0 -1
  38. package/dist/chunk-L3N32JO4.js +0 -153
  39. package/dist/chunk-L3N32JO4.js.map +0 -1
package/README.md CHANGED
@@ -2,18 +2,35 @@
2
2
 
3
3
  Privacy-first, GDPR-native analytics for static sites. No cookies, no persistent IDs, self-hosted.
4
4
 
5
+ ## Live Demo
6
+
7
+ - [Homepage (Pageview)](https://trackr-demo.casoon.dev/)
8
+ - [Shop (E-Commerce Events)](https://trackr-demo.casoon.dev/shop)
9
+ - [Landing (Lead Forms)](https://trackr-demo.casoon.dev/landing)
10
+ - [Stats (Raw Data)](https://trackr-demo.casoon.dev/stats)
11
+
12
+ **UTM-Parameter Test:**
13
+ - [With UTM params](https://trackr-demo.casoon.dev/?utm_source=github&utm_medium=readme&utm_campaign=trackr)
14
+
5
15
  ## Features
6
16
 
7
17
  - **Privacy by Default** - No cookies, no localStorage, no fingerprinting
8
18
  - **Lightweight** - Client script < 1KB gzipped
9
19
  - **Self-Hosted** - Your data stays on your infrastructure
20
+ - **UTM Tracking** - Automatic extraction of campaign parameters
21
+ - **SPA Support** - Auto-tracks `pushState`/`replaceState`/`hashchange` navigation
22
+ - **OS Detection** - Server-side detection (Android, iOS, Windows, macOS, Linux, ChromeOS)
23
+ - **Bot Filtering** - Common crawlers and headless browsers excluded
24
+ - **Flexible Storage** - Postgres, external API, GA4 Measurement Protocol, or fan-out multi-adapter
25
+ - **Webhook** - Forward events to any HTTP endpoint with HMAC-SHA256 signing and retry
26
+ - **Batching** - Buffer events and flush in batches (size- or time-based) to reduce network calls
27
+ - **Pixel Tracking** - Transparent GIF endpoint for email/no-JS contexts
10
28
  - **Astro-First** - Designed for Astro, works with any static site
11
- - **Flexible Storage** - Postgres or external API (Plausible, Umami)
12
29
 
13
30
  ## Installation
14
31
 
15
32
  ```bash
16
- pnpm add @casoon/trackr
33
+ npm install @casoon/trackr
17
34
  ```
18
35
 
19
36
  ## Quick Start
@@ -30,40 +47,29 @@ pnpm add @casoon/trackr
30
47
  </script>
31
48
  ```
32
49
 
33
- Include in your layout:
50
+ Or via CDN (no build step):
34
51
 
35
- ```astro
36
- ---
37
- import Analytics from "../components/Analytics.astro";
38
- ---
39
- <html>
40
- <body>
41
- <slot />
42
- <Analytics />
43
- </body>
44
- </html>
52
+ ```html
53
+ <script src="https://unpkg.com/@casoon/trackr"></script>
54
+ <script>
55
+ trackr.init({ siteId: "your-site-id", endpoint: "https://your-api.com/collect" });
56
+ </script>
45
57
  ```
46
58
 
47
59
  ### 2. Create the API endpoint
48
60
 
49
61
  ```typescript
50
62
  // src/pages/api/track.ts
51
- import type { APIRoute } from "astro";
52
63
  import { createHandler } from "@casoon/trackr/server";
53
64
  import { postgres } from "@casoon/trackr/storage/postgres";
54
65
 
55
66
  const handler = createHandler({
56
67
  storage: postgres(import.meta.env.DATABASE_URL),
57
- privacy: {
58
- anonymizeIp: true,
59
- stripPii: true
60
- },
68
+ privacy: { anonymizeIp: true, stripPii: true },
61
69
  botFilter: true
62
70
  });
63
71
 
64
- export const POST: APIRoute = async ({ request }) => {
65
- return handler(request);
66
- };
72
+ export const POST = async ({ request }) => handler(request);
67
73
  ```
68
74
 
69
75
  ### 3. Set up the database
@@ -76,15 +82,8 @@ CREATE TABLE trackr_events (
76
82
  name TEXT,
77
83
  url TEXT NOT NULL,
78
84
  referrer_domain TEXT,
79
- country TEXT,
80
- device TEXT,
81
- browser TEXT,
82
- session_id TEXT,
83
85
  props JSONB DEFAULT '{}'
84
86
  );
85
-
86
- CREATE INDEX idx_trackr_ts ON trackr_events (ts);
87
- CREATE INDEX idx_trackr_url ON trackr_events (url);
88
87
  ```
89
88
 
90
89
  ## Custom Events
@@ -92,51 +91,120 @@ CREATE INDEX idx_trackr_url ON trackr_events (url);
92
91
  ```typescript
93
92
  import { track } from "@casoon/trackr/client";
94
93
 
95
- // Track button click
96
- document.querySelector("#signup").addEventListener("click", () => {
97
- track("signup_click", { plan: "pro" });
98
- });
94
+ track("signup_click", { plan: "pro" });
99
95
  ```
100
96
 
101
97
  ## Storage Adapters
102
98
 
103
- ### Postgres
99
+ ### Postgres (recommended for GDPR compliance)
104
100
 
105
101
  ```typescript
106
102
  import { postgres } from "@casoon/trackr/storage/postgres";
107
-
108
103
  const storage = postgres(process.env.DATABASE_URL);
109
104
  ```
110
105
 
111
106
  ### External API
112
107
 
113
- Forward events to Plausible, Umami, or any other service:
114
-
115
108
  ```typescript
116
109
  import { api } from "@casoon/trackr/storage/api";
117
-
118
110
  const storage = api({
119
111
  url: "https://plausible.io/api/event",
120
- headers: { "Authorization": "Bearer ..." },
121
- transform: (event) => ({
122
- name: event.type === "pageview" ? "pageview" : event.name,
123
- url: event.url,
124
- domain: "your-domain.com"
125
- })
112
+ transform: (event) => ({ ... })
113
+ });
114
+ ```
115
+
116
+ ### GA4 Measurement Protocol (optional, privacy proxy)
117
+
118
+ Forwards events server-side — the GA script never loads in the user's browser.
119
+
120
+ ```typescript
121
+ import { ga4 } from "@casoon/trackr/storage/ga4";
122
+ const storage = ga4({
123
+ measurementId: "G-XXXXXXXXXX",
124
+ apiSecret: process.env.GA4_API_SECRET,
125
+ nonPersonalizedAds: true, // default true
126
+ stripQueryParams: true, // strip query strings from URLs
127
+ debug: false
128
+ });
129
+ ```
130
+
131
+ > **GDPR note:** GA4 forwarding sends anonymized session IDs. Enable only if you have a legal basis or user consent for GA4 data transfer to Google's US servers.
132
+
133
+ ### Webhook
134
+
135
+ Forward events to any HTTP endpoint. Supports HMAC-SHA256 payload signing and retry with exponential backoff.
136
+
137
+ ```typescript
138
+ import { webhook } from "@casoon/trackr/storage/webhook";
139
+
140
+ const storage = webhook({
141
+ url: "https://api.example.com/events",
142
+ secret: process.env.WEBHOOK_SECRET, // signs payload → X-Trackr-Signature header
143
+ headers: { Authorization: "Bearer ..." },
144
+ retry: { attempts: 3, baseDelay: 500 }, // retries on 5xx with exponential backoff
126
145
  });
127
146
  ```
128
147
 
148
+ ### Multi (fan-out to multiple adapters)
149
+
150
+ ```typescript
151
+ import { multi } from "@casoon/trackr/storage/multi";
152
+ import { postgres } from "@casoon/trackr/storage/postgres";
153
+ import { ga4 } from "@casoon/trackr/storage/ga4";
154
+
155
+ const storage = multi(
156
+ postgres(process.env.DATABASE_URL),
157
+ ga4({ measurementId: "G-XXXXXXXXXX", apiSecret: process.env.GA4_API_SECRET })
158
+ );
159
+ ```
160
+
161
+ ### Batch (buffer & flush)
162
+
163
+ Wraps any adapter. Buffers events in memory and flushes on size threshold or time interval. Uses `saveBatch()` when the wrapped adapter supports it (e.g. webhook), otherwise calls `save()` for each event.
164
+
165
+ ```typescript
166
+ import { batch } from "@casoon/trackr/storage/batch";
167
+ import { webhook } from "@casoon/trackr/storage/webhook";
168
+
169
+ const storage = batch(
170
+ webhook({ url: "https://api.example.com/events", secret: "s3cret" }),
171
+ { maxSize: 20, maxWait: 10_000 } // flush every 20 events or 10s
172
+ );
173
+
174
+ // Graceful shutdown
175
+ process.on("SIGTERM", () => storage.flush());
176
+ ```
177
+
178
+ ## Pixel Tracking
179
+
180
+ For email open tracking or no-JS environments:
181
+
182
+ ```typescript
183
+ import { createPixelHandler } from "@casoon/trackr/server/pixel";
184
+ import { postgres } from "@casoon/trackr/storage/postgres";
185
+
186
+ const pixelHandler = createPixelHandler({
187
+ storage: postgres(process.env.DATABASE_URL),
188
+ privacy: { anonymizeIp: true }
189
+ });
190
+
191
+ // Returns a transparent 1x1 GIF + records a pageview
192
+ export const GET = async ({ request }) => pixelHandler(request);
193
+ ```
194
+
195
+ ```html
196
+ <img src="https://your-api.com/pixel.gif?url=https://your-site.com/page" width="1" height="1" />
197
+ ```
198
+
129
199
  ## Privacy Features
130
200
 
131
201
  - **IP Anonymization** - Last octet removed before any processing
132
202
  - **PII Filtering** - Email, phone, tokens stripped from URLs
133
- - **No Cookies** - Session ID derived from anonymized IP + UA + date
134
- - **Bot Filtering** - Common bots and crawlers excluded
203
+ - **No Cookies** - Session derived from anonymized IP + UA + date (daily rotating)
204
+ - **Bot Filtering** - Common crawlers excluded
205
+ - **UTM Extraction** - Campaign params captured client-side (prefix stripped: `utm_source` → `source`)
206
+ - **OS Detection** - Server-side from User-Agent, not stored raw
135
207
 
136
208
  ## License
137
209
 
138
210
  LGPL-3.0-or-later
139
-
140
- ## Author
141
-
142
- Joern Seidel <joern@casoon.de>
@@ -0,0 +1,141 @@
1
+ // src/server/bot.ts
2
+ var BOT_PATTERNS = [
3
+ /bot/i,
4
+ /crawler/i,
5
+ /spider/i,
6
+ /crawling/i,
7
+ /headless/i,
8
+ /phantom/i,
9
+ /selenium/i,
10
+ /google/i,
11
+ /bing/i,
12
+ /yahoo/i,
13
+ /baidu/i,
14
+ /facebook/i,
15
+ /twitter/i,
16
+ /linkedin/i,
17
+ /slack/i,
18
+ /discord/i,
19
+ /telegram/i,
20
+ /preview/i,
21
+ /curl/i,
22
+ /wget/i,
23
+ /monitoring/i,
24
+ /uptime/i,
25
+ /pingdom/i
26
+ ];
27
+ function isBot(request) {
28
+ const ua = request.headers.get("user-agent") || "";
29
+ if (BOT_PATTERNS.some((p) => p.test(ua))) return true;
30
+ if (!request.headers.get("accept-language")) return true;
31
+ if (ua.length < 20) return true;
32
+ return false;
33
+ }
34
+
35
+ // src/server/privacy.ts
36
+ var DEFAULT_PRIVACY_CONFIG = {
37
+ anonymizeIp: true,
38
+ stripPii: true
39
+ };
40
+ function resolvePrivacyConfig(config) {
41
+ return { ...DEFAULT_PRIVACY_CONFIG, ...config };
42
+ }
43
+ var PII_KEY_PATTERNS = [
44
+ /^e?mail$/i,
45
+ /^phone$/i,
46
+ /^(first|last|full|user|display)[_-]?name$/i,
47
+ /^name$/i,
48
+ /^(token|key|password|passwd|pass|secret|api[_-]?key)$/i,
49
+ /^(address|street|city|zip|postal[_-]?code)$/i,
50
+ /^(user[_-]?id|uid|login|username|user)$/i,
51
+ /^(ssn|social|tax[_-]?id|national[_-]?id)$/i,
52
+ /^(credit[_-]?card|card[_-]?number|cvv|ccn?)$/i,
53
+ /^(ip[_-]?address)$/i,
54
+ /^(dob|date[_-]?of[_-]?birth|birthday)$/i
55
+ ];
56
+ function isPiiKey(key) {
57
+ return PII_KEY_PATTERNS.some((p) => p.test(key));
58
+ }
59
+ var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
60
+ function anonymizeIp(ip) {
61
+ if (ip.includes(".")) {
62
+ return `${ip.split(".").slice(0, 3).join(".")}.0`;
63
+ }
64
+ return `${ip.split(":").slice(0, 4).join(":")}::`;
65
+ }
66
+ function stripPii(url) {
67
+ try {
68
+ const u = new URL(url, "http://localhost");
69
+ const keysToDelete = [...u.searchParams.keys()].filter(isPiiKey);
70
+ for (const k of keysToDelete) {
71
+ u.searchParams.delete(k);
72
+ }
73
+ const pathname = u.pathname.replace(EMAIL_PATTERN, "[redacted]");
74
+ return pathname + (u.search || "");
75
+ } catch {
76
+ return url;
77
+ }
78
+ }
79
+ async function createSessionId(ip, ua, date) {
80
+ const input = `${anonymizeIp(ip)}|${ua}|${date}`;
81
+ const data = new TextEncoder().encode(input);
82
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
83
+ const hashArray = new Uint8Array(hashBuffer);
84
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
85
+ }
86
+ function sanitizeProps(props) {
87
+ const result = {};
88
+ for (const [key, value] of Object.entries(props)) {
89
+ if (isPiiKey(key)) {
90
+ result[key] = "[redacted]";
91
+ continue;
92
+ }
93
+ if (typeof value === "string") {
94
+ result[key] = value.replace(EMAIL_PATTERN, "[redacted]");
95
+ } else {
96
+ result[key] = value;
97
+ }
98
+ }
99
+ return result;
100
+ }
101
+ function applyPrivacy(event, config) {
102
+ const result = { ...event };
103
+ if (config.stripPii && result.url) {
104
+ result.url = stripPii(result.url);
105
+ }
106
+ if (config.stripPii && result.props) {
107
+ result.props = sanitizeProps(result.props);
108
+ }
109
+ if (config.stripQueryParams && result.url) {
110
+ try {
111
+ const u = new URL(result.url, "http://localhost");
112
+ for (const p of config.stripQueryParams) {
113
+ if (p.endsWith("*")) {
114
+ const prefix = p.slice(0, -1);
115
+ const keysToDelete = [...u.searchParams.keys()].filter(
116
+ (k) => k.startsWith(prefix)
117
+ );
118
+ for (const k of keysToDelete) {
119
+ u.searchParams.delete(k);
120
+ }
121
+ } else {
122
+ u.searchParams.delete(p);
123
+ }
124
+ }
125
+ result.url = u.pathname + (u.search || "");
126
+ } catch {
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+
132
+ export {
133
+ isBot,
134
+ resolvePrivacyConfig,
135
+ anonymizeIp,
136
+ stripPii,
137
+ createSessionId,
138
+ sanitizeProps,
139
+ applyPrivacy
140
+ };
141
+ //# sourceMappingURL=chunk-7UN7MXBM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/bot.ts","../src/server/privacy.ts"],"sourcesContent":["const BOT_PATTERNS = [\n /bot/i,\n /crawler/i,\n /spider/i,\n /crawling/i,\n /headless/i,\n /phantom/i,\n /selenium/i,\n /google/i,\n /bing/i,\n /yahoo/i,\n /baidu/i,\n /facebook/i,\n /twitter/i,\n /linkedin/i,\n /slack/i,\n /discord/i,\n /telegram/i,\n /preview/i,\n /curl/i,\n /wget/i,\n /monitoring/i,\n /uptime/i,\n /pingdom/i,\n];\n\nexport function isBot(request: Request): boolean {\n const ua = request.headers.get(\"user-agent\") || \"\";\n\n if (BOT_PATTERNS.some((p) => p.test(ua))) return true;\n if (!request.headers.get(\"accept-language\")) return true;\n if (ua.length < 20) return true;\n\n return false;\n}\n","import type { PrivacyConfig, TrackrEvent } from \"../types.js\";\n\n/** Default privacy config — all protections ON. */\nexport const DEFAULT_PRIVACY_CONFIG: PrivacyConfig = {\n anonymizeIp: true,\n stripPii: true,\n};\n\n/** Merge caller config with safe defaults. */\nexport function resolvePrivacyConfig(config?: PrivacyConfig): PrivacyConfig {\n return { ...DEFAULT_PRIVACY_CONFIG, ...config };\n}\n\n/** Regex patterns that identify PII-bearing parameter/key names. */\nconst PII_KEY_PATTERNS: RegExp[] = [\n /^e?mail$/i,\n /^phone$/i,\n /^(first|last|full|user|display)[_-]?name$/i,\n /^name$/i,\n /^(token|key|password|passwd|pass|secret|api[_-]?key)$/i,\n /^(address|street|city|zip|postal[_-]?code)$/i,\n /^(user[_-]?id|uid|login|username|user)$/i,\n /^(ssn|social|tax[_-]?id|national[_-]?id)$/i,\n /^(credit[_-]?card|card[_-]?number|cvv|ccn?)$/i,\n /^(ip[_-]?address)$/i,\n /^(dob|date[_-]?of[_-]?birth|birthday)$/i,\n];\n\nfunction isPiiKey(key: string): boolean {\n return PII_KEY_PATTERNS.some((p) => p.test(key));\n}\n\nconst EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\n\nexport function anonymizeIp(ip: string): string {\n if (ip.includes(\".\")) {\n return `${ip.split(\".\").slice(0, 3).join(\".\")}.0`;\n }\n return `${ip.split(\":\").slice(0, 4).join(\":\")}::`;\n}\n\nexport function stripPii(url: string): string {\n try {\n const u = new URL(url, \"http://localhost\");\n\n // Remove query params whose key matches a PII pattern\n const keysToDelete = [...u.searchParams.keys()].filter(isPiiKey);\n for (const k of keysToDelete) {\n u.searchParams.delete(k);\n }\n\n // Redact email patterns embedded in URL path segments\n const pathname = u.pathname.replace(EMAIL_PATTERN, \"[redacted]\");\n\n return pathname + (u.search || \"\");\n } catch {\n return url;\n }\n}\n\nexport async function createSessionId(\n ip: string,\n ua: string,\n date: string,\n): Promise<string> {\n const input = `${anonymizeIp(ip)}|${ua}|${date}`;\n const data = new TextEncoder().encode(input);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n return Array.from(hashArray)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n .slice(0, 16);\n}\n\nexport function sanitizeProps(\n props: Record<string, string | number | boolean>,\n): Record<string, string | number | boolean> {\n const result: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(props)) {\n if (isPiiKey(key)) {\n result[key] = \"[redacted]\";\n continue;\n }\n if (typeof value === \"string\") {\n result[key] = value.replace(EMAIL_PATTERN, \"[redacted]\");\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\nexport function applyPrivacy(\n event: TrackrEvent,\n config: PrivacyConfig,\n): TrackrEvent {\n const result = { ...event };\n\n if (config.stripPii && result.url) {\n result.url = stripPii(result.url);\n }\n\n if (config.stripPii && result.props) {\n result.props = sanitizeProps(result.props);\n }\n\n if (config.stripQueryParams && result.url) {\n try {\n const u = new URL(result.url, \"http://localhost\");\n for (const p of config.stripQueryParams) {\n if (p.endsWith(\"*\")) {\n const prefix = p.slice(0, -1);\n const keysToDelete = [...u.searchParams.keys()].filter((k) =>\n k.startsWith(prefix),\n );\n for (const k of keysToDelete) {\n u.searchParams.delete(k);\n }\n } else {\n u.searchParams.delete(p);\n }\n }\n result.url = u.pathname + (u.search || \"\");\n } catch {}\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -5,8 +5,18 @@ function init(options) {
5
5
  trackPageview();
6
6
  if (typeof window !== "undefined") {
7
7
  window.addEventListener("popstate", trackPageview);
8
+ window.addEventListener("hashchange", trackPageview);
9
+ patchHistoryMethod("pushState");
10
+ patchHistoryMethod("replaceState");
8
11
  }
9
12
  }
13
+ function patchHistoryMethod(method) {
14
+ const original = history[method].bind(history);
15
+ history[method] = function(...args) {
16
+ original(...args);
17
+ trackPageview();
18
+ };
19
+ }
10
20
  function track(name, props) {
11
21
  if (!config) {
12
22
  console.warn("[trackr] Not initialized. Call init() first.");
@@ -22,21 +32,42 @@ function track(name, props) {
22
32
  }
23
33
  function trackPageview() {
24
34
  if (!config) return;
35
+ const utm = getUtmParams();
25
36
  sendEvent({
26
37
  type: "pageview",
27
38
  url: getPath(),
28
39
  referrer: document.referrer ? new URL(document.referrer).hostname : void 0,
40
+ ...Object.keys(utm).length > 0 && { utm },
29
41
  ts: Date.now()
30
42
  });
31
43
  }
32
44
  function getPath() {
33
- return window.location.pathname;
45
+ return window.location.pathname + window.location.search;
46
+ }
47
+ function getUtmParams() {
48
+ const params = new URLSearchParams(window.location.search);
49
+ const utm = {};
50
+ const keys = [
51
+ "utm_source",
52
+ "utm_medium",
53
+ "utm_campaign",
54
+ "utm_term",
55
+ "utm_content"
56
+ ];
57
+ for (const key of keys) {
58
+ const value = params.get(key);
59
+ if (value) utm[key.replace("utm_", "")] = value;
60
+ }
61
+ return utm;
34
62
  }
35
63
  function sendEvent(event) {
36
64
  if (!config) return;
37
65
  const body = JSON.stringify(event);
38
66
  if (navigator.sendBeacon) {
39
- navigator.sendBeacon(config.endpoint, body);
67
+ navigator.sendBeacon(
68
+ config.endpoint,
69
+ new Blob([body], { type: "application/json" })
70
+ );
40
71
  } else {
41
72
  fetch(config.endpoint, {
42
73
  method: "POST",
@@ -55,4 +86,4 @@ export {
55
86
  init,
56
87
  track
57
88
  };
58
- //# sourceMappingURL=chunk-ABUPERUQ.js.map
89
+ //# sourceMappingURL=chunk-AOB662OQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/index.ts"],"sourcesContent":["import type { TrackrConfig } from \"../types.js\";\n\nlet config: TrackrConfig | null = null;\n\nexport function init(options: TrackrConfig): void {\n config = options;\n trackPageview();\n\n if (typeof window !== \"undefined\") {\n // popstate: back/forward navigation\n window.addEventListener(\"popstate\", trackPageview);\n\n // hashchange: hash-based SPAs (e.g. Vue Router hash mode)\n window.addEventListener(\"hashchange\", trackPageview);\n\n // pushState / replaceState: history-based SPAs (React Router, Next.js, etc.)\n patchHistoryMethod(\"pushState\");\n patchHistoryMethod(\"replaceState\");\n }\n}\n\nfunction patchHistoryMethod(method: \"pushState\" | \"replaceState\"): void {\n const original = history[method].bind(history);\n // biome-ignore lint/suspicious/noExplicitAny: patching native history API\n (history[method] as any) = function (\n ...args: Parameters<typeof history.pushState>\n ) {\n original(...args);\n trackPageview();\n };\n}\n\nexport function track(\n name: string,\n props?: Record<string, string | number | boolean>,\n): void {\n if (!config) {\n console.warn(\"[trackr] Not initialized. Call init() first.\");\n return;\n }\n\n sendEvent({\n type: \"event\",\n name,\n url: getPath(),\n props,\n ts: Date.now(),\n });\n}\n\nfunction trackPageview(): void {\n if (!config) return;\n\n const utm = getUtmParams();\n\n sendEvent({\n type: \"pageview\",\n url: getPath(),\n referrer: document.referrer\n ? new URL(document.referrer).hostname\n : undefined,\n ...(Object.keys(utm).length > 0 && { utm }),\n ts: Date.now(),\n });\n}\n\nfunction getPath(): string {\n return window.location.pathname + window.location.search;\n}\n\nfunction getUtmParams(): Record<string, string> {\n const params = new URLSearchParams(window.location.search);\n const utm: Record<string, string> = {};\n const keys = [\n \"utm_source\",\n \"utm_medium\",\n \"utm_campaign\",\n \"utm_term\",\n \"utm_content\",\n ];\n\n for (const key of keys) {\n const value = params.get(key);\n // Strip utm_ prefix so stats queries work: props->'utm'->>'source'\n if (value) utm[key.replace(\"utm_\", \"\")] = value;\n }\n\n return utm;\n}\n\nfunction sendEvent(event: Record<string, unknown>): void {\n if (!config) return;\n\n const body = JSON.stringify(event);\n\n if (navigator.sendBeacon) {\n // Use Blob to set Content-Type: application/json — plain string would send as text/plain\n navigator.sendBeacon(\n config.endpoint,\n new Blob([body], { type: \"application/json\" }),\n );\n } else {\n fetch(config.endpoint, {\n method: \"POST\",\n body,\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\" },\n }).catch(() => {});\n }\n\n if (config.debug) {\n console.log(\"[trackr]\", event);\n }\n}\n"],"mappings":";AAEA,IAAI,SAA8B;AAE3B,SAAS,KAAK,SAA6B;AAChD,WAAS;AACT,gBAAc;AAEd,MAAI,OAAO,WAAW,aAAa;AAEjC,WAAO,iBAAiB,YAAY,aAAa;AAGjD,WAAO,iBAAiB,cAAc,aAAa;AAGnD,uBAAmB,WAAW;AAC9B,uBAAmB,cAAc;AAAA,EACnC;AACF;AAEA,SAAS,mBAAmB,QAA4C;AACtE,QAAM,WAAW,QAAQ,MAAM,EAAE,KAAK,OAAO;AAE7C,EAAC,QAAQ,MAAM,IAAY,YACtB,MACH;AACA,aAAS,GAAG,IAAI;AAChB,kBAAc;AAAA,EAChB;AACF;AAEO,SAAS,MACd,MACA,OACM;AACN,MAAI,CAAC,QAAQ;AACX,YAAQ,KAAK,8CAA8C;AAC3D;AAAA,EACF;AAEA,YAAU;AAAA,IACR,MAAM;AAAA,IACN;AAAA,IACA,KAAK,QAAQ;AAAA,IACb;AAAA,IACA,IAAI,KAAK,IAAI;AAAA,EACf,CAAC;AACH;AAEA,SAAS,gBAAsB;AAC7B,MAAI,CAAC,OAAQ;AAEb,QAAM,MAAM,aAAa;AAEzB,YAAU;AAAA,IACR,MAAM;AAAA,IACN,KAAK,QAAQ;AAAA,IACb,UAAU,SAAS,WACf,IAAI,IAAI,SAAS,QAAQ,EAAE,WAC3B;AAAA,IACJ,GAAI,OAAO,KAAK,GAAG,EAAE,SAAS,KAAK,EAAE,IAAI;AAAA,IACzC,IAAI,KAAK,IAAI;AAAA,EACf,CAAC;AACH;AAEA,SAAS,UAAkB;AACzB,SAAO,OAAO,SAAS,WAAW,OAAO,SAAS;AACpD;AAEA,SAAS,eAAuC;AAC9C,QAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,QAAM,MAA8B,CAAC;AACrC,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,OAAO,IAAI,GAAG;AAE5B,QAAI,MAAO,KAAI,IAAI,QAAQ,QAAQ,EAAE,CAAC,IAAI;AAAA,EAC5C;AAEA,SAAO;AACT;AAEA,SAAS,UAAU,OAAsC;AACvD,MAAI,CAAC,OAAQ;AAEb,QAAM,OAAO,KAAK,UAAU,KAAK;AAEjC,MAAI,UAAU,YAAY;AAExB,cAAU;AAAA,MACR,OAAO;AAAA,MACP,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAAA,IAC/C;AAAA,EACF,OAAO;AACL,UAAM,OAAO,UAAU;AAAA,MACrB,QAAQ;AAAA,MACR;AAAA,MACA,WAAW;AAAA,MACX,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnB;AAEA,MAAI,OAAO,OAAO;AAChB,YAAQ,IAAI,YAAY,KAAK;AAAA,EAC/B;AACF;","names":[]}
@@ -0,0 +1,78 @@
1
+ import {
2
+ applyPrivacy,
3
+ createSessionId,
4
+ isBot,
5
+ resolvePrivacyConfig
6
+ } from "./chunk-7UN7MXBM.js";
7
+
8
+ // src/server/index.ts
9
+ function createHandler(config) {
10
+ return async (request) => {
11
+ if (request.method !== "POST") {
12
+ return new Response("Method not allowed", { status: 405 });
13
+ }
14
+ if (config.botFilter && isBot(request)) {
15
+ return new Response("OK");
16
+ }
17
+ try {
18
+ const body = await request.json();
19
+ if (!isValidEvent(body)) {
20
+ return new Response("Invalid event", { status: 400 });
21
+ }
22
+ let event = {
23
+ type: body.type,
24
+ name: body.name,
25
+ url: body.url,
26
+ referrer: body.referrer,
27
+ props: body.props,
28
+ ts: body.ts
29
+ };
30
+ const privacy = resolvePrivacyConfig(config.privacy);
31
+ event = applyPrivacy(event, privacy);
32
+ const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "0.0.0.0";
33
+ const ua = request.headers.get("user-agent") || "";
34
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
35
+ if (privacy.anonymizeIp) {
36
+ event.sessionId = await createSessionId(ip, ua, today);
37
+ }
38
+ event.device = detectDevice(ua);
39
+ event.browser = detectBrowser(ua);
40
+ event.os = detectOs(ua);
41
+ await config.storage.save(event);
42
+ return new Response("OK");
43
+ } catch {
44
+ return new Response("Error", { status: 500 });
45
+ }
46
+ };
47
+ }
48
+ function isValidEvent(e) {
49
+ if (typeof e !== "object" || e === null) return false;
50
+ const obj = e;
51
+ return typeof obj.type === "string" && typeof obj.url === "string" && typeof obj.ts === "number";
52
+ }
53
+ function detectDevice(ua) {
54
+ if (/tablet|ipad/i.test(ua)) return "tablet";
55
+ if (/mobile|android|iphone/i.test(ua)) return "mobile";
56
+ return "desktop";
57
+ }
58
+ function detectBrowser(ua) {
59
+ if (/firefox/i.test(ua)) return "Firefox";
60
+ if (/edg/i.test(ua)) return "Edge";
61
+ if (/chrome/i.test(ua)) return "Chrome";
62
+ if (/safari/i.test(ua)) return "Safari";
63
+ return "Other";
64
+ }
65
+ function detectOs(ua) {
66
+ if (/android/i.test(ua)) return "Android";
67
+ if (/iphone|ipad|ipod/i.test(ua)) return "iOS";
68
+ if (/windows/i.test(ua)) return "Windows";
69
+ if (/cros/i.test(ua)) return "ChromeOS";
70
+ if (/mac os x/i.test(ua)) return "macOS";
71
+ if (/linux/i.test(ua)) return "Linux";
72
+ return "Other";
73
+ }
74
+
75
+ export {
76
+ createHandler
77
+ };
78
+ //# sourceMappingURL=chunk-PEAZRYH7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["import type { HandlerConfig, TrackrEvent } from \"../types.js\";\nimport { isBot } from \"./bot.js\";\nimport { applyPrivacy, createSessionId, resolvePrivacyConfig } from \"./privacy.js\";\n\ninterface RawEvent {\n type: string;\n url: string;\n ts: number;\n name?: string;\n referrer?: string;\n props?: Record<string, string | number | boolean>;\n}\n\nexport function createHandler(\n config: HandlerConfig,\n): (request: Request) => Promise<Response> {\n return async (request: Request): Promise<Response> => {\n if (request.method !== \"POST\") {\n return new Response(\"Method not allowed\", { status: 405 });\n }\n\n if (config.botFilter && isBot(request)) {\n return new Response(\"OK\");\n }\n\n try {\n const body = (await request.json()) as unknown;\n\n if (!isValidEvent(body)) {\n return new Response(\"Invalid event\", { status: 400 });\n }\n\n let event: TrackrEvent = {\n type: body.type as \"pageview\" | \"event\",\n name: body.name,\n url: body.url,\n referrer: body.referrer,\n props: body.props,\n ts: body.ts,\n };\n\n const privacy = resolvePrivacyConfig(config.privacy);\n event = applyPrivacy(event, privacy);\n\n const ip =\n request.headers.get(\"x-forwarded-for\")?.split(\",\")[0]?.trim() ||\n request.headers.get(\"x-real-ip\") ||\n \"0.0.0.0\";\n const ua = request.headers.get(\"user-agent\") || \"\";\n const today = new Date().toISOString().split(\"T\")[0];\n\n if (privacy.anonymizeIp) {\n event.sessionId = await createSessionId(ip, ua, today);\n }\n\n event.device = detectDevice(ua);\n event.browser = detectBrowser(ua);\n event.os = detectOs(ua);\n\n await config.storage.save(event);\n\n return new Response(\"OK\");\n } catch {\n return new Response(\"Error\", { status: 500 });\n }\n };\n}\n\nfunction isValidEvent(e: unknown): e is RawEvent {\n if (typeof e !== \"object\" || e === null) return false;\n const obj = e as Record<string, unknown>;\n return (\n typeof obj.type === \"string\" &&\n typeof obj.url === \"string\" &&\n typeof obj.ts === \"number\"\n );\n}\n\nfunction detectDevice(ua: string): \"desktop\" | \"mobile\" | \"tablet\" {\n if (/tablet|ipad/i.test(ua)) return \"tablet\";\n if (/mobile|android|iphone/i.test(ua)) return \"mobile\";\n return \"desktop\";\n}\n\nfunction detectBrowser(ua: string): string {\n if (/firefox/i.test(ua)) return \"Firefox\";\n if (/edg/i.test(ua)) return \"Edge\";\n if (/chrome/i.test(ua)) return \"Chrome\";\n if (/safari/i.test(ua)) return \"Safari\";\n return \"Other\";\n}\n\nfunction detectOs(ua: string): string {\n if (/android/i.test(ua)) return \"Android\";\n if (/iphone|ipad|ipod/i.test(ua)) return \"iOS\";\n if (/windows/i.test(ua)) return \"Windows\";\n if (/cros/i.test(ua)) return \"ChromeOS\";\n if (/mac os x/i.test(ua)) return \"macOS\";\n if (/linux/i.test(ua)) return \"Linux\";\n return \"Other\";\n}\n\nexport { isBot } from \"./bot.js\";\nexport {\n anonymizeIp,\n applyPrivacy,\n resolvePrivacyConfig,\n sanitizeProps,\n stripPii,\n} from \"./privacy.js\";\n"],"mappings":";;;;;;;;AAaO,SAAS,cACd,QACyC;AACzC,SAAO,OAAO,YAAwC;AACpD,QAAI,QAAQ,WAAW,QAAQ;AAC7B,aAAO,IAAI,SAAS,sBAAsB,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3D;AAEA,QAAI,OAAO,aAAa,MAAM,OAAO,GAAG;AACtC,aAAO,IAAI,SAAS,IAAI;AAAA,IAC1B;AAEA,QAAI;AACF,YAAM,OAAQ,MAAM,QAAQ,KAAK;AAEjC,UAAI,CAAC,aAAa,IAAI,GAAG;AACvB,eAAO,IAAI,SAAS,iBAAiB,EAAE,QAAQ,IAAI,CAAC;AAAA,MACtD;AAEA,UAAI,QAAqB;AAAA,QACvB,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,KAAK,KAAK;AAAA,QACV,UAAU,KAAK;AAAA,QACf,OAAO,KAAK;AAAA,QACZ,IAAI,KAAK;AAAA,MACX;AAEA,YAAM,UAAU,qBAAqB,OAAO,OAAO;AACnD,cAAQ,aAAa,OAAO,OAAO;AAEnC,YAAM,KACJ,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAC5D,QAAQ,QAAQ,IAAI,WAAW,KAC/B;AACF,YAAM,KAAK,QAAQ,QAAQ,IAAI,YAAY,KAAK;AAChD,YAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAEnD,UAAI,QAAQ,aAAa;AACvB,cAAM,YAAY,MAAM,gBAAgB,IAAI,IAAI,KAAK;AAAA,MACvD;AAEA,YAAM,SAAS,aAAa,EAAE;AAC9B,YAAM,UAAU,cAAc,EAAE;AAChC,YAAM,KAAK,SAAS,EAAE;AAEtB,YAAM,OAAO,QAAQ,KAAK,KAAK;AAE/B,aAAO,IAAI,SAAS,IAAI;AAAA,IAC1B,QAAQ;AACN,aAAO,IAAI,SAAS,SAAS,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AACF;AAEA,SAAS,aAAa,GAA2B;AAC/C,MAAI,OAAO,MAAM,YAAY,MAAM,KAAM,QAAO;AAChD,QAAM,MAAM;AACZ,SACE,OAAO,IAAI,SAAS,YACpB,OAAO,IAAI,QAAQ,YACnB,OAAO,IAAI,OAAO;AAEtB;AAEA,SAAS,aAAa,IAA6C;AACjE,MAAI,eAAe,KAAK,EAAE,EAAG,QAAO;AACpC,MAAI,yBAAyB,KAAK,EAAE,EAAG,QAAO;AAC9C,SAAO;AACT;AAEA,SAAS,cAAc,IAAoB;AACzC,MAAI,WAAW,KAAK,EAAE,EAAG,QAAO;AAChC,MAAI,OAAO,KAAK,EAAE,EAAG,QAAO;AAC5B,MAAI,UAAU,KAAK,EAAE,EAAG,QAAO;AAC/B,MAAI,UAAU,KAAK,EAAE,EAAG,QAAO;AAC/B,SAAO;AACT;AAEA,SAAS,SAAS,IAAoB;AACpC,MAAI,WAAW,KAAK,EAAE,EAAG,QAAO;AAChC,MAAI,oBAAoB,KAAK,EAAE,EAAG,QAAO;AACzC,MAAI,WAAW,KAAK,EAAE,EAAG,QAAO;AAChC,MAAI,QAAQ,KAAK,EAAE,EAAG,QAAO;AAC7B,MAAI,YAAY,KAAK,EAAE,EAAG,QAAO;AACjC,MAAI,SAAS,KAAK,EAAE,EAAG,QAAO;AAC9B,SAAO;AACT;","names":[]}
@@ -1,4 +1,4 @@
1
- import { a as TrackrConfig } from '../types-EaeYBDKE.js';
1
+ import { T as TrackrConfig } from '../types-CceMQIhZ.js';
2
2
 
3
3
  declare function init(options: TrackrConfig): void;
4
4
  declare function track(name: string, props?: Record<string, string | number | boolean>): void;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  init,
3
3
  track
4
- } from "../chunk-ABUPERUQ.js";
4
+ } from "../chunk-AOB662OQ.js";
5
5
  import "../chunk-7D4SUZUM.js";
6
6
  export {
7
7
  init,
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { H as HandlerConfig, P as PrivacyConfig, Q as QueryOptions, S as StorageAdapter, a as TrackrConfig, T as TrackrEvent } from './types-EaeYBDKE.js';
2
1
  export { init, track } from './client/index.js';
3
2
  export { createHandler } from './server/index.js';
3
+ export { H as HandlerConfig, P as PrivacyConfig, Q as QueryOptions, S as StorageAdapter, T as TrackrConfig, a as TrackrEvent } from './types-CceMQIhZ.js';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  init,
3
3
  track
4
- } from "./chunk-ABUPERUQ.js";
4
+ } from "./chunk-AOB662OQ.js";
5
5
  import {
6
6
  createHandler
7
- } from "./chunk-L3N32JO4.js";
7
+ } from "./chunk-PEAZRYH7.js";
8
+ import "./chunk-7UN7MXBM.js";
8
9
  import "./chunk-7D4SUZUM.js";
9
10
  export {
10
11
  createHandler,
@@ -1,11 +1,14 @@
1
- import { T as TrackrEvent, P as PrivacyConfig, H as HandlerConfig } from '../types-EaeYBDKE.js';
1
+ import { a as TrackrEvent, P as PrivacyConfig, H as HandlerConfig } from '../types-CceMQIhZ.js';
2
2
 
3
3
  declare function isBot(request: Request): boolean;
4
4
 
5
+ /** Merge caller config with safe defaults. */
6
+ declare function resolvePrivacyConfig(config?: PrivacyConfig): PrivacyConfig;
5
7
  declare function anonymizeIp(ip: string): string;
6
8
  declare function stripPii(url: string): string;
9
+ declare function sanitizeProps(props: Record<string, string | number | boolean>): Record<string, string | number | boolean>;
7
10
  declare function applyPrivacy(event: TrackrEvent, config: PrivacyConfig): TrackrEvent;
8
11
 
9
12
  declare function createHandler(config: HandlerConfig): (request: Request) => Promise<Response>;
10
13
 
11
- export { anonymizeIp, applyPrivacy, createHandler, isBot, stripPii };
14
+ export { anonymizeIp, applyPrivacy, createHandler, isBot, resolvePrivacyConfig, sanitizeProps, stripPii };
@@ -1,16 +1,22 @@
1
+ import {
2
+ createHandler
3
+ } from "../chunk-PEAZRYH7.js";
1
4
  import {
2
5
  anonymizeIp,
3
6
  applyPrivacy,
4
- createHandler,
5
7
  isBot,
8
+ resolvePrivacyConfig,
9
+ sanitizeProps,
6
10
  stripPii
7
- } from "../chunk-L3N32JO4.js";
11
+ } from "../chunk-7UN7MXBM.js";
8
12
  import "../chunk-7D4SUZUM.js";
9
13
  export {
10
14
  anonymizeIp,
11
15
  applyPrivacy,
12
16
  createHandler,
13
17
  isBot,
18
+ resolvePrivacyConfig,
19
+ sanitizeProps,
14
20
  stripPii
15
21
  };
16
22
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ import { H as HandlerConfig } from '../types-CceMQIhZ.js';
2
+
3
+ declare function createPixelHandler(config: HandlerConfig): (request: Request) => Promise<Response>;
4
+
5
+ export { createPixelHandler };