@seneris/nosework 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.
@@ -0,0 +1,157 @@
1
+ export interface TrackPageViewOptions {
2
+ siteId: string;
3
+ url: string;
4
+ referrer?: string | null;
5
+ ip?: string | null;
6
+ userAgent?: string | null;
7
+ userId?: string | null;
8
+ country?: string | null;
9
+ countryCode?: string | null;
10
+ region?: string | null;
11
+ city?: string | null;
12
+ }
13
+ export interface TrackEventOptions {
14
+ siteId: string;
15
+ name: string;
16
+ properties?: Record<string, unknown>;
17
+ url?: string | null;
18
+ ip?: string | null;
19
+ userAgent?: string | null;
20
+ userId?: string | null;
21
+ }
22
+ export interface DateRange {
23
+ startDate: Date;
24
+ endDate: Date;
25
+ }
26
+ export interface QueryOptions extends DateRange {
27
+ siteId: string;
28
+ }
29
+ export interface PaginatedQueryOptions extends QueryOptions {
30
+ limit?: number;
31
+ offset?: number;
32
+ }
33
+ export interface Stats {
34
+ pageViews: number;
35
+ visitors: number;
36
+ sessions: number;
37
+ bounceRate: number;
38
+ }
39
+ export interface TopPage {
40
+ pathname: string;
41
+ pageViews: number;
42
+ visitors: number;
43
+ }
44
+ export interface LocationData {
45
+ country: string | null;
46
+ countryCode: string | null;
47
+ city: string | null;
48
+ pageViews: number;
49
+ visitors: number;
50
+ }
51
+ export interface ReferrerData {
52
+ referrer: string | null;
53
+ pageViews: number;
54
+ visitors: number;
55
+ }
56
+ export interface DeviceData {
57
+ device: string | null;
58
+ browser: string | null;
59
+ os: string | null;
60
+ pageViews: number;
61
+ visitors: number;
62
+ }
63
+ export interface GeoLocation {
64
+ country: string | null;
65
+ countryCode: string | null;
66
+ region: string | null;
67
+ city: string | null;
68
+ }
69
+ export interface ParsedUserAgent {
70
+ browser: string | null;
71
+ browserVer: string | null;
72
+ os: string | null;
73
+ osVer: string | null;
74
+ device: "desktop" | "mobile" | "tablet" | null;
75
+ }
76
+ export interface VisitorInfo {
77
+ visitorHash: string;
78
+ sessionId: string;
79
+ }
80
+ export interface SessionStats {
81
+ totalSessions: number;
82
+ avgDuration: number;
83
+ avgPagesPerSession: number;
84
+ bounceRate: number;
85
+ }
86
+ export interface SessionData {
87
+ sessionId: string;
88
+ visitorHash: string;
89
+ startTime: Date;
90
+ endTime: Date;
91
+ duration: number;
92
+ pageCount: number;
93
+ entryPage: string;
94
+ exitPage: string;
95
+ isBounce: boolean;
96
+ country: string | null;
97
+ device: string | null;
98
+ browser: string | null;
99
+ }
100
+ export interface EntryExitPage {
101
+ pathname: string;
102
+ count: number;
103
+ percentage: number;
104
+ }
105
+ export interface PageFlow {
106
+ path: string[];
107
+ count: number;
108
+ percentage: number;
109
+ }
110
+ export interface TrackErrorOptions {
111
+ siteId: string;
112
+ message: string;
113
+ stack?: string | null;
114
+ url: string;
115
+ visitorHash?: string | null;
116
+ sessionId?: string | null;
117
+ userId?: string | null;
118
+ browser?: string | null;
119
+ browserVer?: string | null;
120
+ os?: string | null;
121
+ device?: string | null;
122
+ metadata?: Record<string, unknown> | null;
123
+ }
124
+ export interface ErrorStats {
125
+ totalErrors: number;
126
+ uniqueErrors: number;
127
+ errorsToday: number;
128
+ openGroups: number;
129
+ }
130
+ export type ErrorGroupStatus = "open" | "resolved" | "ignored";
131
+ export interface ErrorGroupData {
132
+ id: string;
133
+ fingerprint: string;
134
+ message: string;
135
+ stack: string | null;
136
+ count: number;
137
+ firstSeen: Date;
138
+ lastSeen: Date;
139
+ status: ErrorGroupStatus;
140
+ }
141
+ export interface ErrorInstance {
142
+ id: string;
143
+ message: string;
144
+ stack: string | null;
145
+ url: string;
146
+ pathname: string;
147
+ visitorHash: string | null;
148
+ sessionId: string | null;
149
+ userId: string | null;
150
+ browser: string | null;
151
+ browserVer: string | null;
152
+ os: string | null;
153
+ device: string | null;
154
+ metadata: Record<string, unknown> | null;
155
+ timestamp: Date;
156
+ }
157
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/dist/ua.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ParsedUserAgent } from "./types.js";
2
+ export declare function parseUserAgent(userAgent: string | null): ParsedUserAgent;
3
+ //# sourceMappingURL=ua.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ua.d.ts","sourceRoot":"","sources":["../src/ua.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAsCxE"}
package/dist/ua.js ADDED
@@ -0,0 +1,40 @@
1
+ import { UAParser } from "ua-parser-js";
2
+ export function parseUserAgent(userAgent) {
3
+ const emptyResult = {
4
+ browser: null,
5
+ browserVer: null,
6
+ os: null,
7
+ osVer: null,
8
+ device: null,
9
+ };
10
+ if (!userAgent)
11
+ return emptyResult;
12
+ try {
13
+ const parser = new UAParser(userAgent);
14
+ const result = parser.getResult();
15
+ // Determine device type
16
+ let device = null;
17
+ const deviceType = result.device?.type?.toLowerCase();
18
+ if (deviceType === "mobile") {
19
+ device = "mobile";
20
+ }
21
+ else if (deviceType === "tablet") {
22
+ device = "tablet";
23
+ }
24
+ else if (result.os?.name) {
25
+ // If we have an OS but no mobile/tablet device type, assume desktop
26
+ device = "desktop";
27
+ }
28
+ return {
29
+ browser: result.browser?.name ?? null,
30
+ browserVer: result.browser?.version?.split(".")[0] ?? null, // Major version only
31
+ os: result.os?.name ?? null,
32
+ osVer: result.os?.version?.split(".")[0] ?? null, // Major version only
33
+ device,
34
+ };
35
+ }
36
+ catch {
37
+ return emptyResult;
38
+ }
39
+ }
40
+ //# sourceMappingURL=ua.js.map
package/dist/ua.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ua.js","sourceRoot":"","sources":["../src/ua.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAGxC,MAAM,UAAU,cAAc,CAAC,SAAwB;IACrD,MAAM,WAAW,GAAoB;QACnC,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,IAAI;QAChB,EAAE,EAAE,IAAI;QACR,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,IAAI;KACb,CAAC;IAEF,IAAI,CAAC,SAAS;QAAE,OAAO,WAAW,CAAC;IAEnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QAElC,wBAAwB;QACxB,IAAI,MAAM,GAA2C,IAAI,CAAC;QAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QAEtD,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;aAAM,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;aAAM,IAAI,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC;YAC3B,oEAAoE;YACpE,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;QAED,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI;YACrC,UAAU,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,qBAAqB;YACjF,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,IAAI,IAAI;YAC3B,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,qBAAqB;YACvE,MAAM;SACP,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { VisitorInfo } from "./types.js";
2
+ export declare function isBot(userAgent: string | null): boolean;
3
+ export declare function getVisitorInfo(ip: string | null, userAgent: string | null): Promise<VisitorInfo>;
4
+ export declare function extractPathname(url: string): string;
5
+ export declare function cleanupOldSalts(): Promise<number>;
6
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +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"}
package/dist/utils.js ADDED
@@ -0,0 +1,112 @@
1
+ import { createHash, randomBytes } from "crypto";
2
+ import { getClient } from "./client.js";
3
+ // Bot patterns to detect
4
+ const BOT_PATTERNS = [
5
+ /bot/i,
6
+ /crawl/i,
7
+ /spider/i,
8
+ /slurp/i,
9
+ /mediapartners/i,
10
+ /googlebot/i,
11
+ /bingbot/i,
12
+ /yandex/i,
13
+ /baidu/i,
14
+ /duckduckgo/i,
15
+ /facebookexternalhit/i,
16
+ /twitterbot/i,
17
+ /linkedinbot/i,
18
+ /whatsapp/i,
19
+ /telegrambot/i,
20
+ /discordbot/i,
21
+ /slackbot/i,
22
+ /pingdom/i,
23
+ /uptimerobot/i,
24
+ /headless/i,
25
+ /phantomjs/i,
26
+ /puppeteer/i,
27
+ /playwright/i,
28
+ /selenium/i,
29
+ /webdriver/i,
30
+ /lighthouse/i,
31
+ /pagespeed/i,
32
+ /gtmetrix/i,
33
+ /ahrefsbot/i,
34
+ /semrushbot/i,
35
+ /mj12bot/i,
36
+ /dotbot/i,
37
+ /bytespider/i,
38
+ /gptbot/i,
39
+ /claudebot/i,
40
+ /anthropic/i,
41
+ /openai/i,
42
+ /chatgpt/i,
43
+ /petalbot/i,
44
+ ];
45
+ export function isBot(userAgent) {
46
+ if (!userAgent)
47
+ return true; // No user agent = likely bot
48
+ return BOT_PATTERNS.some((pattern) => pattern.test(userAgent));
49
+ }
50
+ function hash(input) {
51
+ return createHash("sha256").update(input).digest("hex").slice(0, 16);
52
+ }
53
+ async function getDailySalt(date) {
54
+ const db = getClient();
55
+ const dateOnly = new Date(date.toISOString().split("T")[0] + "T00:00:00.000Z");
56
+ // Try to get existing salt
57
+ let dailySalt = await db.dailySalt.findUnique({
58
+ where: { date: dateOnly },
59
+ });
60
+ // 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
+ }
74
+ }
75
+ return dailySalt?.salt ?? randomBytes(32).toString("hex");
76
+ }
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}`;
83
+ const visitorHash = hash(visitorInput);
84
+ // Session hash: rotates every 30 minutes
85
+ // This provides session-like behavior without cookies
86
+ const thirtyMinWindow = Math.floor(now.getTime() / (30 * 60 * 1000));
87
+ const sessionInput = `${visitorInput}|${thirtyMinWindow}`;
88
+ const sessionId = hash(sessionInput);
89
+ return { visitorHash, sessionId };
90
+ }
91
+ export function extractPathname(url) {
92
+ try {
93
+ const parsed = new URL(url);
94
+ return parsed.pathname;
95
+ }
96
+ catch {
97
+ // If URL parsing fails, try to extract path manually
98
+ const match = url.match(/^(?:https?:\/\/[^/]+)?(\/?[^?#]*)/);
99
+ return match?.[1] ?? "/";
100
+ }
101
+ }
102
+ // Clean up old daily salts (older than 7 days)
103
+ export async function cleanupOldSalts() {
104
+ const db = getClient();
105
+ 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;
111
+ }
112
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@seneris/nosework",
3
+ "version": "0.1.0",
4
+ "description": "Privacy-focused, self-hosted analytics for your app suite",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./client/errors": {
15
+ "import": "./dist/client/errors.js",
16
+ "types": "./dist/client/errors.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "prisma"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "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"
31
+ },
32
+ "keywords": [
33
+ "analytics",
34
+ "privacy",
35
+ "self-hosted",
36
+ "gdpr",
37
+ "cookieless"
38
+ ],
39
+ "author": "",
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@prisma/client": "^6.2.1",
43
+ "ua-parser-js": "^2.0.1"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "^1.3.5",
47
+ "@types/ua-parser-js": "^0.7.39",
48
+ "prisma": "^6.2.1",
49
+ "typescript": "^5.7.3"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^5"
53
+ }
54
+ }
@@ -0,0 +1,145 @@
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
+ }