locallytics 0.1.3 → 0.1.5
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 +4 -4
- package/dist/client/LocallyticsGrabber.d.ts +28 -0
- package/dist/client/LocallyticsGrabber.d.ts.map +1 -0
- package/dist/client/LocallyticsGrabber.js +71 -0
- package/dist/client/LocallyticsGrabber.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 +100 -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 +129 -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 +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGpE,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 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEjE,iBAAiB;AACjB,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAapE,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 { LocallyticsData } from 'locallytics';
|
|
33
|
+
*
|
|
34
|
+
* export default async function Dashboard() {
|
|
35
|
+
* const data = await LocallyticsData(); // No arguments needed!
|
|
36
|
+
*
|
|
37
|
+
* return <div>{data.pageviews} pageviews</div>;
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function LocallyticsData(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,eAAe,CACnC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CAiE1B"}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { LocallyticsData } from 'locallytics';
|
|
38
|
+
*
|
|
39
|
+
* export default async function Dashboard() {
|
|
40
|
+
* const data = await LocallyticsData(); // No arguments needed!
|
|
41
|
+
*
|
|
42
|
+
* return <div>{data.pageviews} pageviews</div>;
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export async function LocallyticsData(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
|
+
try {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
headers: fetchHeaders,
|
|
71
|
+
cache: "no-store",
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
75
|
+
const errorMessage = errorData["message"] ??
|
|
76
|
+
errorData["error"] ??
|
|
77
|
+
`Failed to fetch analytics: ${response.status}`;
|
|
78
|
+
return {
|
|
79
|
+
pageviews: 0,
|
|
80
|
+
uniqueVisitors: 0,
|
|
81
|
+
topPages: [],
|
|
82
|
+
topReferrers: [],
|
|
83
|
+
dailyStats: [],
|
|
84
|
+
error: errorMessage,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return response.json();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
pageviews: 0,
|
|
92
|
+
uniqueVisitors: 0,
|
|
93
|
+
topPages: [],
|
|
94
|
+
topReferrers: [],
|
|
95
|
+
dailyStats: [],
|
|
96
|
+
error: error instanceof Error ? error.message : "Unknown error occurred",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/server/validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
/** Error message if fetch failed */
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Database interface for analytics operations
|
|
84
|
+
*/
|
|
85
|
+
export interface AnalyticsDB {
|
|
86
|
+
/** Insert a pageview event */
|
|
87
|
+
insertPageview(event: PageviewEvent, ipHash: string | null): Promise<void>;
|
|
88
|
+
/** Get total pageview count */
|
|
89
|
+
getPageviews(dateRange: DateRange): Promise<number>;
|
|
90
|
+
/** Get unique visitor count (distinct sessions) */
|
|
91
|
+
getUniqueVisitors(dateRange: DateRange): Promise<number>;
|
|
92
|
+
/** Get top pages by pageview count */
|
|
93
|
+
getTopPages(dateRange: DateRange, limit: number): Promise<PageStats[]>;
|
|
94
|
+
/** Get top referrers by pageview count */
|
|
95
|
+
getTopReferrers(dateRange: DateRange, limit: number): Promise<PageStats[]>;
|
|
96
|
+
/** Get daily stats breakdown */
|
|
97
|
+
getDailyStats(dateRange: DateRange): Promise<DailyStats[]>;
|
|
98
|
+
/** Close database connection */
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Route handler type for Next.js App Router
|
|
103
|
+
*/
|
|
104
|
+
export interface RouteHandler {
|
|
105
|
+
GET: (request: Request) => Promise<Response>;
|
|
106
|
+
POST: (request: Request) => Promise<Response>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Options for AnalyticsJSON helper
|
|
110
|
+
*/
|
|
111
|
+
export interface AnalyticsDataOptions {
|
|
112
|
+
/** Optional date range for analytics data (default: last7d) */
|
|
113
|
+
dateRange?: DateRange;
|
|
114
|
+
/** API endpoint URL (defaults to /api/analytics) */
|
|
115
|
+
endpoint?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Custom error class for Locallytics errors
|
|
119
|
+
*/
|
|
120
|
+
export declare class LocallyticsError extends Error {
|
|
121
|
+
readonly code: string;
|
|
122
|
+
readonly statusCode: number;
|
|
123
|
+
constructor(message: string, code: string, statusCode?: number);
|
|
124
|
+
static validation(message: string): LocallyticsError;
|
|
125
|
+
static rateLimit(): LocallyticsError;
|
|
126
|
+
static unauthorized(): LocallyticsError;
|
|
127
|
+
static database(message: string): LocallyticsError;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=index.d.ts.map
|