@seneris/nosework 0.1.0 → 0.3.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCrB,CAAC;AAGF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqBlB,CAAC;AAGF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGrB,CAAC;AAGH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoC3B,CAAC;AAGF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuBvB,CAAC;AAGF,MAAM,MAAM,QAAQ,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AACrD,MAAM,MAAM,WAAW,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AACxD,MAAM,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,YAAY,CAAC;AAC/C,MAAM,MAAM,QAAQ,GAAG,OAAO,MAAM,CAAC,YAAY,CAAC;AAClD,MAAM,MAAM,SAAS,GAAG,OAAO,UAAU,CAAC,YAAY,CAAC;AACvD,MAAM,MAAM,cAAc,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AACjE,MAAM,MAAM,iBAAiB,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,101 @@
1
+ import { pgTable, text, timestamp, boolean, integer, json, date, index, unique, } from "drizzle-orm/pg-core";
2
+ // Page view events
3
+ export const pageViews = pgTable("page_views", {
4
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
5
+ siteId: text("siteId").notNull(),
6
+ // Page info
7
+ url: text("url").notNull(),
8
+ pathname: text("pathname").notNull(),
9
+ referrer: text("referrer"),
10
+ // Visitor (cookieless identification)
11
+ visitorHash: text("visitorHash").notNull(),
12
+ sessionId: text("sessionId").notNull(),
13
+ // Location (from Vercel geo headers)
14
+ country: text("country"),
15
+ countryCode: text("countryCode"),
16
+ region: text("region"),
17
+ city: text("city"),
18
+ // Device (parsed from User-Agent)
19
+ browser: text("browser"),
20
+ browserVer: text("browserVer"),
21
+ os: text("os"),
22
+ osVer: text("osVer"),
23
+ device: text("device"),
24
+ // Optional user link
25
+ userId: text("userId"),
26
+ isBot: boolean("isBot").default(false).notNull(),
27
+ timestamp: timestamp("timestamp").defaultNow().notNull(),
28
+ }, (table) => [
29
+ index("page_views_site_timestamp_idx").on(table.siteId, table.timestamp),
30
+ index("page_views_site_pathname_idx").on(table.siteId, table.pathname),
31
+ index("page_views_session_idx").on(table.sessionId),
32
+ index("page_views_user_idx").on(table.userId),
33
+ ]);
34
+ // Custom events
35
+ export const events = pgTable("events", {
36
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
37
+ siteId: text("siteId").notNull(),
38
+ name: text("name").notNull(),
39
+ properties: json("properties"),
40
+ visitorHash: text("visitorHash").notNull(),
41
+ sessionId: text("sessionId").notNull(),
42
+ userId: text("userId"),
43
+ url: text("url"),
44
+ timestamp: timestamp("timestamp").defaultNow().notNull(),
45
+ }, (table) => [
46
+ index("events_site_name_timestamp_idx").on(table.siteId, table.name, table.timestamp),
47
+ index("events_site_timestamp_idx").on(table.siteId, table.timestamp),
48
+ index("events_user_idx").on(table.userId),
49
+ ]);
50
+ // Daily salt for visitor hashing (privacy - rotates daily)
51
+ export const dailySalts = pgTable("daily_salts", {
52
+ date: date("date").primaryKey(),
53
+ salt: text("salt").notNull(),
54
+ });
55
+ // Error tracking - individual occurrences
56
+ export const analyticsErrors = pgTable("analytics_errors", {
57
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
58
+ siteId: text("siteId").notNull(),
59
+ // Error details
60
+ message: text("message").notNull(),
61
+ stack: text("stack"),
62
+ fingerprint: text("fingerprint").notNull(),
63
+ // Context
64
+ url: text("url").notNull(),
65
+ pathname: text("pathname").notNull(),
66
+ // Visitor context
67
+ visitorHash: text("visitorHash"),
68
+ sessionId: text("sessionId"),
69
+ userId: text("userId"),
70
+ // Browser context
71
+ browser: text("browser"),
72
+ browserVer: text("browserVer"),
73
+ os: text("os"),
74
+ device: text("device"),
75
+ // Metadata
76
+ metadata: json("metadata"),
77
+ timestamp: timestamp("timestamp").defaultNow().notNull(),
78
+ }, (table) => [
79
+ index("errors_site_timestamp_idx").on(table.siteId, table.timestamp),
80
+ index("errors_site_fingerprint_idx").on(table.siteId, table.fingerprint),
81
+ index("errors_user_idx").on(table.userId),
82
+ ]);
83
+ // Error groups - aggregated by fingerprint
84
+ export const errorGroups = pgTable("error_groups", {
85
+ id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
86
+ siteId: text("siteId").notNull(),
87
+ fingerprint: text("fingerprint").notNull(),
88
+ // Representative error
89
+ message: text("message").notNull(),
90
+ stack: text("stack"),
91
+ // Counts
92
+ count: integer("count").default(1).notNull(),
93
+ lastSeen: timestamp("lastSeen").notNull(),
94
+ firstSeen: timestamp("firstSeen").notNull(),
95
+ // Status: open, resolved, ignored
96
+ status: text("status").default("open").notNull(),
97
+ }, (table) => [
98
+ unique("error_groups_site_fingerprint_unique").on(table.siteId, table.fingerprint),
99
+ index("error_groups_site_status_lastseen_idx").on(table.siteId, table.status, table.lastSeen),
100
+ ]);
101
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,IAAI,EACJ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAM,GACP,MAAM,qBAAqB,CAAC;AAE7B,mBAAmB;AACnB,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAC9B,YAAY,EACZ;IACE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACjE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAEhC,YAAY;IACZ,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAC1B,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACpC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC;IAE1B,sCAAsC;IACtC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE;IAC1C,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE;IAEtC,qCAAqC;IACrC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;IAChC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IACtB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAElB,kCAAkC;IAClC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC;IAC9B,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC;IACd,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC;IACpB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IAEtB,qBAAqB;IACrB,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAChD,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;CACzD,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,KAAK,CAAC,+BAA+B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;IACxE,KAAK,CAAC,8BAA8B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IACtE,KAAK,CAAC,wBAAwB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;IACnD,KAAK,CAAC,qBAAqB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;CAC9C,CACF,CAAC;AAEF,gBAAgB;AAChB,MAAM,CAAC,MAAM,MAAM,GAAG,OAAO,CAC3B,QAAQ,EACR;IACE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACjE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAEhC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC;IAE9B,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE;IAC1C,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE;IACtC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IACtB,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC;IAEhB,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;CACzD,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACrF,KAAK,CAAC,2BAA2B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;IACpE,KAAK,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;CAC1C,CACF,CAAC;AAEF,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE;IAC/C,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE;IAC/B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;CAC7B,CAAC,CAAC;AAEH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CACpC,kBAAkB,EAClB;IACE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACjE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAEhC,gBAAgB;IAChB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC;IACpB,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE;IAE1C,UAAU;IACV,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE;IAC1B,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IAEpC,kBAAkB;IAClB,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;IAChC,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC;IAC5B,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IAEtB,kBAAkB;IAClB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC;IAC9B,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC;IACd,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC;IAEtB,WAAW;IACX,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC;IAE1B,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE;CACzD,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,KAAK,CAAC,2BAA2B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;IACpE,KAAK,CAAC,6BAA6B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC;IACxE,KAAK,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;CAC1C,CACF,CAAC;AAEF,2CAA2C;AAC3C,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAChC,cAAc,EACd;IACE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IACjE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAChC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE;IAE1C,uBAAuB;IACvB,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE;IAClC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC;IAEpB,SAAS;IACT,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE;IAC5C,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACzC,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE;IAE3C,kCAAkC;IAClC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;CACjD,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,MAAM,CAAC,sCAAsC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,CAAC;IAClF,KAAK,CAAC,uCAAuC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;CAC9F,CACF,CAAC"}
@@ -0,0 +1,15 @@
1
+ export interface SiteSummary {
2
+ siteId: string;
3
+ pageViews: number;
4
+ visitors: number;
5
+ lastSeen: Date | null;
6
+ }
7
+ /**
8
+ * List every site that has ever reported a page view, busiest first.
9
+ *
10
+ * There is no sites table — siteId is a bare column written by whichever app
11
+ * is reporting — so the list is derived from the data. A site appears here as
12
+ * soon as it sends its first non-bot page view, with no registration step.
13
+ */
14
+ export declare function listSites(): Promise<SiteSummary[]>;
15
+ //# sourceMappingURL=sites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.d.ts","sourceRoot":"","sources":["../src/sites.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAqBxD"}
package/dist/sites.js ADDED
@@ -0,0 +1,31 @@
1
+ import { getClient } from "./client.js";
2
+ import { pageViews } from "./schema.js";
3
+ import { count, countDistinct, desc, eq, max } from "drizzle-orm";
4
+ /**
5
+ * List every site that has ever reported a page view, busiest first.
6
+ *
7
+ * There is no sites table — siteId is a bare column written by whichever app
8
+ * is reporting — so the list is derived from the data. A site appears here as
9
+ * soon as it sends its first non-bot page view, with no registration step.
10
+ */
11
+ export async function listSites() {
12
+ const db = getClient();
13
+ const rows = await db
14
+ .select({
15
+ siteId: pageViews.siteId,
16
+ pageViewCount: count(),
17
+ visitorCount: countDistinct(pageViews.visitorHash),
18
+ lastSeen: max(pageViews.timestamp),
19
+ })
20
+ .from(pageViews)
21
+ .where(eq(pageViews.isBot, false))
22
+ .groupBy(pageViews.siteId)
23
+ .orderBy(desc(count()));
24
+ return rows.map((r) => ({
25
+ siteId: r.siteId,
26
+ pageViews: r.pageViewCount,
27
+ visitors: r.visitorCount,
28
+ lastSeen: r.lastSeen ?? null,
29
+ }));
30
+ }
31
+ //# sourceMappingURL=sites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.js","sourceRoot":"","sources":["../src/sites.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AASlE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAEvB,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,MAAM,CAAC;QACN,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,aAAa,EAAE,KAAK,EAAE;QACtB,YAAY,EAAE,aAAa,CAAC,SAAS,CAAC,WAAW,CAAC;QAClD,QAAQ,EAAE,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC;KACnC,CAAC;SACD,IAAI,CAAC,SAAS,CAAC;SACf,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;SACjC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC;SACzB,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAE1B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,SAAS,EAAE,CAAC,CAAC,aAAa;QAC1B,QAAQ,EAAE,CAAC,CAAC,YAAY;QACxB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;KAC7B,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE1E,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAkEf;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB1E"}
1
+ {"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE1E,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAoEf;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB1E"}
package/dist/track.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getClient } from "./client.js";
2
+ import { pageViews, events } from "./schema.js";
2
3
  import { parseUserAgent } from "./ua.js";
