Files
opportunities-extension/src/features/debug/content-log.js

198 lines
6.1 KiB
JavaScript

function createDebugContentLog({
previewDebugBody,
DEBUG_BODY_LIMIT,
debugState,
renderDebugPanel
}) {
function logRequestStart(details) {
const entry = {
id: debugState.requestLog.length + 1,
name: details.name,
method: details.method,
url: details.url,
headers: details.headers,
status: "pending",
startedAt: new Date(),
completedAt: null,
durationMs: null,
httpStatus: null,
statusText: "",
requestHeaders: details.headers,
requestBody: details.body || "",
metadata: details.metadata || {},
responseHeaders: {},
responseBody: "",
responseBodyTruncated: false,
error: ""
};
debugState.requestLog.push(entry);
renderDebugPanel();
return entry;
}
function logRequestSuccess(entry, response, responseBody) {
entry.status = response.ok ? "success" : "failed";
entry.completedAt = new Date();
entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime();
entry.httpStatus = response.status;
entry.statusText = response.statusText || "";
entry.responseHeaders = headersToObject(response.headers);
entry.responseBody = responseBody.value;
entry.responseBodyTruncated = responseBody.truncated;
renderDebugPanel();
}
function logRequestFailure(entry, errorMessage) {
entry.status = "failed";
entry.completedAt = new Date();
entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime();
entry.error = errorMessage;
renderDebugPanel();
}
function appendBackgroundRequestLog(entries) {
if (!Array.isArray(entries) || entries.length === 0) {
return;
}
entries.forEach((entry) => {
const existing = entry.debugKey && debugState.requestLog.find(item => item.debugKey === entry.debugKey);
if (existing && existing.status !== "pending" && entry.status === "pending") return;
const normalized = {
id: existing ? existing.id : debugState.requestLog.length + 1,
debugKey: entry.debugKey,
name: entry.name || "backgroundRequest",
method: entry.method || "GET",
url: entry.url || "",
headers: entry.requestHeaders || {},
status: entry.status || "failed",
startedAt: new Date(entry.startedAt),
completedAt: entry.completedAt ? new Date(entry.completedAt) : null,
durationMs: entry.durationMs,
httpStatus: entry.httpStatus,
statusText: entry.statusText || "",
requestHeaders: entry.requestHeaders || {},
requestBody: entry.requestBody || "",
metadata: entry.metadata || { executionContext: "background" },
responseHeaders: entry.responseHeaders || {},
responseBody: entry.responseBody || "",
responseBodyTruncated: Boolean(entry.responseBodyTruncated),
error: entry.error || ""
};
if (existing) Object.assign(existing, normalized);
else debugState.requestLog.push(normalized);
});
renderDebugPanel();
}
async function readResponseBodyForDebug(response) {
try {
const text = await response.clone().text();
return createDebugBody(text);
} catch (error) {
return {
value: `Unable to read response body: ${error.message || "unknown error"}`,
truncated: false
};
}
}
function createDebugBody(text) {
return previewDebugBody(text, DEBUG_BODY_LIMIT);
}
function headersToObject(headers) {
const headerMap = {};
headers.forEach((value, key) => {
headerMap[key] = value;
});
return headerMap;
}
function formatHeaderBlock(headers) {
const entries = Object.entries(headers);
if (entries.length === 0) {
return "(none)";
}
return entries
.map(([key, value]) => `${key}: ${debugState.debugSecretsVisible || !isSensitiveDebugKey(key)
? value
: formatHiddenDebugValue(value)}`)
.join("\n");
}
function formatDebugBody(body) {
const text = body || "(none)";
try {
const normalized = String(text).replace(/^\uFEFF/, "").trimStart()
.replace(/^\)\]\}',?\s*/, "")
.replace(/^while\s*\(1\);\s*/, "");
let value = JSON.parse(normalized);
// Some services serialize their JSON document inside a JSON string.
for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) {
try { value = JSON.parse(value); } catch { break; }
}
return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2);
} catch (error) {
return text;
}
}
function maskSensitiveDebugValue(value, parentKey = "") {
if (Array.isArray(value)) {
return value.map((item) => maskSensitiveDebugValue(item, parentKey));
}
if (!value || typeof value !== "object") {
return value;
}
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
const isSensitive = isSensitiveDebugKey(key)
|| (key === "value" && /uiaasHeader|uiassHeader/i.test(parentKey));
return [key, isSensitive
? formatHiddenDebugValue(child)
: maskSensitiveDebugValue(child, key)];
}));
}
function isSensitiveDebugKey(key) {
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key);
}
function formatHiddenDebugValue(value) {
if (value === null || value === undefined || value === "") {
return "(missing)";
}
return `(present, ${String(value).length} characters)`;
}
function formatMetadataBlock(metadata) {
const entries = Object.entries(metadata);
if (entries.length === 0) {
return "(none)";
}
return entries
.map(([key, value]) => `${key}: ${value || "(none)"}`)
.join("\n");
}
function maskToken(token) {
if (token.length <= 10) {
return "(present)";
}
return `${token.slice(0, 4)}...${token.slice(-4)}`;
}
return { logRequestStart, logRequestSuccess, logRequestFailure, appendBackgroundRequestLog, readResponseBodyForDebug, createDebugBody, headersToObject, formatHeaderBlock, formatDebugBody, maskSensitiveDebugValue, isSensitiveDebugKey, formatHiddenDebugValue, formatMetadataBlock, maskToken };
}