backtrace-console 0.0.3 → 0.0.4
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/app.js +23 -0
- package/bin/backtrace-server.js +19 -14
- package/bin/www +93 -0
- package/lib/BacktraceCodexTool.js +32 -0
- package/lib/backtrace/analysis.js +356 -0
- package/lib/backtrace/constants.js +27 -0
- package/lib/backtrace/options.js +278 -0
- package/lib/backtrace/query-download.js +117 -0
- package/lib/backtrace/query-session.js +229 -0
- package/lib/backtrace/query.js +506 -0
- package/lib/backtrace/repair-fingerprint.js +405 -0
- package/lib/backtrace/repair.js +530 -0
- package/lib/backtrace/tool.js +364 -0
- package/lib/backtrace/utils.js +297 -0
- package/lib/cli/args.js +177 -0
- package/lib/cli/run.js +191 -0
- package/lib/feishu.js +66 -0
- package/lib/scheduler.js +126 -0
- package/package.json +8 -4
- package/public/chat-components.css +569 -0
- package/public/chat-core.js +635 -0
- package/public/chat-layout.css +290 -0
- package/public/chat-render.js +308 -0
- package/public/chat-send.js +230 -0
- package/public/chat.html +69 -0
- package/public/index-page.js +504 -0
- package/public/index.html +138 -0
- package/public/stylesheets/style.css +186 -0
- package/routes/backtrace-chat.js +389 -0
- package/routes/backtrace-files.js +88 -0
- package/routes/backtrace-fix-plan.js +53 -0
- package/routes/backtrace-run.js +128 -0
- package/routes/backtrace-shared.js +202 -0
- package/routes/backtrace.js +10 -0
- package/routes/index.js +9 -0
- package/routes/users.js +9 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { AuthenticationError, BacktraceSession, buildSiblingUrl, buildLoginUrl, withSessionToken } = require("./query-session");
|
|
4
|
+
const { downloadAttachment, downloadAttachmentWithRetry } = require("./query-download");
|
|
5
|
+
const { logStep, ensureDirectory, normalizeFingerprint, getFingerprintLogsRoot } = require("./utils");
|
|
6
|
+
let undiciModule = null;
|
|
7
|
+
const FINGERPRINT_GROUP_FIELDS = ["fingerprint"];
|
|
8
|
+
const FINGERPRINT_GROUP_FOLD = { "error.message": [["head"]], classifiers: [["head"]], "fingerprint;first_seen": [["head"]], "fingerprint;issues;state": [["head"]] };
|
|
9
|
+
const FINGERPRINT_GROUP_ORDER = [{ name: "fingerprint;first_seen;0", ordering: "descending" }];
|
|
10
|
+
function getUndici() {
|
|
11
|
+
if (!undiciModule) {
|
|
12
|
+
undiciModule = require("undici");
|
|
13
|
+
}
|
|
14
|
+
return undiciModule;
|
|
15
|
+
}
|
|
16
|
+
function createProxyDispatcher(proxyUrl) {
|
|
17
|
+
const normalizedProxy = String(proxyUrl || "").trim();
|
|
18
|
+
if (!normalizedProxy) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const { ProxyAgent } = getUndici();
|
|
23
|
+
return new ProxyAgent(normalizedProxy);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Proxy support requires dependency "undici": ${error instanceof Error ? error.message : String(error)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function withDispatcher(options, dispatcher) {
|
|
29
|
+
if (!dispatcher) {
|
|
30
|
+
return options || {};
|
|
31
|
+
}
|
|
32
|
+
return { ...(options || {}), dispatcher };
|
|
33
|
+
}
|
|
34
|
+
function shouldUseFingerprintGroupQuery(options) { return options.command === "fingerprint"; }
|
|
35
|
+
function getRequestedFingerprints(options) {
|
|
36
|
+
return String(options?.fingerprint || "")
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((item) => String(item || "").trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
function buildQueryBody(options, offset, limit) {
|
|
42
|
+
const filter = {
|
|
43
|
+
timestamp: [["at-most", String(options.to)], ["at-least", String(options.from)]],
|
|
44
|
+
};
|
|
45
|
+
if (options.fingerprint) {
|
|
46
|
+
filter.fingerprint = [["contains", String(options.fingerprint)]];
|
|
47
|
+
}
|
|
48
|
+
if (shouldUseFingerprintGroupQuery(options)) {
|
|
49
|
+
return {
|
|
50
|
+
group: FINGERPRINT_GROUP_FIELDS,
|
|
51
|
+
fold: FINGERPRINT_GROUP_FOLD,
|
|
52
|
+
order: FINGERPRINT_GROUP_ORDER,
|
|
53
|
+
offset,
|
|
54
|
+
limit,
|
|
55
|
+
filter: [filter],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
select: options.select,
|
|
60
|
+
order: [{ name: "timestamp", ordering: "descending" }],
|
|
61
|
+
offset,
|
|
62
|
+
limit,
|
|
63
|
+
filter: [filter],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildFingerprintGroupQueryBody(options, offset, limit, fingerprintFilter) {
|
|
67
|
+
const filter = {
|
|
68
|
+
timestamp: [["at-most", String(options.to)], ["at-least", String(options.from)]],
|
|
69
|
+
};
|
|
70
|
+
if (fingerprintFilter) {
|
|
71
|
+
filter.fingerprint = [["contains", String(fingerprintFilter)]];
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
group: FINGERPRINT_GROUP_FIELDS,
|
|
75
|
+
fold: FINGERPRINT_GROUP_FOLD,
|
|
76
|
+
order: FINGERPRINT_GROUP_ORDER,
|
|
77
|
+
offset,
|
|
78
|
+
limit,
|
|
79
|
+
filter: [filter],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildCompressedFingerprintQueryBody(options, fingerprint) {
|
|
83
|
+
return {
|
|
84
|
+
select: ["_compressed"],
|
|
85
|
+
order: [{ name: "timestamp", ordering: "descending" }],
|
|
86
|
+
offset: 0,
|
|
87
|
+
limit: 1,
|
|
88
|
+
filter: [{
|
|
89
|
+
fingerprint: [["contains", String(fingerprint)]],
|
|
90
|
+
timestamp: [["at-most", String(options.to)], ["at-least", String(options.from)]],
|
|
91
|
+
}],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildFingerprintObjectsQueryBody(options, fingerprint, offset, limit) {
|
|
95
|
+
return {
|
|
96
|
+
select: ["timestamp"],
|
|
97
|
+
order: [{ name: "timestamp", ordering: "descending" }],
|
|
98
|
+
offset,
|
|
99
|
+
limit,
|
|
100
|
+
filter: [{
|
|
101
|
+
fingerprint: [["contains", String(fingerprint)]],
|
|
102
|
+
timestamp: [["at-most", String(options.to)], ["at-least", String(options.from)]],
|
|
103
|
+
}],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function toHexObjectId(value) {
|
|
107
|
+
if (typeof value === "string" && /^[0-9a-f]+$/i.test(value)) {
|
|
108
|
+
return value.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
const numeric = Number(value);
|
|
111
|
+
if (!Number.isFinite(numeric)) {
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
return numeric.toString(16);
|
|
115
|
+
}
|
|
116
|
+
function extractObjectIds(objects) {
|
|
117
|
+
if (!Array.isArray(objects) || objects.length === 0) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const values = [];
|
|
121
|
+
objects.forEach((group) => {
|
|
122
|
+
const [, rows] = group || [];
|
|
123
|
+
if (!Array.isArray(rows)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
rows.forEach((row) => {
|
|
127
|
+
const value = Array.isArray(row) ? row[0] : null;
|
|
128
|
+
if (value !== null && value !== undefined) {
|
|
129
|
+
values.push(value);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
return values;
|
|
134
|
+
}
|
|
135
|
+
function extractSelectedValues(values, selectFields) {
|
|
136
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const [, ...entries] = values[0];
|
|
140
|
+
return entries.map((entry) => {
|
|
141
|
+
const row = {};
|
|
142
|
+
selectFields.forEach((field, index) => {
|
|
143
|
+
row[field] = Array.isArray(entry) ? entry[index] : undefined;
|
|
144
|
+
});
|
|
145
|
+
if (Array.isArray(entry) && entry.length > selectFields.length) {
|
|
146
|
+
row._count = entry[entry.length - 1];
|
|
147
|
+
}
|
|
148
|
+
return row;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function mapResultsToItems(objectIds, selectedValues) {
|
|
152
|
+
return objectIds.map((objectId, index) => ({
|
|
153
|
+
index: index + 1,
|
|
154
|
+
objectIdDecimal: objectId,
|
|
155
|
+
objectIdHex: toHexObjectId(objectId),
|
|
156
|
+
values: selectedValues[index] || {},
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
function optimizeLimit(totalRows, baseLimit) { if (totalRows <= baseLimit) return baseLimit; if (totalRows <= 100) return 50; if (totalRows <= 500) return 100; return 200; }
|
|
160
|
+
function unwrapValueCell(value) {
|
|
161
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
162
|
+
return unwrapValueCell(value[0]);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function normalizeFieldName(value) {
|
|
167
|
+
return String(value || "")
|
|
168
|
+
.replace(/^head\((.+)\)$/i, "$1")
|
|
169
|
+
.replace(/;0$/i, "");
|
|
170
|
+
}
|
|
171
|
+
function extractGroupedFingerprintItems(response) {
|
|
172
|
+
const values = response?.values;
|
|
173
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const rows = values;
|
|
177
|
+
const valueHeaders = Array.isArray(response?.columns_desc) && response.columns_desc.length > 0
|
|
178
|
+
? response.columns_desc.map((entry) => normalizeFieldName(entry?.name))
|
|
179
|
+
: Array.isArray(response?.columns)
|
|
180
|
+
? response.columns.map((entry) => normalizeFieldName(Array.isArray(entry) ? entry[0] : entry))
|
|
181
|
+
: [];
|
|
182
|
+
return rows.map((entry, index) => {
|
|
183
|
+
const rowValues = Array.isArray(entry) ? entry : [];
|
|
184
|
+
const fingerprint = unwrapValueCell(rowValues[0]);
|
|
185
|
+
const groupedColumns = Array.isArray(rowValues[1]) ? rowValues[1] : [];
|
|
186
|
+
const count = rowValues[2];
|
|
187
|
+
const mappedValues = {};
|
|
188
|
+
mappedValues.fingerprint = String(fingerprint || "").trim();
|
|
189
|
+
valueHeaders.forEach((field, fieldIndex) => {
|
|
190
|
+
mappedValues[field] = unwrapValueCell(groupedColumns[fieldIndex]);
|
|
191
|
+
});
|
|
192
|
+
if (count !== undefined) {
|
|
193
|
+
mappedValues._count = unwrapValueCell(count);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
index: index + 1,
|
|
197
|
+
objectIdDecimal: "",
|
|
198
|
+
objectIdHex: "",
|
|
199
|
+
fingerprint: mappedValues.fingerprint,
|
|
200
|
+
values: mappedValues,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function fetchBacktracePage(session, queryUrl, queryBody) {
|
|
205
|
+
logStep("query", "sending query page", queryBody);
|
|
206
|
+
return session.requestJson(
|
|
207
|
+
queryUrl,
|
|
208
|
+
{ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(queryBody) },
|
|
209
|
+
`query offset=${queryBody.offset} limit=${queryBody.limit}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
async function fetchFingerprintProbe(session, options, fingerprint) {
|
|
213
|
+
const queryBody = buildCompressedFingerprintQueryBody(options, fingerprint);
|
|
214
|
+
const result = await fetchBacktracePage(session, options.queryUrl, queryBody);
|
|
215
|
+
return {
|
|
216
|
+
fingerprint,
|
|
217
|
+
totalRows: result?._?.runtime?.filter?.rows ?? 0,
|
|
218
|
+
payload: result,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function queryFingerprintGroups(session, options, fingerprintFilter = "") {
|
|
222
|
+
const probeBody = buildFingerprintGroupQueryBody(options, options.offset, options.limit, fingerprintFilter);
|
|
223
|
+
const probeResult = await fetchBacktracePage(session, options.queryUrl, probeBody);
|
|
224
|
+
const totalRows = probeResult?.response?.cardinalities?.pagination?.groups
|
|
225
|
+
?? probeResult?.response?.cardinalities?.having?.groups
|
|
226
|
+
?? probeResult?.response?.cardinalities?.initial?.groups
|
|
227
|
+
?? 0;
|
|
228
|
+
const pageLimit = optimizeLimit(totalRows, options.limit);
|
|
229
|
+
logStep("query", "fingerprint pagination decided", {
|
|
230
|
+
totalRows,
|
|
231
|
+
initialLimit: options.limit,
|
|
232
|
+
optimizedLimit: pageLimit,
|
|
233
|
+
fingerprint: fingerprintFilter || "all",
|
|
234
|
+
});
|
|
235
|
+
const items = [];
|
|
236
|
+
for (let offset = options.offset; offset < totalRows + options.offset || (offset === options.offset && totalRows === 0); offset += pageLimit) {
|
|
237
|
+
/* eslint-disable no-await-in-loop */
|
|
238
|
+
const result = await fetchBacktracePage(
|
|
239
|
+
session,
|
|
240
|
+
options.queryUrl,
|
|
241
|
+
buildFingerprintGroupQueryBody(options, offset, pageLimit, fingerprintFilter),
|
|
242
|
+
);
|
|
243
|
+
/* eslint-enable no-await-in-loop */
|
|
244
|
+
items.push(...extractGroupedFingerprintItems(result?.response));
|
|
245
|
+
if (totalRows === 0) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
totalRows,
|
|
251
|
+
pageLimit,
|
|
252
|
+
listedCount: items.length,
|
|
253
|
+
objectIds: items.map((item) => item.fingerprint || ""),
|
|
254
|
+
selectedValues: items.map((item) => item.values || {}),
|
|
255
|
+
items,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function fetchFingerprintObjects(session, options, fingerprint, knownTotalRows) {
|
|
259
|
+
const totalRows = Number.isFinite(knownTotalRows)
|
|
260
|
+
? knownTotalRows
|
|
261
|
+
: (() => {
|
|
262
|
+
throw new Error("fetchFingerprintObjects requires a known total row count");
|
|
263
|
+
})();
|
|
264
|
+
const pageLimit = optimizeLimit(totalRows, options.limit);
|
|
265
|
+
const objects = [];
|
|
266
|
+
for (let offset = options.offset; offset < totalRows + options.offset || (offset === options.offset && totalRows === 0); offset += pageLimit) {
|
|
267
|
+
/* eslint-disable no-await-in-loop */
|
|
268
|
+
const result = await fetchBacktracePage(
|
|
269
|
+
session,
|
|
270
|
+
options.queryUrl,
|
|
271
|
+
buildFingerprintObjectsQueryBody(options, fingerprint, offset, pageLimit),
|
|
272
|
+
);
|
|
273
|
+
/* eslint-enable no-await-in-loop */
|
|
274
|
+
const objectIds = extractObjectIds(result?.response?.objects);
|
|
275
|
+
const selectedValues = extractSelectedValues(result?.response?.values, ["timestamp"]);
|
|
276
|
+
objectIds.forEach((objectId, index) => {
|
|
277
|
+
objects.push({
|
|
278
|
+
objectIdDecimal: objectId,
|
|
279
|
+
objectIdHex: toHexObjectId(objectId),
|
|
280
|
+
timestamp: selectedValues[index]?.timestamp || "",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
if (totalRows === 0) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
fingerprint,
|
|
289
|
+
totalRows,
|
|
290
|
+
pageLimit,
|
|
291
|
+
objects,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function queryFingerprintDetail(session, options) {
|
|
295
|
+
const requestedFingerprints = getRequestedFingerprints(options);
|
|
296
|
+
const fingerprint = requestedFingerprints[0];
|
|
297
|
+
const groupResult = await queryFingerprintGroups(session, options, fingerprint);
|
|
298
|
+
const summary = groupResult.items[0] || {
|
|
299
|
+
fingerprint,
|
|
300
|
+
values: {
|
|
301
|
+
fingerprint,
|
|
302
|
+
"error.message": "",
|
|
303
|
+
classifiers: "",
|
|
304
|
+
"fingerprint;first_seen": "",
|
|
305
|
+
"fingerprint;issues;state": "",
|
|
306
|
+
_count: 0,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const probe = await fetchFingerprintProbe(session, options, fingerprint);
|
|
310
|
+
const objectsResult = await fetchFingerprintObjects(session, options, fingerprint, probe.totalRows);
|
|
311
|
+
return {
|
|
312
|
+
fingerprint,
|
|
313
|
+
summary,
|
|
314
|
+
objects: objectsResult.objects,
|
|
315
|
+
querySummary: {
|
|
316
|
+
totalRows: objectsResult.totalRows,
|
|
317
|
+
fetchedRows: objectsResult.objects.length,
|
|
318
|
+
objectCount: objectsResult.objects.length,
|
|
319
|
+
pageLimit: objectsResult.pageLimit,
|
|
320
|
+
objectIds: objectsResult.objects.map((item) => item.objectIdHex),
|
|
321
|
+
selectedValues: objectsResult.objects.map((item) => ({ timestamp: item.timestamp })),
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
async function fetchAllResults(session, options) {
|
|
326
|
+
const probeBody = buildQueryBody(options, options.offset, options.limit);
|
|
327
|
+
const probeResult = await fetchBacktracePage(session, options.queryUrl, probeBody);
|
|
328
|
+
const totalRows = shouldUseFingerprintGroupQuery(options)
|
|
329
|
+
? probeResult?.response?.cardinalities?.pagination?.groups
|
|
330
|
+
?? probeResult?.response?.cardinalities?.having?.groups
|
|
331
|
+
?? probeResult?.response?.cardinalities?.initial?.groups
|
|
332
|
+
?? 0
|
|
333
|
+
: probeResult?._?.runtime?.filter?.rows ?? 0;
|
|
334
|
+
const pageLimit = optimizeLimit(totalRows, options.limit);
|
|
335
|
+
logStep("query", "query pagination decided", { totalRows, initialLimit: options.limit, optimizedLimit: pageLimit });
|
|
336
|
+
const pages = [];
|
|
337
|
+
for (let offset = options.offset; offset < totalRows + options.offset || (offset === options.offset && totalRows === 0); offset += pageLimit) {
|
|
338
|
+
pages.push({ offset, limit: pageLimit });
|
|
339
|
+
if (totalRows === 0) break;
|
|
340
|
+
}
|
|
341
|
+
if (pages.length === 0) {
|
|
342
|
+
pages.push({ offset: options.offset, limit: pageLimit });
|
|
343
|
+
}
|
|
344
|
+
const allObjectIds = [];
|
|
345
|
+
const allSelectedValues = [];
|
|
346
|
+
for (const page of pages) {
|
|
347
|
+
/* eslint-disable no-await-in-loop */
|
|
348
|
+
const result = await fetchBacktracePage(session, options.queryUrl, buildQueryBody(options, page.offset, page.limit));
|
|
349
|
+
/* eslint-enable no-await-in-loop */
|
|
350
|
+
if (shouldUseFingerprintGroupQuery(options)) {
|
|
351
|
+
const groupedItems = extractGroupedFingerprintItems(result?.response);
|
|
352
|
+
allObjectIds.push(...groupedItems.map((item) => item.fingerprint || ""));
|
|
353
|
+
allSelectedValues.push(...groupedItems.map((item) => item.values));
|
|
354
|
+
} else {
|
|
355
|
+
allObjectIds.push(...extractObjectIds(result?.response?.objects));
|
|
356
|
+
allSelectedValues.push(...extractSelectedValues(result?.response?.values, options.select));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { totalRows, pageLimit, listedCount: allObjectIds.length, objectIds: allObjectIds, selectedValues: allSelectedValues };
|
|
360
|
+
}
|
|
361
|
+
async function fetchAttachmentList(session, queryUrl, objectHexId) {
|
|
362
|
+
const source = new URL(queryUrl);
|
|
363
|
+
const url = buildSiblingUrl(queryUrl, "/api/list");
|
|
364
|
+
const project = source.searchParams.get("project");
|
|
365
|
+
if (project) url.searchParams.set("project", project);
|
|
366
|
+
url.searchParams.set("object", objectHexId);
|
|
367
|
+
logStep("attachments", "requesting attachment list", url.toString());
|
|
368
|
+
return session.requestJson(url.toString(), { method: "GET" }, `attachment list ${objectHexId}`);
|
|
369
|
+
}
|
|
370
|
+
async function queryAllItems(session, options) {
|
|
371
|
+
if (options.command === "collect-all") {
|
|
372
|
+
const requestedFingerprints = getRequestedFingerprints(options);
|
|
373
|
+
const fingerprintItems = [];
|
|
374
|
+
let fingerprintPageLimit = 0;
|
|
375
|
+
if (requestedFingerprints.length === 0) {
|
|
376
|
+
const fingerprintResult = await queryFingerprintGroups(session, options);
|
|
377
|
+
fingerprintItems.push(...fingerprintResult.items);
|
|
378
|
+
fingerprintPageLimit = fingerprintResult.pageLimit;
|
|
379
|
+
} else {
|
|
380
|
+
for (const requestedFingerprint of requestedFingerprints) {
|
|
381
|
+
/* eslint-disable no-await-in-loop */
|
|
382
|
+
const fingerprintResult = await queryFingerprintGroups(session, options, requestedFingerprint);
|
|
383
|
+
fingerprintItems.push(...fingerprintResult.items);
|
|
384
|
+
fingerprintPageLimit = Math.max(fingerprintPageLimit, fingerprintResult.pageLimit);
|
|
385
|
+
/* eslint-enable no-await-in-loop */
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const uniqueFingerprintItems = Array.from(
|
|
389
|
+
new Map(fingerprintItems.map((item) => [String(item.fingerprint || item.values.fingerprint || "").trim(), item])).values(),
|
|
390
|
+
).filter((item) => String(item.fingerprint || item.values.fingerprint || "").trim());
|
|
391
|
+
const items = [];
|
|
392
|
+
let totalRows = 0;
|
|
393
|
+
let pageLimit = fingerprintPageLimit;
|
|
394
|
+
for (const fingerprintItem of uniqueFingerprintItems) {
|
|
395
|
+
const fingerprint = String(fingerprintItem.fingerprint || fingerprintItem.values.fingerprint || "").trim();
|
|
396
|
+
if (!fingerprint) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
/* eslint-disable no-await-in-loop */
|
|
400
|
+
const probe = await fetchFingerprintProbe(session, options, fingerprint);
|
|
401
|
+
if (probe.totalRows === 0) {
|
|
402
|
+
logStep("query", "fingerprint probe returned no rows", { fingerprint });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const objectResult = await fetchFingerprintObjects(session, options, fingerprint, probe.totalRows);
|
|
406
|
+
totalRows += objectResult.totalRows;
|
|
407
|
+
pageLimit = Math.max(pageLimit, objectResult.pageLimit);
|
|
408
|
+
objectResult.objects.forEach((objectItem) => {
|
|
409
|
+
items.push({
|
|
410
|
+
index: items.length + 1,
|
|
411
|
+
objectIdDecimal: objectItem.objectIdDecimal,
|
|
412
|
+
objectIdHex: objectItem.objectIdHex,
|
|
413
|
+
fingerprint,
|
|
414
|
+
values: {
|
|
415
|
+
fingerprint,
|
|
416
|
+
timestamp: objectItem.timestamp,
|
|
417
|
+
"error.message": fingerprintItem.values["error.message"] || "",
|
|
418
|
+
classifiers: fingerprintItem.values.classifiers || "",
|
|
419
|
+
"fingerprint;first_seen": fingerprintItem.values["fingerprint;first_seen"] || "",
|
|
420
|
+
"fingerprint;issues;state": fingerprintItem.values["fingerprint;issues;state"] || "",
|
|
421
|
+
_count: fingerprintItem.values._count || 0,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
/* eslint-enable no-await-in-loop */
|
|
426
|
+
}
|
|
427
|
+
logStep("query", "loaded query results", {
|
|
428
|
+
fingerprintCount: uniqueFingerprintItems.length,
|
|
429
|
+
fetchedRows: totalRows,
|
|
430
|
+
objectCount: items.length,
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
totalRows,
|
|
434
|
+
pageLimit,
|
|
435
|
+
listedCount: items.length,
|
|
436
|
+
objectIds: items.map((item) => item.objectIdHex),
|
|
437
|
+
selectedValues: items.map((item) => item.values),
|
|
438
|
+
items,
|
|
439
|
+
fingerprints: uniqueFingerprintItems,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const result = await fetchAllResults(session, options);
|
|
443
|
+
const items = result.selectedValues.map((values, index) => ({
|
|
444
|
+
index: index + 1,
|
|
445
|
+
objectIdDecimal: "",
|
|
446
|
+
objectIdHex: "",
|
|
447
|
+
fingerprint: String(values?.fingerprint || "").trim(),
|
|
448
|
+
values: values || {},
|
|
449
|
+
}));
|
|
450
|
+
logStep("query", "loaded query results", { fetchedRows: result.listedCount, objectCount: items.length });
|
|
451
|
+
return { ...result, items };
|
|
452
|
+
}
|
|
453
|
+
async function downloadItemLogs(session, item, options) {
|
|
454
|
+
const objectHexId = item.objectIdHex;
|
|
455
|
+
const fingerprint = normalizeFingerprint(item.fingerprint || item.values.fingerprint || options.fingerprint);
|
|
456
|
+
logStep("download", "preparing object download", { objectId: objectHexId, from: options.from, to: options.to, fingerprint });
|
|
457
|
+
let attachmentList = null;
|
|
458
|
+
let attachmentListError = null;
|
|
459
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
460
|
+
try {
|
|
461
|
+
/* eslint-disable no-await-in-loop */
|
|
462
|
+
attachmentList = await fetchAttachmentList(session, options.queryUrl, objectHexId);
|
|
463
|
+
/* eslint-enable no-await-in-loop */
|
|
464
|
+
attachmentListError = null;
|
|
465
|
+
break;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
attachmentListError = error;
|
|
468
|
+
logStep("attachments", "attachment list failed", {
|
|
469
|
+
objectId: objectHexId,
|
|
470
|
+
attempt,
|
|
471
|
+
maxAttempts: 5,
|
|
472
|
+
message: error instanceof Error ? error.message : String(error),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!attachmentList) {
|
|
477
|
+
logStep("download", "object skipped after attachment list retries", {
|
|
478
|
+
objectId: objectHexId,
|
|
479
|
+
fingerprint,
|
|
480
|
+
message: attachmentListError instanceof Error ? attachmentListError.message : String(attachmentListError || ""),
|
|
481
|
+
});
|
|
482
|
+
return { ...item, values: { ...item.values, fingerprint }, fingerprint, targetDir: "", downloadedFiles: [], skipped: true, failed: true };
|
|
483
|
+
}
|
|
484
|
+
const attachments = Array.isArray(attachmentList.attachments) ? attachmentList.attachments : [];
|
|
485
|
+
const targetDir = path.join(getFingerprintLogsRoot(options.storageDir, fingerprint), objectHexId);
|
|
486
|
+
await ensureDirectory(targetDir);
|
|
487
|
+
const downloadedFiles = [];
|
|
488
|
+
for (const attachment of attachments) {
|
|
489
|
+
/* eslint-disable no-await-in-loop */
|
|
490
|
+
const downloadedFile = await downloadAttachmentWithRetry(session, options.queryUrl, objectHexId, targetDir, attachment, buildSiblingUrl, 5);
|
|
491
|
+
downloadedFiles.push(downloadedFile);
|
|
492
|
+
if (typeof options.onAttachmentDownloaded === "function") {
|
|
493
|
+
options.onAttachmentDownloaded({
|
|
494
|
+
item,
|
|
495
|
+
objectId: objectHexId,
|
|
496
|
+
fingerprint,
|
|
497
|
+
attachmentName: attachment.name,
|
|
498
|
+
downloadedFile,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/* eslint-enable no-await-in-loop */
|
|
502
|
+
}
|
|
503
|
+
logStep("download", "object download completed", { objectId: objectHexId, fileCount: downloadedFiles.length, targetDir, fingerprint });
|
|
504
|
+
return { ...item, values: { ...item.values, fingerprint }, fingerprint, targetDir, downloadedFiles };
|
|
505
|
+
}
|
|
506
|
+
module.exports = { AuthenticationError, BacktraceSession, buildQueryBody, buildSiblingUrl, buildLoginUrl, withSessionToken, toHexObjectId, extractObjectIds, extractSelectedValues, mapResultsToItems, optimizeLimit, fetchBacktracePage, fetchAllResults, queryFingerprintGroups, queryFingerprintDetail, fetchAttachmentList, downloadAttachment, queryAllItems, downloadItemLogs };
|