locallytics 0.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +499 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +71 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/src/client/index.d.ts +3 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +41 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/db/index.d.ts +4 -0
- package/dist/src/db/index.d.ts.map +1 -0
- package/dist/src/db/index.js +26 -0
- package/dist/src/db/index.js.map +1 -0
- package/dist/src/db/postgres.d.ts +11 -0
- package/dist/src/db/postgres.d.ts.map +1 -0
- package/dist/src/db/postgres.js +63 -0
- package/dist/src/db/postgres.js.map +1 -0
- package/dist/src/db/sqlite.d.ts +11 -0
- package/dist/src/db/sqlite.d.ts.map +1 -0
- package/dist/src/db/sqlite.js +64 -0
- package/dist/src/db/sqlite.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server/data.d.ts +3 -0
- package/dist/src/server/data.d.ts.map +1 -0
- package/dist/src/server/data.js +36 -0
- package/dist/src/server/data.js.map +1 -0
- package/dist/src/server/handler.d.ts +5 -0
- package/dist/src/server/handler.d.ts.map +1 -0
- package/dist/src/server/handler.js +40 -0
- package/dist/src/server/handler.js.map +1 -0
- package/dist/src/types/index.d.ts +34 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/index.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# Locallytics
|
|
2
|
+
|
|
3
|
+
Self-hosted, privacy-first analytics for any JavaScript framework.
|
|
4
|
+
|
|
5
|
+
- **Privacy-first** — No cookies, respects Do Not Track
|
|
6
|
+
- **Self-hosted** — Your data in your database (PostgreSQL or SQLite)
|
|
7
|
+
- **Framework-agnostic** — Works with Next.js, Remix, Express, Hono, etc.
|
|
8
|
+
- **Simple** — Two main functions: `LocallyticsGrabber` and `LocallyticsData`
|
|
9
|
+
- **Multiple formats** — Export analytics as JSON or CSV
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install locallytics
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Migrate Database
|
|
20
|
+
|
|
21
|
+
Set your database URL and run migrations:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export DATABASE_URL="postgres://user:pass@localhost/db"
|
|
25
|
+
# or
|
|
26
|
+
export DATABASE_URL="./analytics.db"
|
|
27
|
+
|
|
28
|
+
npx locallytics migrate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Create API Handler
|
|
32
|
+
|
|
33
|
+
The handler uses Web Standard `Request`/`Response` - works in any framework.
|
|
34
|
+
|
|
35
|
+
**Next.js:**
|
|
36
|
+
```typescript
|
|
37
|
+
// app/api/analytics/route.ts
|
|
38
|
+
import { createHandler } from "locallytics";
|
|
39
|
+
|
|
40
|
+
const handler = await createHandler({
|
|
41
|
+
database: process.env.DATABASE_URL!,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const POST = handler.POST;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Express:**
|
|
48
|
+
```javascript
|
|
49
|
+
import express from "express";
|
|
50
|
+
import { createHandler } from "locallytics";
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
const analytics = await createHandler({
|
|
54
|
+
database: process.env.DATABASE_URL,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.post("/api/analytics", async (req, res) => {
|
|
58
|
+
const response = await analytics.POST(req);
|
|
59
|
+
res.status(response.status).send(await response.text());
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Hono:**
|
|
64
|
+
```typescript
|
|
65
|
+
import { Hono } from "hono";
|
|
66
|
+
import { createHandler } from "locallytics";
|
|
67
|
+
|
|
68
|
+
const app = new Hono();
|
|
69
|
+
const analytics = await createHandler({
|
|
70
|
+
database: process.env.DATABASE_URL!,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post("/api/analytics", (c) => analytics.POST(c.req.raw));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 4. Add Client Tracker
|
|
77
|
+
|
|
78
|
+
Call `LocallyticsGrabber()` on page load to track pageviews.
|
|
79
|
+
|
|
80
|
+
**Next.js (React):**
|
|
81
|
+
```tsx
|
|
82
|
+
// app/layout.tsx
|
|
83
|
+
"use client";
|
|
84
|
+
|
|
85
|
+
import { LocallyticsGrabber } from "locallytics/client";
|
|
86
|
+
import { useEffect } from "react";
|
|
87
|
+
|
|
88
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
LocallyticsGrabber();
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<html>
|
|
95
|
+
<body>{children}</body>
|
|
96
|
+
</html>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Vanilla JavaScript:**
|
|
102
|
+
```html
|
|
103
|
+
<script type="module">
|
|
104
|
+
import { LocallyticsGrabber } from "/node_modules/locallytics/dist/client/index.js";
|
|
105
|
+
LocallyticsGrabber();
|
|
106
|
+
</script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 5. View Analytics
|
|
110
|
+
|
|
111
|
+
Query analytics data on the server:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// app/dashboard/page.tsx (Next.js)
|
|
115
|
+
import { LocallyticsData } from "locallytics";
|
|
116
|
+
|
|
117
|
+
export default async function Dashboard() {
|
|
118
|
+
const data = await LocallyticsData(
|
|
119
|
+
process.env.DATABASE_URL!,
|
|
120
|
+
"last7d" // "last24h" | "last7d" | "last30d"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<p>{data.pageviews} pageviews</p>
|
|
126
|
+
<p>{data.uniqueVisitors} visitors</p>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## API Reference
|
|
135
|
+
|
|
136
|
+
### `LocallyticsGrabber(options?)`
|
|
137
|
+
|
|
138
|
+
Client-side function that tracks pageviews in the browser.
|
|
139
|
+
|
|
140
|
+
**Usage:**
|
|
141
|
+
```typescript
|
|
142
|
+
import { LocallyticsGrabber } from "locallytics/client";
|
|
143
|
+
|
|
144
|
+
LocallyticsGrabber({
|
|
145
|
+
endpoint: "/api/analytics", // default
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Options:**
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
| ---------- | -------- | ------------------ | ------------------------------ |
|
|
153
|
+
| `endpoint` | `string` | `"/api/analytics"` | API endpoint to send data to |
|
|
154
|
+
|
|
155
|
+
**Data Collected:**
|
|
156
|
+
|
|
157
|
+
Each pageview sends:
|
|
158
|
+
- `pageUrl` — Current path
|
|
159
|
+
- `referrer` — Where user came from
|
|
160
|
+
- `sessionId` — Anonymous ID (localStorage)
|
|
161
|
+
- `timestamp` — ISO timestamp
|
|
162
|
+
|
|
163
|
+
**Privacy Features:**
|
|
164
|
+
- **No cookies** — Uses localStorage only
|
|
165
|
+
- **Respects DNT** — Tracking disabled when Do Not Track is on
|
|
166
|
+
- **No PII** — Anonymous session IDs only
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `createHandler(config)`
|
|
171
|
+
|
|
172
|
+
Creates API route handlers for receiving tracking data.
|
|
173
|
+
|
|
174
|
+
**Usage:**
|
|
175
|
+
```typescript
|
|
176
|
+
import { createHandler } from "locallytics";
|
|
177
|
+
|
|
178
|
+
const handler = await createHandler({
|
|
179
|
+
database: process.env.DATABASE_URL!,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const POST = handler.POST;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Config:**
|
|
186
|
+
|
|
187
|
+
| Option | Type | Required | Description |
|
|
188
|
+
| ---------- | -------- | -------- | ------------------------------ |
|
|
189
|
+
| `database` | `string` | Yes | Database connection string |
|
|
190
|
+
|
|
191
|
+
**Returns:**
|
|
192
|
+
|
|
193
|
+
Object with a `POST` method that accepts Web Standard `Request` and returns `Response`.
|
|
194
|
+
|
|
195
|
+
**Response Codes:**
|
|
196
|
+
- `201` — Pageview recorded successfully
|
|
197
|
+
- `204` — Tracking skipped (DNT enabled)
|
|
198
|
+
- `400` — Invalid payload
|
|
199
|
+
- `500` — Server error
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### `LocallyticsData(databaseUrl, range, format?)`
|
|
204
|
+
|
|
205
|
+
Queries analytics data from the database.
|
|
206
|
+
|
|
207
|
+
**Usage:**
|
|
208
|
+
```typescript
|
|
209
|
+
import { LocallyticsData } from "locallytics";
|
|
210
|
+
|
|
211
|
+
// Get JSON data (default)
|
|
212
|
+
const data = await LocallyticsData(
|
|
213
|
+
process.env.DATABASE_URL!,
|
|
214
|
+
"last7d"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
console.log(data.pageviews, data.uniqueVisitors);
|
|
218
|
+
|
|
219
|
+
// Get CSV data
|
|
220
|
+
const csv = await LocallyticsData(
|
|
221
|
+
process.env.DATABASE_URL!,
|
|
222
|
+
"last7d",
|
|
223
|
+
"csv"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
console.log(csv); // CSV formatted string
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
|
|
231
|
+
| Parameter | Type | Required | Description |
|
|
232
|
+
| ------------- | --------------------------------- | -------- | ------------------------------ |
|
|
233
|
+
| `databaseUrl` | `string` | Yes | Database connection string |
|
|
234
|
+
| `range` | `"last24h"` \| `"last7d"` \| `"last30d"` | Yes | Date range to query |
|
|
235
|
+
| `format` | `"json"` \| `"csv"` | No | Output format (default: `"json"`) |
|
|
236
|
+
|
|
237
|
+
**Returns (JSON format):**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
{
|
|
241
|
+
pageviews: number; // Total pageviews
|
|
242
|
+
uniqueVisitors: number; // Unique sessions
|
|
243
|
+
topPages: Array<{
|
|
244
|
+
page: string; // Page URL
|
|
245
|
+
count: number; // View count
|
|
246
|
+
}>; // Top 10 pages
|
|
247
|
+
dailyStats: Array<{
|
|
248
|
+
date: string; // YYYY-MM-DD
|
|
249
|
+
views: number; // Views that day
|
|
250
|
+
}>;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Returns (CSV format):**
|
|
255
|
+
|
|
256
|
+
String with CSV formatted data including summary, top pages, and daily stats.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## CLI Commands
|
|
261
|
+
|
|
262
|
+
### `migrate`
|
|
263
|
+
|
|
264
|
+
Creates or updates database tables.
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
export DATABASE_URL="postgres://user:pass@localhost/db"
|
|
268
|
+
npx locallytics migrate
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `reset`
|
|
272
|
+
|
|
273
|
+
Drops and recreates all tables. **WARNING: This destroys all data!**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
export DATABASE_URL="postgres://user:pass@localhost/db"
|
|
277
|
+
npx locallytics reset
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `help`
|
|
281
|
+
|
|
282
|
+
Shows available commands.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npx locallytics help
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Database Support
|
|
291
|
+
|
|
292
|
+
Locallytics supports PostgreSQL and SQLite. The database type is detected automatically from the connection string.
|
|
293
|
+
|
|
294
|
+
### PostgreSQL
|
|
295
|
+
|
|
296
|
+
**Connection String Format:**
|
|
297
|
+
```
|
|
298
|
+
postgres://user:password@host:port/database
|
|
299
|
+
postgresql://user:password@host:port/database
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Example:**
|
|
303
|
+
```bash
|
|
304
|
+
export DATABASE_URL="postgres://myuser:mypass@localhost:5432/analytics"
|
|
305
|
+
npx locallytics migrate
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Requirements:**
|
|
309
|
+
- Install `pg` peer dependency: `npm install pg`
|
|
310
|
+
|
|
311
|
+
### SQLite
|
|
312
|
+
|
|
313
|
+
**Connection String Format:**
|
|
314
|
+
```
|
|
315
|
+
./path/to/database.db
|
|
316
|
+
/absolute/path/to/database.db
|
|
317
|
+
sqlite:///path/to/database.db
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Example:**
|
|
321
|
+
```bash
|
|
322
|
+
export DATABASE_URL="./analytics.db"
|
|
323
|
+
npx locallytics migrate
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Requirements:**
|
|
327
|
+
- Install `better-sqlite3` peer dependency: `npm install better-sqlite3`
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Privacy Features
|
|
332
|
+
|
|
333
|
+
Locallytics is designed with privacy in mind:
|
|
334
|
+
|
|
335
|
+
### No Cookies
|
|
336
|
+
Uses localStorage for session IDs instead of cookies. No cookie banners required.
|
|
337
|
+
|
|
338
|
+
### Respects Do Not Track
|
|
339
|
+
When a user has Do Not Track (DNT) enabled in their browser:
|
|
340
|
+
- Client-side tracker won't send data
|
|
341
|
+
- Server-side handler won't record data
|
|
342
|
+
|
|
343
|
+
### No Personal Information
|
|
344
|
+
Only collects:
|
|
345
|
+
- Page URLs
|
|
346
|
+
- Referrer URLs
|
|
347
|
+
- Anonymous session IDs (random UUIDs)
|
|
348
|
+
- Timestamps
|
|
349
|
+
|
|
350
|
+
No IP addresses, user agents, or any identifying information.
|
|
351
|
+
|
|
352
|
+
### Self-Hosted
|
|
353
|
+
All data stays in your database. No third-party services involved.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Framework Examples
|
|
358
|
+
|
|
359
|
+
### Next.js (App Router)
|
|
360
|
+
|
|
361
|
+
See [examples/nextjs](./examples/nextjs) for complete example.
|
|
362
|
+
|
|
363
|
+
**API Route:**
|
|
364
|
+
```typescript
|
|
365
|
+
// app/api/analytics/route.ts
|
|
366
|
+
import { createHandler } from "locallytics";
|
|
367
|
+
|
|
368
|
+
const handler = await createHandler({
|
|
369
|
+
database: process.env.DATABASE_URL!,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
export const POST = handler.POST;
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Layout (Client Component):**
|
|
376
|
+
```tsx
|
|
377
|
+
// app/layout.tsx
|
|
378
|
+
"use client";
|
|
379
|
+
|
|
380
|
+
import { LocallyticsGrabber } from "locallytics/client";
|
|
381
|
+
import { useEffect } from "react";
|
|
382
|
+
|
|
383
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
LocallyticsGrabber();
|
|
386
|
+
}, []);
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<html>
|
|
390
|
+
<body>{children}</body>
|
|
391
|
+
</html>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Dashboard (Server Component):**
|
|
397
|
+
```tsx
|
|
398
|
+
// app/dashboard/page.tsx
|
|
399
|
+
import { LocallyticsData } from "locallytics";
|
|
400
|
+
|
|
401
|
+
export default async function Dashboard() {
|
|
402
|
+
const data = await LocallyticsData(process.env.DATABASE_URL!, "last7d");
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div>
|
|
406
|
+
<h1>Analytics</h1>
|
|
407
|
+
<p>Pageviews: {data.pageviews}</p>
|
|
408
|
+
<p>Unique Visitors: {data.uniqueVisitors}</p>
|
|
409
|
+
|
|
410
|
+
<h2>Top Pages</h2>
|
|
411
|
+
<ul>
|
|
412
|
+
{data.topPages.map((page) => (
|
|
413
|
+
<li key={page.page}>
|
|
414
|
+
{page.page}: {page.count} views
|
|
415
|
+
</li>
|
|
416
|
+
))}
|
|
417
|
+
</ul>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Express
|
|
424
|
+
|
|
425
|
+
See [examples/express](./examples/express) for complete example.
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
import express from "express";
|
|
429
|
+
import { createHandler, LocallyticsData } from "locallytics";
|
|
430
|
+
|
|
431
|
+
const app = express();
|
|
432
|
+
const analytics = await createHandler({
|
|
433
|
+
database: process.env.DATABASE_URL,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
app.post("/api/analytics", async (req, res) => {
|
|
437
|
+
const response = await analytics.POST(req);
|
|
438
|
+
res.status(response.status).send(await response.text());
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
app.get("/dashboard", async (req, res) => {
|
|
442
|
+
const data = await LocallyticsData(process.env.DATABASE_URL, "last7d");
|
|
443
|
+
res.json(data);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
app.listen(3000);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Remix
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// app/routes/api.analytics.ts
|
|
453
|
+
import type { ActionFunction } from "@remix-run/node";
|
|
454
|
+
import { createHandler } from "locallytics";
|
|
455
|
+
|
|
456
|
+
const handler = await createHandler({
|
|
457
|
+
database: process.env.DATABASE_URL!,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
export const action: ActionFunction = async ({ request }) => {
|
|
461
|
+
return handler.POST(request);
|
|
462
|
+
};
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Hono
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { Hono } from "hono";
|
|
469
|
+
import { createHandler } from "locallytics";
|
|
470
|
+
|
|
471
|
+
const app = new Hono();
|
|
472
|
+
const analytics = await createHandler({
|
|
473
|
+
database: process.env.DATABASE_URL!,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
app.post("/api/analytics", (c) => analytics.POST(c.req.raw));
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## TypeScript
|
|
482
|
+
|
|
483
|
+
Locallytics is written in TypeScript and includes full type definitions.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import type {
|
|
487
|
+
DateRange,
|
|
488
|
+
DataFormat,
|
|
489
|
+
AnalyticsResult,
|
|
490
|
+
GrabberOptions,
|
|
491
|
+
HandlerConfig,
|
|
492
|
+
} from "locallytics";
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## License
|
|
498
|
+
|
|
499
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createAdapter } from "../src/db/index.js";
|
|
3
|
+
const commands = {
|
|
4
|
+
migrate: async () => {
|
|
5
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
6
|
+
if (!databaseUrl) {
|
|
7
|
+
console.error("❌ DATABASE_URL environment variable is required");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
console.log("🔄 Running migration...");
|
|
11
|
+
try {
|
|
12
|
+
const adapter = await createAdapter(databaseUrl);
|
|
13
|
+
await adapter.migrate();
|
|
14
|
+
adapter.close();
|
|
15
|
+
console.log("✅ Migration completed successfully");
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error("❌ Migration failed:", error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
reset: async () => {
|
|
23
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
24
|
+
if (!databaseUrl) {
|
|
25
|
+
console.error("❌ DATABASE_URL environment variable is required");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log("⚠️ WARNING: This will delete all analytics data!");
|
|
29
|
+
console.log("🔄 Resetting database...");
|
|
30
|
+
try {
|
|
31
|
+
const adapter = await createAdapter(databaseUrl);
|
|
32
|
+
await adapter.reset();
|
|
33
|
+
adapter.close();
|
|
34
|
+
console.log("✅ Database reset successfully");
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("❌ Reset failed:", error);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
help: () => {
|
|
42
|
+
console.log(`
|
|
43
|
+
Locallytics CLI - Database management tool
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
locallytics <command>
|
|
47
|
+
|
|
48
|
+
Commands:
|
|
49
|
+
migrate Create or update database tables
|
|
50
|
+
reset Drop and recreate all tables (destroys data!)
|
|
51
|
+
help Show this help message
|
|
52
|
+
|
|
53
|
+
Environment Variables:
|
|
54
|
+
DATABASE_URL Database connection string (required)
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
export DATABASE_URL="postgres://user:pass@localhost/db"
|
|
58
|
+
locallytics migrate
|
|
59
|
+
|
|
60
|
+
export DATABASE_URL="./analytics.db"
|
|
61
|
+
locallytics migrate
|
|
62
|
+
`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const command = process.argv[2];
|
|
66
|
+
if (!command || !commands[command]) {
|
|
67
|
+
commands.help();
|
|
68
|
+
process.exit(command ? 1 : 0);
|
|
69
|
+
}
|
|
70
|
+
commands[command]();
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,QAAQ,GAAG;IACf,OAAO,EAAE,KAAK,IAAI,EAAE;QAClB,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAE7C,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAEvC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;YACjD,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;YAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,KAAK,EAAE,KAAK,IAAI,EAAE;QAChB,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAE7C,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QAExC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;YACjD,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,IAAI,EAAE,GAAG,EAAE;QACT,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;KAoBX,CAAC,CAAC;IACL,CAAC;CACF,CAAC;AAEF,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAA0B,CAAC;AAEzD,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAmBxD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,cAAmB,GAAG,IAAI,CA2BrE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function getSessionId() {
|
|
2
|
+
const key = "_locallytics_sid";
|
|
3
|
+
let sessionId = localStorage.getItem(key);
|
|
4
|
+
if (!sessionId) {
|
|
5
|
+
sessionId = crypto.randomUUID();
|
|
6
|
+
localStorage.setItem(key, sessionId);
|
|
7
|
+
}
|
|
8
|
+
return sessionId;
|
|
9
|
+
}
|
|
10
|
+
function respectsDNT() {
|
|
11
|
+
const dnt = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
|
|
12
|
+
return dnt === "1" || dnt === "yes";
|
|
13
|
+
}
|
|
14
|
+
export function LocallyticsGrabber(options = {}) {
|
|
15
|
+
if (typeof window === "undefined")
|
|
16
|
+
return;
|
|
17
|
+
if (respectsDNT())
|
|
18
|
+
return;
|
|
19
|
+
const endpoint = options.endpoint || "/api/analytics";
|
|
20
|
+
const payload = {
|
|
21
|
+
pageUrl: window.location.pathname + window.location.search,
|
|
22
|
+
referrer: document.referrer || null,
|
|
23
|
+
sessionId: getSessionId(),
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
27
|
+
if (navigator.sendBeacon) {
|
|
28
|
+
navigator.sendBeacon(endpoint, blob);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
fetch(endpoint, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify(payload),
|
|
35
|
+
keepalive: true,
|
|
36
|
+
}).catch(() => {
|
|
37
|
+
// Silently fail if tracking request fails
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAEA,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,kBAAkB,CAAC;IAC/B,IAAI,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAE1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,SAAS,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QAChC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,IAAK,MAAc,CAAC,UAAU,IAAK,SAAiB,CAAC,YAAY,CAAC;IAClG,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,KAAK,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,UAA0B,EAAE;IAC7D,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO;IAC1C,IAAI,WAAW,EAAE;QAAE,OAAO;IAE1B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IAEtD,MAAM,OAAO,GAAG;QACd,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM;QAC1D,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,IAAI;QACnC,SAAS,EAAE,YAAY,EAAE;QACzB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAE/E,IAAI,SAAS,CAAC,UAAU,EAAE,CAAC;QACzB,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,QAAQ,EAAE;YACd,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YAC7B,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACZ,0CAA0C;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAejF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAapD"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function createAdapter(databaseUrl) {
|
|
2
|
+
if (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://")) {
|
|
3
|
+
const { PostgresAdapter } = await import("./postgres.js");
|
|
4
|
+
return new PostgresAdapter(databaseUrl);
|
|
5
|
+
}
|
|
6
|
+
if (databaseUrl.endsWith(".db") || databaseUrl.endsWith(".sqlite") || databaseUrl.startsWith("sqlite://")) {
|
|
7
|
+
const { SQLiteAdapter } = await import("./sqlite.js");
|
|
8
|
+
const path = databaseUrl.replace("sqlite://", "");
|
|
9
|
+
return new SQLiteAdapter(path);
|
|
10
|
+
}
|
|
11
|
+
throw new Error(`Unsupported database URL: ${databaseUrl}. Supported formats: postgres://, sqlite:// or .db/.sqlite file path`);
|
|
12
|
+
}
|
|
13
|
+
export function getDateCutoff(range) {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
switch (range) {
|
|
16
|
+
case "last24h":
|
|
17
|
+
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
18
|
+
case "last7d":
|
|
19
|
+
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
20
|
+
case "last30d":
|
|
21
|
+
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
22
|
+
default:
|
|
23
|
+
throw new Error(`Invalid date range: ${range}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/db/index.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,WAAmB;IACrD,IAAI,WAAW,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACrF,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;QAC1D,OAAO,IAAI,eAAe,CAAC,WAAW,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1G,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAClD,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,MAAM,IAAI,KAAK,CACb,6BAA6B,WAAW,sEAAsE,CAC/G,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAgB;IAC5C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACvD,KAAK,QAAQ;YACX,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC3D,KAAK,SAAS;YACZ,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC5D;YACE,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseAdapter, PageviewPayload, AnalyticsResult } from "../types/index.js";
|
|
2
|
+
export declare class PostgresAdapter implements DatabaseAdapter {
|
|
3
|
+
private pool;
|
|
4
|
+
constructor(connectionString: string);
|
|
5
|
+
insert(data: PageviewPayload): Promise<void>;
|
|
6
|
+
query(since: Date): Promise<AnalyticsResult>;
|
|
7
|
+
migrate(): Promise<void>;
|
|
8
|
+
reset(): Promise<void>;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../../src/db/postgres.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAiB3F,qBAAa,eAAgB,YAAW,eAAe;IACrD,OAAO,CAAC,IAAI,CAAU;gBAEV,gBAAgB,EAAE,MAAM;IAI9B,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC;IAwC5C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
const { Pool } = pg;
|
|
3
|
+
const SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS pageviews (
|
|
5
|
+
id SERIAL PRIMARY KEY,
|
|
6
|
+
page_url TEXT NOT NULL,
|
|
7
|
+
referrer TEXT,
|
|
8
|
+
session_id TEXT NOT NULL,
|
|
9
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_pageviews_created_at ON pageviews(created_at);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_pageviews_session_id ON pageviews(session_id);
|
|
14
|
+
`;
|
|
15
|
+
export class PostgresAdapter {
|
|
16
|
+
pool;
|
|
17
|
+
constructor(connectionString) {
|
|
18
|
+
this.pool = new Pool({ connectionString });
|
|
19
|
+
}
|
|
20
|
+
async insert(data) {
|
|
21
|
+
await this.pool.query("INSERT INTO pageviews (page_url, referrer, session_id, created_at) VALUES ($1, $2, $3, $4)", [data.pageUrl, data.referrer, data.sessionId, new Date(data.timestamp)]);
|
|
22
|
+
}
|
|
23
|
+
async query(since) {
|
|
24
|
+
const [pageviewsResult, visitorsResult, topPagesResult, dailyStatsResult] = await Promise.all([
|
|
25
|
+
this.pool.query("SELECT COUNT(*) as count FROM pageviews WHERE created_at >= $1", [since]),
|
|
26
|
+
this.pool.query("SELECT COUNT(DISTINCT session_id) as count FROM pageviews WHERE created_at >= $1", [since]),
|
|
27
|
+
this.pool.query(`SELECT page_url as page, COUNT(*) as count
|
|
28
|
+
FROM pageviews
|
|
29
|
+
WHERE created_at >= $1
|
|
30
|
+
GROUP BY page_url
|
|
31
|
+
ORDER BY count DESC
|
|
32
|
+
LIMIT 10`, [since]),
|
|
33
|
+
this.pool.query(`SELECT DATE(created_at) as date, COUNT(*) as views
|
|
34
|
+
FROM pageviews
|
|
35
|
+
WHERE created_at >= $1
|
|
36
|
+
GROUP BY DATE(created_at)
|
|
37
|
+
ORDER BY date ASC`, [since]),
|
|
38
|
+
]);
|
|
39
|
+
return {
|
|
40
|
+
pageviews: Number(pageviewsResult.rows[0].count),
|
|
41
|
+
uniqueVisitors: Number(visitorsResult.rows[0].count),
|
|
42
|
+
topPages: topPagesResult.rows.map((row) => ({
|
|
43
|
+
page: row.page,
|
|
44
|
+
count: Number(row.count),
|
|
45
|
+
})),
|
|
46
|
+
dailyStats: dailyStatsResult.rows.map((row) => ({
|
|
47
|
+
date: row.date.toISOString().split("T")[0],
|
|
48
|
+
views: Number(row.views),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async migrate() {
|
|
53
|
+
await this.pool.query(SCHEMA);
|
|
54
|
+
}
|
|
55
|
+
async reset() {
|
|
56
|
+
await this.pool.query("DROP TABLE IF EXISTS pageviews CASCADE");
|
|
57
|
+
await this.migrate();
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
this.pool.end();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/db/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAEpB,MAAM,MAAM,GAAG;;;;;;;;;;;CAWd,CAAC;AAEF,MAAM,OAAO,eAAe;IAClB,IAAI,CAAU;IAEtB,YAAY,gBAAwB;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAqB;QAChC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CACnB,4FAA4F,EAC5F,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CACxE,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAW;QACrB,MAAM,CAAC,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC5F,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,gEAAgE,EAAE,CAAC,KAAK,CAAC,CAAC;YAC1F,IAAI,CAAC,IAAI,CAAC,KAAK,CACb,kFAAkF,EAClF,CAAC,KAAK,CAAC,CACR;YACD,IAAI,CAAC,IAAI,CAAC,KAAK,CACb;;;;;kBAKU,EACV,CAAC,KAAK,CAAC,CACR;YACD,IAAI,CAAC,IAAI,CAAC,KAAK,CACb;;;;2BAImB,EACnB,CAAC,KAAK,CAAC,CACR;SACF,CAAC,CAAC;QAEH,OAAO;YACL,SAAS,EAAE,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAChD,cAAc,EAAE,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACpD,QAAQ,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC1C,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;aACzB,CAAC,CAAC;YACH,UAAU,EAAE,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC9C,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC1C,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;aACzB,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAChE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseAdapter, PageviewPayload, AnalyticsResult } from "../types/index.js";
|
|
2
|
+
export declare class SQLiteAdapter implements DatabaseAdapter {
|
|
3
|
+
private db;
|
|
4
|
+
constructor(path: string);
|
|
5
|
+
insert(data: PageviewPayload): Promise<void>;
|
|
6
|
+
query(since: Date): Promise<AnalyticsResult>;
|
|
7
|
+
migrate(): Promise<void>;
|
|
8
|
+
reset(): Promise<void>;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../../src/db/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAe3F,qBAAa,aAAc,YAAW,eAAe;IACnD,OAAO,CAAC,EAAE,CAAoB;gBAElB,IAAI,EAAE,MAAM;IAIlB,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC;IAwC5C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
const SCHEMA = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS pageviews (
|
|
4
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5
|
+
page_url TEXT NOT NULL,
|
|
6
|
+
referrer TEXT,
|
|
7
|
+
session_id TEXT NOT NULL,
|
|
8
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_pageviews_created_at ON pageviews(created_at);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_pageviews_session_id ON pageviews(session_id);
|
|
13
|
+
`;
|
|
14
|
+
export class SQLiteAdapter {
|
|
15
|
+
db;
|
|
16
|
+
constructor(path) {
|
|
17
|
+
this.db = new Database(path);
|
|
18
|
+
}
|
|
19
|
+
async insert(data) {
|
|
20
|
+
const stmt = this.db.prepare("INSERT INTO pageviews (page_url, referrer, session_id, created_at) VALUES (?, ?, ?, ?)");
|
|
21
|
+
stmt.run(data.pageUrl, data.referrer, data.sessionId, new Date(data.timestamp).toISOString());
|
|
22
|
+
}
|
|
23
|
+
async query(since) {
|
|
24
|
+
const sinceISO = since.toISOString();
|
|
25
|
+
const pageviews = this.db
|
|
26
|
+
.prepare("SELECT COUNT(*) as count FROM pageviews WHERE created_at >= ?")
|
|
27
|
+
.get(sinceISO);
|
|
28
|
+
const uniqueVisitors = this.db
|
|
29
|
+
.prepare("SELECT COUNT(DISTINCT session_id) as count FROM pageviews WHERE created_at >= ?")
|
|
30
|
+
.get(sinceISO);
|
|
31
|
+
const topPages = this.db
|
|
32
|
+
.prepare(`SELECT page_url as page, COUNT(*) as count
|
|
33
|
+
FROM pageviews
|
|
34
|
+
WHERE created_at >= ?
|
|
35
|
+
GROUP BY page_url
|
|
36
|
+
ORDER BY count DESC
|
|
37
|
+
LIMIT 10`)
|
|
38
|
+
.all(sinceISO);
|
|
39
|
+
const dailyStats = this.db
|
|
40
|
+
.prepare(`SELECT DATE(created_at) as date, COUNT(*) as views
|
|
41
|
+
FROM pageviews
|
|
42
|
+
WHERE created_at >= ?
|
|
43
|
+
GROUP BY DATE(created_at)
|
|
44
|
+
ORDER BY date ASC`)
|
|
45
|
+
.all(sinceISO);
|
|
46
|
+
return {
|
|
47
|
+
pageviews: pageviews.count,
|
|
48
|
+
uniqueVisitors: uniqueVisitors.count,
|
|
49
|
+
topPages,
|
|
50
|
+
dailyStats,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async migrate() {
|
|
54
|
+
this.db.exec(SCHEMA);
|
|
55
|
+
}
|
|
56
|
+
async reset() {
|
|
57
|
+
this.db.exec("DROP TABLE IF EXISTS pageviews");
|
|
58
|
+
await this.migrate();
|
|
59
|
+
}
|
|
60
|
+
close() {
|
|
61
|
+
this.db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../src/db/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAGtC,MAAM,MAAM,GAAG;;;;;;;;;;;CAWd,CAAC;AAEF,MAAM,OAAO,aAAa;IAChB,EAAE,CAAoB;IAE9B,YAAY,IAAY;QACtB,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAqB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAC1B,wFAAwF,CACzF,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAChG,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAW;QACrB,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QAErC,MAAM,SAAS,GAAG,IAAI,CAAC,EAAE;aACtB,OAAO,CAAC,+DAA+D,CAAC;aACxE,GAAG,CAAC,QAAQ,CAAsB,CAAC;QAEtC,MAAM,cAAc,GAAG,IAAI,CAAC,EAAE;aAC3B,OAAO,CAAC,iFAAiF,CAAC;aAC1F,GAAG,CAAC,QAAQ,CAAsB,CAAC;QAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE;aACrB,OAAO,CACN;;;;;kBAKU,CACX;aACA,GAAG,CAAC,QAAQ,CAA2C,CAAC;QAE3D,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE;aACvB,OAAO,CACN;;;;2BAImB,CACpB;aACA,GAAG,CAAC,QAAQ,CAA2C,CAAC;QAE3D,OAAO;YACL,SAAS,EAAE,SAAS,CAAC,KAAK;YAC1B,cAAc,EAAE,cAAc,CAAC,KAAK;YACpC,QAAQ;YACR,UAAU;SACX,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;QAC/C,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { LocallyticsGrabber } from "./client/index.js";
|
|
2
|
+
export { LocallyticsData } from "./server/data.js";
|
|
3
|
+
export { createHandler } from "./server/handler.js";
|
|
4
|
+
export type { DateRange, DataFormat, AnalyticsResult, GrabberOptions, HandlerConfig, } from "./types/index.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../../src/server/data.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA2BhF,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,EAChB,MAAM,GAAE,UAAmB,GAC1B,OAAO,CAAC,eAAe,GAAG,MAAM,CAAC,CAenC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createAdapter, getDateCutoff } from "../db/index.js";
|
|
2
|
+
function formatAsCSV(data) {
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push("# Summary");
|
|
5
|
+
lines.push("Metric,Value");
|
|
6
|
+
lines.push(`Total Pageviews,${data.pageviews}`);
|
|
7
|
+
lines.push(`Unique Visitors,${data.uniqueVisitors}`);
|
|
8
|
+
lines.push("");
|
|
9
|
+
lines.push("# Top Pages");
|
|
10
|
+
lines.push("Page,Views");
|
|
11
|
+
for (const page of data.topPages) {
|
|
12
|
+
lines.push(`${page.page},${page.count}`);
|
|
13
|
+
}
|
|
14
|
+
lines.push("");
|
|
15
|
+
lines.push("# Daily Stats");
|
|
16
|
+
lines.push("Date,Views");
|
|
17
|
+
for (const stat of data.dailyStats) {
|
|
18
|
+
lines.push(`${stat.date},${stat.views}`);
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
export async function LocallyticsData(databaseUrl, range, format = "json") {
|
|
23
|
+
const adapter = await createAdapter(databaseUrl);
|
|
24
|
+
const since = getDateCutoff(range);
|
|
25
|
+
try {
|
|
26
|
+
const data = await adapter.query(since);
|
|
27
|
+
if (format === "csv") {
|
|
28
|
+
return formatAsCSV(data);
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
adapter.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data.js","sourceRoot":"","sources":["../../../src/server/data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG9D,SAAS,WAAW,CAAC,IAAqB;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxB,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IAChD,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;IACrD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1B,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC3C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC5B,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,WAAmB,EACnB,KAAgB,EAChB,SAAqB,MAAM;IAE3B,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAExC,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/server/handler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAcvD,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa;oBAG1B,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;GA4BzD"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createAdapter } from "../db/index.js";
|
|
3
|
+
const PageviewSchema = z.object({
|
|
4
|
+
pageUrl: z.string(),
|
|
5
|
+
referrer: z.string().nullable(),
|
|
6
|
+
sessionId: z.string(),
|
|
7
|
+
timestamp: z.string(),
|
|
8
|
+
});
|
|
9
|
+
function respectsDNT(request) {
|
|
10
|
+
const dnt = request.headers.get("dnt") || request.headers.get("DNT");
|
|
11
|
+
return dnt === "1";
|
|
12
|
+
}
|
|
13
|
+
export async function createHandler(config) {
|
|
14
|
+
const adapter = await createAdapter(config.database);
|
|
15
|
+
const POST = async (request) => {
|
|
16
|
+
if (respectsDNT(request)) {
|
|
17
|
+
return new Response(null, { status: 204 });
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const body = await request.json();
|
|
21
|
+
const data = PageviewSchema.parse(body);
|
|
22
|
+
await adapter.insert(data);
|
|
23
|
+
return new Response(null, { status: 201 });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof z.ZodError) {
|
|
27
|
+
return new Response(JSON.stringify({ error: "Invalid payload", details: error.errors }), {
|
|
28
|
+
status: 400,
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
|
33
|
+
status: 500,
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return { POST };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../src/server/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC;AAEH,SAAS,WAAW,CAAC,OAAgB;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACrE,OAAO,GAAG,KAAK,GAAG,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAqB;IACvD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,KAAK,EAAE,OAAgB,EAAqB,EAAE;QACzD,IAAI,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAExC,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAE3B,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAChC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;oBACvF,MAAM,EAAE,GAAG;oBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;iBAChD,CAAC,CAAC;YACL,CAAC;YAED,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,EAAE;gBACtE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;aAChD,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type DateRange = "last24h" | "last7d" | "last30d";
|
|
2
|
+
export type DataFormat = "json" | "csv";
|
|
3
|
+
export interface PageviewPayload {
|
|
4
|
+
pageUrl: string;
|
|
5
|
+
referrer: string | null;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AnalyticsResult {
|
|
10
|
+
pageviews: number;
|
|
11
|
+
uniqueVisitors: number;
|
|
12
|
+
topPages: Array<{
|
|
13
|
+
page: string;
|
|
14
|
+
count: number;
|
|
15
|
+
}>;
|
|
16
|
+
dailyStats: Array<{
|
|
17
|
+
date: string;
|
|
18
|
+
views: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface DatabaseAdapter {
|
|
22
|
+
insert(data: PageviewPayload): Promise<void>;
|
|
23
|
+
query(since: Date): Promise<AnalyticsResult>;
|
|
24
|
+
migrate(): Promise<void>;
|
|
25
|
+
reset(): Promise<void>;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
export interface GrabberOptions {
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HandlerConfig {
|
|
32
|
+
database: string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,KAAK,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC7C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "locallytics",
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
|
+
"description": "Self-hosted, privacy-first analytics for any JavaScript framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./dist/client/index.d.ts",
|
|
15
|
+
"default": "./dist/client/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"locallytics": "./dist/cli/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "tsc --watch",
|
|
28
|
+
"lint": "biome check .",
|
|
29
|
+
"format": "biome format --write ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"analytics",
|
|
33
|
+
"privacy",
|
|
34
|
+
"self-hosted",
|
|
35
|
+
"pageviews",
|
|
36
|
+
"tracking"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"zod": "^3.22.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"pg": "^8.11.0",
|
|
45
|
+
"better-sqlite3": "^9.4.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"pg": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"better-sqlite3": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "^1.5.3",
|
|
57
|
+
"@types/better-sqlite3": "^7.6.9",
|
|
58
|
+
"@types/node": "^20.11.16",
|
|
59
|
+
"@types/pg": "^8.11.0",
|
|
60
|
+
"typescript": "^5.3.3"
|
|
61
|
+
}
|
|
62
|
+
}
|