@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
|
@@ -0,0 +1,1849 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
appendRawEvent,
|
|
4
|
+
getEventRecord,
|
|
5
|
+
indexRawEvent,
|
|
6
|
+
readRawEvent,
|
|
7
|
+
sanitizeSourceMapArtifactRecord,
|
|
8
|
+
sanitizeSourceMapContextRecord,
|
|
9
|
+
sanitizeSourceMapIdentifierValue,
|
|
10
|
+
sanitizeSourceMapTelemetry,
|
|
11
|
+
sanitizedTestReportMetadata,
|
|
12
|
+
sourceMapFallbackIdentifier,
|
|
13
|
+
upsertIssue,
|
|
14
|
+
upsertSourceMapProjection,
|
|
15
|
+
upsertTestReportProjection,
|
|
16
|
+
withEventStoreLock
|
|
17
|
+
} from "./index-t3x838zw.js";
|
|
18
|
+
import {
|
|
19
|
+
sqlBindings
|
|
20
|
+
} from "./index-b5c72f1p.js";
|
|
21
|
+
import {
|
|
22
|
+
parseTime
|
|
23
|
+
} from "./index-hq6kzaah.js";
|
|
24
|
+
|
|
25
|
+
// src/lib/events.ts
|
|
26
|
+
function searchEvents(db, query = {}) {
|
|
27
|
+
const { where, params } = buildEventWhere(query);
|
|
28
|
+
const limit = clampPositiveInt(query.limit, 100, query.max_limit ?? 1000);
|
|
29
|
+
const offset = clampNonNegativeInt(query.offset, 0);
|
|
30
|
+
const rows = db.query(`
|
|
31
|
+
SELECT *
|
|
32
|
+
FROM event_records
|
|
33
|
+
${where}
|
|
34
|
+
ORDER BY event_time DESC, event_id DESC
|
|
35
|
+
LIMIT ? OFFSET ?
|
|
36
|
+
`).all(...params, limit, offset);
|
|
37
|
+
return rows.map((row) => materializeEvent(db, row, query.include_raw === true));
|
|
38
|
+
}
|
|
39
|
+
function getEvent(db, eventId, includeRaw = true) {
|
|
40
|
+
const record = getEventRecord(db, eventId);
|
|
41
|
+
return record ? materializeEvent(db, record, includeRaw) : null;
|
|
42
|
+
}
|
|
43
|
+
function exportEventsToJson(db, query, writeLine) {
|
|
44
|
+
writeLine("[");
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (const event of searchEvents(db, {
|
|
47
|
+
...query,
|
|
48
|
+
limit: query.limit ?? 1e5,
|
|
49
|
+
max_limit: 1e5
|
|
50
|
+
})) {
|
|
51
|
+
writeLine((count > 0 ? "," : "") + JSON.stringify(event));
|
|
52
|
+
count += 1;
|
|
53
|
+
}
|
|
54
|
+
writeLine("]");
|
|
55
|
+
return count;
|
|
56
|
+
}
|
|
57
|
+
function materializeEvent(db, record, includeRaw) {
|
|
58
|
+
const { metadata, ...rest } = record;
|
|
59
|
+
return {
|
|
60
|
+
...rest,
|
|
61
|
+
metadata: parseMetadata(metadata),
|
|
62
|
+
raw: includeRaw ? readRawEvent(db, record) : undefined
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function buildEventWhere(query) {
|
|
66
|
+
const conditions = [];
|
|
67
|
+
const params = [];
|
|
68
|
+
addScalar(conditions, params, "event_id", query.event_id);
|
|
69
|
+
addList(conditions, params, "event_type", query.event_type);
|
|
70
|
+
addList(conditions, params, "source", query.source);
|
|
71
|
+
addList(conditions, params, "severity", query.severity);
|
|
72
|
+
addScalar(conditions, params, "project_id", query.project_id);
|
|
73
|
+
addScalar(conditions, params, "page_id", query.page_id);
|
|
74
|
+
addScalar(conditions, params, "machine_id", query.machine_id);
|
|
75
|
+
addScalar(conditions, params, "repo_id", query.repo_id);
|
|
76
|
+
addScalar(conditions, params, "app_id", query.app_id);
|
|
77
|
+
addScalar(conditions, params, "process_id", query.process_id);
|
|
78
|
+
addScalar(conditions, params, "run_id", query.run_id);
|
|
79
|
+
addScalar(conditions, params, "trace_id", query.trace_id);
|
|
80
|
+
addScalar(conditions, params, "span_id", query.span_id);
|
|
81
|
+
addScalar(conditions, params, "session_id", query.session_id);
|
|
82
|
+
addScalar(conditions, params, "release_id", query.release_id);
|
|
83
|
+
addScalar(conditions, params, "environment", query.environment);
|
|
84
|
+
if (query.since) {
|
|
85
|
+
conditions.push("event_time >= ?");
|
|
86
|
+
params.push(parseTime(query.since) ?? query.since);
|
|
87
|
+
}
|
|
88
|
+
if (query.until) {
|
|
89
|
+
conditions.push("event_time <= ?");
|
|
90
|
+
params.push(parseTime(query.until) ?? query.until);
|
|
91
|
+
}
|
|
92
|
+
if (query.text) {
|
|
93
|
+
const needle = `%${escapeLike(query.text)}%`;
|
|
94
|
+
conditions.push("(event_id LIKE ? ESCAPE '\\' OR source_event_id LIKE ? ESCAPE '\\' OR message LIKE ? ESCAPE '\\' OR metadata LIKE ? ESCAPE '\\')");
|
|
95
|
+
params.push(needle, needle, needle, needle);
|
|
96
|
+
}
|
|
97
|
+
if (query.exclude_mcp_tool_telemetry) {
|
|
98
|
+
conditions.push("NOT (event_type = 'agent' AND source = 'mcp' AND metadata LIKE ?)");
|
|
99
|
+
params.push('%"category":"mcp_tool_call"%');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
103
|
+
params
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function addScalar(conditions, params, column, value) {
|
|
107
|
+
if (!value)
|
|
108
|
+
return;
|
|
109
|
+
conditions.push(`${column} = ?`);
|
|
110
|
+
params.push(value);
|
|
111
|
+
}
|
|
112
|
+
function addList(conditions, params, column, value) {
|
|
113
|
+
if (!value)
|
|
114
|
+
return;
|
|
115
|
+
const values = (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
116
|
+
if (values.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
conditions.push(`${column} IN (${values.map(() => "?").join(",")})`);
|
|
119
|
+
params.push(...values);
|
|
120
|
+
}
|
|
121
|
+
function parseMetadata(value) {
|
|
122
|
+
if (!value)
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(value);
|
|
126
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function escapeLike(value) {
|
|
132
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
133
|
+
}
|
|
134
|
+
function clampPositiveInt(value, fallback, max) {
|
|
135
|
+
if (!Number.isFinite(value) || value === undefined)
|
|
136
|
+
return fallback;
|
|
137
|
+
return Math.min(Math.max(1, Math.floor(value)), max);
|
|
138
|
+
}
|
|
139
|
+
function clampNonNegativeInt(value, fallback) {
|
|
140
|
+
if (!Number.isFinite(value) || value === undefined)
|
|
141
|
+
return fallback;
|
|
142
|
+
return Math.max(0, Math.floor(value));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/lib/alerts.ts
|
|
146
|
+
function createAlertRule(db, data) {
|
|
147
|
+
return db.prepare(`
|
|
148
|
+
INSERT INTO alert_rules (project_id, name, service, level, threshold_count, window_seconds, action, webhook_url)
|
|
149
|
+
VALUES ($project_id, $name, $service, $level, $threshold_count, $window_seconds, $action, $webhook_url)
|
|
150
|
+
RETURNING *
|
|
151
|
+
`).get({
|
|
152
|
+
$project_id: data.project_id,
|
|
153
|
+
$name: data.name,
|
|
154
|
+
$service: data.service ?? null,
|
|
155
|
+
$level: data.level ?? "error",
|
|
156
|
+
$threshold_count: data.threshold_count ?? 10,
|
|
157
|
+
$window_seconds: data.window_seconds ?? 60,
|
|
158
|
+
$action: data.action ?? "webhook",
|
|
159
|
+
$webhook_url: data.webhook_url ?? null
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function listAlertRules(db, projectId) {
|
|
163
|
+
if (projectId) {
|
|
164
|
+
return db.prepare("SELECT * FROM alert_rules WHERE project_id = $p ORDER BY created_at DESC").all(sqlBindings({ $p: projectId }));
|
|
165
|
+
}
|
|
166
|
+
return db.prepare("SELECT * FROM alert_rules ORDER BY created_at DESC").all();
|
|
167
|
+
}
|
|
168
|
+
function updateAlertRule(db, id, data) {
|
|
169
|
+
const fields = Object.keys(data).map((k) => `${k} = $${k}`).join(", ");
|
|
170
|
+
if (!fields)
|
|
171
|
+
return db.prepare("SELECT * FROM alert_rules WHERE id = $id").get(sqlBindings({ $id: id }));
|
|
172
|
+
const params = Object.fromEntries(Object.entries(data).map(([k, v]) => [`$${k}`, v]));
|
|
173
|
+
params.$id = id;
|
|
174
|
+
return db.prepare(`UPDATE alert_rules SET ${fields} WHERE id = $id RETURNING *`).get(sqlBindings(params));
|
|
175
|
+
}
|
|
176
|
+
function deleteAlertRule(db, id) {
|
|
177
|
+
db.prepare("DELETE FROM alert_rules WHERE id = $id").run(sqlBindings({ $id: id }));
|
|
178
|
+
}
|
|
179
|
+
async function evaluateAlerts(db, projectId, service, level) {
|
|
180
|
+
const rules = db.prepare(`
|
|
181
|
+
SELECT * FROM alert_rules
|
|
182
|
+
WHERE project_id = $p AND level = $level AND enabled = 1
|
|
183
|
+
AND ($service IS NULL OR service IS NULL OR service = $service)
|
|
184
|
+
`).all(sqlBindings({ $p: projectId, $level: level, $service: service }));
|
|
185
|
+
for (const rule of rules) {
|
|
186
|
+
const since = new Date(Date.now() - rule.window_seconds * 1000).toISOString();
|
|
187
|
+
const conditions = [
|
|
188
|
+
"project_id = $p",
|
|
189
|
+
"level = $level",
|
|
190
|
+
"timestamp >= $since"
|
|
191
|
+
];
|
|
192
|
+
const params = {
|
|
193
|
+
$p: projectId,
|
|
194
|
+
$level: rule.level,
|
|
195
|
+
$since: since
|
|
196
|
+
};
|
|
197
|
+
if (rule.service) {
|
|
198
|
+
conditions.push("service = $service");
|
|
199
|
+
params.$service = rule.service;
|
|
200
|
+
}
|
|
201
|
+
const { count } = db.prepare(`SELECT COUNT(*) as count FROM logs WHERE ${conditions.join(" AND ")}`).get(sqlBindings(params));
|
|
202
|
+
if (count >= rule.threshold_count) {
|
|
203
|
+
await fireAlert(db, rule, count);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function fireAlert(db, rule, count) {
|
|
208
|
+
if (rule.last_fired_at) {
|
|
209
|
+
const lastFired = new Date(rule.last_fired_at).getTime();
|
|
210
|
+
if (Date.now() - lastFired < rule.window_seconds * 1000)
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
db.prepare("UPDATE alert_rules SET last_fired_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = $id").run(sqlBindings({ $id: rule.id }));
|
|
214
|
+
const payload = {
|
|
215
|
+
alert: rule.name,
|
|
216
|
+
project_id: rule.project_id,
|
|
217
|
+
level: rule.level,
|
|
218
|
+
service: rule.service,
|
|
219
|
+
count,
|
|
220
|
+
threshold: rule.threshold_count,
|
|
221
|
+
window_seconds: rule.window_seconds,
|
|
222
|
+
fired_at: new Date().toISOString()
|
|
223
|
+
};
|
|
224
|
+
if (rule.action === "webhook" && rule.webhook_url) {
|
|
225
|
+
try {
|
|
226
|
+
await fetch(rule.webhook_url, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify(payload)
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(`Alert webhook failed for rule ${rule.id}:`, err);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
console.warn(`[ALERT] ${rule.name}:`, JSON.stringify(payload));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/lib/ingest.ts
|
|
240
|
+
import { randomBytes } from "crypto";
|
|
241
|
+
|
|
242
|
+
// src/lib/event-bus.ts
|
|
243
|
+
class LogEventBus {
|
|
244
|
+
bufferSize;
|
|
245
|
+
buffer = [];
|
|
246
|
+
subscribers = new Set;
|
|
247
|
+
sequence = 0;
|
|
248
|
+
constructor(bufferSize = readPositiveInt("HASNA_LOGS_STREAM_BUFFER_SIZE", 1000)) {
|
|
249
|
+
this.bufferSize = bufferSize;
|
|
250
|
+
}
|
|
251
|
+
publish(row) {
|
|
252
|
+
const event = {
|
|
253
|
+
kind: "log",
|
|
254
|
+
sequence: ++this.sequence,
|
|
255
|
+
id: row.id,
|
|
256
|
+
row,
|
|
257
|
+
created_at: new Date().toISOString()
|
|
258
|
+
};
|
|
259
|
+
this.buffer.push(event);
|
|
260
|
+
while (this.buffer.length > this.bufferSize)
|
|
261
|
+
this.buffer.shift();
|
|
262
|
+
for (const subscriber of this.subscribers) {
|
|
263
|
+
if (matchesLogFilter(row, subscriber.filter))
|
|
264
|
+
this.enqueue(subscriber, event);
|
|
265
|
+
}
|
|
266
|
+
return event;
|
|
267
|
+
}
|
|
268
|
+
subscribe(filter = {}, opts = {}) {
|
|
269
|
+
const subscriber = {
|
|
270
|
+
filter,
|
|
271
|
+
maxQueue: opts.maxQueue ?? readPositiveInt("HASNA_LOGS_STREAM_SUBSCRIBER_QUEUE", 500),
|
|
272
|
+
queue: [],
|
|
273
|
+
closed: false
|
|
274
|
+
};
|
|
275
|
+
this.subscribers.add(subscriber);
|
|
276
|
+
return {
|
|
277
|
+
[Symbol.asyncIterator]() {
|
|
278
|
+
return this;
|
|
279
|
+
},
|
|
280
|
+
next: () => this.next(subscriber),
|
|
281
|
+
return: async () => {
|
|
282
|
+
this.close(subscriber);
|
|
283
|
+
return { done: true, value: undefined };
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
hasBufferedLog(id) {
|
|
288
|
+
return this.buffer.some((event) => event.kind === "log" && event.id === id);
|
|
289
|
+
}
|
|
290
|
+
clearForTests() {
|
|
291
|
+
for (const subscriber of this.subscribers)
|
|
292
|
+
this.close(subscriber);
|
|
293
|
+
this.buffer = [];
|
|
294
|
+
this.sequence = 0;
|
|
295
|
+
}
|
|
296
|
+
enqueue(subscriber, event) {
|
|
297
|
+
if (subscriber.closed)
|
|
298
|
+
return;
|
|
299
|
+
if (subscriber.queue.length >= subscriber.maxQueue) {
|
|
300
|
+
const dropped = subscriber.queue.length;
|
|
301
|
+
subscriber.queue = [
|
|
302
|
+
{
|
|
303
|
+
kind: "overflow",
|
|
304
|
+
sequence: ++this.sequence,
|
|
305
|
+
dropped,
|
|
306
|
+
reason: "subscriber_queue_overflow",
|
|
307
|
+
created_at: new Date().toISOString()
|
|
308
|
+
}
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
subscriber.queue.push(event);
|
|
312
|
+
if (subscriber.resolve) {
|
|
313
|
+
const resolve = subscriber.resolve;
|
|
314
|
+
subscriber.resolve = undefined;
|
|
315
|
+
const next = subscriber.queue.shift();
|
|
316
|
+
if (next)
|
|
317
|
+
resolve({ done: false, value: next });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
next(subscriber) {
|
|
321
|
+
if (subscriber.closed)
|
|
322
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
323
|
+
const next = subscriber.queue.shift();
|
|
324
|
+
if (next)
|
|
325
|
+
return Promise.resolve({ done: false, value: next });
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
subscriber.resolve = resolve;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
close(subscriber) {
|
|
331
|
+
if (subscriber.closed)
|
|
332
|
+
return;
|
|
333
|
+
subscriber.closed = true;
|
|
334
|
+
this.subscribers.delete(subscriber);
|
|
335
|
+
if (subscriber.resolve) {
|
|
336
|
+
const resolve = subscriber.resolve;
|
|
337
|
+
subscriber.resolve = undefined;
|
|
338
|
+
resolve({ done: true, value: undefined });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class EventCatalogBus {
|
|
344
|
+
bufferSize;
|
|
345
|
+
buffer = [];
|
|
346
|
+
subscribers = new Set;
|
|
347
|
+
sequence = 0;
|
|
348
|
+
constructor(bufferSize = readPositiveInt("HASNA_LOGS_STREAM_BUFFER_SIZE", 1000)) {
|
|
349
|
+
this.bufferSize = bufferSize;
|
|
350
|
+
}
|
|
351
|
+
publish(entry) {
|
|
352
|
+
const event = {
|
|
353
|
+
kind: "event",
|
|
354
|
+
sequence: ++this.sequence,
|
|
355
|
+
id: entry.event_id,
|
|
356
|
+
entry,
|
|
357
|
+
created_at: new Date().toISOString()
|
|
358
|
+
};
|
|
359
|
+
this.buffer.push(event);
|
|
360
|
+
while (this.buffer.length > this.bufferSize)
|
|
361
|
+
this.buffer.shift();
|
|
362
|
+
for (const subscriber of this.subscribers) {
|
|
363
|
+
if (matchesEventCatalogFilter(entry, subscriber.filter))
|
|
364
|
+
this.enqueue(subscriber, event);
|
|
365
|
+
}
|
|
366
|
+
return event;
|
|
367
|
+
}
|
|
368
|
+
subscribe(filter = {}, opts = {}) {
|
|
369
|
+
const subscriber = {
|
|
370
|
+
filter,
|
|
371
|
+
maxQueue: opts.maxQueue ?? readPositiveInt("HASNA_LOGS_STREAM_SUBSCRIBER_QUEUE", 500),
|
|
372
|
+
queue: [],
|
|
373
|
+
closed: false
|
|
374
|
+
};
|
|
375
|
+
this.subscribers.add(subscriber);
|
|
376
|
+
return {
|
|
377
|
+
[Symbol.asyncIterator]() {
|
|
378
|
+
return this;
|
|
379
|
+
},
|
|
380
|
+
next: () => this.next(subscriber),
|
|
381
|
+
return: async () => {
|
|
382
|
+
this.close(subscriber);
|
|
383
|
+
return { done: true, value: undefined };
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
hasBufferedEvent(id) {
|
|
388
|
+
return this.buffer.some((event) => event.kind === "event" && event.id === id);
|
|
389
|
+
}
|
|
390
|
+
clearForTests() {
|
|
391
|
+
for (const subscriber of this.subscribers)
|
|
392
|
+
this.close(subscriber);
|
|
393
|
+
this.buffer = [];
|
|
394
|
+
this.sequence = 0;
|
|
395
|
+
}
|
|
396
|
+
enqueue(subscriber, event) {
|
|
397
|
+
if (subscriber.closed)
|
|
398
|
+
return;
|
|
399
|
+
if (subscriber.queue.length >= subscriber.maxQueue) {
|
|
400
|
+
const dropped = subscriber.queue.length;
|
|
401
|
+
subscriber.queue = [
|
|
402
|
+
{
|
|
403
|
+
kind: "overflow",
|
|
404
|
+
sequence: ++this.sequence,
|
|
405
|
+
dropped,
|
|
406
|
+
reason: "subscriber_queue_overflow",
|
|
407
|
+
created_at: new Date().toISOString()
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
subscriber.queue.push(event);
|
|
412
|
+
if (subscriber.resolve) {
|
|
413
|
+
const resolve = subscriber.resolve;
|
|
414
|
+
subscriber.resolve = undefined;
|
|
415
|
+
const next = subscriber.queue.shift();
|
|
416
|
+
if (next)
|
|
417
|
+
resolve({ done: false, value: next });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
next(subscriber) {
|
|
421
|
+
if (subscriber.closed)
|
|
422
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
423
|
+
const next = subscriber.queue.shift();
|
|
424
|
+
if (next)
|
|
425
|
+
return Promise.resolve({ done: false, value: next });
|
|
426
|
+
return new Promise((resolve) => {
|
|
427
|
+
subscriber.resolve = resolve;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
close(subscriber) {
|
|
431
|
+
if (subscriber.closed)
|
|
432
|
+
return;
|
|
433
|
+
subscriber.closed = true;
|
|
434
|
+
this.subscribers.delete(subscriber);
|
|
435
|
+
if (subscriber.resolve) {
|
|
436
|
+
const resolve = subscriber.resolve;
|
|
437
|
+
subscriber.resolve = undefined;
|
|
438
|
+
resolve({ done: true, value: undefined });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
var logEventBus = new LogEventBus;
|
|
443
|
+
var eventCatalogBus = new EventCatalogBus;
|
|
444
|
+
function publishLogEvent(row) {
|
|
445
|
+
return logEventBus.publish(row);
|
|
446
|
+
}
|
|
447
|
+
function publishEventCatalogEvent(entry) {
|
|
448
|
+
return eventCatalogBus.publish(entry);
|
|
449
|
+
}
|
|
450
|
+
function subscribeLogEvents(filter, opts) {
|
|
451
|
+
return logEventBus.subscribe(filter, opts);
|
|
452
|
+
}
|
|
453
|
+
function subscribeEventCatalogEvents(filter, opts) {
|
|
454
|
+
return eventCatalogBus.subscribe(filter, opts);
|
|
455
|
+
}
|
|
456
|
+
function hasBufferedLogEvent(id) {
|
|
457
|
+
return logEventBus.hasBufferedLog(id);
|
|
458
|
+
}
|
|
459
|
+
function hasBufferedEventCatalogEvent(id) {
|
|
460
|
+
return eventCatalogBus.hasBufferedEvent(id);
|
|
461
|
+
}
|
|
462
|
+
function matchesLogFilter(row, filter) {
|
|
463
|
+
if (filter.project_id && row.project_id !== filter.project_id)
|
|
464
|
+
return false;
|
|
465
|
+
if (filter.service && row.service !== filter.service)
|
|
466
|
+
return false;
|
|
467
|
+
if (filter.levels && filter.levels.length > 0 && !filter.levels.includes(row.level))
|
|
468
|
+
return false;
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
function matchesEventCatalogFilter(entry, filter) {
|
|
472
|
+
if (filter.event_type && filter.event_type.length > 0 && !filter.event_type.includes(entry.event_type))
|
|
473
|
+
return false;
|
|
474
|
+
if (filter.source && filter.source.length > 0 && !filter.source.includes(entry.source))
|
|
475
|
+
return false;
|
|
476
|
+
if (filter.severity && filter.severity.length > 0 && (!entry.severity || !filter.severity.includes(entry.severity)))
|
|
477
|
+
return false;
|
|
478
|
+
if (filter.project_id && entry.project_id !== filter.project_id)
|
|
479
|
+
return false;
|
|
480
|
+
if (filter.page_id && entry.page_id !== filter.page_id)
|
|
481
|
+
return false;
|
|
482
|
+
if (filter.machine_id && entry.machine_id !== filter.machine_id)
|
|
483
|
+
return false;
|
|
484
|
+
if (filter.repo_id && entry.repo_id !== filter.repo_id)
|
|
485
|
+
return false;
|
|
486
|
+
if (filter.app_id && entry.app_id !== filter.app_id)
|
|
487
|
+
return false;
|
|
488
|
+
if (filter.process_id && entry.process_id !== filter.process_id)
|
|
489
|
+
return false;
|
|
490
|
+
if (filter.run_id && entry.run_id !== filter.run_id)
|
|
491
|
+
return false;
|
|
492
|
+
if (filter.trace_id && entry.trace_id !== filter.trace_id)
|
|
493
|
+
return false;
|
|
494
|
+
if (filter.span_id && entry.span_id !== filter.span_id)
|
|
495
|
+
return false;
|
|
496
|
+
if (filter.session_id && entry.session_id !== filter.session_id)
|
|
497
|
+
return false;
|
|
498
|
+
if (filter.release_id && entry.release_id !== filter.release_id)
|
|
499
|
+
return false;
|
|
500
|
+
if (filter.environment && entry.environment !== filter.environment)
|
|
501
|
+
return false;
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
function readPositiveInt(name, fallback) {
|
|
505
|
+
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
|
506
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/lib/redaction.ts
|
|
510
|
+
var REDACTED = "[REDACTED]";
|
|
511
|
+
var SENSITIVE_KEY = /(?:authorization|cookie|set-cookie|api[_-]?key|token|secret|password|passwd|pwd|private[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?secret|client[_-]?secret)/i;
|
|
512
|
+
var SENSITIVE_FLAG = /^(?:authorization|auth|api[-_]?key|token|secret|password|passwd|pwd|private[-_]?key|access[-_]?token|refresh[-_]?token|session[-_]?secret|client[-_]?secret)$/i;
|
|
513
|
+
var SENSITIVE_FLAG_NAME = /(?:authorization|api[-_]?key|token|secret|password|passwd|pwd|private[-_]?key|access[-_]?token|refresh[-_]?token|session[-_]?secret|client[-_]?secret)/i;
|
|
514
|
+
var STRING_PATTERNS = [
|
|
515
|
+
{
|
|
516
|
+
label: "openlogs_canary",
|
|
517
|
+
pattern: /\b(?:OPENLOGS|LOGS)[_-]?SECRET[_-]?CANARY[_-]?[A-Za-z0-9._-]*/gi,
|
|
518
|
+
replacement: REDACTED
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
label: "bearer_token",
|
|
522
|
+
pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
523
|
+
replacement: `Bearer ${REDACTED}`
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
label: "github_token",
|
|
527
|
+
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g,
|
|
528
|
+
replacement: REDACTED
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
label: "github_pat",
|
|
532
|
+
pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
533
|
+
replacement: REDACTED
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
label: "openai_key",
|
|
537
|
+
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
538
|
+
replacement: REDACTED
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
label: "aws_access_key",
|
|
542
|
+
pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
543
|
+
replacement: REDACTED
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
label: "jwt",
|
|
547
|
+
pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
|
|
548
|
+
replacement: REDACTED
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
label: "email",
|
|
552
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
553
|
+
replacement: REDACTED
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
label: "secret_assignment",
|
|
557
|
+
pattern: /\b(api[_-]?key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;&}]+)/gi,
|
|
558
|
+
replacement: (_match, key) => `${key}=${REDACTED}`
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
label: "secret_flag_argument",
|
|
562
|
+
pattern: /(--[A-Za-z0-9._-]*(?:authorization|api[-_]?key|token|secret|password|passwd|pwd|private[-_]?key|access[-_]?token|refresh[-_]?token|session[-_]?secret|client[-_]?secret)[A-Za-z0-9._-]*\s+)(?:"[^"]*"|'[^']*'|[^\s,;&}]+)/gi,
|
|
563
|
+
replacement: (_match, prefix) => `${prefix}${REDACTED}`
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
label: "auth_flag_argument",
|
|
567
|
+
pattern: /(--auth\s+)(?:"[^"]*"|'[^']*'|[^\s,;&}]+)/gi,
|
|
568
|
+
replacement: (_match, prefix) => `${prefix}${REDACTED}`
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
label: "secret_query_param",
|
|
572
|
+
pattern: /([?&](?:api[_-]?key|token|secret|password|passwd|pwd|access[_-]?token|refresh[_-]?token|auth|code)=)[^&#\s]+/gi,
|
|
573
|
+
replacement: (_match, prefix) => `${prefix}${REDACTED}`
|
|
574
|
+
}
|
|
575
|
+
];
|
|
576
|
+
function redactLogEntry(entry) {
|
|
577
|
+
const reports = [];
|
|
578
|
+
const next = { ...entry };
|
|
579
|
+
if (typeof entry.message === "string") {
|
|
580
|
+
const result = redactString(entry.message, "message");
|
|
581
|
+
next.message = result.value;
|
|
582
|
+
reports.push(result.report);
|
|
583
|
+
}
|
|
584
|
+
if (typeof entry.url === "string") {
|
|
585
|
+
const result = redactString(entry.url, "url");
|
|
586
|
+
next.url = result.value;
|
|
587
|
+
reports.push(result.report);
|
|
588
|
+
}
|
|
589
|
+
if (typeof entry.stack_trace === "string") {
|
|
590
|
+
const result = redactString(entry.stack_trace, "stack_trace");
|
|
591
|
+
next.stack_trace = result.value;
|
|
592
|
+
reports.push(result.report);
|
|
593
|
+
}
|
|
594
|
+
if (entry.metadata) {
|
|
595
|
+
const result = redactValue(entry.metadata, "metadata");
|
|
596
|
+
next.metadata = result.value;
|
|
597
|
+
reports.push(result.report);
|
|
598
|
+
}
|
|
599
|
+
const report = mergeRedactionReports(...reports);
|
|
600
|
+
if (report.applied) {
|
|
601
|
+
next.metadata = {
|
|
602
|
+
...next.metadata ?? {},
|
|
603
|
+
redaction: redactionMetadata(report)
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return { value: next, report };
|
|
607
|
+
}
|
|
608
|
+
function redactString(input, path = "$") {
|
|
609
|
+
let output = input;
|
|
610
|
+
const fields = [];
|
|
611
|
+
let replacements = 0;
|
|
612
|
+
for (const { label, pattern, replacement } of STRING_PATTERNS) {
|
|
613
|
+
let matched = false;
|
|
614
|
+
output = output.replace(pattern, (...args) => {
|
|
615
|
+
matched = true;
|
|
616
|
+
replacements += 1;
|
|
617
|
+
if (typeof replacement === "function")
|
|
618
|
+
return replacement(args[0] ?? "", ...args.slice(1));
|
|
619
|
+
return replacement;
|
|
620
|
+
});
|
|
621
|
+
if (matched)
|
|
622
|
+
fields.push(`${path}:${label}`);
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
value: output,
|
|
626
|
+
report: { applied: replacements > 0, fields, replacements }
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function redactValue(input, path = "$", depth = 0) {
|
|
630
|
+
if (input === null || input === undefined) {
|
|
631
|
+
return { value: input, report: emptyReport() };
|
|
632
|
+
}
|
|
633
|
+
if (typeof input === "string") {
|
|
634
|
+
return redactString(input, path);
|
|
635
|
+
}
|
|
636
|
+
if (typeof input !== "object" || depth >= 12) {
|
|
637
|
+
return { value: input, report: emptyReport() };
|
|
638
|
+
}
|
|
639
|
+
if (Array.isArray(input)) {
|
|
640
|
+
const values2 = [];
|
|
641
|
+
const reports2 = [];
|
|
642
|
+
let previousWasSensitiveFlag = false;
|
|
643
|
+
input.forEach((item, index) => {
|
|
644
|
+
const itemPath = `${path}[${index}]`;
|
|
645
|
+
if (previousWasSensitiveFlag && typeof item === "string") {
|
|
646
|
+
values2.push(REDACTED);
|
|
647
|
+
reports2.push({ applied: true, fields: [itemPath], replacements: 1 });
|
|
648
|
+
previousWasSensitiveFlag = false;
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const result = redactValue(item, `${path}[${index}]`, depth + 1);
|
|
652
|
+
values2.push(result.value);
|
|
653
|
+
reports2.push(result.report);
|
|
654
|
+
previousWasSensitiveFlag = typeof result.value === "string" && isSensitiveFlag(result.value);
|
|
655
|
+
});
|
|
656
|
+
return { value: values2, report: mergeRedactionReports(...reports2) };
|
|
657
|
+
}
|
|
658
|
+
const values = {};
|
|
659
|
+
const reports = [];
|
|
660
|
+
for (const [key, value] of Object.entries(input)) {
|
|
661
|
+
const childPath = `${path}.${key}`;
|
|
662
|
+
if (SENSITIVE_KEY.test(key) && value !== null && value !== undefined) {
|
|
663
|
+
values[key] = REDACTED;
|
|
664
|
+
reports.push({ applied: true, fields: [childPath], replacements: 1 });
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const result = redactValue(value, childPath, depth + 1);
|
|
668
|
+
values[key] = result.value;
|
|
669
|
+
reports.push(result.report);
|
|
670
|
+
}
|
|
671
|
+
return { value: values, report: mergeRedactionReports(...reports) };
|
|
672
|
+
}
|
|
673
|
+
function mergeRedactionReports(...reports) {
|
|
674
|
+
const fields = [...new Set(reports.flatMap((report) => report.fields))];
|
|
675
|
+
const replacements = reports.reduce((sum, report) => sum + report.replacements, 0);
|
|
676
|
+
return {
|
|
677
|
+
applied: replacements > 0,
|
|
678
|
+
fields,
|
|
679
|
+
replacements
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function redactionMetadata(report) {
|
|
683
|
+
return {
|
|
684
|
+
applied: report.applied,
|
|
685
|
+
fields: report.fields,
|
|
686
|
+
replacements: report.replacements
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function emptyReport() {
|
|
690
|
+
return { applied: false, fields: [], replacements: 0 };
|
|
691
|
+
}
|
|
692
|
+
function isSensitiveFlag(value) {
|
|
693
|
+
const normalized = value.trim().replace(/^-+/, "");
|
|
694
|
+
if (!normalized || normalized.includes("="))
|
|
695
|
+
return false;
|
|
696
|
+
return SENSITIVE_FLAG.test(normalized) || SENSITIVE_FLAG_NAME.test(normalized) || SENSITIVE_KEY.test(normalized.replace(/-/g, "_"));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/lib/ingest.ts
|
|
700
|
+
var ERROR_LEVELS = new Set(["warn", "error", "fatal"]);
|
|
701
|
+
function ingestLog(db, entry) {
|
|
702
|
+
return withEventStoreLock(db, () => ingestLogLocked(db, entry));
|
|
703
|
+
}
|
|
704
|
+
function ingestLogLocked(db, entry) {
|
|
705
|
+
const eventId = entry.id ?? createEventId();
|
|
706
|
+
const existing = db.prepare("SELECT * FROM logs WHERE id = ?").get(eventId);
|
|
707
|
+
if (existing)
|
|
708
|
+
return existing;
|
|
709
|
+
const eventTime = entry.timestamp ?? new Date().toISOString();
|
|
710
|
+
const ingestTime = new Date().toISOString();
|
|
711
|
+
const source = entry.source ?? "sdk";
|
|
712
|
+
const sourceEventId = entry.source_event_id ?? entry.id ?? null;
|
|
713
|
+
const normalized = {
|
|
714
|
+
...entry,
|
|
715
|
+
id: eventId,
|
|
716
|
+
timestamp: eventTime,
|
|
717
|
+
source,
|
|
718
|
+
source_event_id: sourceEventId ?? undefined,
|
|
719
|
+
privacy: entry.privacy ?? "internal"
|
|
720
|
+
};
|
|
721
|
+
const redacted = redactLogEntry(normalized);
|
|
722
|
+
const safeEntry = redacted.value;
|
|
723
|
+
const identity = extractIdentity(safeEntry);
|
|
724
|
+
const envelope = createLogEnvelope(safeEntry, eventId, eventTime, ingestTime, identity);
|
|
725
|
+
const write = appendRawEvent(db, envelope);
|
|
726
|
+
const stmt = db.prepare(`
|
|
727
|
+
INSERT INTO logs (id, timestamp, project_id, page_id, level, source, service, message, trace_id, session_id, agent, url, stack_trace, metadata)
|
|
728
|
+
VALUES ($id, $timestamp, $project_id, $page_id, $level, $source, $service, $message, $trace_id, $session_id, $agent, $url, $stack_trace, $metadata)
|
|
729
|
+
RETURNING *
|
|
730
|
+
`);
|
|
731
|
+
const row = db.transaction(() => {
|
|
732
|
+
const inserted = stmt.get({
|
|
733
|
+
$id: eventId,
|
|
734
|
+
$timestamp: eventTime,
|
|
735
|
+
$project_id: safeEntry.project_id ?? null,
|
|
736
|
+
$page_id: safeEntry.page_id ?? null,
|
|
737
|
+
$level: safeEntry.level,
|
|
738
|
+
$source: source,
|
|
739
|
+
$service: safeEntry.service ?? null,
|
|
740
|
+
$message: safeEntry.message,
|
|
741
|
+
$trace_id: safeEntry.trace_id ?? null,
|
|
742
|
+
$session_id: safeEntry.session_id ?? null,
|
|
743
|
+
$agent: safeEntry.agent ?? null,
|
|
744
|
+
$url: safeEntry.url ?? null,
|
|
745
|
+
$stack_trace: safeEntry.stack_trace ?? null,
|
|
746
|
+
$metadata: safeEntry.metadata ? JSON.stringify(safeEntry.metadata) : null
|
|
747
|
+
});
|
|
748
|
+
indexRawEvent(db, {
|
|
749
|
+
event_id: eventId,
|
|
750
|
+
schema_version: envelope.schema_version,
|
|
751
|
+
source_event_id: sourceEventId,
|
|
752
|
+
event_type: envelope.type,
|
|
753
|
+
event_time: eventTime,
|
|
754
|
+
ingest_time: ingestTime,
|
|
755
|
+
severity: safeEntry.level,
|
|
756
|
+
source,
|
|
757
|
+
project_id: safeEntry.project_id ?? null,
|
|
758
|
+
page_id: safeEntry.page_id ?? null,
|
|
759
|
+
log_id: inserted.id,
|
|
760
|
+
machine_id: identity.machine_id,
|
|
761
|
+
repo_id: identity.repo_id,
|
|
762
|
+
app_id: identity.app_id,
|
|
763
|
+
process_id: identity.process_id,
|
|
764
|
+
run_id: identity.run_id,
|
|
765
|
+
trace_id: safeEntry.trace_id ?? null,
|
|
766
|
+
span_id: identity.span_id,
|
|
767
|
+
parent_span_id: identity.parent_span_id,
|
|
768
|
+
session_id: safeEntry.session_id ?? null,
|
|
769
|
+
release_id: identity.release_id,
|
|
770
|
+
environment: identity.environment,
|
|
771
|
+
artifact_id: identity.artifact_id,
|
|
772
|
+
privacy_tier: identity.privacy_tier,
|
|
773
|
+
message: safeEntry.message,
|
|
774
|
+
metadata: safeEntry.metadata ?? null
|
|
775
|
+
}, write);
|
|
776
|
+
return inserted;
|
|
777
|
+
})();
|
|
778
|
+
if (ERROR_LEVELS.has(safeEntry.level)) {
|
|
779
|
+
if (safeEntry.project_id) {
|
|
780
|
+
upsertIssue(db, {
|
|
781
|
+
project_id: safeEntry.project_id,
|
|
782
|
+
level: safeEntry.level,
|
|
783
|
+
service: safeEntry.service,
|
|
784
|
+
message: safeEntry.message,
|
|
785
|
+
stack_trace: safeEntry.stack_trace
|
|
786
|
+
});
|
|
787
|
+
evaluateAlerts(db, safeEntry.project_id, safeEntry.service ?? null, safeEntry.level).catch(() => {});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
publishLogEvent(row);
|
|
791
|
+
const catalogEvent = getEvent(db, eventId, false);
|
|
792
|
+
if (catalogEvent)
|
|
793
|
+
publishEventCatalogEvent(catalogEvent);
|
|
794
|
+
return row;
|
|
795
|
+
}
|
|
796
|
+
function ingestBatch(db, entries, sharedTraceId) {
|
|
797
|
+
const mappedEntries = sharedTraceId ? entries.map((e) => e.trace_id ? e : { ...e, trace_id: sharedTraceId }) : entries;
|
|
798
|
+
return mappedEntries.map((entry) => ingestLog(db, entry));
|
|
799
|
+
}
|
|
800
|
+
function createLogEnvelope(entry, eventId, eventTime, ingestTime, identity) {
|
|
801
|
+
return {
|
|
802
|
+
schema_version: 1,
|
|
803
|
+
event_id: eventId,
|
|
804
|
+
source_event_id: entry.source_event_id ?? null,
|
|
805
|
+
event_time: eventTime,
|
|
806
|
+
ingest_time: ingestTime,
|
|
807
|
+
type: "log",
|
|
808
|
+
source: entry.source ?? "sdk",
|
|
809
|
+
severity: entry.level,
|
|
810
|
+
privacy: identity.privacy_tier,
|
|
811
|
+
machine_id: identity.machine_id,
|
|
812
|
+
repo_id: identity.repo_id,
|
|
813
|
+
app_id: identity.app_id,
|
|
814
|
+
process_id: identity.process_id,
|
|
815
|
+
run_id: identity.run_id,
|
|
816
|
+
trace_id: entry.trace_id ?? null,
|
|
817
|
+
span_id: identity.span_id,
|
|
818
|
+
parent_span_id: identity.parent_span_id,
|
|
819
|
+
session_id: entry.session_id ?? null,
|
|
820
|
+
release_id: identity.release_id,
|
|
821
|
+
environment: identity.environment,
|
|
822
|
+
message: entry.message,
|
|
823
|
+
body: {
|
|
824
|
+
log: {
|
|
825
|
+
id: eventId,
|
|
826
|
+
timestamp: eventTime,
|
|
827
|
+
source_event_id: entry.source_event_id ?? null,
|
|
828
|
+
project_id: entry.project_id ?? null,
|
|
829
|
+
page_id: entry.page_id ?? null,
|
|
830
|
+
level: entry.level,
|
|
831
|
+
source: entry.source ?? "sdk",
|
|
832
|
+
service: entry.service ?? null,
|
|
833
|
+
message: entry.message,
|
|
834
|
+
privacy: identity.privacy_tier,
|
|
835
|
+
machine_id: identity.machine_id,
|
|
836
|
+
repo_id: identity.repo_id,
|
|
837
|
+
app_id: identity.app_id,
|
|
838
|
+
process_id: identity.process_id,
|
|
839
|
+
run_id: identity.run_id,
|
|
840
|
+
trace_id: entry.trace_id ?? null,
|
|
841
|
+
span_id: identity.span_id,
|
|
842
|
+
parent_span_id: identity.parent_span_id,
|
|
843
|
+
session_id: entry.session_id ?? null,
|
|
844
|
+
release_id: identity.release_id,
|
|
845
|
+
environment: identity.environment,
|
|
846
|
+
agent: entry.agent ?? null,
|
|
847
|
+
url: entry.url ?? null,
|
|
848
|
+
stack_trace: entry.stack_trace ?? null,
|
|
849
|
+
metadata: entry.metadata ?? null
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
attributes: {
|
|
853
|
+
project_id: entry.project_id ?? null,
|
|
854
|
+
page_id: entry.page_id ?? null,
|
|
855
|
+
service: entry.service ?? null,
|
|
856
|
+
trace_id: entry.trace_id ?? null,
|
|
857
|
+
session_id: entry.session_id ?? null,
|
|
858
|
+
agent: entry.agent ?? null,
|
|
859
|
+
url: entry.url ?? null,
|
|
860
|
+
...identity
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function extractIdentity(entry) {
|
|
865
|
+
const metadata = entry.metadata;
|
|
866
|
+
return {
|
|
867
|
+
machine_id: entry.machine_id ?? stringMetadata(metadata, "machine_id"),
|
|
868
|
+
repo_id: entry.repo_id ?? stringMetadata(metadata, "repo_id"),
|
|
869
|
+
app_id: entry.app_id ?? stringMetadata(metadata, "app_id"),
|
|
870
|
+
process_id: entry.process_id ?? stringMetadata(metadata, "process_id"),
|
|
871
|
+
run_id: entry.run_id ?? stringMetadata(metadata, "run_id"),
|
|
872
|
+
span_id: entry.span_id ?? stringMetadata(metadata, "span_id"),
|
|
873
|
+
parent_span_id: entry.parent_span_id ?? stringMetadata(metadata, "parent_span_id"),
|
|
874
|
+
release_id: entry.release_id ?? stringMetadata(metadata, "release_id"),
|
|
875
|
+
environment: entry.environment ?? stringMetadata(metadata, "environment"),
|
|
876
|
+
artifact_id: stringMetadata(metadata, "artifact_id"),
|
|
877
|
+
privacy_tier: entry.privacy ?? stringMetadata(metadata, "privacy_tier") ?? stringMetadata(metadata, "privacy") ?? "internal"
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function stringMetadata(metadata, key) {
|
|
881
|
+
const value = metadata?.[key];
|
|
882
|
+
if (typeof value === "string")
|
|
883
|
+
return value;
|
|
884
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
885
|
+
return String(value);
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
function createEventId() {
|
|
889
|
+
return randomBytes(16).toString("hex");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/lib/package-meta.ts
|
|
893
|
+
import { existsSync, readFileSync } from "fs";
|
|
894
|
+
var PACKAGE_JSON_CANDIDATES = [
|
|
895
|
+
"../../package.json",
|
|
896
|
+
"../package.json",
|
|
897
|
+
"./package.json"
|
|
898
|
+
];
|
|
899
|
+
function readPackageJson(baseUrl = import.meta.url) {
|
|
900
|
+
for (const relativePath of PACKAGE_JSON_CANDIDATES) {
|
|
901
|
+
const candidate = new URL(relativePath, baseUrl);
|
|
902
|
+
if (!existsSync(candidate))
|
|
903
|
+
continue;
|
|
904
|
+
return JSON.parse(readFileSync(candidate, "utf8"));
|
|
905
|
+
}
|
|
906
|
+
throw new Error(`Unable to locate package.json from ${String(baseUrl)}`);
|
|
907
|
+
}
|
|
908
|
+
function readPackageVersion(baseUrl = import.meta.url) {
|
|
909
|
+
return readPackageJson(baseUrl).version ?? "0.0.0";
|
|
910
|
+
}
|
|
911
|
+
var PACKAGE_VERSION = readPackageVersion();
|
|
912
|
+
function exitIfMetadataRequest(spec, argv = process.argv.slice(2)) {
|
|
913
|
+
if (argv.includes("--version") || argv.includes("-V")) {
|
|
914
|
+
console.log(PACKAGE_VERSION);
|
|
915
|
+
process.exit(0);
|
|
916
|
+
}
|
|
917
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
918
|
+
const options = spec.options ?? [];
|
|
919
|
+
const renderedOptions = [
|
|
920
|
+
" -V, --version output the version number",
|
|
921
|
+
" -h, --help display help for command",
|
|
922
|
+
...options
|
|
923
|
+
];
|
|
924
|
+
console.log([
|
|
925
|
+
`Usage: ${spec.name} [options]`,
|
|
926
|
+
"",
|
|
927
|
+
spec.description,
|
|
928
|
+
"",
|
|
929
|
+
"Options:",
|
|
930
|
+
...renderedOptions
|
|
931
|
+
].join(`
|
|
932
|
+
`));
|
|
933
|
+
process.exit(0);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function readOptionValue(names, argv = process.argv.slice(2)) {
|
|
937
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
938
|
+
const arg = argv[index];
|
|
939
|
+
if (!arg)
|
|
940
|
+
continue;
|
|
941
|
+
const inline = names.find((name) => arg.startsWith(`${name}=`));
|
|
942
|
+
if (inline)
|
|
943
|
+
return arg.slice(inline.length + 1);
|
|
944
|
+
if (names.includes(arg)) {
|
|
945
|
+
const next = argv[index + 1];
|
|
946
|
+
if (next && !next.startsWith("-"))
|
|
947
|
+
return next;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
function hasOption(names, argv = process.argv.slice(2)) {
|
|
953
|
+
return argv.some((arg) => names.includes(arg));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/lib/projects.ts
|
|
957
|
+
function createProject(db, data) {
|
|
958
|
+
return db.prepare(`
|
|
959
|
+
INSERT INTO projects (name, github_repo, base_url, description)
|
|
960
|
+
VALUES ($name, $github_repo, $base_url, $description)
|
|
961
|
+
RETURNING *
|
|
962
|
+
`).get(sqlBindings({
|
|
963
|
+
$name: data.name,
|
|
964
|
+
$github_repo: data.github_repo ?? null,
|
|
965
|
+
$base_url: data.base_url ?? null,
|
|
966
|
+
$description: data.description ?? null
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
969
|
+
function listProjects(db) {
|
|
970
|
+
return db.prepare("SELECT * FROM projects ORDER BY created_at DESC").all();
|
|
971
|
+
}
|
|
972
|
+
function getProject(db, id) {
|
|
973
|
+
return db.prepare("SELECT * FROM projects WHERE id = $id").get(sqlBindings({ $id: id }));
|
|
974
|
+
}
|
|
975
|
+
function updateProject(db, id, data) {
|
|
976
|
+
const fields = Object.keys(data).map((k) => `${k} = $${k}`).join(", ");
|
|
977
|
+
if (!fields)
|
|
978
|
+
return getProject(db, id);
|
|
979
|
+
const params = Object.fromEntries(Object.entries(data).map(([k, v]) => [`$${k}`, v]));
|
|
980
|
+
params.$id = id;
|
|
981
|
+
return db.prepare(`UPDATE projects SET ${fields} WHERE id = $id RETURNING *`).get(sqlBindings(params));
|
|
982
|
+
}
|
|
983
|
+
function createPage(db, data) {
|
|
984
|
+
return db.prepare(`
|
|
985
|
+
INSERT INTO pages (project_id, url, path, name)
|
|
986
|
+
VALUES ($project_id, $url, $path, $name)
|
|
987
|
+
ON CONFLICT(project_id, url) DO UPDATE SET name = excluded.name
|
|
988
|
+
RETURNING *
|
|
989
|
+
`).get(sqlBindings({
|
|
990
|
+
$project_id: data.project_id,
|
|
991
|
+
$url: data.url,
|
|
992
|
+
$path: data.path ?? new URL(data.url).pathname,
|
|
993
|
+
$name: data.name ?? null
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
function listPages(db, projectId) {
|
|
997
|
+
return db.prepare("SELECT * FROM pages WHERE project_id = $p ORDER BY created_at ASC").all(sqlBindings({ $p: projectId }));
|
|
998
|
+
}
|
|
999
|
+
function getPage(db, id) {
|
|
1000
|
+
return db.prepare("SELECT * FROM pages WHERE id = $id").get(sqlBindings({ $id: id }));
|
|
1001
|
+
}
|
|
1002
|
+
function resolveProjectId(db, idOrName) {
|
|
1003
|
+
if (!idOrName)
|
|
1004
|
+
return null;
|
|
1005
|
+
if (/^[0-9a-f]{8,}$/i.test(idOrName))
|
|
1006
|
+
return idOrName;
|
|
1007
|
+
const p = db.prepare("SELECT id FROM projects WHERE LOWER(name) = LOWER($n)").get(sqlBindings({ $n: idOrName }));
|
|
1008
|
+
return p?.id ?? null;
|
|
1009
|
+
}
|
|
1010
|
+
function touchPage(db, id) {
|
|
1011
|
+
db.prepare("UPDATE pages SET last_scanned_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = $id").run(sqlBindings({ $id: id }));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/lib/perf.ts
|
|
1015
|
+
function saveSnapshot(db, data) {
|
|
1016
|
+
return db.prepare(`
|
|
1017
|
+
INSERT INTO performance_snapshots (project_id, page_id, url, lcp, fcp, cls, tti, ttfb, score, raw_audit)
|
|
1018
|
+
VALUES ($project_id, $page_id, $url, $lcp, $fcp, $cls, $tti, $ttfb, $score, $raw_audit)
|
|
1019
|
+
RETURNING *
|
|
1020
|
+
`).get(sqlBindings({
|
|
1021
|
+
$project_id: data.project_id,
|
|
1022
|
+
$page_id: data.page_id ?? null,
|
|
1023
|
+
$url: data.url,
|
|
1024
|
+
$lcp: data.lcp ?? null,
|
|
1025
|
+
$fcp: data.fcp ?? null,
|
|
1026
|
+
$cls: data.cls ?? null,
|
|
1027
|
+
$tti: data.tti ?? null,
|
|
1028
|
+
$ttfb: data.ttfb ?? null,
|
|
1029
|
+
$score: data.score ?? null,
|
|
1030
|
+
$raw_audit: data.raw_audit ?? null
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
function getLatestSnapshot(db, projectId, pageId) {
|
|
1034
|
+
if (pageId) {
|
|
1035
|
+
return db.prepare("SELECT * FROM performance_snapshots WHERE project_id = $p AND page_id = $pg ORDER BY timestamp DESC LIMIT 1").get(sqlBindings({ $p: projectId, $pg: pageId }));
|
|
1036
|
+
}
|
|
1037
|
+
return db.prepare("SELECT * FROM performance_snapshots WHERE project_id = $p ORDER BY timestamp DESC LIMIT 1").get(sqlBindings({ $p: projectId }));
|
|
1038
|
+
}
|
|
1039
|
+
function getPerfTrend(db, projectId, pageId, since, limit = 50) {
|
|
1040
|
+
const conditions = ["project_id = $p"];
|
|
1041
|
+
const params = { $p: projectId, $limit: limit };
|
|
1042
|
+
if (pageId) {
|
|
1043
|
+
conditions.push("page_id = $pg");
|
|
1044
|
+
params.$pg = pageId;
|
|
1045
|
+
}
|
|
1046
|
+
if (since) {
|
|
1047
|
+
conditions.push("timestamp >= $since");
|
|
1048
|
+
params.$since = since;
|
|
1049
|
+
}
|
|
1050
|
+
return db.prepare(`SELECT * FROM performance_snapshots WHERE ${conditions.join(" AND ")} ORDER BY timestamp DESC LIMIT $limit`).all(sqlBindings(params));
|
|
1051
|
+
}
|
|
1052
|
+
function scoreLabel(score) {
|
|
1053
|
+
if (score === null)
|
|
1054
|
+
return "unknown";
|
|
1055
|
+
if (score >= 90)
|
|
1056
|
+
return "green";
|
|
1057
|
+
if (score >= 50)
|
|
1058
|
+
return "yellow";
|
|
1059
|
+
return "red";
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/lib/summarize.ts
|
|
1063
|
+
function summarizeLogs(db, projectId, since, until) {
|
|
1064
|
+
const conditions = ["level IN ('warn','error','fatal')"];
|
|
1065
|
+
const params = {};
|
|
1066
|
+
if (projectId) {
|
|
1067
|
+
conditions.push("project_id = $project_id");
|
|
1068
|
+
params.$project_id = projectId;
|
|
1069
|
+
}
|
|
1070
|
+
if (since) {
|
|
1071
|
+
conditions.push("timestamp >= $since");
|
|
1072
|
+
params.$since = parseTime(since) ?? since;
|
|
1073
|
+
}
|
|
1074
|
+
if (until) {
|
|
1075
|
+
conditions.push("timestamp <= $until");
|
|
1076
|
+
params.$until = parseTime(until) ?? until;
|
|
1077
|
+
}
|
|
1078
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
1079
|
+
const sql = `
|
|
1080
|
+
SELECT project_id, service, page_id, level,
|
|
1081
|
+
COUNT(*) as count,
|
|
1082
|
+
MAX(timestamp) as latest
|
|
1083
|
+
FROM logs ${where}
|
|
1084
|
+
GROUP BY project_id, service, page_id, level
|
|
1085
|
+
ORDER BY count DESC
|
|
1086
|
+
`;
|
|
1087
|
+
return db.prepare(sql).all(sqlBindings(params));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/lib/test-reports.ts
|
|
1091
|
+
function searchTestReports(db, query = {}) {
|
|
1092
|
+
const { where, params } = buildReportWhere(query);
|
|
1093
|
+
const limit = clampPositiveInt2(query.limit, 100, query.max_limit ?? 1000);
|
|
1094
|
+
const offset = clampNonNegativeInt2(query.offset, 0);
|
|
1095
|
+
const rows = db.query(`
|
|
1096
|
+
SELECT *
|
|
1097
|
+
FROM test_reports
|
|
1098
|
+
${where}
|
|
1099
|
+
ORDER BY event_time DESC, id DESC
|
|
1100
|
+
LIMIT ? OFFSET ?
|
|
1101
|
+
`).all(...params, limit, offset);
|
|
1102
|
+
const reports = rows.map(materializeReport);
|
|
1103
|
+
if (query.include_cases === true) {
|
|
1104
|
+
const casesByReport = loadCasesForReports(db, reports.map((report) => report.id));
|
|
1105
|
+
for (const report of reports)
|
|
1106
|
+
report.cases = casesByReport.get(report.id) ?? [];
|
|
1107
|
+
}
|
|
1108
|
+
return reports;
|
|
1109
|
+
}
|
|
1110
|
+
function getTestReport(db, reportId, includeCases = true) {
|
|
1111
|
+
const row = db.query("SELECT * FROM test_reports WHERE id = ?").get(reportId);
|
|
1112
|
+
if (!row)
|
|
1113
|
+
return null;
|
|
1114
|
+
const report = materializeReport(row);
|
|
1115
|
+
if (includeCases) {
|
|
1116
|
+
report.cases = loadCasesForReport(db, report.id);
|
|
1117
|
+
}
|
|
1118
|
+
return report;
|
|
1119
|
+
}
|
|
1120
|
+
function buildReportWhere(query) {
|
|
1121
|
+
const conditions = [];
|
|
1122
|
+
const params = [];
|
|
1123
|
+
addScalar2(conditions, params, "id", query.report_id);
|
|
1124
|
+
addScalar2(conditions, params, "event_id", query.event_id);
|
|
1125
|
+
addScalar2(conditions, params, "project_id", query.project_id ?? undefined);
|
|
1126
|
+
addScalar2(conditions, params, "machine_id", query.machine_id);
|
|
1127
|
+
addScalar2(conditions, params, "repo_id", query.repo_id);
|
|
1128
|
+
addScalar2(conditions, params, "app_id", query.app_id);
|
|
1129
|
+
addScalar2(conditions, params, "process_id", query.process_id);
|
|
1130
|
+
addScalar2(conditions, params, "run_id", query.run_id);
|
|
1131
|
+
addScalar2(conditions, params, "environment", query.environment);
|
|
1132
|
+
addScalar2(conditions, params, "source", query.source);
|
|
1133
|
+
addScalar2(conditions, params, "parser", query.parser);
|
|
1134
|
+
addScalar2(conditions, params, "parse_status", query.parse_status);
|
|
1135
|
+
addScalar2(conditions, params, "path", query.path);
|
|
1136
|
+
if (query.case_status) {
|
|
1137
|
+
conditions.push("EXISTS (SELECT 1 FROM test_cases WHERE test_cases.report_id = test_reports.id AND test_cases.status = ?)");
|
|
1138
|
+
params.push(query.case_status);
|
|
1139
|
+
}
|
|
1140
|
+
addOutcomeFilter(conditions, query.outcome);
|
|
1141
|
+
addMinimum(conditions, params, "failures", query.min_failures);
|
|
1142
|
+
addMinimum(conditions, params, "errors", query.min_errors);
|
|
1143
|
+
addMinimum(conditions, params, "skipped", query.min_skipped);
|
|
1144
|
+
if (query.since) {
|
|
1145
|
+
conditions.push("event_time >= ?");
|
|
1146
|
+
params.push(parseTime(query.since) ?? query.since);
|
|
1147
|
+
}
|
|
1148
|
+
if (query.until) {
|
|
1149
|
+
conditions.push("event_time <= ?");
|
|
1150
|
+
params.push(parseTime(query.until) ?? query.until);
|
|
1151
|
+
}
|
|
1152
|
+
if (query.text) {
|
|
1153
|
+
const needle = `%${escapeLike2(query.text)}%`;
|
|
1154
|
+
conditions.push(`(
|
|
1155
|
+
id LIKE ? ESCAPE '\\'
|
|
1156
|
+
OR event_id LIKE ? ESCAPE '\\'
|
|
1157
|
+
OR source_event_id LIKE ? ESCAPE '\\'
|
|
1158
|
+
OR path LIKE ? ESCAPE '\\'
|
|
1159
|
+
OR parser LIKE ? ESCAPE '\\'
|
|
1160
|
+
OR parse_status LIKE ? ESCAPE '\\'
|
|
1161
|
+
OR metadata LIKE ? ESCAPE '\\'
|
|
1162
|
+
OR EXISTS (
|
|
1163
|
+
SELECT 1 FROM test_cases
|
|
1164
|
+
WHERE test_cases.report_id = test_reports.id
|
|
1165
|
+
AND (
|
|
1166
|
+
test_cases.name LIKE ? ESCAPE '\\'
|
|
1167
|
+
OR test_cases.classname LIKE ? ESCAPE '\\'
|
|
1168
|
+
OR test_cases.file LIKE ? ESCAPE '\\'
|
|
1169
|
+
OR test_cases.status LIKE ? ESCAPE '\\'
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
)`);
|
|
1173
|
+
params.push(needle, needle, needle, needle, needle, needle, needle, needle, needle, needle, needle);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
1177
|
+
params
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function addOutcomeFilter(conditions, outcome) {
|
|
1181
|
+
if (!outcome)
|
|
1182
|
+
return;
|
|
1183
|
+
if (outcome === "failed") {
|
|
1184
|
+
conditions.push("COALESCE(failures, 0) > 0");
|
|
1185
|
+
} else if (outcome === "error") {
|
|
1186
|
+
conditions.push("COALESCE(errors, 0) > 0");
|
|
1187
|
+
} else if (outcome === "nonpassing") {
|
|
1188
|
+
conditions.push("(COALESCE(failures, 0) > 0 OR COALESCE(errors, 0) > 0)");
|
|
1189
|
+
} else if (outcome === "skipped") {
|
|
1190
|
+
conditions.push("COALESCE(skipped, 0) > 0");
|
|
1191
|
+
} else if (outcome === "passed") {
|
|
1192
|
+
conditions.push("parse_status = 'parsed' AND COALESCE(failures, 0) = 0 AND COALESCE(errors, 0) = 0");
|
|
1193
|
+
} else if (outcome === "parse_problem") {
|
|
1194
|
+
conditions.push("(parse_status IS NULL OR parse_status != 'parsed')");
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function loadCasesForReports(db, reportIds) {
|
|
1198
|
+
const result = new Map;
|
|
1199
|
+
for (const reportId of reportIds)
|
|
1200
|
+
result.set(reportId, []);
|
|
1201
|
+
if (reportIds.length === 0)
|
|
1202
|
+
return result;
|
|
1203
|
+
const placeholders = reportIds.map(() => "?").join(",");
|
|
1204
|
+
const rows = db.query(`
|
|
1205
|
+
SELECT *
|
|
1206
|
+
FROM test_cases
|
|
1207
|
+
WHERE report_id IN (${placeholders})
|
|
1208
|
+
ORDER BY report_id ASC, suite_index ASC, case_index ASC, id ASC
|
|
1209
|
+
`).all(...reportIds);
|
|
1210
|
+
for (const row of rows) {
|
|
1211
|
+
const cases = result.get(row.report_id);
|
|
1212
|
+
if (cases)
|
|
1213
|
+
cases.push(materializeCase(row));
|
|
1214
|
+
}
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
function loadCasesForReport(db, reportId) {
|
|
1218
|
+
return db.query(`
|
|
1219
|
+
SELECT *
|
|
1220
|
+
FROM test_cases
|
|
1221
|
+
WHERE report_id = ?
|
|
1222
|
+
ORDER BY suite_index ASC, case_index ASC, id ASC
|
|
1223
|
+
`).all(reportId).map(materializeCase);
|
|
1224
|
+
}
|
|
1225
|
+
function materializeReport(row) {
|
|
1226
|
+
return {
|
|
1227
|
+
...row,
|
|
1228
|
+
truncated: Boolean(row.truncated),
|
|
1229
|
+
metadata: parseMetadata2(row.metadata)
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function materializeCase(row) {
|
|
1233
|
+
return {
|
|
1234
|
+
...row,
|
|
1235
|
+
metadata: parseMetadata2(row.metadata)
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function addScalar2(conditions, params, column, value) {
|
|
1239
|
+
if (!value)
|
|
1240
|
+
return;
|
|
1241
|
+
conditions.push(`${column} = ?`);
|
|
1242
|
+
params.push(value);
|
|
1243
|
+
}
|
|
1244
|
+
function addMinimum(conditions, params, column, value) {
|
|
1245
|
+
if (!Number.isFinite(value) || value === undefined)
|
|
1246
|
+
return;
|
|
1247
|
+
conditions.push(`COALESCE(${column}, 0) >= ?`);
|
|
1248
|
+
params.push(Math.max(0, Math.floor(value)));
|
|
1249
|
+
}
|
|
1250
|
+
function parseMetadata2(value) {
|
|
1251
|
+
if (!value)
|
|
1252
|
+
return null;
|
|
1253
|
+
try {
|
|
1254
|
+
const parsed = JSON.parse(value);
|
|
1255
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function escapeLike2(value) {
|
|
1261
|
+
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1262
|
+
}
|
|
1263
|
+
function clampPositiveInt2(value, fallback, max) {
|
|
1264
|
+
if (!Number.isFinite(value) || value === undefined)
|
|
1265
|
+
return fallback;
|
|
1266
|
+
return Math.min(Math.max(1, Math.floor(value)), max);
|
|
1267
|
+
}
|
|
1268
|
+
function clampNonNegativeInt2(value, fallback) {
|
|
1269
|
+
if (!Number.isFinite(value) || value === undefined)
|
|
1270
|
+
return fallback;
|
|
1271
|
+
return Math.max(0, Math.floor(value));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/lib/universal-ingest.ts
|
|
1275
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
1276
|
+
var UNIVERSAL_EVENT_TYPES = [
|
|
1277
|
+
"log",
|
|
1278
|
+
"exception",
|
|
1279
|
+
"span",
|
|
1280
|
+
"metric",
|
|
1281
|
+
"profile",
|
|
1282
|
+
"replay",
|
|
1283
|
+
"monitor",
|
|
1284
|
+
"release",
|
|
1285
|
+
"build",
|
|
1286
|
+
"process",
|
|
1287
|
+
"agent",
|
|
1288
|
+
"artifact",
|
|
1289
|
+
"network",
|
|
1290
|
+
"filesystem",
|
|
1291
|
+
"session"
|
|
1292
|
+
];
|
|
1293
|
+
var SEVERITIES = new Set(["debug", "info", "warn", "error", "fatal"]);
|
|
1294
|
+
var PRIVACY_TIERS = new Set([
|
|
1295
|
+
"public",
|
|
1296
|
+
"internal",
|
|
1297
|
+
"sensitive",
|
|
1298
|
+
"secret",
|
|
1299
|
+
"pii"
|
|
1300
|
+
]);
|
|
1301
|
+
var REDACTABLE_TOP_LEVEL_FIELDS = [
|
|
1302
|
+
"source_event_id",
|
|
1303
|
+
"source",
|
|
1304
|
+
"machine_id",
|
|
1305
|
+
"repo_id",
|
|
1306
|
+
"app_id",
|
|
1307
|
+
"process_id",
|
|
1308
|
+
"run_id",
|
|
1309
|
+
"trace_id",
|
|
1310
|
+
"span_id",
|
|
1311
|
+
"parent_span_id",
|
|
1312
|
+
"session_id",
|
|
1313
|
+
"release_id",
|
|
1314
|
+
"environment"
|
|
1315
|
+
];
|
|
1316
|
+
var UNIVERSAL_EVENT_KEYS = new Set([
|
|
1317
|
+
"schema_version",
|
|
1318
|
+
"event_id",
|
|
1319
|
+
"id",
|
|
1320
|
+
"source_event_id",
|
|
1321
|
+
"event_time",
|
|
1322
|
+
"timestamp",
|
|
1323
|
+
"type",
|
|
1324
|
+
"source",
|
|
1325
|
+
"severity",
|
|
1326
|
+
"level",
|
|
1327
|
+
"privacy",
|
|
1328
|
+
"project_id",
|
|
1329
|
+
"page_id",
|
|
1330
|
+
"machine_id",
|
|
1331
|
+
"repo_id",
|
|
1332
|
+
"app_id",
|
|
1333
|
+
"process_id",
|
|
1334
|
+
"run_id",
|
|
1335
|
+
"trace_id",
|
|
1336
|
+
"span_id",
|
|
1337
|
+
"parent_span_id",
|
|
1338
|
+
"session_id",
|
|
1339
|
+
"release_id",
|
|
1340
|
+
"environment",
|
|
1341
|
+
"artifact_id",
|
|
1342
|
+
"message",
|
|
1343
|
+
"body",
|
|
1344
|
+
"attributes",
|
|
1345
|
+
"metadata"
|
|
1346
|
+
]);
|
|
1347
|
+
function validateUniversalEventInput(value, path = "event") {
|
|
1348
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1349
|
+
throw new Error(`${path} must be an object`);
|
|
1350
|
+
}
|
|
1351
|
+
const input = value;
|
|
1352
|
+
for (const key of Object.keys(input)) {
|
|
1353
|
+
if (!UNIVERSAL_EVENT_KEYS.has(key)) {
|
|
1354
|
+
throw new Error(`${path}.${key} is not a supported event field`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (typeof input.type !== "string" || !UNIVERSAL_EVENT_TYPES.includes(input.type)) {
|
|
1358
|
+
throw new Error(`${path}.type must be one of ${UNIVERSAL_EVENT_TYPES.join(", ")}`);
|
|
1359
|
+
}
|
|
1360
|
+
if (input.schema_version !== undefined && (!Number.isInteger(input.schema_version) || Number(input.schema_version) < 1)) {
|
|
1361
|
+
throw new Error(`${path}.schema_version must be a positive integer`);
|
|
1362
|
+
}
|
|
1363
|
+
const severity = input.severity ?? input.level;
|
|
1364
|
+
if (severity !== undefined && severity !== null && !SEVERITIES.has(String(severity))) {
|
|
1365
|
+
throw new Error(`${path}.severity must be one of debug, info, warn, error, fatal`);
|
|
1366
|
+
}
|
|
1367
|
+
if (input.privacy !== undefined && input.privacy !== null && !PRIVACY_TIERS.has(String(input.privacy))) {
|
|
1368
|
+
throw new Error(`${path}.privacy must be one of public, internal, sensitive, secret, pii`);
|
|
1369
|
+
}
|
|
1370
|
+
for (const key of ["event_time", "timestamp"]) {
|
|
1371
|
+
const item = input[key];
|
|
1372
|
+
if (typeof item === "string" && item.length > 0 && Number.isNaN(new Date(item).getTime())) {
|
|
1373
|
+
throw new Error(`${path}.${key} must be an ISO timestamp`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
for (const key of [
|
|
1377
|
+
"event_id",
|
|
1378
|
+
"id",
|
|
1379
|
+
"source_event_id",
|
|
1380
|
+
"event_time",
|
|
1381
|
+
"timestamp",
|
|
1382
|
+
"source",
|
|
1383
|
+
"severity",
|
|
1384
|
+
"level",
|
|
1385
|
+
"privacy",
|
|
1386
|
+
"project_id",
|
|
1387
|
+
"page_id",
|
|
1388
|
+
"machine_id",
|
|
1389
|
+
"repo_id",
|
|
1390
|
+
"app_id",
|
|
1391
|
+
"process_id",
|
|
1392
|
+
"run_id",
|
|
1393
|
+
"trace_id",
|
|
1394
|
+
"span_id",
|
|
1395
|
+
"parent_span_id",
|
|
1396
|
+
"session_id",
|
|
1397
|
+
"release_id",
|
|
1398
|
+
"environment",
|
|
1399
|
+
"artifact_id",
|
|
1400
|
+
"message"
|
|
1401
|
+
]) {
|
|
1402
|
+
const item = input[key];
|
|
1403
|
+
if (item !== undefined && item !== null && typeof item !== "string") {
|
|
1404
|
+
throw new Error(`${path}.${key} must be a string`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
for (const key of ["body", "attributes", "metadata"]) {
|
|
1408
|
+
const item = input[key];
|
|
1409
|
+
if (item !== undefined && (!item || typeof item !== "object" || Array.isArray(item))) {
|
|
1410
|
+
throw new Error(`${path}.${key} must be an object`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return input;
|
|
1414
|
+
}
|
|
1415
|
+
function ingestUniversalEvent(db, input) {
|
|
1416
|
+
const envelope = normalizeUniversalEvent(validateUniversalEventInput(input));
|
|
1417
|
+
return withEventStoreLock(db, () => {
|
|
1418
|
+
const existing = getEventRecord(db, envelope.event_id);
|
|
1419
|
+
if (existing) {
|
|
1420
|
+
const event2 = getEvent(db, existing.event_id, false);
|
|
1421
|
+
if (!event2)
|
|
1422
|
+
throw new Error(`Event index exists but cannot be read: ${existing.event_id}`);
|
|
1423
|
+
return { inserted: false, event: event2 };
|
|
1424
|
+
}
|
|
1425
|
+
const redacted = redactEnvelope(envelope);
|
|
1426
|
+
const safeEnvelope = redacted.envelope;
|
|
1427
|
+
const write = appendRawEvent(db, safeEnvelope);
|
|
1428
|
+
db.transaction(() => {
|
|
1429
|
+
const index = indexFromEnvelope(db, safeEnvelope, redacted.metadata);
|
|
1430
|
+
indexRawEvent(db, index, write);
|
|
1431
|
+
applyCompatibilityProjections(db, safeEnvelope, index);
|
|
1432
|
+
})();
|
|
1433
|
+
const event = getEvent(db, safeEnvelope.event_id, false);
|
|
1434
|
+
if (!event)
|
|
1435
|
+
throw new Error(`Event was written but cannot be read: ${safeEnvelope.event_id}`);
|
|
1436
|
+
publishEventCatalogEvent(event);
|
|
1437
|
+
return { inserted: true, event };
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
function normalizeUniversalEvent(input) {
|
|
1441
|
+
const now = new Date().toISOString();
|
|
1442
|
+
const source = sanitizeOptionalString(input.source) ?? "sdk";
|
|
1443
|
+
const sourceEventId = sanitizeOptionalString(input.source_event_id);
|
|
1444
|
+
const eventId = sanitizeOptionalString(input.event_id ?? input.id) ?? deterministicSourceEventId(source, sourceEventId) ?? `evt_${randomBytes2(16).toString("hex")}`;
|
|
1445
|
+
const eventTime = normalizeIsoTime(input.event_time ?? input.timestamp, "event_time");
|
|
1446
|
+
const severity = sanitizeOptionalString(input.severity ?? input.level);
|
|
1447
|
+
const privacy = sanitizeOptionalString(input.privacy) ?? "internal";
|
|
1448
|
+
if (!UNIVERSAL_EVENT_TYPES.includes(input.type)) {
|
|
1449
|
+
throw new Error(`type must be one of ${UNIVERSAL_EVENT_TYPES.join(", ")}`);
|
|
1450
|
+
}
|
|
1451
|
+
if (severity && !SEVERITIES.has(severity)) {
|
|
1452
|
+
throw new Error("severity must be one of debug, info, warn, error, fatal");
|
|
1453
|
+
}
|
|
1454
|
+
if (!PRIVACY_TIERS.has(privacy)) {
|
|
1455
|
+
throw new Error("privacy must be one of public, internal, sensitive, secret, pii");
|
|
1456
|
+
}
|
|
1457
|
+
const attributes = compactObject({
|
|
1458
|
+
...input.metadata ?? {},
|
|
1459
|
+
...input.attributes ?? {},
|
|
1460
|
+
project_id: input.project_id ?? input.attributes?.project_id ?? input.metadata?.project_id,
|
|
1461
|
+
page_id: input.page_id ?? input.attributes?.page_id ?? input.metadata?.page_id,
|
|
1462
|
+
artifact_id: input.artifact_id ?? input.attributes?.artifact_id ?? input.metadata?.artifact_id
|
|
1463
|
+
});
|
|
1464
|
+
return {
|
|
1465
|
+
schema_version: input.schema_version ?? 1,
|
|
1466
|
+
event_id: eventId,
|
|
1467
|
+
source_event_id: sourceEventId ?? null,
|
|
1468
|
+
event_time: eventTime ?? now,
|
|
1469
|
+
ingest_time: now,
|
|
1470
|
+
type: input.type,
|
|
1471
|
+
source,
|
|
1472
|
+
severity: severity ?? null,
|
|
1473
|
+
privacy,
|
|
1474
|
+
machine_id: sanitizeNullableString(input.machine_id),
|
|
1475
|
+
repo_id: sanitizeNullableString(input.repo_id),
|
|
1476
|
+
app_id: sanitizeNullableString(input.app_id),
|
|
1477
|
+
process_id: sanitizeNullableString(input.process_id),
|
|
1478
|
+
run_id: sanitizeNullableString(input.run_id),
|
|
1479
|
+
trace_id: sanitizeNullableString(input.trace_id),
|
|
1480
|
+
span_id: sanitizeNullableString(input.span_id),
|
|
1481
|
+
parent_span_id: sanitizeNullableString(input.parent_span_id),
|
|
1482
|
+
session_id: sanitizeNullableString(input.session_id),
|
|
1483
|
+
release_id: sanitizeNullableString(input.release_id),
|
|
1484
|
+
environment: sanitizeNullableString(input.environment),
|
|
1485
|
+
message: sanitizeNullableString(input.message),
|
|
1486
|
+
body: input.body ?? {},
|
|
1487
|
+
attributes
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function redactEnvelope(envelope) {
|
|
1491
|
+
const topLevelResults = REDACTABLE_TOP_LEVEL_FIELDS.map((field) => [field, redactNullableString(envelope[field], field)]);
|
|
1492
|
+
const message = envelope.message ? redactString(envelope.message, "message") : null;
|
|
1493
|
+
const body = redactValue(envelope.body ?? {}, "body");
|
|
1494
|
+
const attributes = redactValue(envelope.attributes ?? {}, "attributes");
|
|
1495
|
+
const report = mergeRedactionReports(...topLevelResults.map(([, result]) => result.report), message?.report ?? emptyRedactionReport(), body.report, attributes.report);
|
|
1496
|
+
const safeAttributes = attributes.value;
|
|
1497
|
+
if (report.applied) {
|
|
1498
|
+
safeAttributes.redaction = redactionMetadata(report);
|
|
1499
|
+
}
|
|
1500
|
+
const safeEnvelope = sanitizeUniversalSourceMapPayloads({
|
|
1501
|
+
...envelope,
|
|
1502
|
+
...Object.fromEntries(topLevelResults.map(([field, result]) => [field, result.value])),
|
|
1503
|
+
message: message ? message.value : envelope.message,
|
|
1504
|
+
body: body.value,
|
|
1505
|
+
attributes: safeAttributes
|
|
1506
|
+
});
|
|
1507
|
+
return { envelope: safeEnvelope, metadata: safeEnvelope.attributes ?? {} };
|
|
1508
|
+
}
|
|
1509
|
+
function sanitizeUniversalSourceMapPayloads(envelope) {
|
|
1510
|
+
const body = sanitizeSourceMapContainers(envelope.body ?? {});
|
|
1511
|
+
const sanitizedAttributes = sanitizeSourceMapContainers(envelope.attributes ?? {});
|
|
1512
|
+
const attributes = hasSourceMapContainer(body) || hasSourceMapContainer(sanitizedAttributes) ? sanitizeSourceMapContextRecord(sanitizedAttributes) : sanitizedAttributes;
|
|
1513
|
+
return {
|
|
1514
|
+
...envelope,
|
|
1515
|
+
body,
|
|
1516
|
+
attributes
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function sanitizeSourceMapContainers(record) {
|
|
1520
|
+
const output = { ...record };
|
|
1521
|
+
if ("source_map" in output) {
|
|
1522
|
+
const sourceMap = sanitizeSourceMapTelemetry(output.source_map);
|
|
1523
|
+
if (sourceMap)
|
|
1524
|
+
output.source_map = sourceMap;
|
|
1525
|
+
else
|
|
1526
|
+
output.source_map = undefined;
|
|
1527
|
+
}
|
|
1528
|
+
const artifact = objectRecord(output.artifact);
|
|
1529
|
+
if (Object.keys(artifact).length > 0) {
|
|
1530
|
+
output.artifact = sanitizeSourceMapArtifactRecord(artifact);
|
|
1531
|
+
}
|
|
1532
|
+
return shouldSanitizeRootSourceMapArtifact(output) ? sanitizeSourceMapArtifactRecord(output) : output;
|
|
1533
|
+
}
|
|
1534
|
+
function shouldSanitizeRootSourceMapArtifact(record) {
|
|
1535
|
+
const artifactType = stringAttr(record, "artifact_type") ?? stringAttr(record, "type");
|
|
1536
|
+
const path = stringAttr(record, "path");
|
|
1537
|
+
const hasSourceArrayShape = "sources" in record && (("version" in record) || ("mappings" in record) || ("sourcesContent" in record) || ("source_map_path" in record) || ("file" in record) || ("sourceRoot" in record) || ("source_root" in record));
|
|
1538
|
+
return artifactType === "source_map" || artifactType === "source-map" || artifactType === "sourcemap" || Boolean(path?.endsWith(".map")) || hasSourceArrayShape || [
|
|
1539
|
+
"source_map_id",
|
|
1540
|
+
"source_map_artifact_id",
|
|
1541
|
+
"source_map_path",
|
|
1542
|
+
"javascript_artifact_id",
|
|
1543
|
+
"javascript_path",
|
|
1544
|
+
"linked_by",
|
|
1545
|
+
"file",
|
|
1546
|
+
"sourceRoot",
|
|
1547
|
+
"source_root",
|
|
1548
|
+
"validation_status",
|
|
1549
|
+
"validation_error",
|
|
1550
|
+
"source_count",
|
|
1551
|
+
"section_count",
|
|
1552
|
+
"names_count",
|
|
1553
|
+
"has_sources_content",
|
|
1554
|
+
"sources",
|
|
1555
|
+
"sections",
|
|
1556
|
+
"sourcesContent",
|
|
1557
|
+
"names",
|
|
1558
|
+
"mappings",
|
|
1559
|
+
"mappings_length",
|
|
1560
|
+
"raw_json"
|
|
1561
|
+
].some((key) => hasSourceMapRootValue(record[key]));
|
|
1562
|
+
}
|
|
1563
|
+
function hasSourceMapRootValue(value) {
|
|
1564
|
+
if (value === null || value === undefined)
|
|
1565
|
+
return false;
|
|
1566
|
+
if (Array.isArray(value))
|
|
1567
|
+
return value.length > 0;
|
|
1568
|
+
if (typeof value === "string")
|
|
1569
|
+
return value.length > 0;
|
|
1570
|
+
if (typeof value === "boolean")
|
|
1571
|
+
return value;
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
function hasSourceMapContainer(record) {
|
|
1575
|
+
if (Object.keys(objectRecord(record.source_map)).length > 0)
|
|
1576
|
+
return true;
|
|
1577
|
+
const artifact = objectRecord(record.artifact);
|
|
1578
|
+
if (Object.keys(objectRecord(artifact.source_map)).length > 0)
|
|
1579
|
+
return true;
|
|
1580
|
+
const artifactType = stringAttr(record, "artifact_type") ?? stringAttr(record, "type") ?? stringAttr(artifact, "artifact_type") ?? stringAttr(artifact, "type");
|
|
1581
|
+
const path = stringAttr(record, "path") ?? stringAttr(artifact, "path");
|
|
1582
|
+
return artifactType === "source_map" || artifactType === "source-map" || artifactType === "sourcemap" || Boolean(path?.endsWith(".map"));
|
|
1583
|
+
}
|
|
1584
|
+
function indexFromEnvelope(db, envelope, metadata) {
|
|
1585
|
+
const rawAttrs = envelope.attributes ?? {};
|
|
1586
|
+
const rawBody = envelope.body ?? {};
|
|
1587
|
+
const rawArtifact = objectRecord(rawBody.artifact);
|
|
1588
|
+
const body = envelope.type === "artifact" ? sanitizeSourceMapArtifactRecord(rawBody) : rawBody;
|
|
1589
|
+
const artifact = sanitizeSourceMapArtifactRecord(rawArtifact);
|
|
1590
|
+
const isSourceMapArtifact = envelope.type === "artifact" && (hasSourceMapContainer(body) || hasSourceMapContainer(artifact) || hasSourceMapContainer(rawAttrs));
|
|
1591
|
+
const attrs = isSourceMapArtifact ? sanitizeSourceMapContextRecord(rawAttrs) : rawAttrs;
|
|
1592
|
+
const testReport = objectRecord(body.test_report);
|
|
1593
|
+
const projectId = stringAttr(attrs, "project_id");
|
|
1594
|
+
const pageId = stringAttr(attrs, "page_id");
|
|
1595
|
+
const rootArtifact = { ...body };
|
|
1596
|
+
rootArtifact.artifact = undefined;
|
|
1597
|
+
const artifactContext = isSourceMapArtifact ? compactObject({ ...rootArtifact, ...artifact }) : artifact;
|
|
1598
|
+
const attributeMetadata = compactObject(metadata);
|
|
1599
|
+
const metadataForIndex = envelope.type === "artifact" ? compactObject({ ...artifactContext, ...attributeMetadata }) : stringAttr(attrs, "category") === "test_report" ? sanitizedTestReportMetadata(testReport, metadata, attrs) : metadata;
|
|
1600
|
+
return {
|
|
1601
|
+
event_id: envelope.event_id,
|
|
1602
|
+
schema_version: envelope.schema_version,
|
|
1603
|
+
source_event_id: envelope.source_event_id ?? null,
|
|
1604
|
+
event_type: envelope.type,
|
|
1605
|
+
event_time: envelope.event_time,
|
|
1606
|
+
ingest_time: envelope.ingest_time,
|
|
1607
|
+
severity: envelope.severity ?? null,
|
|
1608
|
+
source: envelope.source,
|
|
1609
|
+
project_id: projectId && rowExists(db, "projects", projectId) ? projectId : null,
|
|
1610
|
+
page_id: pageId && rowExists(db, "pages", pageId) ? pageId : null,
|
|
1611
|
+
machine_id: envelope.machine_id ?? stringAttr(attrs, "machine_id"),
|
|
1612
|
+
repo_id: envelope.repo_id ?? stringAttr(attrs, "repo_id"),
|
|
1613
|
+
app_id: envelope.app_id ?? stringAttr(attrs, "app_id"),
|
|
1614
|
+
process_id: envelope.process_id ?? stringAttr(attrs, "process_id"),
|
|
1615
|
+
run_id: envelope.run_id ?? stringAttr(attrs, "run_id"),
|
|
1616
|
+
trace_id: envelope.trace_id ?? stringAttr(attrs, "trace_id"),
|
|
1617
|
+
span_id: envelope.span_id ?? stringAttr(attrs, "span_id"),
|
|
1618
|
+
parent_span_id: envelope.parent_span_id ?? stringAttr(attrs, "parent_span_id"),
|
|
1619
|
+
session_id: envelope.session_id ?? stringAttr(attrs, "session_id"),
|
|
1620
|
+
release_id: envelope.release_id ?? stringAttr(attrs, "release_id"),
|
|
1621
|
+
environment: envelope.environment ?? stringAttr(attrs, "environment"),
|
|
1622
|
+
artifact_id: stringAttr(attrs, "artifact_id") ?? stringAttr(body, "artifact_id") ?? stringAttr(artifact, "artifact_id"),
|
|
1623
|
+
privacy_tier: envelope.privacy ?? stringAttr(attrs, "privacy_tier") ?? stringAttr(attrs, "privacy"),
|
|
1624
|
+
message: envelope.message ?? null,
|
|
1625
|
+
metadata: metadataForIndex
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function applyCompatibilityProjections(db, envelope, index) {
|
|
1629
|
+
const skipProcessRunProjection = isTestReportBuildEvent(envelope);
|
|
1630
|
+
if (index.trace_id)
|
|
1631
|
+
upsertTraceProjection(db, envelope, index);
|
|
1632
|
+
if (envelope.type === "span")
|
|
1633
|
+
upsertSpanProjection(db, envelope, index);
|
|
1634
|
+
if (index.session_id || envelope.type === "session")
|
|
1635
|
+
upsertSessionProjection(db, envelope, index);
|
|
1636
|
+
if (index.release_id || envelope.type === "release")
|
|
1637
|
+
upsertReleaseProjection(db, envelope, index);
|
|
1638
|
+
if (index.artifact_id || envelope.type === "artifact") {
|
|
1639
|
+
upsertArtifactProjection(db, envelope, index);
|
|
1640
|
+
upsertSourceMapProjection(db, envelope, index);
|
|
1641
|
+
}
|
|
1642
|
+
if (skipProcessRunProjection)
|
|
1643
|
+
upsertTestReportProjection(db, envelope, index);
|
|
1644
|
+
if (!skipProcessRunProjection && (index.process_id || index.run_id || envelope.type === "process" || envelope.type === "build"))
|
|
1645
|
+
upsertProcessRunProjection(db, envelope, index);
|
|
1646
|
+
if (envelope.type === "exception" && envelope.message) {
|
|
1647
|
+
upsertIssue(db, {
|
|
1648
|
+
project_id: index.project_id ?? undefined,
|
|
1649
|
+
level: envelope.severity ?? "error",
|
|
1650
|
+
service: stringAttr(envelope.attributes, "service"),
|
|
1651
|
+
message: envelope.message,
|
|
1652
|
+
stack_trace: stringAttr(envelope.attributes, "stack_trace") ?? stringAttr(envelope.body, "stack_trace")
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function isTestReportBuildEvent(envelope) {
|
|
1657
|
+
return envelope.type === "build" && stringAttr(envelope.attributes, "category") === "test_report";
|
|
1658
|
+
}
|
|
1659
|
+
function upsertTraceProjection(db, envelope, index) {
|
|
1660
|
+
if (!index.trace_id)
|
|
1661
|
+
return;
|
|
1662
|
+
db.prepare(`
|
|
1663
|
+
INSERT INTO traces (id, project_id, app_id, root_span_id, started_at, ended_at, status, metadata)
|
|
1664
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1665
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1666
|
+
project_id = COALESCE(traces.project_id, excluded.project_id),
|
|
1667
|
+
app_id = COALESCE(traces.app_id, excluded.app_id),
|
|
1668
|
+
root_span_id = COALESCE(traces.root_span_id, excluded.root_span_id),
|
|
1669
|
+
started_at = CASE
|
|
1670
|
+
WHEN excluded.started_at IS NOT NULL
|
|
1671
|
+
AND (traces.started_at IS NULL OR excluded.started_at < traces.started_at)
|
|
1672
|
+
THEN excluded.started_at
|
|
1673
|
+
ELSE traces.started_at
|
|
1674
|
+
END,
|
|
1675
|
+
ended_at = COALESCE(excluded.ended_at, traces.ended_at),
|
|
1676
|
+
status = COALESCE(excluded.status, traces.status),
|
|
1677
|
+
metadata = excluded.metadata
|
|
1678
|
+
`).run(index.trace_id, index.project_id ?? null, index.app_id ?? null, index.span_id ?? null, stringAttr(envelope.attributes, "started_at") ?? envelope.event_time, stringAttr(envelope.attributes, "ended_at"), stringAttr(envelope.attributes, "status"), JSON.stringify(index.metadata ?? {}));
|
|
1679
|
+
}
|
|
1680
|
+
function upsertSpanProjection(db, envelope, index) {
|
|
1681
|
+
const spanId = index.span_id ?? envelope.event_id;
|
|
1682
|
+
db.prepare(`
|
|
1683
|
+
INSERT INTO spans (id, trace_id, parent_span_id, app_id, process_id, name, operation, status, started_at, ended_at, duration_ms, metadata)
|
|
1684
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1685
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1686
|
+
trace_id = COALESCE(spans.trace_id, excluded.trace_id),
|
|
1687
|
+
parent_span_id = COALESCE(spans.parent_span_id, excluded.parent_span_id),
|
|
1688
|
+
app_id = COALESCE(spans.app_id, excluded.app_id),
|
|
1689
|
+
process_id = COALESCE(spans.process_id, excluded.process_id),
|
|
1690
|
+
name = COALESCE(spans.name, excluded.name),
|
|
1691
|
+
operation = COALESCE(spans.operation, excluded.operation),
|
|
1692
|
+
ended_at = COALESCE(excluded.ended_at, spans.ended_at),
|
|
1693
|
+
duration_ms = COALESCE(excluded.duration_ms, spans.duration_ms),
|
|
1694
|
+
status = COALESCE(excluded.status, spans.status),
|
|
1695
|
+
metadata = excluded.metadata
|
|
1696
|
+
`).run(...sqlArgs(spanId, index.trace_id ?? null, index.parent_span_id ?? null, index.app_id ?? null, index.process_id ?? null, stringAttr(envelope.attributes, "name") ?? stringAttr(envelope.body, "name") ?? envelope.message ?? null, stringAttr(envelope.attributes, "operation") ?? stringAttr(envelope.body, "operation"), stringAttr(envelope.attributes, "status") ?? envelope.severity ?? null, stringAttr(envelope.attributes, "started_at") ?? envelope.event_time, stringAttr(envelope.attributes, "ended_at"), numberAttr(envelope.attributes, "duration_ms") ?? numberAttr(envelope.body, "duration_ms"), JSON.stringify(index.metadata ?? {})));
|
|
1697
|
+
}
|
|
1698
|
+
function upsertSessionProjection(db, envelope, index) {
|
|
1699
|
+
const sessionId = index.session_id ?? envelope.event_id;
|
|
1700
|
+
db.prepare(`
|
|
1701
|
+
INSERT INTO sessions (id, project_id, app_id, user_hash, started_at, ended_at, status, metadata)
|
|
1702
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1703
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1704
|
+
project_id = COALESCE(sessions.project_id, excluded.project_id),
|
|
1705
|
+
app_id = COALESCE(sessions.app_id, excluded.app_id),
|
|
1706
|
+
user_hash = COALESCE(sessions.user_hash, excluded.user_hash),
|
|
1707
|
+
started_at = CASE
|
|
1708
|
+
WHEN excluded.started_at IS NOT NULL
|
|
1709
|
+
AND (sessions.started_at IS NULL OR excluded.started_at < sessions.started_at)
|
|
1710
|
+
THEN excluded.started_at
|
|
1711
|
+
ELSE sessions.started_at
|
|
1712
|
+
END,
|
|
1713
|
+
ended_at = COALESCE(excluded.ended_at, sessions.ended_at),
|
|
1714
|
+
status = COALESCE(excluded.status, sessions.status),
|
|
1715
|
+
metadata = excluded.metadata
|
|
1716
|
+
`).run(sessionId, index.project_id ?? null, index.app_id ?? null, stringAttr(envelope.attributes, "user_hash"), stringAttr(envelope.attributes, "started_at") ?? envelope.event_time, stringAttr(envelope.attributes, "ended_at"), stringAttr(envelope.attributes, "status"), JSON.stringify(index.metadata ?? {}));
|
|
1717
|
+
}
|
|
1718
|
+
function upsertReleaseProjection(db, envelope, index) {
|
|
1719
|
+
const releaseId = index.release_id ?? envelope.event_id;
|
|
1720
|
+
db.prepare(`
|
|
1721
|
+
INSERT INTO releases (id, project_id, app_id, version, commit_sha, build_id, deployed_at, metadata)
|
|
1722
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1723
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1724
|
+
project_id = COALESCE(releases.project_id, excluded.project_id),
|
|
1725
|
+
app_id = COALESCE(releases.app_id, excluded.app_id),
|
|
1726
|
+
version = COALESCE(releases.version, excluded.version),
|
|
1727
|
+
commit_sha = COALESCE(releases.commit_sha, excluded.commit_sha),
|
|
1728
|
+
build_id = COALESCE(releases.build_id, excluded.build_id),
|
|
1729
|
+
deployed_at = COALESCE(excluded.deployed_at, releases.deployed_at),
|
|
1730
|
+
metadata = excluded.metadata
|
|
1731
|
+
`).run(...sqlArgs(releaseId, index.project_id ?? null, index.app_id ?? null, stringAttr(envelope.attributes, "version") ?? envelope.message ?? null, stringAttr(envelope.attributes, "commit_sha"), stringAttr(envelope.attributes, "build_id"), stringAttr(envelope.attributes, "deployed_at") ?? envelope.event_time, JSON.stringify(index.metadata ?? {})));
|
|
1732
|
+
}
|
|
1733
|
+
function upsertArtifactProjection(db, envelope, index) {
|
|
1734
|
+
const body = envelope.body ?? {};
|
|
1735
|
+
const artifact = objectRecord(body.artifact);
|
|
1736
|
+
const isSourceMapArtifact = hasSourceMapContainer(body) || hasSourceMapContainer(artifact) || hasSourceMapContainer(envelope.attributes ?? {});
|
|
1737
|
+
const artifactIdCandidate = index.artifact_id ?? stringAttr(envelope.attributes, "artifact_id") ?? stringAttr(body, "artifact_id") ?? stringAttr(artifact, "artifact_id");
|
|
1738
|
+
const artifactId = isSourceMapArtifact ? sanitizeSourceMapIdentifierValue(artifactIdCandidate) ?? sourceMapFallbackIdentifier(envelope.event_id) : artifactIdCandidate ?? envelope.event_id;
|
|
1739
|
+
db.prepare(`
|
|
1740
|
+
INSERT INTO artifacts (id, release_id, artifact_type, path, content_hash, size_bytes, metadata)
|
|
1741
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1742
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1743
|
+
release_id = COALESCE(artifacts.release_id, excluded.release_id),
|
|
1744
|
+
artifact_type = COALESCE(artifacts.artifact_type, excluded.artifact_type),
|
|
1745
|
+
path = COALESCE(excluded.path, artifacts.path),
|
|
1746
|
+
content_hash = COALESCE(excluded.content_hash, artifacts.content_hash),
|
|
1747
|
+
size_bytes = COALESCE(excluded.size_bytes, artifacts.size_bytes),
|
|
1748
|
+
metadata = excluded.metadata
|
|
1749
|
+
`).run(artifactId, index.release_id ?? null, stringAttr(envelope.attributes, "artifact_type") ?? stringAttr(envelope.attributes, "type") ?? stringAttr(artifact, "artifact_type") ?? stringAttr(artifact, "type") ?? envelope.type, stringAttr(envelope.attributes, "path") ?? stringAttr(body, "path") ?? stringAttr(artifact, "path"), stringAttr(envelope.attributes, "content_hash") ?? stringAttr(body, "content_hash") ?? stringAttr(artifact, "content_hash"), numberAttr(envelope.attributes, "size_bytes") ?? numberAttr(body, "size_bytes") ?? numberAttr(artifact, "size_bytes"), JSON.stringify(index.metadata ?? {}));
|
|
1750
|
+
}
|
|
1751
|
+
function upsertProcessRunProjection(db, envelope, index) {
|
|
1752
|
+
if (index.process_id) {
|
|
1753
|
+
db.prepare(`
|
|
1754
|
+
INSERT INTO processes (id, machine_id, repo_id, app_id, pid, ppid, command, cwd, started_at, ended_at, exit_code, metadata)
|
|
1755
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1756
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1757
|
+
machine_id = COALESCE(processes.machine_id, excluded.machine_id),
|
|
1758
|
+
repo_id = COALESCE(processes.repo_id, excluded.repo_id),
|
|
1759
|
+
app_id = COALESCE(processes.app_id, excluded.app_id),
|
|
1760
|
+
pid = COALESCE(processes.pid, excluded.pid),
|
|
1761
|
+
ppid = COALESCE(processes.ppid, excluded.ppid),
|
|
1762
|
+
command = COALESCE(processes.command, excluded.command),
|
|
1763
|
+
cwd = COALESCE(processes.cwd, excluded.cwd),
|
|
1764
|
+
ended_at = COALESCE(excluded.ended_at, processes.ended_at),
|
|
1765
|
+
exit_code = COALESCE(excluded.exit_code, processes.exit_code),
|
|
1766
|
+
metadata = excluded.metadata
|
|
1767
|
+
`).run(index.process_id, index.machine_id ?? null, index.repo_id ?? null, index.app_id ?? null, numberAttr(envelope.attributes, "pid"), numberAttr(envelope.attributes, "ppid"), stringAttr(envelope.attributes, "command"), stringAttr(envelope.attributes, "cwd"), stringAttr(envelope.attributes, "started_at") ?? envelope.event_time, stringAttr(envelope.attributes, "ended_at"), numberAttr(envelope.attributes, "exit_code"), JSON.stringify(index.metadata ?? {}));
|
|
1768
|
+
}
|
|
1769
|
+
if (index.run_id) {
|
|
1770
|
+
db.prepare(`
|
|
1771
|
+
INSERT INTO runs (id, process_id, run_type, name, status, started_at, ended_at, exit_code, metadata)
|
|
1772
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1773
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1774
|
+
process_id = COALESCE(runs.process_id, excluded.process_id),
|
|
1775
|
+
run_type = COALESCE(runs.run_type, excluded.run_type),
|
|
1776
|
+
name = COALESCE(runs.name, excluded.name),
|
|
1777
|
+
ended_at = COALESCE(excluded.ended_at, runs.ended_at),
|
|
1778
|
+
exit_code = COALESCE(excluded.exit_code, runs.exit_code),
|
|
1779
|
+
status = COALESCE(excluded.status, runs.status),
|
|
1780
|
+
metadata = excluded.metadata
|
|
1781
|
+
`).run(...sqlArgs(index.run_id, index.process_id ?? null, stringAttr(envelope.attributes, "run_type") ?? envelope.type, stringAttr(envelope.attributes, "name") ?? envelope.message ?? null, stringAttr(envelope.attributes, "status"), stringAttr(envelope.attributes, "started_at") ?? envelope.event_time, stringAttr(envelope.attributes, "ended_at"), numberAttr(envelope.attributes, "exit_code"), JSON.stringify(index.metadata ?? {})));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
function normalizeIsoTime(value, field) {
|
|
1785
|
+
if (value === undefined || value.length === 0)
|
|
1786
|
+
return null;
|
|
1787
|
+
const date = new Date(value);
|
|
1788
|
+
if (Number.isNaN(date.getTime()))
|
|
1789
|
+
throw new Error(`${field} must be an ISO timestamp`);
|
|
1790
|
+
return date.toISOString();
|
|
1791
|
+
}
|
|
1792
|
+
function deterministicSourceEventId(source, sourceEventId) {
|
|
1793
|
+
if (!sourceEventId)
|
|
1794
|
+
return;
|
|
1795
|
+
const digest = createHash("sha256").update(source).update("\x00").update(sourceEventId).digest("hex").slice(0, 32);
|
|
1796
|
+
return `evt_src_${digest}`;
|
|
1797
|
+
}
|
|
1798
|
+
function compactObject(value) {
|
|
1799
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null));
|
|
1800
|
+
}
|
|
1801
|
+
function sanitizeOptionalString(value) {
|
|
1802
|
+
if (value === undefined || value === null)
|
|
1803
|
+
return;
|
|
1804
|
+
if (typeof value !== "string")
|
|
1805
|
+
throw new Error("expected string field");
|
|
1806
|
+
return value;
|
|
1807
|
+
}
|
|
1808
|
+
function sanitizeNullableString(value) {
|
|
1809
|
+
return sanitizeOptionalString(value) ?? null;
|
|
1810
|
+
}
|
|
1811
|
+
function redactNullableString(value, path) {
|
|
1812
|
+
if (value === null || value === undefined)
|
|
1813
|
+
return { value: value ?? null, report: emptyRedactionReport() };
|
|
1814
|
+
return redactString(value, path);
|
|
1815
|
+
}
|
|
1816
|
+
function objectRecord(value) {
|
|
1817
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1818
|
+
}
|
|
1819
|
+
function stringAttr(record, key) {
|
|
1820
|
+
const value = record?.[key];
|
|
1821
|
+
if (typeof value === "string")
|
|
1822
|
+
return value;
|
|
1823
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
1824
|
+
return String(value);
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
function numberAttr(record, key) {
|
|
1828
|
+
const value = record?.[key];
|
|
1829
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1830
|
+
return value;
|
|
1831
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1832
|
+
const parsed = Number(value);
|
|
1833
|
+
if (Number.isFinite(parsed))
|
|
1834
|
+
return parsed;
|
|
1835
|
+
}
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
function rowExists(db, table, id) {
|
|
1839
|
+
const row = db.prepare(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id);
|
|
1840
|
+
return Boolean(row);
|
|
1841
|
+
}
|
|
1842
|
+
function emptyRedactionReport() {
|
|
1843
|
+
return { applied: false, fields: [], replacements: 0 };
|
|
1844
|
+
}
|
|
1845
|
+
function sqlArgs(...values) {
|
|
1846
|
+
return values;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
export { publishEventCatalogEvent, subscribeLogEvents, subscribeEventCatalogEvents, hasBufferedLogEvent, hasBufferedEventCatalogEvent, searchEvents, getEvent, exportEventsToJson, createAlertRule, listAlertRules, updateAlertRule, deleteAlertRule, redactString, redactValue, mergeRedactionReports, redactionMetadata, ingestLog, ingestBatch, PACKAGE_VERSION, exitIfMetadataRequest, readOptionValue, hasOption, createProject, listProjects, getProject, updateProject, createPage, listPages, getPage, resolveProjectId, touchPage, saveSnapshot, getLatestSnapshot, getPerfTrend, scoreLabel, summarizeLogs, searchTestReports, getTestReport, UNIVERSAL_EVENT_TYPES, validateUniversalEventInput, ingestUniversalEvent };
|