@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.
- package/README.md +33 -10
- package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
- package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/cli/index.js +8511 -177
- package/dist/count-bmj4r2zb.js +10 -0
- package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
- package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
- package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
- package/dist/index-931pbyn5.js +141 -0
- package/dist/index-b5c72f1p.js +7 -0
- package/dist/{index-gc0zvs88.js → index-bnr19y0h.js} +596 -37
- package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
- package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
- package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
- package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
- package/dist/index-hq6kzaah.js +26 -0
- package/dist/index-j34f36wy.js +5672 -0
- package/dist/index-p4dbdzx4.js +1849 -0
- package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
- package/dist/index-t3x838zw.js +2583 -0
- package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
- package/dist/index.js +2982 -22
- package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
- package/dist/mcp/index.js +1473 -4286
- package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
- package/dist/server/index.js +2944 -417
- package/dist/storage.js +50 -0
- package/package.json +27 -8
- package/biome.json +0 -13
- package/bun.lock +0 -376
- package/dashboard/README.md +0 -73
- package/dashboard/bun.lock +0 -526
- package/dashboard/eslint.config.js +0 -23
- package/dashboard/index.html +0 -13
- package/dashboard/package.json +0 -32
- package/dashboard/src/App.css +0 -184
- package/dashboard/src/App.tsx +0 -49
- package/dashboard/src/api.ts +0 -33
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +0 -1
- package/dashboard/src/assets/vite.svg +0 -1
- package/dashboard/src/index.css +0 -111
- package/dashboard/src/main.tsx +0 -10
- package/dashboard/src/pages/Alerts.tsx +0 -69
- package/dashboard/src/pages/Issues.tsx +0 -50
- package/dashboard/src/pages/Perf.tsx +0 -75
- package/dashboard/src/pages/Projects.tsx +0 -67
- package/dashboard/src/pages/Summary.tsx +0 -67
- package/dashboard/src/pages/Tail.tsx +0 -65
- package/dashboard/tsconfig.app.json +0 -28
- package/dashboard/tsconfig.json +0 -7
- package/dashboard/tsconfig.node.json +0 -26
- package/dashboard/vite.config.ts +0 -14
- package/dist/count-x3n7qg3c.js +0 -9
- package/dist/index-997bkzr2.js +0 -15
- package/dist/index-pen6t0yc.js +0 -10794
- package/sdk/package.json +0 -27
- package/sdk/src/index.ts +0 -143
- package/sdk/src/types.ts +0 -56
- package/src/cli/entrypoints.test.ts +0 -63
- package/src/cli/index.ts +0 -471
- package/src/db/index.test.ts +0 -33
- package/src/db/index.ts +0 -189
- package/src/db/migrations/001_alert_rules.ts +0 -21
- package/src/db/migrations/002_issues.ts +0 -21
- package/src/db/migrations/003_retention.ts +0 -15
- package/src/db/migrations/004_page_auth.ts +0 -13
- package/src/db/pg-migrations.ts +0 -167
- package/src/index.ts +0 -1
- package/src/lib/alerts.test.ts +0 -67
- package/src/lib/alerts.ts +0 -117
- package/src/lib/browser-script.test.ts +0 -35
- package/src/lib/browser-script.ts +0 -31
- package/src/lib/compare.test.ts +0 -52
- package/src/lib/compare.ts +0 -85
- package/src/lib/count.test.ts +0 -44
- package/src/lib/count.ts +0 -55
- package/src/lib/diagnose.test.ts +0 -55
- package/src/lib/diagnose.ts +0 -91
- package/src/lib/export.test.ts +0 -66
- package/src/lib/export.ts +0 -65
- package/src/lib/github.ts +0 -38
- package/src/lib/health.test.ts +0 -48
- package/src/lib/health.ts +0 -51
- package/src/lib/ingest.test.ts +0 -57
- package/src/lib/ingest.ts +0 -78
- package/src/lib/issues.test.ts +0 -79
- package/src/lib/issues.ts +0 -70
- package/src/lib/jobs.test.ts +0 -69
- package/src/lib/jobs.ts +0 -63
- package/src/lib/lighthouse.ts +0 -65
- package/src/lib/package-meta.test.ts +0 -43
- package/src/lib/package-meta.ts +0 -80
- package/src/lib/page-auth.test.ts +0 -54
- package/src/lib/page-auth.ts +0 -48
- package/src/lib/parse-time.test.ts +0 -37
- package/src/lib/parse-time.ts +0 -14
- package/src/lib/perf.test.ts +0 -45
- package/src/lib/perf.ts +0 -46
- package/src/lib/projects.test.ts +0 -73
- package/src/lib/projects.ts +0 -69
- package/src/lib/query.test.ts +0 -104
- package/src/lib/query.ts +0 -84
- package/src/lib/retention.test.ts +0 -42
- package/src/lib/retention.ts +0 -62
- package/src/lib/rotate.test.ts +0 -37
- package/src/lib/rotate.ts +0 -27
- package/src/lib/scanner.ts +0 -131
- package/src/lib/scheduler.ts +0 -63
- package/src/lib/session-context.ts +0 -28
- package/src/lib/summarize.test.ts +0 -38
- package/src/lib/summarize.ts +0 -23
- package/src/mcp/http.test.ts +0 -92
- package/src/mcp/http.ts +0 -135
- package/src/mcp/index.test.ts +0 -27
- package/src/mcp/index.ts +0 -444
- package/src/server/index.ts +0 -61
- package/src/server/routes/alerts.ts +0 -32
- package/src/server/routes/issues.ts +0 -43
- package/src/server/routes/jobs.ts +0 -32
- package/src/server/routes/logs.ts +0 -113
- package/src/server/routes/perf.ts +0 -23
- package/src/server/routes/projects.ts +0 -67
- package/src/server/routes/stream.ts +0 -43
- package/src/server/server.test.ts +0 -194
- package/src/types/index.ts +0 -119
- package/tsconfig.json +0 -22
- /package/dashboard/{public → dist}/favicon.svg +0 -0
- /package/dashboard/{public → dist}/icons.svg +0 -0
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono"
|
|
2
|
-
import type { Database } from "bun:sqlite"
|
|
3
|
-
import { ingestBatch, ingestLog } from "../../lib/ingest.ts"
|
|
4
|
-
import { getLogContext, searchLogs, tailLogs } from "../../lib/query.ts"
|
|
5
|
-
import { summarizeLogs } from "../../lib/summarize.ts"
|
|
6
|
-
import { exportToCsv, exportToJson } from "../../lib/export.ts"
|
|
7
|
-
import { countLogs } from "../../lib/count.ts"
|
|
8
|
-
import { parseTime } from "../../lib/parse-time.ts"
|
|
9
|
-
import { resolveProjectId } from "../../lib/projects.ts"
|
|
10
|
-
import type { LogEntry, LogLevel } from "../../types/index.ts"
|
|
11
|
-
|
|
12
|
-
export function logsRoutes(db: Database) {
|
|
13
|
-
const app = new Hono()
|
|
14
|
-
|
|
15
|
-
// POST /api/logs — ingest single or batch
|
|
16
|
-
app.post("/", async (c) => {
|
|
17
|
-
const body = await c.req.json()
|
|
18
|
-
if (Array.isArray(body)) {
|
|
19
|
-
const rows = ingestBatch(db, body as LogEntry[])
|
|
20
|
-
return c.json({ inserted: rows.length }, 201)
|
|
21
|
-
}
|
|
22
|
-
const row = ingestLog(db, body as LogEntry)
|
|
23
|
-
return c.json(row, 201)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
// GET /api/logs
|
|
27
|
-
app.get("/", (c) => {
|
|
28
|
-
const { project_id, page_id, level, service, since, until, text, trace_id, limit, offset, fields } = c.req.query()
|
|
29
|
-
const rows = searchLogs(db, {
|
|
30
|
-
project_id: project_id || undefined,
|
|
31
|
-
page_id: page_id || undefined,
|
|
32
|
-
level: level ? (level.split(",") as LogLevel[]) : undefined,
|
|
33
|
-
service: service || undefined,
|
|
34
|
-
since: since || undefined,
|
|
35
|
-
until: until || undefined,
|
|
36
|
-
text: text || undefined,
|
|
37
|
-
trace_id: trace_id || undefined,
|
|
38
|
-
limit: limit ? Number(limit) : 100,
|
|
39
|
-
offset: offset ? Number(offset) : 0,
|
|
40
|
-
})
|
|
41
|
-
if (fields) {
|
|
42
|
-
const keys = fields.split(",")
|
|
43
|
-
return c.json(rows.map(r => Object.fromEntries(keys.map(k => [k, (r as Record<string, unknown>)[k]]))))
|
|
44
|
-
}
|
|
45
|
-
return c.json(rows)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// GET /api/logs/tail
|
|
49
|
-
app.get("/tail", (c) => {
|
|
50
|
-
const { project_id, n } = c.req.query()
|
|
51
|
-
const rows = tailLogs(db, project_id || undefined, n ? Number(n) : 50)
|
|
52
|
-
return c.json(rows)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
// GET /api/logs/summary
|
|
56
|
-
app.get("/summary", (c) => {
|
|
57
|
-
const { project_id, since } = c.req.query()
|
|
58
|
-
const summary = summarizeLogs(db, resolveProjectId(db, project_id) || undefined, parseTime(since) || since || undefined)
|
|
59
|
-
return c.json(summary)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// GET /api/logs/count
|
|
63
|
-
app.get("/count", (c) => {
|
|
64
|
-
const { project_id, service, level, since, until } = c.req.query()
|
|
65
|
-
return c.json(countLogs(db, {
|
|
66
|
-
project_id: resolveProjectId(db, project_id) || undefined,
|
|
67
|
-
service: service || undefined,
|
|
68
|
-
level: level || undefined,
|
|
69
|
-
since: since || undefined,
|
|
70
|
-
until: until || undefined,
|
|
71
|
-
}))
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// GET /api/logs/recent-errors
|
|
75
|
-
app.get("/recent-errors", (c) => {
|
|
76
|
-
const { project_id, since, limit } = c.req.query()
|
|
77
|
-
const rows = searchLogs(db, {
|
|
78
|
-
project_id: resolveProjectId(db, project_id) || undefined,
|
|
79
|
-
level: ["error", "fatal"],
|
|
80
|
-
since: parseTime(since || "1h"),
|
|
81
|
-
limit: limit ? Number(limit) : 20,
|
|
82
|
-
})
|
|
83
|
-
return c.json(rows.map(r => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service, age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000) })))
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// GET /api/logs/:trace_id/context
|
|
87
|
-
app.get("/:trace_id/context", (c) => {
|
|
88
|
-
const rows = getLogContext(db, c.req.param("trace_id"))
|
|
89
|
-
return c.json(rows)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// GET /api/logs/export?format=json|csv&project_id=&since=&level=
|
|
93
|
-
app.get("/export", (c) => {
|
|
94
|
-
const { project_id, since, until, level, service, format, limit } = c.req.query()
|
|
95
|
-
const opts = { project_id: project_id || undefined, since: since || undefined, until: until || undefined, level: level || undefined, service: service || undefined, limit: limit ? Number(limit) : undefined }
|
|
96
|
-
|
|
97
|
-
if (format === "csv") {
|
|
98
|
-
c.header("Content-Type", "text/csv")
|
|
99
|
-
c.header("Content-Disposition", "attachment; filename=logs.csv")
|
|
100
|
-
const chunks: string[] = []
|
|
101
|
-
exportToCsv(db, opts, s => chunks.push(s))
|
|
102
|
-
return c.text(chunks.join(""))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
c.header("Content-Type", "application/json")
|
|
106
|
-
c.header("Content-Disposition", "attachment; filename=logs.json")
|
|
107
|
-
const chunks: string[] = []
|
|
108
|
-
exportToJson(db, opts, s => chunks.push(s))
|
|
109
|
-
return c.text(chunks.join("\n"))
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
return app
|
|
113
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono"
|
|
2
|
-
import type { Database } from "bun:sqlite"
|
|
3
|
-
import { getLatestSnapshot, getPerfTrend } from "../../lib/perf.ts"
|
|
4
|
-
|
|
5
|
-
export function perfRoutes(db: Database) {
|
|
6
|
-
const app = new Hono()
|
|
7
|
-
|
|
8
|
-
app.get("/", (c) => {
|
|
9
|
-
const { project_id, page_id, since } = c.req.query()
|
|
10
|
-
if (!project_id) return c.json({ error: "project_id is required" }, 422)
|
|
11
|
-
const snap = getLatestSnapshot(db, project_id, page_id || undefined)
|
|
12
|
-
return c.json(snap)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
app.get("/trend", (c) => {
|
|
16
|
-
const { project_id, page_id, since, limit } = c.req.query()
|
|
17
|
-
if (!project_id) return c.json({ error: "project_id is required" }, 422)
|
|
18
|
-
const trend = getPerfTrend(db, project_id, page_id || undefined, since || undefined, limit ? Number(limit) : 50)
|
|
19
|
-
return c.json(trend)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
return app
|
|
23
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono"
|
|
2
|
-
import type { Database } from "bun:sqlite"
|
|
3
|
-
import { createPage, createProject, getProject, listPages, listProjects } from "../../lib/projects.ts"
|
|
4
|
-
import { syncGithubRepo } from "../../lib/github.ts"
|
|
5
|
-
import { runRetentionForProject, setRetentionPolicy } from "../../lib/retention.ts"
|
|
6
|
-
import { deletePageAuth, setPageAuth } from "../../lib/page-auth.ts"
|
|
7
|
-
|
|
8
|
-
export function projectsRoutes(db: Database) {
|
|
9
|
-
const app = new Hono()
|
|
10
|
-
|
|
11
|
-
app.post("/", async (c) => {
|
|
12
|
-
const body = await c.req.json()
|
|
13
|
-
if (!body.name) return c.json({ error: "name is required" }, 422)
|
|
14
|
-
const project = createProject(db, body)
|
|
15
|
-
return c.json(project, 201)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
app.get("/", (c) => c.json(listProjects(db)))
|
|
19
|
-
|
|
20
|
-
app.get("/:id", (c) => {
|
|
21
|
-
const project = getProject(db, c.req.param("id"))
|
|
22
|
-
if (!project) return c.json({ error: "not found" }, 404)
|
|
23
|
-
return c.json(project)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
app.post("/:id/pages", async (c) => {
|
|
27
|
-
const body = await c.req.json()
|
|
28
|
-
if (!body.url) return c.json({ error: "url is required" }, 422)
|
|
29
|
-
const page = createPage(db, { ...body, project_id: c.req.param("id") })
|
|
30
|
-
return c.json(page, 201)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
app.get("/:id/pages", (c) => c.json(listPages(db, c.req.param("id"))))
|
|
34
|
-
|
|
35
|
-
app.put("/:id/retention", async (c) => {
|
|
36
|
-
const body = await c.req.json()
|
|
37
|
-
setRetentionPolicy(db, c.req.param("id"), body)
|
|
38
|
-
return c.json({ updated: true })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
app.post("/:id/retention/run", (c) => {
|
|
42
|
-
const result = runRetentionForProject(db, c.req.param("id"))
|
|
43
|
-
return c.json(result)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
app.post("/:id/pages/:page_id/auth", async (c) => {
|
|
47
|
-
const { type, credentials } = await c.req.json()
|
|
48
|
-
if (!type || !credentials) return c.json({ error: "type and credentials required" }, 422)
|
|
49
|
-
const result = setPageAuth(db, c.req.param("page_id"), type, credentials)
|
|
50
|
-
return c.json({ id: result.id, type: result.type, created_at: result.created_at }, 201)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
app.delete("/:id/pages/:page_id/auth", (c) => {
|
|
54
|
-
deletePageAuth(db, c.req.param("page_id"))
|
|
55
|
-
return c.json({ deleted: true })
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
app.post("/:id/sync-repo", async (c) => {
|
|
59
|
-
const project = getProject(db, c.req.param("id"))
|
|
60
|
-
if (!project) return c.json({ error: "not found" }, 404)
|
|
61
|
-
if (!project.github_repo) return c.json({ error: "no github_repo set" }, 422)
|
|
62
|
-
const updated = await syncGithubRepo(db, project)
|
|
63
|
-
return c.json(updated)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
return app
|
|
67
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono"
|
|
2
|
-
import { streamSSE } from "hono/streaming"
|
|
3
|
-
import type { Database } from "bun:sqlite"
|
|
4
|
-
import type { LogLevel, LogRow } from "../../types/index.ts"
|
|
5
|
-
|
|
6
|
-
export function streamRoutes(db: Database) {
|
|
7
|
-
const app = new Hono()
|
|
8
|
-
|
|
9
|
-
// GET /api/logs/stream?project_id=&level=&service=
|
|
10
|
-
app.get("/", (c) => {
|
|
11
|
-
const { project_id, level, service } = c.req.query()
|
|
12
|
-
|
|
13
|
-
return streamSSE(c, async (stream) => {
|
|
14
|
-
let lastId: string | null = null
|
|
15
|
-
|
|
16
|
-
// Seed lastId with the most recent log so we only stream new ones
|
|
17
|
-
const latest = db.prepare("SELECT id FROM logs ORDER BY timestamp DESC LIMIT 1").get() as { id: string } | null
|
|
18
|
-
lastId = latest?.id ?? null
|
|
19
|
-
|
|
20
|
-
while (true) {
|
|
21
|
-
const conditions: string[] = []
|
|
22
|
-
const params: Record<string, unknown> = {}
|
|
23
|
-
|
|
24
|
-
if (lastId) { conditions.push("rowid > (SELECT rowid FROM logs WHERE id = $lastId)"); params.$lastId = lastId }
|
|
25
|
-
if (project_id) { conditions.push("project_id = $project_id"); params.$project_id = project_id }
|
|
26
|
-
if (level) { conditions.push("level IN (" + level.split(",").map((l, i) => `$l${i}`).join(",") + ")"); level.split(",").forEach((l, i) => { params[`$l${i}`] = l }) }
|
|
27
|
-
if (service) { conditions.push("service = $service"); params.$service = service }
|
|
28
|
-
|
|
29
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""
|
|
30
|
-
const rows = db.prepare(`SELECT * FROM logs ${where} ORDER BY timestamp ASC LIMIT 50`).all(params) as LogRow[]
|
|
31
|
-
|
|
32
|
-
for (const row of rows) {
|
|
33
|
-
await stream.writeSSE({ data: JSON.stringify(row), id: row.id, event: row.level })
|
|
34
|
-
lastId = row.id
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await stream.sleep(500)
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return app
|
|
43
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach } from "bun:test"
|
|
2
|
-
import { Hono } from "hono"
|
|
3
|
-
import { cors } from "hono/cors"
|
|
4
|
-
import { createTestDb } from "../db/index.ts"
|
|
5
|
-
import { logsRoutes } from "./routes/logs.ts"
|
|
6
|
-
import { projectsRoutes } from "./routes/projects.ts"
|
|
7
|
-
import { jobsRoutes } from "./routes/jobs.ts"
|
|
8
|
-
import { perfRoutes } from "./routes/perf.ts"
|
|
9
|
-
|
|
10
|
-
function buildApp() {
|
|
11
|
-
const db = createTestDb()
|
|
12
|
-
const app = new Hono()
|
|
13
|
-
app.use("*", cors())
|
|
14
|
-
app.route("/api/logs", logsRoutes(db))
|
|
15
|
-
app.route("/api/projects", projectsRoutes(db))
|
|
16
|
-
app.route("/api/jobs", jobsRoutes(db))
|
|
17
|
-
app.route("/api/perf", perfRoutes(db))
|
|
18
|
-
return { app, db }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("POST /api/logs", () => {
|
|
22
|
-
it("ingests a single log", async () => {
|
|
23
|
-
const { app } = buildApp()
|
|
24
|
-
const res = await app.request("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ level: "error", message: "boom" }) })
|
|
25
|
-
expect(res.status).toBe(201)
|
|
26
|
-
const body = await res.json() as { level: string; message: string }
|
|
27
|
-
expect(body.level).toBe("error")
|
|
28
|
-
expect(body.message).toBe("boom")
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it("ingests a batch", async () => {
|
|
32
|
-
const { app } = buildApp()
|
|
33
|
-
const res = await app.request("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify([{ level: "info", message: "a" }, { level: "warn", message: "b" }]) })
|
|
34
|
-
expect(res.status).toBe(201)
|
|
35
|
-
const body = await res.json() as { inserted: number }
|
|
36
|
-
expect(body.inserted).toBe(2)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe("GET /api/logs", () => {
|
|
41
|
-
it("lists logs", async () => {
|
|
42
|
-
const { app, db } = buildApp()
|
|
43
|
-
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
44
|
-
ingestBatch(db, [{ level: "error", message: "e1" }, { level: "info", message: "i1" }])
|
|
45
|
-
const res = await app.request("/api/logs")
|
|
46
|
-
expect(res.status).toBe(200)
|
|
47
|
-
const body = await res.json() as unknown[]
|
|
48
|
-
expect(body.length).toBeGreaterThanOrEqual(2)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it("filters by level", async () => {
|
|
52
|
-
const { app, db } = buildApp()
|
|
53
|
-
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
54
|
-
ingestBatch(db, [{ level: "error", message: "e1" }, { level: "info", message: "i1" }])
|
|
55
|
-
const res = await app.request("/api/logs?level=error")
|
|
56
|
-
const body = await res.json() as { level: string }[]
|
|
57
|
-
expect(body.every(r => r.level === "error")).toBe(true)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it("supports ?fields= projection", async () => {
|
|
61
|
-
const { app, db } = buildApp()
|
|
62
|
-
const { ingestLog } = await import("../lib/ingest.ts")
|
|
63
|
-
ingestLog(db, { level: "info", message: "hello" })
|
|
64
|
-
const res = await app.request("/api/logs?fields=level,message")
|
|
65
|
-
const body = await res.json() as Record<string, unknown>[]
|
|
66
|
-
expect(Object.keys(body[0]!).sort()).toEqual(["level", "message"].sort())
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe("GET /api/logs/tail", () => {
|
|
71
|
-
it("returns recent logs", async () => {
|
|
72
|
-
const { app, db } = buildApp()
|
|
73
|
-
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
74
|
-
ingestBatch(db, Array.from({ length: 10 }, (_, i) => ({ level: "info" as const, message: `m${i}` })))
|
|
75
|
-
const res = await app.request("/api/logs/tail?n=5")
|
|
76
|
-
const body = await res.json() as unknown[]
|
|
77
|
-
expect(body).toHaveLength(5)
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe("GET /api/logs/summary", () => {
|
|
82
|
-
it("returns summary of errors/warns", async () => {
|
|
83
|
-
const { app, db } = buildApp()
|
|
84
|
-
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
85
|
-
ingestBatch(db, [{ level: "error", message: "x", service: "api" }, { level: "warn", message: "y", service: "db" }])
|
|
86
|
-
const res = await app.request("/api/logs/summary")
|
|
87
|
-
const body = await res.json() as unknown[]
|
|
88
|
-
expect(body.length).toBeGreaterThan(0)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
describe("GET /api/logs/:trace_id/context", () => {
|
|
93
|
-
it("returns logs for trace", async () => {
|
|
94
|
-
const { app, db } = buildApp()
|
|
95
|
-
const { ingestBatch } = await import("../lib/ingest.ts")
|
|
96
|
-
ingestBatch(db, [{ level: "info", message: "a", trace_id: "t99" }, { level: "error", message: "b", trace_id: "t99" }])
|
|
97
|
-
const res = await app.request("/api/logs/t99/context")
|
|
98
|
-
const body = await res.json() as unknown[]
|
|
99
|
-
expect(body).toHaveLength(2)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe("POST /api/projects", () => {
|
|
104
|
-
it("creates a project", async () => {
|
|
105
|
-
const { app } = buildApp()
|
|
106
|
-
const res = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "myapp" }) })
|
|
107
|
-
expect(res.status).toBe(201)
|
|
108
|
-
const body = await res.json() as { name: string }
|
|
109
|
-
expect(body.name).toBe("myapp")
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it("returns 422 without name", async () => {
|
|
113
|
-
const { app } = buildApp()
|
|
114
|
-
const res = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
115
|
-
expect(res.status).toBe(422)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe("GET /api/projects", () => {
|
|
120
|
-
it("lists projects", async () => {
|
|
121
|
-
const { app } = buildApp()
|
|
122
|
-
await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "p1" }) })
|
|
123
|
-
const res = await app.request("/api/projects")
|
|
124
|
-
const body = await res.json() as unknown[]
|
|
125
|
-
expect(body.length).toBeGreaterThanOrEqual(1)
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
describe("POST /api/projects/:id/pages", () => {
|
|
130
|
-
it("registers a page", async () => {
|
|
131
|
-
const { app } = buildApp()
|
|
132
|
-
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "app" }) })
|
|
133
|
-
const project = await pRes.json() as { id: string }
|
|
134
|
-
const res = await app.request(`/api/projects/${project.id}/pages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://app.com/home" }) })
|
|
135
|
-
expect(res.status).toBe(201)
|
|
136
|
-
const page = await res.json() as { url: string }
|
|
137
|
-
expect(page.url).toBe("https://app.com/home")
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it("returns 422 without url", async () => {
|
|
141
|
-
const { app } = buildApp()
|
|
142
|
-
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "app2" }) })
|
|
143
|
-
const project = await pRes.json() as { id: string }
|
|
144
|
-
const res = await app.request(`/api/projects/${project.id}/pages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
145
|
-
expect(res.status).toBe(422)
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
describe("jobs routes", () => {
|
|
150
|
-
it("creates and lists jobs", async () => {
|
|
151
|
-
const { app } = buildApp()
|
|
152
|
-
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "appj" }) })
|
|
153
|
-
const { id } = await pRes.json() as { id: string }
|
|
154
|
-
const jRes = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_id: id, schedule: "*/5 * * * *" }) })
|
|
155
|
-
expect(jRes.status).toBe(201)
|
|
156
|
-
const listRes = await app.request(`/api/jobs?project_id=${id}`)
|
|
157
|
-
const jobs = await listRes.json() as unknown[]
|
|
158
|
-
expect(jobs).toHaveLength(1)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it("returns 422 without required fields", async () => {
|
|
162
|
-
const { app } = buildApp()
|
|
163
|
-
const res = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) })
|
|
164
|
-
expect(res.status).toBe(422)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it("deletes a job", async () => {
|
|
168
|
-
const { app } = buildApp()
|
|
169
|
-
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "appd" }) })
|
|
170
|
-
const { id } = await pRes.json() as { id: string }
|
|
171
|
-
const jRes = await app.request("/api/jobs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_id: id, schedule: "*/5 * * * *" }) })
|
|
172
|
-
const job = await jRes.json() as { id: string }
|
|
173
|
-
const del = await app.request(`/api/jobs/${job.id}`, { method: "DELETE" })
|
|
174
|
-
expect(del.status).toBe(200)
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
describe("perf routes", () => {
|
|
179
|
-
it("returns 422 without project_id", async () => {
|
|
180
|
-
const { app } = buildApp()
|
|
181
|
-
const res = await app.request("/api/perf")
|
|
182
|
-
expect(res.status).toBe(422)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it("returns null when no snapshot exists", async () => {
|
|
186
|
-
const { app } = buildApp()
|
|
187
|
-
const pRes = await app.request("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "perf-app" }) })
|
|
188
|
-
const { id } = await pRes.json() as { id: string }
|
|
189
|
-
const res = await app.request(`/api/perf?project_id=${id}`)
|
|
190
|
-
expect(res.status).toBe(200)
|
|
191
|
-
const body = await res.json()
|
|
192
|
-
expect(body).toBeNull()
|
|
193
|
-
})
|
|
194
|
-
})
|
package/src/types/index.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"
|
|
2
|
-
export type LogSource = "sdk" | "script" | "scanner"
|
|
3
|
-
|
|
4
|
-
export interface LogEntry {
|
|
5
|
-
id?: string
|
|
6
|
-
timestamp?: string
|
|
7
|
-
project_id?: string
|
|
8
|
-
page_id?: string
|
|
9
|
-
level: LogLevel
|
|
10
|
-
source?: LogSource
|
|
11
|
-
service?: string
|
|
12
|
-
message: string
|
|
13
|
-
trace_id?: string
|
|
14
|
-
session_id?: string
|
|
15
|
-
agent?: string
|
|
16
|
-
url?: string
|
|
17
|
-
stack_trace?: string
|
|
18
|
-
metadata?: Record<string, unknown>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface LogRow {
|
|
22
|
-
id: string
|
|
23
|
-
timestamp: string
|
|
24
|
-
project_id: string | null
|
|
25
|
-
page_id: string | null
|
|
26
|
-
level: LogLevel
|
|
27
|
-
source: LogSource
|
|
28
|
-
service: string | null
|
|
29
|
-
message: string
|
|
30
|
-
trace_id: string | null
|
|
31
|
-
session_id: string | null
|
|
32
|
-
agent: string | null
|
|
33
|
-
url: string | null
|
|
34
|
-
stack_trace: string | null
|
|
35
|
-
metadata: string | null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Project {
|
|
39
|
-
id: string
|
|
40
|
-
name: string
|
|
41
|
-
github_repo: string | null
|
|
42
|
-
base_url: string | null
|
|
43
|
-
description: string | null
|
|
44
|
-
github_description: string | null
|
|
45
|
-
github_branch: string | null
|
|
46
|
-
github_sha: string | null
|
|
47
|
-
last_synced_at: string | null
|
|
48
|
-
created_at: string
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface Page {
|
|
52
|
-
id: string
|
|
53
|
-
project_id: string
|
|
54
|
-
url: string
|
|
55
|
-
path: string
|
|
56
|
-
name: string | null
|
|
57
|
-
last_scanned_at: string | null
|
|
58
|
-
created_at: string
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ScanJob {
|
|
62
|
-
id: string
|
|
63
|
-
project_id: string
|
|
64
|
-
page_id: string | null
|
|
65
|
-
schedule: string
|
|
66
|
-
enabled: number
|
|
67
|
-
last_run_at: string | null
|
|
68
|
-
created_at: string
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ScanRun {
|
|
72
|
-
id: string
|
|
73
|
-
job_id: string
|
|
74
|
-
page_id: string | null
|
|
75
|
-
started_at: string
|
|
76
|
-
finished_at: string | null
|
|
77
|
-
status: "running" | "completed" | "failed"
|
|
78
|
-
logs_collected: number
|
|
79
|
-
errors_found: number
|
|
80
|
-
perf_score: number | null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface PerformanceSnapshot {
|
|
84
|
-
id: string
|
|
85
|
-
timestamp: string
|
|
86
|
-
project_id: string
|
|
87
|
-
page_id: string | null
|
|
88
|
-
url: string
|
|
89
|
-
lcp: number | null
|
|
90
|
-
fcp: number | null
|
|
91
|
-
cls: number | null
|
|
92
|
-
tti: number | null
|
|
93
|
-
ttfb: number | null
|
|
94
|
-
score: number | null
|
|
95
|
-
raw_audit: string | null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface LogQuery {
|
|
99
|
-
project_id?: string
|
|
100
|
-
page_id?: string
|
|
101
|
-
level?: LogLevel | LogLevel[]
|
|
102
|
-
service?: string
|
|
103
|
-
since?: string
|
|
104
|
-
until?: string
|
|
105
|
-
text?: string
|
|
106
|
-
trace_id?: string
|
|
107
|
-
limit?: number
|
|
108
|
-
offset?: number
|
|
109
|
-
fields?: string[]
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface LogSummary {
|
|
113
|
-
project_id: string | null
|
|
114
|
-
service: string | null
|
|
115
|
-
page_id: string | null
|
|
116
|
-
level: LogLevel
|
|
117
|
-
count: number
|
|
118
|
-
latest: string
|
|
119
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"lib": ["ESNext"],
|
|
4
|
-
"target": "ESNext",
|
|
5
|
-
"module": "Preserve",
|
|
6
|
-
"moduleDetection": "force",
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"allowJs": true,
|
|
9
|
-
"moduleResolution": "bundler",
|
|
10
|
-
"allowImportingTsExtensions": true,
|
|
11
|
-
"verbatimModuleSyntax": true,
|
|
12
|
-
"noEmit": true,
|
|
13
|
-
"strict": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"noUncheckedIndexedAccess": true,
|
|
17
|
-
"noImplicitOverride": true,
|
|
18
|
-
"noUnusedLocals": false,
|
|
19
|
-
"noUnusedParameters": false,
|
|
20
|
-
"noPropertyAccessFromIndexSignature": false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
File without changes
|
|
File without changes
|