locallytics 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +19 -82
  2. package/package.json +3 -2
  3. package/dist/client/AnalyticsGrabber.d.ts +0 -28
  4. package/dist/client/AnalyticsGrabber.d.ts.map +0 -1
  5. package/dist/client/AnalyticsGrabber.js +0 -71
  6. package/dist/client/AnalyticsGrabber.js.map +0 -1
  7. package/dist/client/batcher.d.ts +0 -48
  8. package/dist/client/batcher.d.ts.map +0 -1
  9. package/dist/client/batcher.js +0 -139
  10. package/dist/client/batcher.js.map +0 -1
  11. package/dist/client/tracker.d.ts +0 -18
  12. package/dist/client/tracker.d.ts.map +0 -1
  13. package/dist/client/tracker.js +0 -121
  14. package/dist/client/tracker.js.map +0 -1
  15. package/dist/db/postgres.d.ts +0 -16
  16. package/dist/db/postgres.d.ts.map +0 -1
  17. package/dist/db/postgres.js +0 -143
  18. package/dist/db/postgres.js.map +0 -1
  19. package/dist/index.d.ts +0 -5
  20. package/dist/index.d.ts.map +0 -1
  21. package/dist/index.js +0 -6
  22. package/dist/index.js.map +0 -1
  23. package/dist/server/handlers.d.ts +0 -10
  24. package/dist/server/handlers.d.ts.map +0 -1
  25. package/dist/server/handlers.js +0 -105
  26. package/dist/server/handlers.js.map +0 -1
  27. package/dist/server/index.d.ts +0 -42
  28. package/dist/server/index.d.ts.map +0 -1
  29. package/dist/server/index.js +0 -79
  30. package/dist/server/index.js.map +0 -1
  31. package/dist/server/queries.d.ts +0 -10
  32. package/dist/server/queries.d.ts.map +0 -1
  33. package/dist/server/queries.js +0 -24
  34. package/dist/server/queries.js.map +0 -1
  35. package/dist/server/validator.d.ts +0 -32
  36. package/dist/server/validator.d.ts.map +0 -1
  37. package/dist/server/validator.js +0 -149
  38. package/dist/server/validator.js.map +0 -1
  39. package/dist/types/index.d.ts +0 -127
  40. package/dist/types/index.d.ts.map +0 -1
  41. package/dist/types/index.js +0 -24
  42. package/dist/types/index.js.map +0 -1
  43. package/dist/utils/hash.d.ts +0 -16
  44. package/dist/utils/hash.d.ts.map +0 -1
  45. package/dist/utils/hash.js +0 -36
  46. package/dist/utils/hash.js.map +0 -1
  47. package/dist/utils/rate-limit.d.ts +0 -32
  48. package/dist/utils/rate-limit.d.ts.map +0 -1
  49. package/dist/utils/rate-limit.js +0 -73
  50. package/dist/utils/rate-limit.js.map +0 -1
package/README.md CHANGED
@@ -1,14 +1,6 @@
1
1
  # Locallytics
2
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
3
+ Self-hosted, privacy-first analytics SDK for Next.js.
12
4
 
13
5
  ## Installation
14
6
 
@@ -18,26 +10,15 @@ npm install locallytics
18
10
 
19
11
  ## Quick Start
20
12
 
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
- );
13
+ ### 1. Setup Database
14
+
15
+ Use our CLI to set up your PostgreSQL database automatically:
16
+
17
+ ```bash
18
+ npx locallytics-cli migrate
38
19
  ```
39
20
 
40
- ### 2. Create the API route
21
+ ### 2. Add API Route
41
22
 
42
23
  ```typescript
43
24
  // app/api/analytics/route.ts
@@ -45,13 +26,12 @@ import { locallytics } from "locallytics";
45
26
 
46
27
  const analytics = await locallytics({
47
28
  database: process.env.DATABASE_URL!,
48
- apiKey: process.env.ANALYTICS_API_KEY, // optional
49
29
  });
50
30
 
51
31
  export const { GET, POST } = analytics;
52
32
  ```
53
33
 
54
- ### 3. Add the tracker
34
+ ### 3. Add Tracker
55
35
 
56
36
  ```tsx
57
37
  // app/layout.tsx
@@ -69,67 +49,24 @@ export default function RootLayout({ children }) {
69
49
  }
70
50
  ```
71
51
 
72
- ### 4. Fetch analytics data
52
+ ### 4. Fetch Data
73
53
 
74
54
  ```tsx
75
- // app/dashboard/page.tsx
76
- import { AnalyticsJSON } from "locallytics";
77
- import { headers } from "next/headers";
55
+ // app/page.tsx
56
+ import { AnalyticsData } from "locallytics";
78
57
 
79
- export default async function Dashboard() {
80
- const data = await AnalyticsJSON({
81
- headersReader: headers,
82
- dateRange: "last7d",
83
- });
58
+ export default async function Home() {
59
+ const data = await AnalyticsData(); // Simple! No arguments.
84
60
 
85
- return (
86
- <div>
87
- <h1>Analytics</h1>
88
- <p>{data.pageviews} pageviews</p>
89
- <p>{data.uniqueVisitors} unique visitors</p>
90
- </div>
91
- );
61
+ return <pre>{JSON.stringify(data, null, 2)}</pre>;
92
62
  }
93
63
  ```
94
64
 
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:
65
+ ## Features
128
66
 
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
67
+ - **Privacy**: No cookies, hashed IPs, supports Do Not Track.
68
+ - **Simplicity**: Zero-config data fetching.
69
+ - **Speed**: Lightweight (< 5KB) and fast.
133
70
 
134
71
  ## License
135
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "locallytics",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Self-hosted, privacy-first analytics SDK for Next.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -10,7 +10,8 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
- }
13
+ },
14
+ "./schema.sql": "./src/db/schema.sql"
14
15
  },
15
16
  "files": [
16
17
  "dist",
@@ -1,28 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,71 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,48 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,139 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"batcher.js","sourceRoot":"","sources":["../../src/client/batcher.ts"],"names":[],"mappings":"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"}
@@ -1,18 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,121 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/client/tracker.ts"],"names":[],"mappings":"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"}
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
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"}