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.
Files changed (51) hide show
  1. package/README.md +136 -0
  2. package/dist/client/AnalyticsGrabber.d.ts +28 -0
  3. package/dist/client/AnalyticsGrabber.d.ts.map +1 -0
  4. package/dist/client/AnalyticsGrabber.js +71 -0
  5. package/dist/client/AnalyticsGrabber.js.map +1 -0
  6. package/dist/client/batcher.d.ts +48 -0
  7. package/dist/client/batcher.d.ts.map +1 -0
  8. package/dist/client/batcher.js +139 -0
  9. package/dist/client/batcher.js.map +1 -0
  10. package/dist/client/tracker.d.ts +18 -0
  11. package/dist/client/tracker.d.ts.map +1 -0
  12. package/dist/client/tracker.js +121 -0
  13. package/dist/client/tracker.js.map +1 -0
  14. package/dist/db/postgres.d.ts +16 -0
  15. package/dist/db/postgres.d.ts.map +1 -0
  16. package/dist/db/postgres.js +143 -0
  17. package/dist/db/postgres.js.map +1 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +6 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server/handlers.d.ts +10 -0
  23. package/dist/server/handlers.d.ts.map +1 -0
  24. package/dist/server/handlers.js +105 -0
  25. package/dist/server/handlers.js.map +1 -0
  26. package/dist/server/index.d.ts +42 -0
  27. package/dist/server/index.d.ts.map +1 -0
  28. package/dist/server/index.js +79 -0
  29. package/dist/server/index.js.map +1 -0
  30. package/dist/server/queries.d.ts +10 -0
  31. package/dist/server/queries.d.ts.map +1 -0
  32. package/dist/server/queries.js +24 -0
  33. package/dist/server/queries.js.map +1 -0
  34. package/dist/server/validator.d.ts +32 -0
  35. package/dist/server/validator.d.ts.map +1 -0
  36. package/dist/server/validator.js +149 -0
  37. package/dist/server/validator.js.map +1 -0
  38. package/dist/types/index.d.ts +127 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +24 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/utils/hash.d.ts +16 -0
  43. package/dist/utils/hash.d.ts.map +1 -0
  44. package/dist/utils/hash.js +36 -0
  45. package/dist/utils/hash.js.map +1 -0
  46. package/dist/utils/rate-limit.d.ts +32 -0
  47. package/dist/utils/rate-limit.d.ts.map +1 -0
  48. package/dist/utils/rate-limit.js +73 -0
  49. package/dist/utils/rate-limit.js.map +1 -0
  50. package/package.json +48 -0
  51. package/src/db/schema.sql +35 -0
