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 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/server/validator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,cAAc,GAAG,IAAI,CAAC;AAC5B,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,wCAAwC;QACxC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAEhD,kEAAkE;QAClE,IAAI,SAAS,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;QAEhD,eAAe;QACf,IAAI,SAAS,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;YACtC,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;QACjD,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,8BAA8B;QAC9B,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAgB;IACvC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAEhC,mBAAmB;IACnB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,gDAAgD;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAa;IACjD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpB,MAAM,gBAAgB,CAAC,UAAU,CAAC,yBAAyB,CAAC,CAAC;IAC/D,CAAC;IAED,YAAY;IACZ,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5E,MAAM,gBAAgB,CAAC,UAAU,CAAC,sCAAsC,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,qBAAqB,EAAE,CAAC;QACrD,MAAM,gBAAgB,CAAC,UAAU,CAC/B,6BAA6B,qBAAqB,aAAa,CAChE,CAAC;IACJ,CAAC;IAED,UAAU;IACV,IAAI,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,gBAAgB,CAAC,UAAU,CAAC,oCAAoC,CAAC,CAAC;IAC1E,CAAC;IAED,sBAAsB;IACtB,IACE,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI;QACzB,IAAI,CAAC,UAAU,CAAC,KAAK,SAAS;QAC9B,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,QAAQ,EACpC,CAAC;QACD,MAAM,gBAAgB,CAAC,UAAU,CAAC,mCAAmC,CAAC,CAAC;IACzE,CAAC;IAED,YAAY;IACZ,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,gBAAgB,CAAC,UAAU,CAAC,4BAA4B,CAAC,CAAC;IAClE,CAAC;IAED,cAAc;IACd,IACE,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,QAAQ;QACvC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;QACxB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,EACrC,CAAC;QACD,MAAM,gBAAgB,CAAC,UAAU,CAAC,uCAAuC,CAAC,CAAC;IAC7E,CAAC;IAED,eAAe;IACf,IACE,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,QAAQ;QACxC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;QACzB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,EACtC,CAAC;QACD,MAAM,gBAAgB,CAAC,UAAU,CAAC,wCAAwC,CAAC,CAAC;IAC9E,CAAC;IAED,YAAY;IACZ,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,gBAAgB,CAAC,UAAU,CAAC,4BAA4B,CAAC,CAAC;IAClE,CAAC;IAED,2BAA2B;IAC3B,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAClD,IAAI,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACnC,MAAM,gBAAgB,CAAC,UAAU,CAC/B,yCAAyC,CAC1C,CAAC;IACJ,CAAC;IAED,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;QAC5B,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrC,QAAQ,EACN,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,QAAQ;YAClC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,CAAC,CAAC,IAAI;QACV,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC;QAC5D,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5C,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC9C,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;KAC7B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAa;IACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,gBAAgB,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAC;IAC9D,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,gBAAgB,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QACjC,MAAM,gBAAgB,CAAC,UAAU,CAC/B,uBAAuB,cAAc,SAAS,CAC/C,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,gBAAgB,EAAE,CAAC;gBACtC,MAAM,gBAAgB,CAAC,UAAU,CAC/B,kBAAkB,KAAK,KAAK,KAAK,CAAC,OAAO,EAAE,CAC5C,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Configuration for the locallytics() function
3
+ */
4
+ export interface LocallyticsConfig {
5
+ /** PostgreSQL connection string */
6
+ database: string;
7
+ /** Optional API key for GET endpoint authentication */
8
+ apiKey?: string;
9
+ }
10
+ /**
11
+ * Pageview event sent from client
12
+ */
13
+ export interface PageviewEvent {
14
+ /** Client-generated session ID (ephemeral) */
15
+ sessionId: string;
16
+ /** Sanitized page URL (pathname + search) */
17
+ pageUrl: string;
18
+ /** Sanitized referrer URL (nullable) */
19
+ referrer: string | null;
20
+ /** User agent string (limited to 512 chars) */
21
+ userAgent: string;
22
+ /** Screen width in pixels */
23
+ screenWidth: number;
24
+ /** Screen height in pixels */
25
+ screenHeight: number;
26
+ /** ISO 8601 timestamp */
27
+ timestamp: string;
28
+ }
29
+ /**
30
+ * Pageview as stored in database
31
+ */
32
+ export interface StoredPageview extends PageviewEvent {
33
+ /** UUID primary key */
34
+ id: string;
35
+ /** SHA-256 hashed IP address (nullable) */
36
+ ipHash: string | null;
37
+ }
38
+ /**
39
+ * Date range for analytics queries
40
+ */
41
+ export type DateRange = "last24h" | "last7d" | "last30d" | {
42
+ start: Date;
43
+ end: Date;
44
+ };
45
+ /**
46
+ * Page statistics
47
+ */
48
+ export interface PageStats {
49
+ /** Page URL */
50
+ pageUrl: string;
51
+ /** Number of pageviews */
52
+ count: number;
53
+ }
54
+ /**
55
+ * Daily statistics
56
+ */
57
+ export interface DailyStats {
58
+ /** Date in YYYY-MM-DD format */
59
+ date: string;
60
+ /** Total pageviews */
61
+ pageviews: number;
62
+ /** Unique visitors (distinct sessions) */
63
+ uniqueVisitors: number;
64
+ }
65
+ /**
66
+ * Complete analytics data response
67
+ */
68
+ export interface AnalyticsResult {
69
+ /** Total pageviews in date range */
70
+ pageviews: number;
71
+ /** Unique visitors (distinct sessions) in date range */
72
+ uniqueVisitors: number;
73
+ /** Top pages by pageview count */
74
+ topPages: PageStats[];
75
+ /** Top referrers by pageview count */
76
+ topReferrers: PageStats[];
77
+ /** Daily breakdown of stats */
78
+ dailyStats: DailyStats[];
79
+ }
80
+ /**
81
+ * Database interface for analytics operations
82
+ */
83
+ export interface AnalyticsDB {
84
+ /** Insert a pageview event */
85
+ insertPageview(event: PageviewEvent, ipHash: string | null): Promise<void>;
86
+ /** Get total pageview count */
87
+ getPageviews(dateRange: DateRange): Promise<number>;
88
+ /** Get unique visitor count (distinct sessions) */
89
+ getUniqueVisitors(dateRange: DateRange): Promise<number>;
90
+ /** Get top pages by pageview count */
91
+ getTopPages(dateRange: DateRange, limit: number): Promise<PageStats[]>;
92
+ /** Get top referrers by pageview count */
93
+ getTopReferrers(dateRange: DateRange, limit: number): Promise<PageStats[]>;
94
+ /** Get daily stats breakdown */
95
+ getDailyStats(dateRange: DateRange): Promise<DailyStats[]>;
96
+ /** Close database connection */
97
+ close(): Promise<void>;
98
+ }
99
+ /**
100
+ * Route handler type for Next.js App Router
101
+ */
102
+ export interface RouteHandler {
103
+ GET: (request: Request) => Promise<Response>;
104
+ POST: (request: Request) => Promise<Response>;
105
+ }
106
+ /**
107
+ * Options for AnalyticsJSON helper
108
+ */
109
+ export interface AnalyticsDataOptions {
110
+ /** Optional date range for analytics data (default: last7d) */
111
+ dateRange?: DateRange;
112
+ /** API endpoint URL (defaults to /api/analytics) */
113
+ endpoint?: string;
114
+ }
115
+ /**
116
+ * Custom error class for Locallytics errors
117
+ */
118
+ export declare class LocallyticsError extends Error {
119
+ readonly code: string;
120
+ readonly statusCode: number;
121
+ constructor(message: string, code: string, statusCode?: number);
122
+ static validation(message: string): LocallyticsError;
123
+ static rateLimit(): LocallyticsError;
124
+ static unauthorized(): LocallyticsError;
125
+ static database(message: string): LocallyticsError;
126
+ }
127
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Custom error class for Locallytics errors
3
+ */
4
+ export class LocallyticsError extends Error {
5
+ constructor(message, code, statusCode = 500) {
6
+ super(message);
7
+ this.code = code;
8
+ this.statusCode = statusCode;
9
+ this.name = "LocallyticsError";
10
+ }
11
+ static validation(message) {
12
+ return new LocallyticsError(message, "VALIDATION_ERROR", 400);
13
+ }
14
+ static rateLimit() {
15
+ return new LocallyticsError("Rate limit exceeded", "RATE_LIMIT_EXCEEDED", 429);
16
+ }
17
+ static unauthorized() {
18
+ return new LocallyticsError("Unauthorized", "UNAUTHORIZED", 401);
19
+ }
20
+ static database(message) {
21
+ return new LocallyticsError(message, "DATABASE_ERROR", 500);
22
+ }
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AA6HA;;GAEG;AACH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YACE,OAAe,EACC,IAAY,EACZ,aAAqB,GAAG;QAExC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAQ;QACZ,eAAU,GAAV,UAAU,CAAc;QAGxC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;IAED,MAAM,CAAC,UAAU,CAAC,OAAe;QAC/B,OAAO,IAAI,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,CAAC,SAAS;QACd,OAAO,IAAI,gBAAgB,CACzB,qBAAqB,EACrB,qBAAqB,EACrB,GAAG,CACJ,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,YAAY;QACjB,OAAO,IAAI,gBAAgB,CAAC,cAAc,EAAE,cAAc,EAAE,GAAG,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,OAAe;QAC7B,OAAO,IAAI,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hash an IP address using SHA-256 for privacy
3
+ *
4
+ * @param ip - Raw IP address
5
+ * @returns SHA-256 hash of the IP address
6
+ */
7
+ export declare function hashIP(ip: string): string;
8
+ /**
9
+ * Extract IP address from request headers
10
+ * Checks x-forwarded-for and x-real-ip headers
11
+ *
12
+ * @param request - Incoming request
13
+ * @returns IP address or null if not found
14
+ */
15
+ export declare function getIPFromRequest(request: Request): string | null;
16
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAoBhE"}
@@ -0,0 +1,36 @@
1
+ import { createHash } from "crypto";
2
+ /**
3
+ * Hash an IP address using SHA-256 for privacy
4
+ *
5
+ * @param ip - Raw IP address
6
+ * @returns SHA-256 hash of the IP address
7
+ */
8
+ export function hashIP(ip) {
9
+ return createHash("sha256").update(ip).digest("hex");
10
+ }
11
+ /**
12
+ * Extract IP address from request headers
13
+ * Checks x-forwarded-for and x-real-ip headers
14
+ *
15
+ * @param request - Incoming request
16
+ * @returns IP address or null if not found
17
+ */
18
+ export function getIPFromRequest(request) {
19
+ const headers = request.headers;
20
+ // Check x-forwarded-for first (common for proxies/load balancers)
21
+ const forwardedFor = headers.get("x-forwarded-for");
22
+ if (forwardedFor) {
23
+ // Take the first IP in the chain (original client)
24
+ const firstIP = forwardedFor.split(",")[0];
25
+ if (firstIP) {
26
+ return firstIP.trim();
27
+ }
28
+ }
29
+ // Check x-real-ip (nginx)
30
+ const realIP = headers.get("x-real-ip");
31
+ if (realIP) {
32
+ return realIP.trim();
33
+ }
34
+ return null;
35
+ }
36
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,EAAU;IAC/B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAEhC,kEAAkE;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACpD,IAAI,YAAY,EAAE,CAAC;QACjB,mDAAmD;QACnD,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3C,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAED,0BAA0B;IAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Simple in-memory rate limiter
3
+ */
4
+ export declare class RateLimiter {
5
+ private readonly requests;
6
+ private readonly maxRequests;
7
+ private readonly windowMs;
8
+ private cleanupInterval;
9
+ /**
10
+ * Create a new rate limiter
11
+ *
12
+ * @param maxRequests - Maximum requests allowed in the time window
13
+ * @param windowMs - Time window in milliseconds
14
+ */
15
+ constructor(maxRequests?: number, windowMs?: number);
16
+ /**
17
+ * Check if an identifier is within rate limits
18
+ *
19
+ * @param identifier - Unique identifier (e.g., IP address)
20
+ * @returns true if request is allowed, false if rate limited
21
+ */
22
+ check(identifier: string): boolean;
23
+ /**
24
+ * Clean up expired entries to prevent memory leaks
25
+ */
26
+ private cleanup;
27
+ /**
28
+ * Stop the cleanup interval (for graceful shutdown)
29
+ */
30
+ destroy(): void;
31
+ }
32
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/utils/rate-limit.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,eAAe,CAA+C;IAEtE;;;;;OAKG;gBACS,WAAW,GAAE,MAAY,EAAE,QAAQ,GAAE,MAAc;IAe/D;;;;;OAKG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAsBlC;;OAEG;IACH,OAAO,CAAC,OAAO;IAef;;OAEG;IACH,OAAO,IAAI,IAAI;CAMhB"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Simple in-memory rate limiter
3
+ */
4
+ export class RateLimiter {
5
+ /**
6
+ * Create a new rate limiter
7
+ *
8
+ * @param maxRequests - Maximum requests allowed in the time window
9
+ * @param windowMs - Time window in milliseconds
10
+ */
11
+ constructor(maxRequests = 100, windowMs = 60000) {
12
+ this.requests = new Map();
13
+ this.cleanupInterval = null;
14
+ this.maxRequests = maxRequests;
15
+ this.windowMs = windowMs;
16
+ // Cleanup expired entries every minute
17
+ this.cleanupInterval = setInterval(() => {
18
+ this.cleanup();
19
+ }, 60000);
20
+ // Prevent the interval from keeping the process alive
21
+ if (this.cleanupInterval.unref) {
22
+ this.cleanupInterval.unref();
23
+ }
24
+ }
25
+ /**
26
+ * Check if an identifier is within rate limits
27
+ *
28
+ * @param identifier - Unique identifier (e.g., IP address)
29
+ * @returns true if request is allowed, false if rate limited
30
+ */
31
+ check(identifier) {
32
+ const now = Date.now();
33
+ const windowStart = now - this.windowMs;
34
+ // Get existing requests for this identifier
35
+ const existing = this.requests.get(identifier) ?? [];
36
+ // Filter to only requests within the current window
37
+ const recentRequests = existing.filter((time) => time > windowStart);
38
+ // Check if limit exceeded
39
+ if (recentRequests.length >= this.maxRequests) {
40
+ return false;
41
+ }
42
+ // Add current request
43
+ recentRequests.push(now);
44
+ this.requests.set(identifier, recentRequests);
45
+ return true;
46
+ }
47
+ /**
48
+ * Clean up expired entries to prevent memory leaks
49
+ */
50
+ cleanup() {
51
+ const now = Date.now();
52
+ const windowStart = now - this.windowMs;
53
+ for (const [identifier, timestamps] of this.requests.entries()) {
54
+ const recentRequests = timestamps.filter((time) => time > windowStart);
55
+ if (recentRequests.length === 0) {
56
+ this.requests.delete(identifier);
57
+ }
58
+ else {
59
+ this.requests.set(identifier, recentRequests);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Stop the cleanup interval (for graceful shutdown)
65
+ */
66
+ destroy() {
67
+ if (this.cleanupInterval) {
68
+ clearInterval(this.cleanupInterval);
69
+ this.cleanupInterval = null;
70
+ }
71
+ }
72
+ }
73
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/utils/rate-limit.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,WAAW;IAMtB;;;;;OAKG;IACH,YAAY,cAAsB,GAAG,EAAE,WAAmB,KAAK;QAX9C,aAAQ,GAA0B,IAAI,GAAG,EAAE,CAAC;QAGrD,oBAAe,GAA0C,IAAI,CAAC;QASpE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAEzB,uCAAuC;QACvC,IAAI,CAAC,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE;YACtC,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,EAAE,KAAK,CAAC,CAAC;QAEV,sDAAsD;QACtD,IAAI,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC/B,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAkB;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;QAExC,4CAA4C;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAErD,oDAAoD;QACpD,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,WAAW,CAAC,CAAC;QAErE,0BAA0B;QAC1B,IAAI,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC9C,OAAO,KAAK,CAAC;QACf,CAAC;QAED,sBAAsB;QACtB,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAE9C,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,OAAO;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;QAExC,KAAK,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;YAC/D,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,WAAW,CAAC,CAAC;YAEvE,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAChC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACnC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,OAAO;QACL,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACpC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC;IACH,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "locallytics",
3
+ "version": "0.1.0",
4
+ "description": "Self-hosted, privacy-first analytics SDK for Next.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src/db/schema.sql"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest"
24
+ },
25
+ "keywords": [
26
+ "analytics",
27
+ "privacy",
28
+ "nextjs",
29
+ "postgresql",
30
+ "self-hosted"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "pg": "^8.11.3"
36
+ },
37
+ "peerDependencies": {
38
+ "next": ">=13.0.0",
39
+ "react": ">=18.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.10.0",
43
+ "@types/pg": "^8.10.9",
44
+ "@types/react": "^18.2.0",
45
+ "typescript": "^5.3.0",
46
+ "vitest": "^1.1.0"
47
+ }
48
+ }
@@ -0,0 +1,35 @@
1
+ -- Locallytics PostgreSQL Schema
2
+ -- Run this to create the required table and indexes
3
+
4
+ CREATE TABLE IF NOT EXISTS locallytics_pageviews (
5
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ session_id TEXT NOT NULL,
7
+ page_url TEXT NOT NULL,
8
+ referrer TEXT,
9
+ user_agent TEXT NOT NULL,
10
+ screen_width INTEGER NOT NULL,
11
+ screen_height INTEGER NOT NULL,
12
+ ip_hash TEXT,
13
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
14
+ );
15
+
16
+ -- Index for time-based queries (most common)
17
+ CREATE INDEX IF NOT EXISTS idx_pageviews_timestamp
18
+ ON locallytics_pageviews(timestamp DESC);
19
+
20
+ -- Index for session lookups
21
+ CREATE INDEX IF NOT EXISTS idx_pageviews_session
22
+ ON locallytics_pageviews(session_id);
23
+
24
+ -- Index for page URL grouping
25
+ CREATE INDEX IF NOT EXISTS idx_pageviews_page_url
26
+ ON locallytics_pageviews(page_url);
27
+
28
+ -- Partial index for referrer (only non-null values)
29
+ CREATE INDEX IF NOT EXISTS idx_pageviews_referrer
30
+ ON locallytics_pageviews(referrer)
31
+ WHERE referrer IS NOT NULL;
32
+
33
+ -- Composite index for time + session queries
34
+ CREATE INDEX IF NOT EXISTS idx_pageviews_timestamp_session
35
+ ON locallytics_pageviews(timestamp DESC, session_id);