@hasna/logs 0.3.26 → 0.3.27

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 (130) hide show
  1. package/README.md +33 -10
  2. package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
  3. package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dist/cli/index.js +8511 -177
  6. package/dist/count-bmj4r2zb.js +10 -0
  7. package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
  8. package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
  9. package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
  10. package/dist/index-931pbyn5.js +141 -0
  11. package/dist/index-b5c72f1p.js +7 -0
  12. package/dist/{index-gc0zvs88.js → index-bnr19y0h.js} +596 -37
  13. package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
  14. package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
  15. package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
  16. package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
  17. package/dist/index-hq6kzaah.js +26 -0
  18. package/dist/index-j34f36wy.js +5672 -0
  19. package/dist/index-p4dbdzx4.js +1849 -0
  20. package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
  21. package/dist/index-t3x838zw.js +2583 -0
  22. package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
  23. package/dist/index.js +2982 -22
  24. package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
  25. package/dist/mcp/index.js +1473 -4286
  26. package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
  27. package/dist/server/index.js +2944 -417
  28. package/dist/storage.js +50 -0
  29. package/package.json +27 -8
  30. package/biome.json +0 -13
  31. package/bun.lock +0 -376
  32. package/dashboard/README.md +0 -73
  33. package/dashboard/bun.lock +0 -526
  34. package/dashboard/eslint.config.js +0 -23
  35. package/dashboard/index.html +0 -13
  36. package/dashboard/package.json +0 -32
  37. package/dashboard/src/App.css +0 -184
  38. package/dashboard/src/App.tsx +0 -49
  39. package/dashboard/src/api.ts +0 -33
  40. package/dashboard/src/assets/hero.png +0 -0
  41. package/dashboard/src/assets/react.svg +0 -1
  42. package/dashboard/src/assets/vite.svg +0 -1
  43. package/dashboard/src/index.css +0 -111
  44. package/dashboard/src/main.tsx +0 -10
  45. package/dashboard/src/pages/Alerts.tsx +0 -69
  46. package/dashboard/src/pages/Issues.tsx +0 -50
  47. package/dashboard/src/pages/Perf.tsx +0 -75
  48. package/dashboard/src/pages/Projects.tsx +0 -67
  49. package/dashboard/src/pages/Summary.tsx +0 -67
  50. package/dashboard/src/pages/Tail.tsx +0 -65
  51. package/dashboard/tsconfig.app.json +0 -28
  52. package/dashboard/tsconfig.json +0 -7
  53. package/dashboard/tsconfig.node.json +0 -26
  54. package/dashboard/vite.config.ts +0 -14
  55. package/dist/count-x3n7qg3c.js +0 -9
  56. package/dist/index-997bkzr2.js +0 -15
  57. package/dist/index-pen6t0yc.js +0 -10794
  58. package/sdk/package.json +0 -27
  59. package/sdk/src/index.ts +0 -143
  60. package/sdk/src/types.ts +0 -56
  61. package/src/cli/entrypoints.test.ts +0 -63
  62. package/src/cli/index.ts +0 -471
  63. package/src/db/index.test.ts +0 -33
  64. package/src/db/index.ts +0 -189
  65. package/src/db/migrations/001_alert_rules.ts +0 -21
  66. package/src/db/migrations/002_issues.ts +0 -21
  67. package/src/db/migrations/003_retention.ts +0 -15
  68. package/src/db/migrations/004_page_auth.ts +0 -13
  69. package/src/db/pg-migrations.ts +0 -167
  70. package/src/index.ts +0 -1
  71. package/src/lib/alerts.test.ts +0 -67
  72. package/src/lib/alerts.ts +0 -117
  73. package/src/lib/browser-script.test.ts +0 -35
  74. package/src/lib/browser-script.ts +0 -31
  75. package/src/lib/compare.test.ts +0 -52
  76. package/src/lib/compare.ts +0 -85
  77. package/src/lib/count.test.ts +0 -44
  78. package/src/lib/count.ts +0 -55
  79. package/src/lib/diagnose.test.ts +0 -55
  80. package/src/lib/diagnose.ts +0 -91
  81. package/src/lib/export.test.ts +0 -66
  82. package/src/lib/export.ts +0 -65
  83. package/src/lib/github.ts +0 -38
  84. package/src/lib/health.test.ts +0 -48
  85. package/src/lib/health.ts +0 -51
  86. package/src/lib/ingest.test.ts +0 -57
  87. package/src/lib/ingest.ts +0 -78
  88. package/src/lib/issues.test.ts +0 -79
  89. package/src/lib/issues.ts +0 -70
  90. package/src/lib/jobs.test.ts +0 -69
  91. package/src/lib/jobs.ts +0 -63
  92. package/src/lib/lighthouse.ts +0 -65
  93. package/src/lib/package-meta.test.ts +0 -43
  94. package/src/lib/package-meta.ts +0 -80
  95. package/src/lib/page-auth.test.ts +0 -54
  96. package/src/lib/page-auth.ts +0 -48
  97. package/src/lib/parse-time.test.ts +0 -37
  98. package/src/lib/parse-time.ts +0 -14
  99. package/src/lib/perf.test.ts +0 -45
  100. package/src/lib/perf.ts +0 -46
  101. package/src/lib/projects.test.ts +0 -73
  102. package/src/lib/projects.ts +0 -69
  103. package/src/lib/query.test.ts +0 -104
  104. package/src/lib/query.ts +0 -84
  105. package/src/lib/retention.test.ts +0 -42
  106. package/src/lib/retention.ts +0 -62
  107. package/src/lib/rotate.test.ts +0 -37
  108. package/src/lib/rotate.ts +0 -27
  109. package/src/lib/scanner.ts +0 -131
  110. package/src/lib/scheduler.ts +0 -63
  111. package/src/lib/session-context.ts +0 -28
  112. package/src/lib/summarize.test.ts +0 -38
  113. package/src/lib/summarize.ts +0 -23
  114. package/src/mcp/http.test.ts +0 -92
  115. package/src/mcp/http.ts +0 -135
  116. package/src/mcp/index.test.ts +0 -27
  117. package/src/mcp/index.ts +0 -444
  118. package/src/server/index.ts +0 -61
  119. package/src/server/routes/alerts.ts +0 -32
  120. package/src/server/routes/issues.ts +0 -43
  121. package/src/server/routes/jobs.ts +0 -32
  122. package/src/server/routes/logs.ts +0 -113
  123. package/src/server/routes/perf.ts +0 -23
  124. package/src/server/routes/projects.ts +0 -67
  125. package/src/server/routes/stream.ts +0 -43
  126. package/src/server/server.test.ts +0 -194
  127. package/src/types/index.ts +0 -119
  128. package/tsconfig.json +0 -22
  129. /package/dashboard/{public → dist}/favicon.svg +0 -0
  130. /package/dashboard/{public → dist}/icons.svg +0 -0