@@ -0,0 +1,143 @@
1
+ import { Pool } from "pg";
2
+ /**
3
+ * Get SQL WHERE clause parts for a date range
4
+ */
5
+ function getDateRangeFilter(dateRange) {
6
+ const now = new Date();
7
+ if (dateRange === "last24h") {
8
+ const start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
9
+ return {
10
+ whereClause: "timestamp >= $1",
11
+ params: [start.toISOString()],
12
+ paramOffset: 1,
13
+ };
14
+ }
15
+ if (dateRange === "last7d") {
16
+ const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
17
+ return {
18
+ whereClause: "timestamp >= $1",
19
+ params: [start.toISOString()],
20
+ paramOffset: 1,
21
+ };
22
+ }
23
+ if (dateRange === "last30d") {
24
+ const start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
25
+ return {
26
+ whereClause: "timestamp >= $1",
27
+ params: [start.toISOString()],
28
+ paramOffset: 1,
29
+ };
30
+ }
31
+ // Custom date range
32
+ return {
33
+ whereClause: "timestamp >= $1 AND timestamp <= $2",
34
+ params: [dateRange.start.toISOString(), dateRange.end.toISOString()],
35
+ paramOffset: 2,
36
+ };
37
+ }
38
+ /**
39
+ * PostgreSQL implementation of the analytics database
40
+ */
41
+ export class PostgresDB {
42
+ constructor(connectionString) {
43
+ this.pool = new Pool({
44
+ connectionString,
45
+ max: 10,
46
+ idleTimeoutMillis: 30000,
47
+ connectionTimeoutMillis: 5000,
48
+ });
49
+ }
50
+ async insertPageview(event, ipHash) {
51
+ const query = `
52
+ INSERT INTO locallytics_pageviews (
53
+ session_id, page_url, referrer, user_agent,
54
+ screen_width, screen_height, ip_hash, timestamp
55
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
56
+ `;
57
+ await this.pool.query(query, [
58
+ event.sessionId,
59
+ event.pageUrl,
60
+ event.referrer,
61
+ event.userAgent,
62
+ event.screenWidth,
63
+ event.screenHeight,
64
+ ipHash,
65
+ event.timestamp,
66
+ ]);
67
+ }
68
+ async getPageviews(dateRange) {
69
+ const { whereClause, params } = getDateRangeFilter(dateRange);
70
+ const query = `
71
+ SELECT COUNT(*) as count
72
+ FROM locallytics_pageviews
73
+ WHERE ${whereClause}
74
+ `;
75
+ const result = await this.pool.query(query, params);
76
+ return parseInt(result.rows[0]?.count ?? "0", 10);
77
+ }
78
+ async getUniqueVisitors(dateRange) {
79
+ const { whereClause, params } = getDateRangeFilter(dateRange);
80
+ const query = `
81
+ SELECT COUNT(DISTINCT session_id) as count
82
+ FROM locallytics_pageviews
83
+ WHERE ${whereClause}
84
+ `;
85
+ const result = await this.pool.query(query, params);
86
+ return parseInt(result.rows[0]?.count ?? "0", 10);
87
+ }
88
+ async getTopPages(dateRange, limit) {
89
+ const { whereClause, params, paramOffset } = getDateRangeFilter(dateRange);
90
+ const query = `
91
+ SELECT page_url as "pageUrl", COUNT(*) as count
92
+ FROM locallytics_pageviews
93
+ WHERE ${whereClause}
94
+ GROUP BY page_url
95
+ ORDER BY count DESC
96
+ LIMIT $${paramOffset + 1}
97
+ `;
98
+ const result = await this.pool.query(query, [...params, limit]);
99
+ return result.rows.map((row) => ({
100
+ pageUrl: row.pageUrl,
101
+ count: parseInt(row.count, 10),
102
+ }));
103
+ }
104
+ async getTopReferrers(dateRange, limit) {
105
+ const { whereClause, params, paramOffset } = getDateRangeFilter(dateRange);
106
+ const query = `
107
+ SELECT referrer as "pageUrl", COUNT(*) as count
108
+ FROM locallytics_pageviews
109
+ WHERE ${whereClause} AND referrer IS NOT NULL
110
+ GROUP BY referrer
111
+ ORDER BY count DESC
112
+ LIMIT $${paramOffset + 1}
113
+ `;
114
+ const result = await this.pool.query(query, [...params, limit]);
115
+ return result.rows.map((row) => ({
116
+ pageUrl: row.pageUrl,
117
+ count: parseInt(row.count, 10),
118
+ }));
119
+ }
120
+ async getDailyStats(dateRange) {
121
+ const { whereClause, params } = getDateRangeFilter(dateRange);
122
+ const query = `
123
+ SELECT
124
+ DATE(timestamp) as date,
125
+ COUNT(*) as pageviews,
126
+ COUNT(DISTINCT session_id) as "uniqueVisitors"
127
+ FROM locallytics_pageviews
128
+ WHERE ${whereClause}
129
+ GROUP BY DATE(timestamp)
130
+ ORDER BY date DESC
131
+ `;
132
+ const result = await this.pool.query(query, params);
133
+ return result.rows.map((row) => ({
134
+ date: row.date.toISOString().split("T")[0] ?? "",
135
+ pageviews: parseInt(row.pageviews, 10),
136
+ uniqueVisitors: parseInt(row.uniqueVisitors, 10),
137
+ }));
138
+ }
139
+ async close() {
140
+ await this.pool.end();
141
+ }
142
+ }
143
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/db/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAS1B;;GAEG;AACH,SAAS,kBAAkB,CAAC,SAAoB;IAK9C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC5D,OAAO;YACL,WAAW,EAAE,iBAAiB;YAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAC7B,WAAW,EAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAChE,OAAO;YACL,WAAW,EAAE,iBAAiB;YAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAC7B,WAAW,EAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACjE,OAAO;YACL,WAAW,EAAE,iBAAiB;YAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAC7B,WAAW,EAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,OAAO;QACL,WAAW,EAAE,qCAAqC;QAClD,MAAM,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACpE,WAAW,EAAE,CAAC;KACf,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,UAAU;IAGrB,YAAY,gBAAwB;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,CAAC;YACnB,gBAAgB;YAChB,GAAG,EAAE,EAAE;YACP,iBAAiB,EAAE,KAAK;YACxB,uBAAuB,EAAE,IAAI;SAC9B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,KAAoB,EACpB,MAAqB;QAErB,MAAM,KAAK,GAAG;;;;;KAKb,CAAC;QAEF,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;YAC3B,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,OAAO;YACb,KAAK,CAAC,QAAQ;YACd,KAAK,CAAC,SAAS;YACf,KAAK,CAAC,WAAW;YACjB,KAAK,CAAC,YAAY;YAClB,MAAM;YACN,KAAK,CAAC,SAAS;SAChB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAoB;QACrC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE9D,MAAM,KAAK,GAAG;;;cAGJ,WAAW;KACpB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAoB,KAAK,EAAE,MAAM,CAAC,CAAC;QACvE,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,SAAoB;QAC1C,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE9D,MAAM,KAAK,GAAG;;;cAGJ,WAAW;KACpB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAoB,KAAK,EAAE,MAAM,CAAC,CAAC;QACvE,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,SAAoB,EAAE,KAAa;QACnD,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE3E,MAAM,KAAK,GAAG;;;cAGJ,WAAW;;;eAGV,WAAW,GAAG,CAAC;KACzB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,KAAK,EACL,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CACnB,CAAC;QAEF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;SAC/B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,SAAoB,EACpB,KAAa;QAEb,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE3E,MAAM,KAAK,GAAG;;;cAGJ,WAAW;;;eAGV,WAAW,GAAG,CAAC;KACzB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAClC,KAAK,EACL,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CACnB,CAAC;QAEF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;SAC/B,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAAoB;QACtC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE9D,MAAM,KAAK,GAAG;;;;;;cAMJ,WAAW;;;KAGpB,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAIjC,KAAK,EAAE,MAAM,CAAC,CAAC;QAElB,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC/B,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;YAChD,SAAS,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACtC,cAAc,EAAE,QAAQ,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,CAAC;SACjD,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ export { locallytics, AnalyticsData } from "./server/index.js";
2
+ export { AnalyticsGrabber } from "./client/AnalyticsGrabber.js";
3
+ export type { LocallyticsConfig, AnalyticsResult, DateRange, PageStats, DailyStats, PageviewEvent, AnalyticsDataOptions, } from "./types/index.js";
4
+ export { LocallyticsError } 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":"AACA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGhE,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Server exports
2
+ export { locallytics, AnalyticsData } from "./server/index.js";
3
+ // Client exports
4
+ export { AnalyticsGrabber } from "./client/AnalyticsGrabber.js";
5
+ export { LocallyticsError } from "./types/index.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE/D,iBAAiB;AACjB,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAahE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { AnalyticsDB, LocallyticsConfig, RouteHandler } from "../types/index.js";
2
+ /**
3
+ * Create route handlers for analytics API
4
+ *
5
+ * @param db - Database instance
6
+ * @param config - Locallytics configuration
7
+ * @returns GET and POST handlers
8
+ */
9
+ export declare function createHandlers(db: AnalyticsDB, config: LocallyticsConfig): RouteHandler;
10
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAmC3B;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,iBAAiB,GACxB,YAAY,CAiGd"}
@@ -0,0 +1,105 @@
1
+ import { LocallyticsError } from "../types/index.js";
2
+ import { RateLimiter } from "../utils/rate-limit.js";
3
+ import { hashIP, getIPFromRequest } from "../utils/hash.js";
4
+ import { validatePageviewBatch, checkDNT } from "./validator.js";
5
+ import { getAnalyticsData } from "./queries.js";
6
+ /**
7
+ * Parse date range from query string
8
+ */
9
+ function parseDateRange(value) {
10
+ if (!value || value === "last7d") {
11
+ return "last7d";
12
+ }
13
+ if (value === "last24h" || value === "last30d") {
14
+ return value;
15
+ }
16
+ // Try to parse custom range (format: start,end as ISO strings)
17
+ if (value.includes(",")) {
18
+ const [startStr, endStr] = value.split(",");
19
+ if (startStr && endStr) {
20
+ const start = new Date(startStr);
21
+ const end = new Date(endStr);
22
+ if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
23
+ return { start, end };
24
+ }
25
+ }
26
+ }
27
+ return "last7d";
28
+ }
29
+ /**
30
+ * Create route handlers for analytics API
31
+ *
32
+ * @param db - Database instance
33
+ * @param config - Locallytics configuration
34
+ * @returns GET and POST handlers
35
+ */
36
+ export function createHandlers(db, config) {
37
+ const rateLimiter = new RateLimiter(100, 60000);
38
+ /**
39
+ * POST handler - track pageview events
40
+ */
41
+ async function POST(request) {
42
+ try {
43
+ // Check DNT - respect user privacy preference
44
+ if (checkDNT(request)) {
45
+ return Response.json({ success: true, tracked: false });
46
+ }
47
+ // Rate limiting by IP
48
+ const ip = getIPFromRequest(request);
49
+ const rateLimitKey = ip ?? "unknown";
50
+ if (!rateLimiter.check(rateLimitKey)) {
51
+ const error = LocallyticsError.rateLimit();
52
+ return Response.json({ error: error.message, code: error.code }, { status: error.statusCode });
53
+ }
54
+ // Parse and validate batch
55
+ const body = await request.json();
56
+ const events = validatePageviewBatch(body);
57
+ // Hash IP for privacy
58
+ const ipHash = ip ? hashIP(ip) : null;
59
+ // Insert all events
60
+ await Promise.all(events.map((event) => db.insertPageview(event, ipHash)));
61
+ return Response.json({
62
+ success: true,
63
+ tracked: true,
64
+ count: events.length,
65
+ });
66
+ }
67
+ catch (error) {
68
+ if (error instanceof LocallyticsError) {
69
+ return Response.json({ error: error.message, code: error.code }, { status: error.statusCode });
70
+ }
71
+ // Log unexpected errors but don't expose details
72
+ console.error("[Locallytics] POST error:", error);
73
+ return Response.json({ error: "Internal server error", code: "INTERNAL_ERROR" }, { status: 500 });
74
+ }
75
+ }
76
+ /**
77
+ * GET handler - fetch analytics data
78
+ */
79
+ async function GET(request) {
80
+ try {
81
+ // Check API key authentication if configured
82
+ if (config.apiKey) {
83
+ const authHeader = request.headers.get("authorization");
84
+ const providedKey = authHeader?.replace("Bearer ", "");
85
+ if (providedKey !== config.apiKey) {
86
+ const error = LocallyticsError.unauthorized();
87
+ return Response.json({ error: error.message, code: error.code }, { status: error.statusCode });
88
+ }
89
+ }
90
+ // Parse date range from query params
91
+ const url = new URL(request.url);
92
+ const dateRange = parseDateRange(url.searchParams.get("range"));
93
+ // Fetch analytics data
94
+ const data = await getAnalyticsData(db, dateRange);
95
+ return Response.json(data);
96
+ }
97
+ catch (error) {
98
+ // Log unexpected errors but don't expose details
99
+ console.error("[Locallytics] GET error:", error);
100
+ return Response.json({ error: "Internal server error", code: "INTERNAL_ERROR" }, { status: 500 });
101
+ }
102
+ }
103
+ return { GET, POST };
104
+ }
105
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { LocallyticsConfig, RouteHandler, AnalyticsResult, AnalyticsDataOptions } from "../types/index.js";
2
+ /**
3
+ * Initialize Locallytics and create route handlers
4
+ *
5
+ * @param config - Locallytics configuration
6
+ * @returns GET and POST route handlers for Next.js App Router
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // app/api/analytics/route.ts
11
+ * import { locallytics } from 'locallytics';
12
+ *
13
+ * const analytics = await locallytics({
14
+ * database: process.env.DATABASE_URL!,
15
+ * apiKey: process.env.ANALYTICS_API_KEY,
16
+ * });
17
+ *
18
+ * export const { GET, POST } = analytics;
19
+ * ```
20
+ */
21
+ export declare function locallytics(config: LocallyticsConfig): Promise<RouteHandler>;
22
+ /**
23
+ * Server-side helper to fetch analytics data
24
+ * Use in React Server Components
25
+ *
26
+ * @param options - Options including date range (optional)
27
+ * @returns Analytics data
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * // app/dashboard/page.tsx
32
+ * import { AnalyticsData } from 'locallytics';
33
+ *
34
+ * export default async function Dashboard() {
35
+ * const data = await AnalyticsData(); // No arguments needed!
36
+ *
37
+ * return <div>{data.pageviews} pageviews</div>;
38
+ * }
39
+ * ```
40
+ */
41
+ export declare function AnalyticsData(options?: AnalyticsDataOptions): Promise<AnalyticsResult>;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAI3B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CAGvB;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CA6C1B"}
@@ -0,0 +1,79 @@
1
+ import { PostgresDB } from "../db/postgres.js";
2
+ import { createHandlers } from "./handlers.js";
3
+ /**
4
+ * Initialize Locallytics and create route handlers
5
+ *
6
+ * @param config - Locallytics configuration
7
+ * @returns GET and POST route handlers for Next.js App Router
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // app/api/analytics/route.ts
12
+ * import { locallytics } from 'locallytics';
13
+ *
14
+ * const analytics = await locallytics({
15
+ * database: process.env.DATABASE_URL!,
16
+ * apiKey: process.env.ANALYTICS_API_KEY,
17
+ * });
18
+ *
19
+ * export const { GET, POST } = analytics;
20
+ * ```
21
+ */
22
+ export async function locallytics(config) {
23
+ const db = new PostgresDB(config.database);
24
+ return createHandlers(db, config);
25
+ }
26
+ import { headers } from "next/headers";
27
+ /**
28
+ * Server-side helper to fetch analytics data
29
+ * Use in React Server Components
30
+ *
31
+ * @param options - Options including date range (optional)
32
+ * @returns Analytics data
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * // app/dashboard/page.tsx
37
+ * import { AnalyticsData } from 'locallytics';
38
+ *
39
+ * export default async function Dashboard() {
40
+ * const data = await AnalyticsData(); // No arguments needed!
41
+ *
42
+ * return <div>{data.pageviews} pageviews</div>;
43
+ * }
44
+ * ```
45
+ */
46
+ export async function AnalyticsData(options = {}) {
47
+ const { dateRange = "last7d", endpoint = "/api/analytics" } = options;
48
+ // Get headers to extract host for URL construction
49
+ const headersList = await Promise.resolve(headers());
50
+ const host = headersList.get("host") ?? "localhost:3000";
51
+ const protocol = headersList.get("x-forwarded-proto") ?? "http";
52
+ // Build the range query parameter
53
+ let rangeParam;
54
+ if (typeof dateRange === "string") {
55
+ rangeParam = dateRange;
56
+ }
57
+ else {
58
+ rangeParam = `${dateRange.start.toISOString()},${dateRange.end.toISOString()}`;
59
+ }
60
+ // Construct the full URL
61
+ const url = `${protocol}://${host}${endpoint}?range=${encodeURIComponent(rangeParam)}`;
62
+ // Forward authorization header if present
63
+ const authHeader = headersList.get("authorization");
64
+ const fetchHeaders = {};
65
+ if (authHeader) {
66
+ fetchHeaders["authorization"] = authHeader;
67
+ }
68
+ const response = await fetch(url, {
69
+ headers: fetchHeaders,
70
+ cache: "no-store",
71
+ });
72
+ if (!response.ok) {
73
+ const error = (await response.json().catch(() => ({})));
74
+ throw new Error(error["message"] ??
75
+ `Failed to fetch analytics: ${response.status}`);
76
+ }
77
+ return response.json();
78
+ }
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAyB;IAEzB,MAAM,EAAE,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3C,OAAO,cAAc,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAgC,EAAE;IAElC,MAAM,EAAE,SAAS,GAAG,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,EAAE,GAAG,OAAO,CAAC;IAEtE,mDAAmD;IACnD,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,gBAAgB,CAAC;IACzD,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,MAAM,CAAC;IAEhE,kCAAkC;IAClC,IAAI,UAAkB,CAAC;IACvB,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,UAAU,GAAG,SAAS,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;IACjF,CAAC;IAED,yBAAyB;IACzB,MAAM,GAAG,GAAG,GAAG,QAAQ,MAAM,IAAI,GAAG,QAAQ,UAAU,kBAAkB,CACtE,UAAU,CACX,EAAE,CAAC;IAEJ,0CAA0C;IAC1C,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACpD,MAAM,YAAY,GAA2B,EAAE,CAAC;IAChD,IAAI,UAAU,EAAE,CAAC;QACf,YAAY,CAAC,eAAe,CAAC,GAAG,UAAU,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,OAAO,EAAE,YAAY;QACrB,KAAK,EAAE,UAAU;KAClB,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,KAAK,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAGrD,CAAC;QACF,MAAM,IAAI,KAAK,CACZ,KAAK,CAAC,SAAS,CAAY;YAC1B,8BAA8B,QAAQ,CAAC,MAAM,EAAE,CAClD,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAA8B,CAAC;AACrD,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { AnalyticsDB, AnalyticsResult, DateRange } from "../types/index.js";
2
+ /**
3
+ * Fetch all analytics data in parallel
4
+ *
5
+ * @param db - Database instance
6
+ * @param dateRange - Date range for the query
7
+ * @returns Complete analytics data
8
+ */
9
+ export declare function getAnalyticsData(db: AnalyticsDB, dateRange: DateRange): Promise<AnalyticsResult>;
10
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/server/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,SAAS,EACV,MAAM,mBAAmB,CAAC;AAE3B;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,eAAe,CAAC,CAiB1B"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Fetch all analytics data in parallel
3
+ *
4
+ * @param db - Database instance
5
+ * @param dateRange - Date range for the query
6
+ * @returns Complete analytics data
7
+ */
8
+ export async function getAnalyticsData(db, dateRange) {
9
+ const [pageviews, uniqueVisitors, topPages, topReferrers, dailyStats] = await Promise.all([
10
+ db.getPageviews(dateRange),
11
+ db.getUniqueVisitors(dateRange),
12
+ db.getTopPages(dateRange, 10),
13
+ db.getTopReferrers(dateRange, 10),
14
+ db.getDailyStats(dateRange),
15
+ ]);
16
+ return {
17
+ pageviews,
18
+ uniqueVisitors,
19
+ topPages,
20
+ topReferrers,
21
+ dailyStats,
22
+ };
23
+ }
24
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/server/queries.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,EAAe,EACf,SAAoB;IAEpB,MAAM,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,GACnE,MAAM,OAAO,CAAC,GAAG,CAAC;QAChB,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC;QAC1B,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;QAC/B,EAAE,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC;QAC7B,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,EAAE,CAAC;QACjC,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC;KAC5B,CAAC,CAAC;IAEL,OAAO;QACL,SAAS;QACT,cAAc;QACd,QAAQ;QACR,YAAY;QACZ,UAAU;KACX,CAAC;AACJ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { PageviewEvent } from "../types/index.js";
2
+ /**
3
+ * Sanitize a URL by extracting pathname and search, removing credentials
4
+ *
5
+ * @param url - Raw URL string
6
+ * @returns Sanitized URL (pathname + search only)
7
+ */
8
+ export declare function sanitizeUrl(url: string): string;
9
+ /**
10
+ * Check if Do Not Track is enabled in the request
11
+ *
12
+ * @param request - Incoming request
13
+ * @returns true if DNT is enabled
14
+ */
15
+ export declare function checkDNT(request: Request): boolean;
16
+ /**
17
+ * Validate a single pageview event
18
+ *
19
+ * @param data - Unknown input data
20
+ * @returns Validated PageviewEvent
21
+ * @throws LocallyticsError if validation fails
22
+ */
23
+ export declare function validatePageviewEvent(data: unknown): PageviewEvent;
24
+ /**
25
+ * Validate a batch of pageview events
26
+ *
27
+ * @param data - Unknown input data
28
+ * @returns Array of validated PageviewEvents
29
+ * @throws LocallyticsError if validation fails
30
+ */
31
+ export declare function validatePageviewBatch(data: unknown): PageviewEvent[];
32
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/server/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkB/C;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAgBlD;AASD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,CA6ElE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,CA2BpE"}
@@ -0,0 +1,149 @@
1
+ import { LocallyticsError } from "../types/index.js";
2
+ const MAX_SESSION_ID_LENGTH = 255;
3
+ const MAX_USER_AGENT_LENGTH = 512;
4
+ const MAX_URL_LENGTH = 2048;
5
+ const MAX_BATCH_SIZE = 100;
6
+ /**
7
+ * Sanitize a URL by extracting pathname and search, removing credentials
8
+ *
9
+ * @param url - Raw URL string
10
+ * @returns Sanitized URL (pathname + search only)
11
+ */
12
+ export function sanitizeUrl(url) {
13
+ try {
14
+ // Handle relative URLs by adding a base
15
+ const parsed = new URL(url, "http://localhost");
16
+ // Extract pathname and search only (no origin, hash, credentials)
17
+ let sanitized = parsed.pathname + parsed.search;
18
+ // Limit length
19
+ if (sanitized.length > MAX_URL_LENGTH) {
20
+ sanitized = sanitized.slice(0, MAX_URL_LENGTH);
21
+ }
22
+ return sanitized;
23
+ }
24
+ catch {
25
+ // On parse error, return root
26
+ return "/";
27
+ }
28
+ }
29
+ /**
30
+ * Check if Do Not Track is enabled in the request
31
+ *
32
+ * @param request - Incoming request
33
+ * @returns true if DNT is enabled
34
+ */
35
+ export function checkDNT(request) {
36
+ const headers = request.headers;
37
+ // Check DNT header
38
+ const dnt = headers.get("dnt");
39
+ if (dnt === "1") {
40
+ return true;
41
+ }
42
+ // Check Sec-GPC header (Global Privacy Control)
43
+ const gpc = headers.get("sec-gpc");
44
+ if (gpc === "1") {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ /**
50
+ * Type guard to check if value is a record
51
+ */
52
+ function isRecord(value) {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
55
+ /**
56
+ * Validate a single pageview event
57
+ *
58
+ * @param data - Unknown input data
59
+ * @returns Validated PageviewEvent
60
+ * @throws LocallyticsError if validation fails
61
+ */
62
+ export function validatePageviewEvent(data) {
63
+ if (!isRecord(data)) {
64
+ throw LocallyticsError.validation("Event must be an object");
65
+ }
66
+ // sessionId
67
+ if (typeof data["sessionId"] !== "string" || data["sessionId"].length === 0) {
68
+ throw LocallyticsError.validation("sessionId must be a non-empty string");
69
+ }
70
+ if (data["sessionId"].length > MAX_SESSION_ID_LENGTH) {
71
+ throw LocallyticsError.validation(`sessionId must be at most ${MAX_SESSION_ID_LENGTH} characters`);
72
+ }
73
+ // pageUrl
74
+ if (typeof data["pageUrl"] !== "string" || data["pageUrl"].length === 0) {
75
+ throw LocallyticsError.validation("pageUrl must be a non-empty string");
76
+ }
77
+ // referrer (nullable)
78
+ if (data["referrer"] !== null &&
79
+ data["referrer"] !== undefined &&
80
+ typeof data["referrer"] !== "string") {
81
+ throw LocallyticsError.validation("referrer must be a string or null");
82
+ }
83
+ // userAgent
84
+ if (typeof data["userAgent"] !== "string") {
85
+ throw LocallyticsError.validation("userAgent must be a string");
86
+ }
87
+ // screenWidth
88
+ if (typeof data["screenWidth"] !== "number" ||
89
+ data["screenWidth"] <= 0 ||
90
+ !Number.isFinite(data["screenWidth"])) {
91
+ throw LocallyticsError.validation("screenWidth must be a positive number");
92
+ }
93
+ // screenHeight
94
+ if (typeof data["screenHeight"] !== "number" ||
95
+ data["screenHeight"] <= 0 ||
96
+ !Number.isFinite(data["screenHeight"])) {
97
+ throw LocallyticsError.validation("screenHeight must be a positive number");
98
+ }
99
+ // timestamp
100
+ if (typeof data["timestamp"] !== "string") {
101
+ throw LocallyticsError.validation("timestamp must be a string");
102
+ }
103
+ // Validate ISO 8601 format
104
+ const timestampDate = new Date(data["timestamp"]);
105
+ if (isNaN(timestampDate.getTime())) {
106
+ throw LocallyticsError.validation("timestamp must be a valid ISO 8601 date");
107
+ }
108
+ return {
109
+ sessionId: data["sessionId"],
110
+ pageUrl: sanitizeUrl(data["pageUrl"]),
111
+ referrer: typeof data["referrer"] === "string"
112
+ ? sanitizeUrl(data["referrer"])
113
+ : null,
114
+ userAgent: data["userAgent"].slice(0, MAX_USER_AGENT_LENGTH),
115
+ screenWidth: Math.round(data["screenWidth"]),
116
+ screenHeight: Math.round(data["screenHeight"]),
117
+ timestamp: data["timestamp"],
118
+ };
119
+ }
120
+ /**
121
+ * Validate a batch of pageview events
122
+ *
123
+ * @param data - Unknown input data
124
+ * @returns Array of validated PageviewEvents
125
+ * @throws LocallyticsError if validation fails
126
+ */
127
+ export function validatePageviewBatch(data) {
128
+ if (!Array.isArray(data)) {
129
+ throw LocallyticsError.validation("Batch must be an array");
130
+ }
131
+ if (data.length === 0) {
132
+ throw LocallyticsError.validation("Batch cannot be empty");
133
+ }
134
+ if (data.length > MAX_BATCH_SIZE) {
135
+ throw LocallyticsError.validation(`Batch cannot exceed ${MAX_BATCH_SIZE} events`);
136
+ }
137
+ return data.map((event, index) => {
138
+ try {
139
+ return validatePageviewEvent(event);
140
+ }
141
+ catch (error) {
142
+ if (error instanceof LocallyticsError) {
143
+ throw LocallyticsError.validation(`Event at index ${index}: ${error.message}`);
144
+ }
145
+ throw error;
146
+ }
147
+ });
148
+ }
149
+ //# sourceMappingURL=validator.js.map