@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.
- package/README.md +117 -49
- package/dist/chunk-7UN7MXBM.js +141 -0
- package/dist/chunk-7UN7MXBM.js.map +1 -0
- package/dist/{chunk-ABUPERUQ.js → chunk-AOB662OQ.js} +34 -3
- package/dist/chunk-AOB662OQ.js.map +1 -0
- package/dist/chunk-PEAZRYH7.js +78 -0
- package/dist/chunk-PEAZRYH7.js.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +8 -2
- package/dist/server/pixel.d.ts +5 -0
- package/dist/server/pixel.js +97 -0
- package/dist/server/pixel.js.map +1 -0
- package/dist/storage/api.d.ts +1 -1
- package/dist/storage/api.js.map +1 -1
- package/dist/storage/batch.d.ts +19 -0
- package/dist/storage/batch.js +59 -0
- package/dist/storage/batch.js.map +1 -0
- package/dist/storage/ga4.d.ts +47 -0
- package/dist/storage/ga4.js +131 -0
- package/dist/storage/ga4.js.map +1 -0
- package/dist/storage/multi.d.ts +21 -0
- package/dist/storage/multi.js +14 -0
- package/dist/storage/multi.js.map +1 -0
- package/dist/storage/postgres.d.ts +1 -1
- package/dist/storage/postgres.js +3 -1
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/webhook.d.ts +22 -0
- package/dist/storage/webhook.js +54 -0
- package/dist/storage/webhook.js.map +1 -0
- package/dist/{types-EaeYBDKE.d.ts → types-CceMQIhZ.d.ts} +3 -1
- package/package.json +26 -2
- package/script.js +1 -0
- package/dist/chunk-ABUPERUQ.js.map +0 -1
- package/dist/chunk-L3N32JO4.js +0 -153
- 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
|
-
|
|
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
|
-
|
|
50
|
+
Or via CDN (no build step):
|
|
34
51
|
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
134
|
-
- **Bot Filtering** - Common
|
|
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(
|
|
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-
|
|
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":[]}
|
package/dist/client/index.d.ts
CHANGED
package/dist/client/index.js
CHANGED
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-
|
|
4
|
+
} from "./chunk-AOB662OQ.js";
|
|
5
5
|
import {
|
|
6
6
|
createHandler
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-PEAZRYH7.js";
|
|
8
|
+
import "./chunk-7UN7MXBM.js";
|
|
8
9
|
import "./chunk-7D4SUZUM.js";
|
|
9
10
|
export {
|
|
10
11
|
createHandler,
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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-
|
|
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
|