locallytics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -0
- package/dist/client/AnalyticsGrabber.d.ts +28 -0
- package/dist/client/AnalyticsGrabber.d.ts.map +1 -0
- package/dist/client/AnalyticsGrabber.js +71 -0
- package/dist/client/AnalyticsGrabber.js.map +1 -0
- package/dist/client/batcher.d.ts +48 -0
- package/dist/client/batcher.d.ts.map +1 -0
- package/dist/client/batcher.js +139 -0
- package/dist/client/batcher.js.map +1 -0
- package/dist/client/tracker.d.ts +18 -0
- package/dist/client/tracker.d.ts.map +1 -0
- package/dist/client/tracker.js +121 -0
- package/dist/client/tracker.js.map +1 -0
- package/dist/db/postgres.d.ts +16 -0
- package/dist/db/postgres.d.ts.map +1 -0
- package/dist/db/postgres.js +143 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server/handlers.d.ts +10 -0
- package/dist/server/handlers.d.ts.map +1 -0
- package/dist/server/handlers.js +105 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.d.ts +42 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +79 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/queries.d.ts +10 -0
- package/dist/server/queries.d.ts.map +1 -0
- package/dist/server/queries.js +24 -0
- package/dist/server/queries.js.map +1 -0
- package/dist/server/validator.d.ts +32 -0
- package/dist/server/validator.d.ts.map +1 -0
- package/dist/server/validator.js +149 -0
- package/dist/server/validator.js.map +1 -0
- package/dist/types/index.d.ts +127 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +24 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/hash.d.ts +16 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +36 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/rate-limit.d.ts +32 -0
- package/dist/utils/rate-limit.d.ts.map +1 -0
- package/dist/utils/rate-limit.js +73 -0
- package/dist/utils/rate-limit.js.map +1 -0
- package/package.json +48 -0
- package/src/db/schema.sql +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Locallytics
|
|
2
|
+
|
|
3
|
+
Self-hosted, privacy-first analytics SDK for Next.js applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Privacy-first** - No cookies, no localStorage, hashed IPs only
|
|
8
|
+
- 🚀 **Lightweight** - < 5KB client bundle
|
|
9
|
+
- 📊 **PostgreSQL only** - Simple, reliable, self-hosted
|
|
10
|
+
- âš¡ **Fast** - Event batching, parallel queries
|
|
11
|
+
- 🎯 **Next.js optimized** - Works with App Router
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install locallytics
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Set up the database
|
|
22
|
+
|
|
23
|
+
Run the schema in your PostgreSQL database:
|
|
24
|
+
|
|
25
|
+
```sql
|
|
26
|
+
-- See src/db/schema.sql for the full schema
|
|
27
|
+
CREATE TABLE IF NOT EXISTS locallytics_pageviews (
|
|
28
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
29
|
+
session_id TEXT NOT NULL,
|
|
30
|
+
page_url TEXT NOT NULL,
|
|
31
|
+
referrer TEXT,
|
|
32
|
+
user_agent TEXT NOT NULL,
|
|
33
|
+
screen_width INTEGER NOT NULL,
|
|
34
|
+
screen_height INTEGER NOT NULL,
|
|
35
|
+
ip_hash TEXT,
|
|
36
|
+
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
37
|
+
);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Create the API route
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// app/api/analytics/route.ts
|
|
44
|
+
import { locallytics } from "locallytics";
|
|
45
|
+
|
|
46
|
+
const analytics = await locallytics({
|
|
47
|
+
database: process.env.DATABASE_URL!,
|
|
48
|
+
apiKey: process.env.ANALYTICS_API_KEY, // optional
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const { GET, POST } = analytics;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Add the tracker
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app/layout.tsx
|
|
58
|
+
import { AnalyticsGrabber } from "locallytics";
|
|
59
|
+
|
|
60
|
+
export default function RootLayout({ children }) {
|
|
61
|
+
return (
|
|
62
|
+
<html>
|
|
63
|
+
<body>
|
|
64
|
+
{children}
|
|
65
|
+
<AnalyticsGrabber />
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 4. Fetch analytics data
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// app/dashboard/page.tsx
|
|
76
|
+
import { AnalyticsJSON } from "locallytics";
|
|
77
|
+
import { headers } from "next/headers";
|
|
78
|
+
|
|
79
|
+
export default async function Dashboard() {
|
|
80
|
+
const data = await AnalyticsJSON({
|
|
81
|
+
headersReader: headers,
|
|
82
|
+
dateRange: "last7d",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<h1>Analytics</h1>
|
|
88
|
+
<p>{data.pageviews} pageviews</p>
|
|
89
|
+
<p>{data.uniqueVisitors} unique visitors</p>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
### `locallytics(config)`
|
|
98
|
+
|
|
99
|
+
| Option | Type | Required | Description |
|
|
100
|
+
| ---------- | -------- | -------- | ----------------------------- |
|
|
101
|
+
| `database` | `string` | Yes | PostgreSQL connection string |
|
|
102
|
+
| `apiKey` | `string` | No | API key for GET endpoint auth |
|
|
103
|
+
|
|
104
|
+
### `AnalyticsGrabber` props
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
| ---------- | -------- | ---------------- | ---------------- |
|
|
108
|
+
| `endpoint` | `string` | `/api/analytics` | API endpoint URL |
|
|
109
|
+
|
|
110
|
+
### `AnalyticsJSON(options)`
|
|
111
|
+
|
|
112
|
+
| Option | Type | Required | Description |
|
|
113
|
+
| --------------- | --------------- | -------- | ---------------------------------------- |
|
|
114
|
+
| `headersReader` | `() => Headers` | Yes | Headers reader from `next/headers` |
|
|
115
|
+
| `dateRange` | `DateRange` | No | Date range (default: `'last7d'`) |
|
|
116
|
+
| `endpoint` | `string` | No | API endpoint (default: `/api/analytics`) |
|
|
117
|
+
|
|
118
|
+
## Date Ranges
|
|
119
|
+
|
|
120
|
+
- `'last24h'` - Last 24 hours
|
|
121
|
+
- `'last7d'` - Last 7 days (default)
|
|
122
|
+
- `'last30d'` - Last 30 days
|
|
123
|
+
- `{ start: Date, end: Date }` - Custom range
|
|
124
|
+
|
|
125
|
+
## Privacy
|
|
126
|
+
|
|
127
|
+
Locallytics respects user privacy:
|
|
128
|
+
|
|
129
|
+
- **No cookies** - Session IDs are generated client-side and ephemeral
|
|
130
|
+
- **No localStorage** - Nothing stored in the browser
|
|
131
|
+
- **IP hashing** - IPs are SHA-256 hashed before storage
|
|
132
|
+
- **DNT respected** - Honors Do Not Track header
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface AnalyticsGrabberProps {
|
|
2
|
+
/** API endpoint URL (defaults to /api/analytics) */
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* React component for tracking pageviews
|
|
7
|
+
* Drop this into your layout to automatically track pageviews
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // app/layout.tsx
|
|
12
|
+
* import { AnalyticsGrabber } from 'locallytics';
|
|
13
|
+
*
|
|
14
|
+
* export default function RootLayout({ children }) {
|
|
15
|
+
* return (
|
|
16
|
+
* <html>
|
|
17
|
+
* <body>
|
|
18
|
+
* {children}
|
|
19
|
+
* <AnalyticsGrabber />
|
|
20
|
+
* </body>
|
|
21
|
+
* </html>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function AnalyticsGrabber({ endpoint, }: AnalyticsGrabberProps): null;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=AnalyticsGrabber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnalyticsGrabber.d.ts","sourceRoot":"","sources":["../../src/client/AnalyticsGrabber.tsx"],"names":[],"mappings":"AAKA,UAAU,qBAAqB;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAA2B,GAC5B,EAAE,qBAAqB,GAAG,IAAI,CAuD9B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Tracker } from "./tracker.js";
|
|
4
|
+
/**
|
|
5
|
+
* React component for tracking pageviews
|
|
6
|
+
* Drop this into your layout to automatically track pageviews
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // app/layout.tsx
|
|
11
|
+
* import { AnalyticsGrabber } from 'locallytics';
|
|
12
|
+
*
|
|
13
|
+
* export default function RootLayout({ children }) {
|
|
14
|
+
* return (
|
|
15
|
+
* <html>
|
|
16
|
+
* <body>
|
|
17
|
+
* {children}
|
|
18
|
+
* <AnalyticsGrabber />
|
|
19
|
+
* </body>
|
|
20
|
+
* </html>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function AnalyticsGrabber({ endpoint = "/api/analytics", }) {
|
|
26
|
+
const trackerRef = useRef(null);
|
|
27
|
+
const lastPathRef = useRef("");
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Initialize tracker
|
|
30
|
+
const tracker = new Tracker(endpoint);
|
|
31
|
+
trackerRef.current = tracker;
|
|
32
|
+
// Track initial pageview
|
|
33
|
+
tracker.trackPageview();
|
|
34
|
+
lastPathRef.current = window.location.pathname;
|
|
35
|
+
// Handle browser back/forward navigation
|
|
36
|
+
const handlePopstate = () => {
|
|
37
|
+
if (window.location.pathname !== lastPathRef.current) {
|
|
38
|
+
lastPathRef.current = window.location.pathname;
|
|
39
|
+
tracker.trackPageview();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener("popstate", handlePopstate);
|
|
43
|
+
// Handle client-side navigation (Next.js App Router)
|
|
44
|
+
// Use MutationObserver to detect URL changes
|
|
45
|
+
let observer = null;
|
|
46
|
+
const checkForNavigation = () => {
|
|
47
|
+
if (window.location.pathname !== lastPathRef.current) {
|
|
48
|
+
lastPathRef.current = window.location.pathname;
|
|
49
|
+
tracker.trackPageview();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Observe changes to the document that might indicate navigation
|
|
53
|
+
observer = new MutationObserver(() => {
|
|
54
|
+
// Use requestAnimationFrame to batch checks
|
|
55
|
+
requestAnimationFrame(checkForNavigation);
|
|
56
|
+
});
|
|
57
|
+
observer.observe(document, {
|
|
58
|
+
subtree: true,
|
|
59
|
+
childList: true,
|
|
60
|
+
});
|
|
61
|
+
// Cleanup
|
|
62
|
+
return () => {
|
|
63
|
+
window.removeEventListener("popstate", handlePopstate);
|
|
64
|
+
observer?.disconnect();
|
|
65
|
+
tracker.destroy();
|
|
66
|
+
};
|
|
67
|
+
}, [endpoint]);
|
|
68
|
+
// This component renders nothing
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=AnalyticsGrabber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnalyticsGrabber.js","sourceRoot":"","sources":["../../src/client/AnalyticsGrabber.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAOvC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,GAAG,gBAAgB,GACL;IACtB,MAAM,UAAU,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,MAAM,CAAS,EAAE,CAAC,CAAC;IAEvC,SAAS,CAAC,GAAG,EAAE;QACb,qBAAqB;QACrB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;QACtC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;QAE7B,yBAAyB;QACzB,OAAO,CAAC,aAAa,EAAE,CAAC;QACxB,WAAW,CAAC,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAE/C,yCAAyC;QACzC,MAAM,cAAc,GAAG,GAAS,EAAE;YAChC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;gBACrD,WAAW,CAAC,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC/C,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAEpD,qDAAqD;QACrD,6CAA6C;QAC7C,IAAI,QAAQ,GAA4B,IAAI,CAAC;QAE7C,MAAM,kBAAkB,GAAG,GAAS,EAAE;YACpC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,WAAW,CAAC,OAAO,EAAE,CAAC;gBACrD,WAAW,CAAC,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC/C,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC;QAEF,iEAAiE;QACjE,QAAQ,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE;YACnC,4CAA4C;YAC5C,qBAAqB,CAAC,kBAAkB,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE;YACzB,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;QAEH,UAAU;QACV,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;YACvD,QAAQ,EAAE,UAAU,EAAE,CAAC;YACvB,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,iCAAiC;IACjC,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PageviewEvent } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Batches events and sends them to the server
|
|
4
|
+
* Uses sendBeacon for reliable delivery on page unload
|
|
5
|
+
*/
|
|
6
|
+
export declare class EventBatcher {
|
|
7
|
+
private readonly endpoint;
|
|
8
|
+
private readonly maxSize;
|
|
9
|
+
private readonly maxWaitMs;
|
|
10
|
+
private events;
|
|
11
|
+
private flushTimeout;
|
|
12
|
+
private isFlushing;
|
|
13
|
+
constructor(endpoint: string, maxSize?: number, maxWaitMs?: number);
|
|
14
|
+
/**
|
|
15
|
+
* Add an event to the batch queue
|
|
16
|
+
*/
|
|
17
|
+
add(event: PageviewEvent): void;
|
|
18
|
+
/**
|
|
19
|
+
* Flush events to the server using fetch
|
|
20
|
+
*/
|
|
21
|
+
flush(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Flush events synchronously using sendBeacon
|
|
24
|
+
* Used for beforeunload when async requests may not complete
|
|
25
|
+
*/
|
|
26
|
+
private flushSync;
|
|
27
|
+
/**
|
|
28
|
+
* Start the flush timer
|
|
29
|
+
*/
|
|
30
|
+
private startTimer;
|
|
31
|
+
/**
|
|
32
|
+
* Clear the flush timer
|
|
33
|
+
*/
|
|
34
|
+
private clearTimer;
|
|
35
|
+
/**
|
|
36
|
+
* Handle page unload - use sendBeacon for reliable delivery
|
|
37
|
+
*/
|
|
38
|
+
private handleUnload;
|
|
39
|
+
/**
|
|
40
|
+
* Handle visibility change - flush when page becomes hidden
|
|
41
|
+
*/
|
|
42
|
+
private handleVisibilityChange;
|
|
43
|
+
/**
|
|
44
|
+
* Clean up event listeners and timers
|
|
45
|
+
*/
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=batcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../../src/client/batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,UAAU,CAAkB;gBAGlC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAyB,EAClC,SAAS,GAAE,MAA4B;IAazC;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAc/B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuC5B;;;OAGG;IACH,OAAO,CAAC,SAAS;IAmBjB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,UAAU;IAOlB;;OAEG;IACH,OAAO,CAAC,YAAY,CAElB;IAEF;;OAEG;IACH,OAAO,CAAC,sBAAsB,CAI5B;IAEF;;OAEG;IACH,OAAO,IAAI,IAAI;CAWhB"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_MAX_SIZE = 10;
|
|
2
|
+
const DEFAULT_MAX_WAIT_MS = 30000;
|
|
3
|
+
/**
|
|
4
|
+
* Batches events and sends them to the server
|
|
5
|
+
* Uses sendBeacon for reliable delivery on page unload
|
|
6
|
+
*/
|
|
7
|
+
export class EventBatcher {
|
|
8
|
+
constructor(endpoint, maxSize = DEFAULT_MAX_SIZE, maxWaitMs = DEFAULT_MAX_WAIT_MS) {
|
|
9
|
+
this.events = [];
|
|
10
|
+
this.flushTimeout = null;
|
|
11
|
+
this.isFlushing = false;
|
|
12
|
+
/**
|
|
13
|
+
* Handle page unload - use sendBeacon for reliable delivery
|
|
14
|
+
*/
|
|
15
|
+
this.handleUnload = () => {
|
|
16
|
+
this.flushSync();
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Handle visibility change - flush when page becomes hidden
|
|
20
|
+
*/
|
|
21
|
+
this.handleVisibilityChange = () => {
|
|
22
|
+
if (document.visibilityState === "hidden") {
|
|
23
|
+
this.flushSync();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
this.endpoint = endpoint;
|
|
27
|
+
this.maxSize = maxSize;
|
|
28
|
+
this.maxWaitMs = maxWaitMs;
|
|
29
|
+
// Set up unload handlers for reliable delivery
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
window.addEventListener("beforeunload", this.handleUnload);
|
|
32
|
+
window.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Add an event to the batch queue
|
|
37
|
+
*/
|
|
38
|
+
add(event) {
|
|
39
|
+
this.events.push(event);
|
|
40
|
+
// Start the timer if this is the first event
|
|
41
|
+
if (this.events.length === 1) {
|
|
42
|
+
this.startTimer();
|
|
43
|
+
}
|
|
44
|
+
// Flush immediately if we've reached max size
|
|
45
|
+
if (this.events.length >= this.maxSize) {
|
|
46
|
+
void this.flush();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Flush events to the server using fetch
|
|
51
|
+
*/
|
|
52
|
+
async flush() {
|
|
53
|
+
if (this.events.length === 0 || this.isFlushing) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.isFlushing = true;
|
|
57
|
+
this.clearTimer();
|
|
58
|
+
const eventsToSend = [...this.events];
|
|
59
|
+
this.events = [];
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(this.endpoint, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(eventsToSend),
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
// On failure, add events back to queue for retry
|
|
70
|
+
console.error("[Locallytics] Failed to send events:", response.status);
|
|
71
|
+
this.events = [...eventsToSend, ...this.events];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// On error, add events back to queue for retry
|
|
76
|
+
console.error("[Locallytics] Error sending events:", error);
|
|
77
|
+
this.events = [...eventsToSend, ...this.events];
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.isFlushing = false;
|
|
81
|
+
// Restart timer if there are pending events
|
|
82
|
+
if (this.events.length > 0) {
|
|
83
|
+
this.startTimer();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Flush events synchronously using sendBeacon
|
|
89
|
+
* Used for beforeunload when async requests may not complete
|
|
90
|
+
*/
|
|
91
|
+
flushSync() {
|
|
92
|
+
if (this.events.length === 0) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const eventsToSend = [...this.events];
|
|
96
|
+
this.events = [];
|
|
97
|
+
this.clearTimer();
|
|
98
|
+
try {
|
|
99
|
+
const blob = new Blob([JSON.stringify(eventsToSend)], {
|
|
100
|
+
type: "application/json",
|
|
101
|
+
});
|
|
102
|
+
navigator.sendBeacon(this.endpoint, blob);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("[Locallytics] sendBeacon failed:", error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Start the flush timer
|
|
110
|
+
*/
|
|
111
|
+
startTimer() {
|
|
112
|
+
if (this.flushTimeout) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.flushTimeout = setTimeout(() => {
|
|
116
|
+
void this.flush();
|
|
117
|
+
}, this.maxWaitMs);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear the flush timer
|
|
121
|
+
*/
|
|
122
|
+
clearTimer() {
|
|
123
|
+
if (this.flushTimeout) {
|
|
124
|
+
clearTimeout(this.flushTimeout);
|
|
125
|
+
this.flushTimeout = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clean up event listeners and timers
|
|
130
|
+
*/
|
|
131
|
+
destroy() {
|
|
132
|
+
this.clearTimer();
|
|
133
|
+
if (typeof window !== "undefined") {
|
|
134
|
+
window.removeEventListener("beforeunload", this.handleUnload);
|
|
135
|
+
window.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=batcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batcher.js","sourceRoot":"","sources":["../../src/client/batcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side tracker for pageviews
|
|
3
|
+
*/
|
|
4
|
+
export declare class Tracker {
|
|
5
|
+
private readonly batcher;
|
|
6
|
+
private readonly sessionId;
|
|
7
|
+
private dntEnabled;
|
|
8
|
+
constructor(endpoint?: string);
|
|
9
|
+
/**
|
|
10
|
+
* Track the current pageview
|
|
11
|
+
*/
|
|
12
|
+
trackPageview(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Clean up the tracker
|
|
15
|
+
*/
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/client/tracker.ts"],"names":[],"mappings":"AAiGA;;GAEG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,UAAU,CAAU;gBAEhB,QAAQ,GAAE,MAAyB;IAO/C;;OAEG;IACH,aAAa,IAAI,IAAI;IAwBrB;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { EventBatcher } from "./batcher.js";
|
|
2
|
+
const DEFAULT_ENDPOINT = "/api/analytics";
|
|
3
|
+
const STORAGE_KEY = "locallytics_sid";
|
|
4
|
+
/**
|
|
5
|
+
* Generate a unique session ID
|
|
6
|
+
*/
|
|
7
|
+
function generateSessionId() {
|
|
8
|
+
// Use crypto.randomUUID if available (modern browsers)
|
|
9
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
10
|
+
return crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
// Fallback: timestamp + random
|
|
13
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get or create a persistent session ID
|
|
17
|
+
*/
|
|
18
|
+
function getPersistentSessionId(dntEnabled) {
|
|
19
|
+
// If DNT is enabled, do not use storage and return a temporary ID
|
|
20
|
+
if (dntEnabled) {
|
|
21
|
+
return generateSessionId();
|
|
22
|
+
}
|
|
23
|
+
if (typeof window === "undefined") {
|
|
24
|
+
return generateSessionId();
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
// Check localStorage
|
|
28
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
if (stored) {
|
|
30
|
+
return stored;
|
|
31
|
+
}
|
|
32
|
+
// Create new and store
|
|
33
|
+
const newId = generateSessionId();
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, newId);
|
|
35
|
+
return newId;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Use ephemeral ID if storage access is denied
|
|
39
|
+
return generateSessionId();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if Do Not Track is enabled in the browser
|
|
44
|
+
*/
|
|
45
|
+
function isDNTEnabled() {
|
|
46
|
+
if (typeof navigator === "undefined") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// Check navigator.doNotTrack
|
|
50
|
+
const dnt = navigator.doNotTrack;
|
|
51
|
+
if (dnt === "1" || dnt === "yes") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// Check window.doNotTrack (non-standard but used by some browsers)
|
|
55
|
+
if (typeof window !== "undefined" &&
|
|
56
|
+
"doNotTrack" in window &&
|
|
57
|
+
window.doNotTrack === "1") {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Check Global Privacy Control
|
|
61
|
+
if (typeof navigator !== "undefined" &&
|
|
62
|
+
"globalPrivacyControl" in navigator &&
|
|
63
|
+
navigator
|
|
64
|
+
.globalPrivacyControl === true) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sanitize URL for tracking (extract pathname + search)
|
|
71
|
+
*/
|
|
72
|
+
function sanitizeUrl(url) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = new URL(url);
|
|
75
|
+
return parsed.pathname + parsed.search;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return "/";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Client-side tracker for pageviews
|
|
83
|
+
*/
|
|
84
|
+
export class Tracker {
|
|
85
|
+
constructor(endpoint = DEFAULT_ENDPOINT) {
|
|
86
|
+
this.batcher = new EventBatcher(endpoint);
|
|
87
|
+
this.dntEnabled = isDNTEnabled();
|
|
88
|
+
// Get persistent ID (returns ephemeral if DNT is on)
|
|
89
|
+
this.sessionId = getPersistentSessionId(this.dntEnabled);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Track the current pageview
|
|
93
|
+
*/
|
|
94
|
+
trackPageview() {
|
|
95
|
+
// Respect DNT - don't track if enabled
|
|
96
|
+
if (this.dntEnabled) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Skip if not in browser
|
|
100
|
+
if (typeof window === "undefined") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const event = {
|
|
104
|
+
sessionId: this.sessionId,
|
|
105
|
+
pageUrl: sanitizeUrl(window.location.href),
|
|
106
|
+
referrer: document.referrer ? sanitizeUrl(document.referrer) : null,
|
|
107
|
+
userAgent: navigator.userAgent.slice(0, 512),
|
|
108
|
+
screenWidth: window.screen.width,
|
|
109
|
+
screenHeight: window.screen.height,
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
this.batcher.add(event);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Clean up the tracker
|
|
116
|
+
*/
|
|
117
|
+
destroy() {
|
|
118
|
+
this.batcher.destroy();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/client/tracker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnalyticsDB, PageviewEvent, DateRange, PageStats, DailyStats } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* PostgreSQL implementation of the analytics database
|
|
4
|
+
*/
|
|
5
|
+
export declare class PostgresDB implements AnalyticsDB {
|
|
6
|
+
private readonly pool;
|
|
7
|
+
constructor(connectionString: string);
|
|
8
|
+
insertPageview(event: PageviewEvent, ipHash: string | null): Promise<void>;
|
|
9
|
+
getPageviews(dateRange: DateRange): Promise<number>;
|
|
10
|
+
getUniqueVisitors(dateRange: DateRange): Promise<number>;
|
|
11
|
+
getTopPages(dateRange: DateRange, limit: number): Promise<PageStats[]>;
|
|
12
|
+
getTopReferrers(dateRange: DateRange, limit: number): Promise<PageStats[]>;
|
|
13
|
+
getDailyStats(dateRange: DateRange): Promise<DailyStats[]>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# 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,EACV,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,UAAU,EACX,MAAM,mBAAmB,CAAC;AA+C3B;;GAEG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;gBAEhB,gBAAgB,EAAE,MAAM;IAS9B,cAAc,CAClB,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,IAAI,CAAC;IAoBV,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAanD,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAaxD,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAuBtE,eAAe,CACnB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,SAAS,EAAE,CAAC;IAuBjB,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IA2B1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|