3
4
  import { isBot, getVisitorInfo, extractPathname } from "./utils.js";
4
5
  export async function trackPageView(options) {
@@ -9,7 +10,7 @@ export async function trackPageView(options) {
9
10
  const ipNorm = ip ?? null;
10
11
  const uaNorm = userAgent ?? null;
11
12
  // Get visitor info (hashes for privacy)
12
- const { visitorHash, sessionId } = await getVisitorInfo(ipNorm, uaNorm);
13
+ const { visitorHash, sessionId } = await getVisitorInfo(siteId, ipNorm, uaNorm);
13
14
  // Parse user agent
14
15
  const ua = parseUserAgent(uaNorm);
15
16
  // Detect bots
@@ -29,26 +30,24 @@ export async function trackPageView(options) {
29
30
  }
30
31
  }
31
32
  const db = getClient();
32
- await db.pageView.create({
33
- data: {
34
- siteId,
35
- url,
36
- pathname,
37
- referrer: cleanReferrer,
38
- visitorHash,
39
- sessionId,
40
- country: country ?? null,
41
- countryCode: countryCode ?? null,
42
- region: region ?? null,
43
- city: city ?? null,
44
- browser: ua.browser,
45
- browserVer: ua.browserVer,
46
- os: ua.os,
47
- osVer: ua.osVer,
48
- device: ua.device,
49
- userId,
50
- isBot: isBotRequest,
51
- },
33
+ await db.insert(pageViews).values({
34
+ siteId,
35
+ url,
36
+ pathname,
37
+ referrer: cleanReferrer,
38
+ visitorHash,
39
+ sessionId,
40
+ country: country ?? null,
41
+ countryCode: countryCode ?? null,
42
+ region: region ?? null,
43
+ city: city ?? null,
44
+ browser: ua.browser,
45
+ browserVer: ua.browserVer,
46
+ os: ua.os,
47
+ osVer: ua.osVer,
48
+ device: ua.device,
49
+ userId: userId ?? null,
50
+ isBot: isBotRequest,
52
51
  });
53
52
  }
54
53
  export async function trackEvent(options) {
@@ -57,18 +56,16 @@ export async function trackEvent(options) {
57
56
  const ipNorm = ip ?? null;
58
57
  const uaNorm = userAgent ?? null;
59
58
  // Get visitor info (hashes for privacy)
60
- const { visitorHash, sessionId } = await getVisitorInfo(ipNorm, uaNorm);
59
+ const { visitorHash, sessionId } = await getVisitorInfo(siteId, ipNorm, uaNorm);
61
60
  const db = getClient();
62
- await db.event.create({
63
- data: {
64
- siteId,
65
- name,
66
- properties: properties,
67
- url,
68
- visitorHash,
69
- sessionId,
70
- userId,
71
- },
61
+ await db.insert(events).values({
62
+ siteId,
63
+ name,
64
+ properties: properties ?? null,
65
+ url: url ?? null,
66
+ visitorHash,
67
+ sessionId,
68
+ userId: userId ?? null,
72
69
  });
73
70
  }
74
71
  //# sourceMappingURL=track.js.map
package/dist/track.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"track.js","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"track.js","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"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"}
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type { VisitorInfo } from "./types.js";
2
2
  export declare function isBot(userAgent: string | null): boolean;
3
- export declare function getVisitorInfo(ip: string | null, userAgent: string | null): Promise<VisitorInfo>;
3
+ /**
4
+ * Pure derivation of visitor and session identifiers.
5
+ *
6
+ * Separated from getVisitorInfo() so the privacy-critical logic can be tested
7
+ * without a database. The inputs and their order are load-bearing: changing
8
+ * them resets every visitor's identity.
9
+ *
10
+ * siteId is part of the hash input so that the same person, on the same day,
11
+ * gets a different visitorHash on each site in a deployment. The daily salt is
12
+ * one row for the whole database, so without the site component a group-by on
13
+ * visitorHash would reconstruct a day of cross-site browsing. This is the same
14
+ * reason Plausible folds the domain into its hash.
15
+ */
16
+ export declare function computeVisitorIds(siteId: string, ip: string | null, userAgent: string | null, salt: string, now: Date): VisitorInfo;
17
+ export declare function getVisitorInfo(siteId: string, ip: string | null, userAgent: string | null): Promise<VisitorInfo>;
4
18
  export declare function extractPathname(url: string): string;
5
19
  export declare function cleanupOldSalts(): Promise<number>;
6
20
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA6C9C,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIvD;AAiCD,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,WAAW,CAAC,CAgBtB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASnD;AAGD,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAUvD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA6C9C,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIvD;AAsCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,IAAI,GACR,WAAW,CAWb;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,GAAG,IAAI,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,WAAW,CAAC,CAItB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASnD;AASD,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAYvD"}
package/dist/utils.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash, randomBytes } from "crypto";
2
2
  import { getClient } from "./client.js";
3
+ import { dailySalts } from "./schema.js";
4
+ import { eq, lt } from "drizzle-orm";
3
5
  // Bot patterns to detect
4
6
  const BOT_PATTERNS = [
5
7
  /bot/i,
@@ -52,42 +54,61 @@ function hash(input) {
52
54
  }
53
55
  async function getDailySalt(date) {
54
56
  const db = getClient();
55
- const dateOnly = new Date(date.toISOString().split("T")[0] + "T00:00:00.000Z");
57
+ // Format date as YYYY-MM-DD string for the date column
58
+ const dateStr = date.toISOString().split("T")[0];
56
59
  // Try to get existing salt
57
- let dailySalt = await db.dailySalt.findUnique({
58
- where: { date: dateOnly },
59
- });
60
+ const [existingSalt] = await db
61
+ .select()
62
+ .from(dailySalts)
63
+ .where(eq(dailySalts.date, dateStr))
64
+ .limit(1);
65
+ if (existingSalt) {
66
+ return existingSalt.salt;
67
+ }
60
68
  // Create if doesn't exist
61
- if (!dailySalt) {
62
- const salt = randomBytes(32).toString("hex");
63
- try {
64
- dailySalt = await db.dailySalt.create({
65
- data: { date: dateOnly, salt },
66
- });
67
- }
68
- catch {
69
- // Race condition - another request created it
70
- dailySalt = await db.dailySalt.findUnique({
71
- where: { date: dateOnly },
72
- });
73
- }
69
+ const salt = randomBytes(32).toString("hex");
70
+ try {
71
+ await db.insert(dailySalts).values({ date: dateStr, salt });
72
+ return salt;
73
+ }
74
+ catch {
75
+ // Race condition - another request created it, try to fetch again
76
+ const [retry] = await db
77
+ .select()
78
+ .from(dailySalts)
79
+ .where(eq(dailySalts.date, dateStr))
80
+ .limit(1);
81
+ return retry?.salt ?? salt;
74
82
  }
75
- return dailySalt?.salt ?? randomBytes(32).toString("hex");
76
83
  }
77
- export async function getVisitorInfo(ip, userAgent) {
78
- const now = new Date();
79
- const salt = await getDailySalt(now);
80
- // Visitor hash: rotates daily
81
- // Uses IP + UA + daily salt for privacy
82
- const visitorInput = `${ip ?? ""}|${userAgent ?? ""}|${salt}`;
84
+ /**
85
+ * Pure derivation of visitor and session identifiers.
86
+ *
87
+ * Separated from getVisitorInfo() so the privacy-critical logic can be tested
88
+ * without a database. The inputs and their order are load-bearing: changing
89
+ * them resets every visitor's identity.
90
+ *
91
+ * siteId is part of the hash input so that the same person, on the same day,
92
+ * gets a different visitorHash on each site in a deployment. The daily salt is
93
+ * one row for the whole database, so without the site component a group-by on
94
+ * visitorHash would reconstruct a day of cross-site browsing. This is the same
95
+ * reason Plausible folds the domain into its hash.
96
+ */
97
+ export function computeVisitorIds(siteId, ip, userAgent, salt, now) {
98
+ // Visitor hash: site-scoped, and rotates daily because the salt does
99
+ const visitorInput = `${siteId}|${ip ?? ""}|${userAgent ?? ""}|${salt}`;
83
100
  const visitorHash = hash(visitorInput);
84
101
  // Session hash: rotates every 30 minutes
85
102
  // This provides session-like behavior without cookies
86
103
  const thirtyMinWindow = Math.floor(now.getTime() / (30 * 60 * 1000));
87
- const sessionInput = `${visitorInput}|${thirtyMinWindow}`;
88
- const sessionId = hash(sessionInput);
104
+ const sessionId = hash(`${visitorInput}|${thirtyMinWindow}`);
89
105
  return { visitorHash, sessionId };
90
106
  }
107
+ export async function getVisitorInfo(siteId, ip, userAgent) {
108
+ const now = new Date();
109
+ const salt = await getDailySalt(now);
110
+ return computeVisitorIds(siteId, ip, userAgent, salt, now);
111
+ }
91
112
  export function extractPathname(url) {
92
113
  try {
93
114
  const parsed = new URL(url);
@@ -100,13 +121,21 @@ export function extractPathname(url) {
100
121
  }
101
122
  }
102
123
  // Clean up old daily salts (older than 7 days)
124
+ //
125
+ // The arithmetic is done in UTC throughout: salts are keyed by UTC calendar
126
+ // day (getDailySalt() derives its key from toISOString()), so subtracting in
127
+ // local time and then formatting in UTC would land the cutoff a day early or
128
+ // late on any host that isn't at UTC+0 — salts would live 6 or 8 days rather
129
+ // than 7. docs/PRIVACY.md makes the 7-day number load-bearing.
103
130
  export async function cleanupOldSalts() {
104
131
  const db = getClient();
105
132
  const sevenDaysAgo = new Date();
106
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
107
- const result = await db.dailySalt.deleteMany({
108
- where: { date: { lt: sevenDaysAgo } },
109
- });
110
- return result.count;
133
+ sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
134
+ const dateStr = sevenDaysAgo.toISOString().split("T")[0];
135
+ const deleted = await db
136
+ .delete(dailySalts)
137
+ .where(lt(dailySalts.date, dateStr))
138
+ .returning({ date: dailySalts.date });
139
+ return deleted.length;
111
140
  }
112
141
  //# sourceMappingURL=utils.js.map
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seneris/nosework",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Privacy-focused, self-hosted analytics for your app suite",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,17 +17,15 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist",
21
- "prisma"
20
+ "dist"
22
21
  ],
23
22
  "scripts": {
24
23
  "build": "tsc",
25
24
  "dev": "tsc --watch",
26
- "db:generate": "prisma generate",
27
- "db:migrate": "prisma migrate dev",
28
- "db:push": "prisma db push",
29
- "postinstall": "prisma generate",
30
- "prepublishOnly": "bun run build"
25
+ "test": "bun test",
26
+ "prepublishOnly": "bun run build",
27
+ "db:generate": "drizzle-kit generate",
28
+ "db:migrate": "drizzle-kit migrate"
31
29
  },
32
30
  "keywords": [
33
31
  "analytics",
@@ -39,13 +37,14 @@
39
37
  "author": "",
40
38
  "license": "MIT",
41
39
  "dependencies": {
42
- "@prisma/client": "^6.2.1",
40
+ "drizzle-orm": "^0.38.3",
41
+ "postgres": "^3.4.5",
43
42
  "ua-parser-js": "^2.0.1"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@types/bun": "^1.3.5",
47
46
  "@types/ua-parser-js": "^0.7.39",
48
- "prisma": "^6.2.1",
47
+ "drizzle-kit": "^0.31.10",
49
48
  "typescript": "^5.7.3"
50
49
  },
51
50
  "peerDependencies": {
@@ -1,145 +0,0 @@
1
- generator client {
2
- provider = "prisma-client-js"
3
- }
4
-
5
- datasource db {
6
- provider = "postgresql"
7
- url = env("ANALYTICS_DATABASE_URL")
8
- }
9
-
10
- model Site {
11
- id String @id @default(cuid())
12
- name String
13
- domain String @unique
14
- createdAt DateTime @default(now())
15
-
16
- pageViews PageView[]
17
- events Event[]
18
- errors Error[]
19
- errorGroups ErrorGroup[]
20
- }
21
-
22
- model PageView {
23
- id String @id @default(cuid())
24
- siteId String
25
- site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
26
-
27
- // Page info
28
- url String
29
- pathname String
30
- referrer String?
31
-
32
- // Visitor (cookieless)
33
- visitorHash String // Hash of IP + UA + daily salt
34
- sessionId String // Hash with shorter rotation (30 min)
35
-
36
- // Location (from GeoIP)
37
- country String?
38
- countryCode String?
39
- region String?
40
- city String?
41
-
42
- // Device (from User-Agent)
43
- browser String?
44
- browserVer String?
45
- os String?
46
- osVer String?
47
- device String? // desktop, mobile, tablet
48
-
49
- // User context (optional, from your OAuth)
50
- userId String?
51
-
52
- // Bot detection
53
- isBot Boolean @default(false)
54
-
55
- timestamp DateTime @default(now())
56
-
57
- @@index([siteId, timestamp])
58
- @@index([siteId, pathname])
59
- @@index([siteId, country])
60
- @@index([visitorHash, timestamp])
61
- @@index([sessionId])
62
- }
63
-
64
- model Event {
65
- id String @id @default(cuid())
66
- siteId String
67
- site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
68
-
69
- name String // e.g., "signup", "purchase", "button_click"
70
- properties Json? // Flexible event data
71
-
72
- visitorHash String
73
- sessionId String
74
- userId String?
75
-
76
- url String?
77
-
78
- timestamp DateTime @default(now())
79
-
80
- @@index([siteId, name, timestamp])
81
- @@index([siteId, timestamp])
82
- }
83
-
84
- // Daily salt for visitor hashing (privacy - rotates daily)
85
- model DailySalt {
86
- date DateTime @id @db.Date
87
- salt String
88
- }
89
-
90
- // Error tracking
91
- model Error {
92
- id String @id @default(cuid())
93
- siteId String
94
- site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
95
-
96
- // Error details
97
- message String
98
- stack String? @db.Text
99
- fingerprint String // Hash for grouping similar errors
100
-
101
- // Context
102
- url String
103
- pathname String
104
-
105
- // Visitor context (reuse existing pattern)
106
- visitorHash String?
107
- sessionId String?
108
- userId String?
109
-
110
- // Browser context
111
- browser String?
112
- browserVer String?
113
- os String?
114
- device String?
115
-
116
- // Metadata
117
- metadata Json? // Custom context from app
118
-
119
- timestamp DateTime @default(now())
120
-
121
- @@index([siteId, timestamp])
122
- @@index([siteId, fingerprint])
123
- }
124
-
125
- model ErrorGroup {
126
- id String @id @default(cuid())
127
- siteId String
128
- site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)
129
- fingerprint String
130
-
131
- // Representative error
132
- message String
133
- stack String? @db.Text
134
-
135
- // Counts
136
- count Int @default(1)
137
- lastSeen DateTime
138
- firstSeen DateTime
139
-
140
- // Status: open, resolved, ignored
141
- status String @default("open")
142
-
143
- @@unique([siteId, fingerprint])
144
- @@index([siteId, status, lastSeen])
145
- }