package/src/db/index.ts DELETED
@@ -1,189 +0,0 @@
1
- import { SqliteAdapter as Database } from "@hasna/cloud"
2
- import { join } from "node:path"
3
- import { existsSync, mkdirSync, cpSync } from "node:fs"
4
- import { migrateAlertRules } from "./migrations/001_alert_rules.ts"
5
- import { migrateIssues } from "./migrations/002_issues.ts"
6
- import { migrateRetention } from "./migrations/003_retention.ts"
7
- import { migratePageAuth } from "./migrations/004_page_auth.ts"
8
-
9
- function resolveDataDir(): string {
10
- const explicit = process.env.HASNA_LOGS_DATA_DIR ?? process.env.LOGS_DATA_DIR
11
- if (explicit) return explicit
12
-
13
- const home = process.env.HOME ?? "~"
14
- const newDir = join(home, ".hasna", "logs")
15
- const oldDir = join(home, ".logs")
16
-
17
- // Auto-migrate: copy old data to new location if needed
18
- if (!existsSync(newDir) && existsSync(oldDir)) {
19
- mkdirSync(join(home, ".hasna"), { recursive: true })
20
- cpSync(oldDir, newDir, { recursive: true })
21
- }
22
-
23
- return newDir
24
- }
25
-
26
- const DATA_DIR = resolveDataDir()
27
- const DB_PATH = process.env.HASNA_LOGS_DB_PATH ?? process.env.LOGS_DB_PATH ?? join(DATA_DIR, "logs.db")
28
-
29
- let _db: Database | null = null
30
-
31
- export function getDb(): Database {
32
- if (_db) return _db
33
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true })
34
- _db = new Database(DB_PATH)
35
- _db.run("PRAGMA journal_mode=WAL")
36
- _db.run("PRAGMA foreign_keys=ON")
37
- migrate(_db)
38
- _db.run(`CREATE TABLE IF NOT EXISTS feedback (
39
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
40
- message TEXT NOT NULL,
41
- email TEXT,
42
- category TEXT DEFAULT 'general',
43
- version TEXT,
44
- machine_id TEXT,
45
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
46
- )`)
47
- return _db
48
- }
49
-
50
- export function closeDb(): void {
51
- _db?.close()
52
- _db = null
53
- }
54
-
55
- export function createTestDb(): Database {
56
- const db = new Database(":memory:")
57
- db.run("PRAGMA journal_mode=WAL")
58
- db.run("PRAGMA foreign_keys=ON")
59
- migrate(db)
60
- return db
61
- }
62
-
63
- function migrate(db: Database): void {
64
- db.run(`
65
- CREATE TABLE IF NOT EXISTS projects (
66
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
67
- name TEXT NOT NULL UNIQUE,
68
- github_repo TEXT,
69
- base_url TEXT,
70
- description TEXT,
71
- github_description TEXT,
72
- github_branch TEXT,
73
- github_sha TEXT,
74
- last_synced_at TEXT,
75
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
76
- )
77
- `)
78
-
79
- db.run(`
80
- CREATE TABLE IF NOT EXISTS pages (
81
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
82
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
83
- url TEXT NOT NULL,
84
- path TEXT NOT NULL DEFAULT '/',
85
- name TEXT,
86
- last_scanned_at TEXT,
87
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
88
- UNIQUE(project_id, url)
89
- )
90
- `)
91
-
92
- db.run(`
93
- CREATE TABLE IF NOT EXISTS logs (
94
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
95
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
96
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
97
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
98
- level TEXT NOT NULL CHECK(level IN ('debug','info','warn','error','fatal')),
99
- source TEXT NOT NULL DEFAULT 'sdk' CHECK(source IN ('sdk','script','scanner')),
100
- service TEXT,
101
- message TEXT NOT NULL,
102
- trace_id TEXT,
103
- session_id TEXT,
104
- agent TEXT,
105
- url TEXT,
106
- stack_trace TEXT,
107
- metadata TEXT
108
- )
109
- `)
110
-
111
- db.run(`CREATE INDEX IF NOT EXISTS idx_logs_project_level_ts ON logs(project_id, level, timestamp DESC)`)
112
- db.run(`CREATE INDEX IF NOT EXISTS idx_logs_trace ON logs(trace_id)`)
113
- db.run(`CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service)`)
114
- db.run(`CREATE INDEX IF NOT EXISTS idx_logs_page ON logs(page_id)`)
115
- db.run(`CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp DESC)`)
116
-
117
- db.run(`
118
- CREATE VIRTUAL TABLE IF NOT EXISTS logs_fts USING fts5(
119
- message, service, stack_trace,
120
- content=logs, content_rowid=rowid
121
- )
122
- `)
123
-
124
- db.run(`
125
- CREATE TRIGGER IF NOT EXISTS logs_fts_insert AFTER INSERT ON logs BEGIN
126
- INSERT INTO logs_fts(rowid, message, service, stack_trace)
127
- VALUES (new.rowid, new.message, new.service, new.stack_trace);
128
- END
129
- `)
130
-
131
- db.run(`
132
- CREATE TRIGGER IF NOT EXISTS logs_fts_delete AFTER DELETE ON logs BEGIN
133
- INSERT INTO logs_fts(logs_fts, rowid, message, service, stack_trace)
134
- VALUES ('delete', old.rowid, old.message, old.service, old.stack_trace);
135
- END
136
- `)
137
-
138
- db.run(`
139
- CREATE TABLE IF NOT EXISTS scan_jobs (
140
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
141
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
142
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
143
- schedule TEXT NOT NULL DEFAULT '*/30 * * * *',
144
- enabled INTEGER NOT NULL DEFAULT 1,
145
- last_run_at TEXT,
146
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
147
- )
148
- `)
149
-
150
- db.run(`
151
- CREATE TABLE IF NOT EXISTS scan_runs (
152
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
153
- job_id TEXT NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE,
154
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
155
- started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
156
- finished_at TEXT,
157
- status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running','completed','failed')),
158
- logs_collected INTEGER NOT NULL DEFAULT 0,
159
- errors_found INTEGER NOT NULL DEFAULT 0,
160
- perf_score REAL
161
- )
162
- `)
163
-
164
- db.run(`
165
- CREATE TABLE IF NOT EXISTS performance_snapshots (
166
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
167
- timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
168
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
169
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
170
- url TEXT NOT NULL,
171
- lcp REAL,
172
- fcp REAL,
173
- cls REAL,
174
- tti REAL,
175
- ttfb REAL,
176
- score REAL,
177
- raw_audit TEXT
178
- )
179
- `)
180
-
181
- db.run(`CREATE INDEX IF NOT EXISTS idx_perf_project_ts ON performance_snapshots(project_id, timestamp DESC)`)
182
- db.run(`CREATE INDEX IF NOT EXISTS idx_perf_page ON performance_snapshots(page_id)`)
183
-
184
- // QoL migrations
185
- migrateAlertRules(db)
186
- migrateIssues(db)
187
- migrateRetention(db)
188
- migratePageAuth(db)
189
- }
@@ -1,21 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- export function migrateAlertRules(db: Database): void {
4
- db.run(`
5
- CREATE TABLE IF NOT EXISTS alert_rules (
6
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
7
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
8
- name TEXT NOT NULL,
9
- service TEXT,
10
- level TEXT NOT NULL DEFAULT 'error' CHECK(level IN ('debug','info','warn','error','fatal')),
11
- threshold_count INTEGER NOT NULL DEFAULT 10,
12
- window_seconds INTEGER NOT NULL DEFAULT 60,
13
- action TEXT NOT NULL DEFAULT 'webhook' CHECK(action IN ('webhook','log')),
14
- webhook_url TEXT,
15
- enabled INTEGER NOT NULL DEFAULT 1,
16
- last_fired_at TEXT,
17
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
18
- )
19
- `)
20
- db.run(`CREATE INDEX IF NOT EXISTS idx_alert_rules_project ON alert_rules(project_id)`)
21
- }
@@ -1,21 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- export function migrateIssues(db: Database): void {
4
- db.run(`
5
- CREATE TABLE IF NOT EXISTS issues (
6
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
7
- project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
8
- fingerprint TEXT NOT NULL,
9
- level TEXT NOT NULL,
10
- service TEXT,
11
- message_template TEXT NOT NULL,
12
- first_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
13
- last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
14
- count INTEGER NOT NULL DEFAULT 1,
15
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','resolved','ignored')),
16
- UNIQUE(project_id, fingerprint)
17
- )
18
- `)
19
- db.run(`CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id, status)`)
20
- db.run(`CREATE INDEX IF NOT EXISTS idx_issues_fingerprint ON issues(fingerprint)`)
21
- }
@@ -1,15 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- const RETENTION_COLUMNS = [
4
- "max_rows INTEGER NOT NULL DEFAULT 100000",
5
- "debug_ttl_hours INTEGER NOT NULL DEFAULT 24",
6
- "info_ttl_hours INTEGER NOT NULL DEFAULT 168",
7
- "warn_ttl_hours INTEGER NOT NULL DEFAULT 720",
8
- "error_ttl_hours INTEGER NOT NULL DEFAULT 2160",
9
- ]
10
-
11
- export function migrateRetention(db: Database): void {
12
- for (const col of RETENTION_COLUMNS) {
13
- try { db.run(`ALTER TABLE projects ADD COLUMN ${col}`) } catch { /* already exists */ }
14
- }
15
- }
@@ -1,13 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- export function migratePageAuth(db: Database): void {
4
- db.run(`
5
- CREATE TABLE IF NOT EXISTS page_auth (
6
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
7
- page_id TEXT NOT NULL UNIQUE REFERENCES pages(id) ON DELETE CASCADE,
8
- type TEXT NOT NULL CHECK(type IN ('cookie','bearer','basic')),
9
- credentials TEXT NOT NULL,
10
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
11
- )
12
- `)
13
- }
@@ -1,167 +0,0 @@
1
- /**
2
- * PostgreSQL migrations for open-logs cloud sync.
3
- *
4
- * Equivalent to the SQLite schema in index.ts + migrations/, translated for PostgreSQL.
5
- */
6
-
7
- export const PG_MIGRATIONS: string[] = [
8
- // Migration 1: projects table
9
- `CREATE TABLE IF NOT EXISTS projects (
10
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
11
- name TEXT NOT NULL UNIQUE,
12
- github_repo TEXT,
13
- base_url TEXT,
14
- description TEXT,
15
- github_description TEXT,
16
- github_branch TEXT,
17
- github_sha TEXT,
18
- last_synced_at TEXT,
19
- max_rows INTEGER NOT NULL DEFAULT 100000,
20
- debug_ttl_hours INTEGER NOT NULL DEFAULT 24,
21
- info_ttl_hours INTEGER NOT NULL DEFAULT 168,
22
- warn_ttl_hours INTEGER NOT NULL DEFAULT 720,
23
- error_ttl_hours INTEGER NOT NULL DEFAULT 2160,
24
- created_at TEXT NOT NULL DEFAULT NOW()::text
25
- )`,
26
-
27
- // Migration 2: pages table
28
- `CREATE TABLE IF NOT EXISTS pages (
29
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
30
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
31
- url TEXT NOT NULL,
32
- path TEXT NOT NULL DEFAULT '/',
33
- name TEXT,
34
- last_scanned_at TEXT,
35
- created_at TEXT NOT NULL DEFAULT NOW()::text,
36
- UNIQUE(project_id, url)
37
- )`,
38
-
39
- // Migration 3: logs table
40
- `CREATE TABLE IF NOT EXISTS logs (
41
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
42
- timestamp TEXT NOT NULL DEFAULT NOW()::text,
43
- project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
44
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
45
- level TEXT NOT NULL CHECK(level IN ('debug','info','warn','error','fatal')),
46
- source TEXT NOT NULL DEFAULT 'sdk' CHECK(source IN ('sdk','script','scanner')),
47
- service TEXT,
48
- message TEXT NOT NULL,
49
- trace_id TEXT,
50
- session_id TEXT,
51
- agent TEXT,
52
- url TEXT,
53
- stack_trace TEXT,
54
- metadata TEXT
55
- )`,
56
-
57
- `CREATE INDEX IF NOT EXISTS idx_logs_project_level_ts ON logs(project_id, level, timestamp DESC)`,
58
-
59
- `CREATE INDEX IF NOT EXISTS idx_logs_trace ON logs(trace_id)`,
60
-
61
- `CREATE INDEX IF NOT EXISTS idx_logs_service ON logs(service)`,
62
-
63
- `CREATE INDEX IF NOT EXISTS idx_logs_page ON logs(page_id)`,
64
-
65
- `CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp DESC)`,
66
-
67
- // Migration 4: scan_jobs table
68
- `CREATE TABLE IF NOT EXISTS scan_jobs (
69
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
70
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
71
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
72
- schedule TEXT NOT NULL DEFAULT '*/30 * * * *',
73
- enabled BOOLEAN NOT NULL DEFAULT TRUE,
74
- last_run_at TEXT,
75
- created_at TEXT NOT NULL DEFAULT NOW()::text
76
- )`,
77
-
78
- // Migration 5: scan_runs table
79
- `CREATE TABLE IF NOT EXISTS scan_runs (
80
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
81
- job_id TEXT NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE,
82
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
83
- started_at TEXT NOT NULL DEFAULT NOW()::text,
84
- finished_at TEXT,
85
- status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running','completed','failed')),
86
- logs_collected INTEGER NOT NULL DEFAULT 0,
87
- errors_found INTEGER NOT NULL DEFAULT 0,
88
- perf_score REAL
89
- )`,
90
-
91
- // Migration 6: performance_snapshots table
92
- `CREATE TABLE IF NOT EXISTS performance_snapshots (
93
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
94
- timestamp TEXT NOT NULL DEFAULT NOW()::text,
95
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
96
- page_id TEXT REFERENCES pages(id) ON DELETE SET NULL,
97
- url TEXT NOT NULL,
98
- lcp REAL,
99
- fcp REAL,
100
- cls REAL,
101
- tti REAL,
102
- ttfb REAL,
103
- score REAL,
104
- raw_audit TEXT
105
- )`,
106
-
107
- `CREATE INDEX IF NOT EXISTS idx_perf_project_ts ON performance_snapshots(project_id, timestamp DESC)`,
108
-
109
- `CREATE INDEX IF NOT EXISTS idx_perf_page ON performance_snapshots(page_id)`,
110
-
111
- // Migration 7: alert_rules table
112
- `CREATE TABLE IF NOT EXISTS alert_rules (
113
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
114
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
115
- name TEXT NOT NULL,
116
- service TEXT,
117
- level TEXT NOT NULL DEFAULT 'error' CHECK(level IN ('debug','info','warn','error','fatal')),
118
- threshold_count INTEGER NOT NULL DEFAULT 10,
119
- window_seconds INTEGER NOT NULL DEFAULT 60,
120
- action TEXT NOT NULL DEFAULT 'webhook' CHECK(action IN ('webhook','log')),
121
- webhook_url TEXT,
122
- enabled BOOLEAN NOT NULL DEFAULT TRUE,
123
- last_fired_at TEXT,
124
- created_at TEXT NOT NULL DEFAULT NOW()::text
125
- )`,
126
-
127
- `CREATE INDEX IF NOT EXISTS idx_alert_rules_project ON alert_rules(project_id)`,
128
-
129
- // Migration 8: issues table
130
- `CREATE TABLE IF NOT EXISTS issues (
131
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
132
- project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
133
- fingerprint TEXT NOT NULL,
134
- level TEXT NOT NULL,
135
- service TEXT,
136
- message_template TEXT NOT NULL,
137
- first_seen TEXT NOT NULL DEFAULT NOW()::text,
138
- last_seen TEXT NOT NULL DEFAULT NOW()::text,
139
- count INTEGER NOT NULL DEFAULT 1,
140
- status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','resolved','ignored')),
141
- UNIQUE(project_id, fingerprint)
142
- )`,
143
-
144
- `CREATE INDEX IF NOT EXISTS idx_issues_project ON issues(project_id, status)`,
145
-
146
- `CREATE INDEX IF NOT EXISTS idx_issues_fingerprint ON issues(fingerprint)`,
147
-
148
- // Migration 9: page_auth table
149
- `CREATE TABLE IF NOT EXISTS page_auth (
150
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
151
- page_id TEXT NOT NULL UNIQUE REFERENCES pages(id) ON DELETE CASCADE,
152
- type TEXT NOT NULL CHECK(type IN ('cookie','bearer','basic')),
153
- credentials TEXT NOT NULL,
154
- created_at TEXT NOT NULL DEFAULT NOW()::text
155
- )`,
156
-
157
- // Migration 10: feedback table
158
- `CREATE TABLE IF NOT EXISTS feedback (
159
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
160
- message TEXT NOT NULL,
161
- email TEXT,
162
- category TEXT DEFAULT 'general',
163
- version TEXT,
164
- machine_id TEXT,
165
- created_at TEXT NOT NULL DEFAULT NOW()::text
166
- )`,
167
- ];
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "../sdk/src/index.ts";
@@ -1,67 +0,0 @@
1
- import { describe, expect, it, mock } from "bun:test"
2
- import { createTestDb } from "../db/index.ts"
3
- import { createAlertRule, deleteAlertRule, evaluateAlerts, listAlertRules, updateAlertRule } from "./alerts.ts"
4
- import { ingestBatch } from "./ingest.ts"
5
-
6
- function seedProject(db: ReturnType<typeof createTestDb>) {
7
- return db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
8
- }
9
-
10
- describe("alert rules CRUD", () => {
11
- it("creates an alert rule", () => {
12
- const db = createTestDb()
13
- const p = seedProject(db)
14
- const rule = createAlertRule(db, { project_id: p.id, name: "High errors", level: "error", threshold_count: 5, window_seconds: 60 })
15
- expect(rule.id).toBeTruthy()
16
- expect(rule.name).toBe("High errors")
17
- expect(rule.threshold_count).toBe(5)
18
- expect(rule.enabled).toBe(1)
19
- })
20
-
21
- it("lists rules for a project", () => {
22
- const db = createTestDb()
23
- const p = seedProject(db)
24
- createAlertRule(db, { project_id: p.id, name: "r1" })
25
- createAlertRule(db, { project_id: p.id, name: "r2" })
26
- expect(listAlertRules(db, p.id)).toHaveLength(2)
27
- })
28
-
29
- it("updates a rule", () => {
30
- const db = createTestDb()
31
- const p = seedProject(db)
32
- const rule = createAlertRule(db, { project_id: p.id, name: "r1" })
33
- const updated = updateAlertRule(db, rule.id, { enabled: 0, threshold_count: 99 })
34
- expect(updated?.enabled).toBe(0)
35
- expect(updated?.threshold_count).toBe(99)
36
- })
37
-
38
- it("deletes a rule", () => {
39
- const db = createTestDb()
40
- const p = seedProject(db)
41
- const rule = createAlertRule(db, { project_id: p.id, name: "r1" })
42
- deleteAlertRule(db, rule.id)
43
- expect(listAlertRules(db, p.id)).toHaveLength(0)
44
- })
45
- })
46
-
47
- describe("alert evaluation", () => {
48
- it("does not fire when under threshold", async () => {
49
- const db = createTestDb()
50
- const p = seedProject(db)
51
- createAlertRule(db, { project_id: p.id, name: "r", level: "error", threshold_count: 10, window_seconds: 60, action: "log" })
52
- ingestBatch(db, Array.from({ length: 5 }, () => ({ level: "error" as const, message: "e", project_id: p.id })))
53
- // No throw = passes
54
- await expect(evaluateAlerts(db, p.id, null, "error")).resolves.toBeUndefined()
55
- })
56
-
57
- it("fires when threshold exceeded (log action)", async () => {
58
- const db = createTestDb()
59
- const p = seedProject(db)
60
- createAlertRule(db, { project_id: p.id, name: "r", level: "error", threshold_count: 3, window_seconds: 3600, action: "log" })
61
- ingestBatch(db, Array.from({ length: 5 }, () => ({ level: "error" as const, message: "e", project_id: p.id })))
62
- await expect(evaluateAlerts(db, p.id, null, "error")).resolves.toBeUndefined()
63
- // Verify last_fired_at was set
64
- const rule = db.prepare("SELECT last_fired_at FROM alert_rules WHERE project_id = ?").get(p.id) as { last_fired_at: string | null }
65
- expect(rule.last_fired_at).toBeTruthy()
66
- })
67
- })
package/src/lib/alerts.ts DELETED
@@ -1,117 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
-
3
- export interface AlertRule {
4
- id: string
5
- project_id: string
6
- name: string
7
- service: string | null
8
- level: string
9
- threshold_count: number
10
- window_seconds: number
11
- action: "webhook" | "log"
12
- webhook_url: string | null
13
- enabled: number
14
- last_fired_at: string | null
15
- created_at: string
16
- }
17
-
18
- export function createAlertRule(db: Database, data: {
19
- project_id: string
20
- name: string
21
- service?: string
22
- level?: string
23
- threshold_count?: number
24
- window_seconds?: number
25
- action?: "webhook" | "log"
26
- webhook_url?: string
27
- }): AlertRule {
28
- return db.prepare(`
29
- INSERT INTO alert_rules (project_id, name, service, level, threshold_count, window_seconds, action, webhook_url)
30
- VALUES ($project_id, $name, $service, $level, $threshold_count, $window_seconds, $action, $webhook_url)
31
- RETURNING *
32
- `).get({
33
- $project_id: data.project_id,
34
- $name: data.name,
35
- $service: data.service ?? null,
36
- $level: data.level ?? "error",
37
- $threshold_count: data.threshold_count ?? 10,
38
- $window_seconds: data.window_seconds ?? 60,
39
- $action: data.action ?? "webhook",
40
- $webhook_url: data.webhook_url ?? null,
41
- }) as AlertRule
42
- }
43
-
44
- export function listAlertRules(db: Database, projectId?: string): AlertRule[] {
45
- if (projectId) {
46
- return db.prepare("SELECT * FROM alert_rules WHERE project_id = $p ORDER BY created_at DESC").all({ $p: projectId }) as AlertRule[]
47
- }
48
- return db.prepare("SELECT * FROM alert_rules ORDER BY created_at DESC").all() as AlertRule[]
49
- }
50
-
51
- export function updateAlertRule(db: Database, id: string, data: Partial<Pick<AlertRule, "enabled" | "threshold_count" | "window_seconds" | "webhook_url">>): AlertRule | null {
52
- const fields = Object.keys(data).map(k => `${k} = $${k}`).join(", ")
53
- if (!fields) return db.prepare("SELECT * FROM alert_rules WHERE id = $id").get({ $id: id }) as AlertRule | null
54
- const params = Object.fromEntries(Object.entries(data).map(([k, v]) => [`$${k}`, v]))
55
- params.$id = id
56
- return db.prepare(`UPDATE alert_rules SET ${fields} WHERE id = $id RETURNING *`).get(params) as AlertRule | null
57
- }
58
-
59
- export function deleteAlertRule(db: Database, id: string): void {
60
- db.run("DELETE FROM alert_rules WHERE id = $id", { $id: id })
61
- }
62
-
63
- export async function evaluateAlerts(db: Database, projectId: string, service: string | null, level: string): Promise<void> {
64
- const rules = db.prepare(`
65
- SELECT * FROM alert_rules
66
- WHERE project_id = $p AND level = $level AND enabled = 1
67
- AND ($service IS NULL OR service IS NULL OR service = $service)
68
- `).all({ $p: projectId, $level: level, $service: service }) as AlertRule[]
69
-
70
- for (const rule of rules) {
71
- const since = new Date(Date.now() - rule.window_seconds * 1000).toISOString()
72
- const conditions = ["project_id = $p", "level = $level", "timestamp >= $since"]
73
- const params: Record<string, unknown> = { $p: projectId, $level: rule.level, $since: since }
74
- if (rule.service) { conditions.push("service = $service"); params.$service = rule.service }
75
-
76
- const { count } = db.prepare(`SELECT COUNT(*) as count FROM logs WHERE ${conditions.join(" AND ")}`).get(params) as { count: number }
77
-
78
- if (count >= rule.threshold_count) {
79
- await fireAlert(db, rule, count)
80
- }
81
- }
82
- }
83
-
84
- async function fireAlert(db: Database, rule: AlertRule, count: number): Promise<void> {
85
- // Debounce: don't fire more than once per window
86
- if (rule.last_fired_at) {
87
- const lastFired = new Date(rule.last_fired_at).getTime()
88
- if (Date.now() - lastFired < rule.window_seconds * 1000) return
89
- }
90
-
91
- db.run("UPDATE alert_rules SET last_fired_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = $id", { $id: rule.id })
92
-
93
- const payload = {
94
- alert: rule.name,
95
- project_id: rule.project_id,
96
- level: rule.level,
97
- service: rule.service,
98
- count,
99
- threshold: rule.threshold_count,
100
- window_seconds: rule.window_seconds,
101
- fired_at: new Date().toISOString(),
102
- }
103
-
104
- if (rule.action === "webhook" && rule.webhook_url) {
105
- try {
106
- await fetch(rule.webhook_url, {
107
- method: "POST",
108
- headers: { "Content-Type": "application/json" },
109
- body: JSON.stringify(payload),
110
- })
111
- } catch (err) {
112
- console.error(`Alert webhook failed for rule ${rule.id}:`, err)
113
- }
114
- } else {
115
- console.warn(`[ALERT] ${rule.name}:`, JSON.stringify(payload))
116
- }
117
- }
@@ -1,35 +0,0 @@
1
- import { describe, expect, it } from "bun:test"
2
- import { getBrowserScript } from "./browser-script.ts"
3
-
4
- describe("getBrowserScript", () => {
5
- it("returns a non-empty string", () => {
6
- const script = getBrowserScript("http://localhost:3460")
7
- expect(typeof script).toBe("string")
8
- expect(script.length).toBeGreaterThan(100)
9
- })
10
-
11
- it("embeds the server URL", () => {
12
- const script = getBrowserScript("http://localhost:3460")
13
- expect(script).toContain("http://localhost:3460")
14
- })
15
-
16
- it("hooks console.error", () => {
17
- const script = getBrowserScript("http://localhost:3460")
18
- expect(script).toContain("console.error")
19
- })
20
-
21
- it("hooks window.onerror / unhandledrejection", () => {
22
- const script = getBrowserScript("http://localhost:3460")
23
- expect(script).toContain("unhandledrejection")
24
- })
25
-
26
- it("pushes to /api/logs", () => {
27
- const script = getBrowserScript("http://localhost:3460")
28
- expect(script).toContain("/api/logs")
29
- })
30
-
31
- it("uses data-project attribute", () => {
32
- const script = getBrowserScript("http://localhost:3460")
33
- expect(script).toContain("data-project")
34
- })
35
- })
@@ -1,31 +0,0 @@
1
- /** Returns the minified browser tracking script served at GET /script.js */
2
- export function getBrowserScript(serverUrl: string): string {
3
- return `(function(){
4
- var cfg={url:'${serverUrl}',projectId:null};
5
- var el=document.currentScript;
6
- if(el){cfg.projectId=el.getAttribute('data-project')||null;}
7
- var q=[];
8
- function flush(){if(!q.length)return;var b=q.splice(0);fetch(cfg.url+'/api/logs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b),keepalive:true}).catch(function(){});}
9
- setInterval(flush,2000);
10
- function push(level,msg,extra){
11
- q.push(Object.assign({level:level,message:String(msg),source:'script',url:location.href,timestamp:new Date().toISOString()},cfg.projectId?{project_id:cfg.projectId}:{},extra||{}));
12
- if(q.length>=10)flush();
13
- }
14
- var _ce=console.error.bind(console);
15
- console.error=function(){_ce.apply(console,arguments);push('error',Array.from(arguments).join(' '));};
16
- var _cw=console.warn.bind(console);
17
- console.warn=function(){_cw.apply(console,arguments);push('warn',Array.from(arguments).join(' '));};
18
- window.addEventListener('error',function(e){push('error',e.message,{stack_trace:e.error?e.error.stack:null,url:e.filename});});
19
- window.addEventListener('unhandledrejection',function(e){push('error','Unhandled promise rejection: '+(e.reason&&e.reason.message||String(e.reason)),{stack_trace:e.reason&&e.reason.stack||null});});
20
- window.addEventListener('beforeunload',flush);
21
- window.__logs={push:push,flush:flush,config:cfg};
22
- })();`
23
- }
24
-
25
- export function initLogsScript(config: { projectId: string; url: string }): void {
26
- if (typeof window === "undefined") return
27
- const script = document.createElement("script")
28
- script.src = `${config.url}/script.js`
29
- script.setAttribute("data-project", config.projectId)
30
- document.head.appendChild(script)
31
- }