Visual improvements to debug mode for better information visibility

This commit is contained in:
2026-09-23 18:19:35 -03:00
parent 6c5db94aaa
commit 3ef0d2aafd
16 changed files with 862 additions and 107 deletions

View File

@@ -1,4 +1,5 @@
function createDebugContentLog({
previewDebugBody,
DEBUG_BODY_LIMIT,
debugState,
renderDebugPanel
@@ -98,17 +99,7 @@ function createDebugContentLog({
}
function createDebugBody(text) {
if (text.length > DEBUG_BODY_LIMIT) {
return {
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
truncated: true
};
}
return {
value: text || "(empty response body)",
truncated: false
};
return previewDebugBody(text, DEBUG_BODY_LIMIT);
}
function headersToObject(headers) {
@@ -137,13 +128,16 @@ function createDebugContentLog({
function formatDebugBody(body) {
const text = body || "(none)";
if (debugState.debugSecretsVisible) {
return text;
}
try {
return JSON.stringify(maskSensitiveDebugValue(JSON.parse(text)), null, 2);
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;
}

View File

@@ -5,6 +5,7 @@ function createDebugContentView({
formatDebugBody,
formatHeaderBlock,
formatMetadataBlock,
createCloseIcon,
environment
}) {
const { document } = environment;
@@ -16,6 +17,7 @@ function createDebugContentView({
];
let selectedCategory = "all";
const expandedIds = new Set();
let returnFocus = null;
function getRequestCategory(entry) {
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
@@ -45,8 +47,22 @@ function createDebugContentView({
return;
}
panel.hidden = !panel.hidden;
if (!panel.hidden) {
closeDebugPanel();
return;
}
returnFocus = document.activeElement;
panel.hidden = false;
renderDebugPanel();
panel.querySelectorAll(".opportunities-extension-debug-close-button")[0]?.focus({ preventScroll: true });
}
function closeDebugPanel() {
const panel = document.getElementById(DEBUG_PANEL_ID);
if (!panel) return;
panel.hidden = true;
if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
returnFocus = null;
}
function renderDebugPanel() {
@@ -62,14 +78,21 @@ function createDebugContentView({
else expandedIds.delete(id);
}
const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category");
const focusedControl = document.activeElement && document.activeElement.getAttribute("data-debug-focus");
const listScrollTop = panel.querySelectorAll(".opportunities-extension-debug-list")[0]?.scrollTop || 0;
const scrollTop = panel.scrollTop;
panel.textContent = "";
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
panel.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-title`);
const focusTargets = new Map();
const panelHeader = document.createElement("header");
panelHeader.className = "opportunities-extension-debug-panel-header";
const heading = document.createElement("div");
const title = document.createElement("h2");
title.id = `${DEBUG_PANEL_ID}-title`;
title.textContent = "Debug Requests";
const summary = document.createElement("p");
@@ -81,6 +104,8 @@ function createDebugContentView({
const visibilityButton = document.createElement("button");
visibilityButton.type = "button";
visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button";
visibilityButton.setAttribute("data-debug-focus", "visibility");
focusTargets.set("visibility", visibilityButton);
visibilityButton.setAttribute("aria-pressed", String(debugState.debugSecretsVisible));
visibilityButton.setAttribute("aria-label", debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values");
visibilityButton.title = debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values";
@@ -89,7 +114,19 @@ function createDebugContentView({
debugState.debugSecretsVisible = !debugState.debugSecretsVisible;
renderDebugPanel();
});
panelHeader.append(heading, visibilityButton);
const closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "opportunities-extension-icon-button opportunities-extension-debug-close-button";
closeButton.title = "Close debug";
closeButton.setAttribute("aria-label", "Close debug");
closeButton.setAttribute("data-debug-focus", "close");
closeButton.append(createCloseIcon());
closeButton.addEventListener("click", closeDebugPanel);
focusTargets.set("close", closeButton);
const actions = document.createElement("div");
actions.className = "opportunities-extension-debug-actions";
actions.append(visibilityButton, closeButton);
panelHeader.append(heading, actions);
const entries = getChronologicalRequests();
const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key));
@@ -136,6 +173,9 @@ function createDebugContentView({
list.setAttribute("role", "tabpanel");
list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`);
list.tabIndex = 0;
list.setAttribute("data-debug-focus", "list");
focusTargets.set("list", list);
const summaries = [];
if (debugState.requestLog.length === 0) {
const empty = document.createElement("div");
@@ -152,6 +192,9 @@ function createDebugContentView({
const heading = document.createElement("summary");
heading.className = "opportunities-extension-debug-heading";
heading.setAttribute("data-debug-focus", `request-${entry.id}`);
focusTargets.set(`request-${entry.id}`, heading);
summaries.push(heading);
const name = document.createElement("strong");
name.textContent = `${entry.id}. ${entry.name}`;
@@ -162,6 +205,7 @@ function createDebugContentView({
const httpStatus = document.createElement("span");
httpStatus.className = "opportunities-extension-debug-http-status";
httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending";
httpStatus.setAttribute("data-http-class", entry.httpStatus ? String(Math.floor(entry.httpStatus / 100)) : "pending");
const status = document.createElement("span");
status.className = "opportunities-extension-debug-status";
@@ -173,7 +217,11 @@ function createDebugContentView({
startedTime.className = "opportunities-extension-debug-time";
startedTime.textContent = formatDebugTime(entry.startedAt);
startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`;
summaryMeta.append(startedTime, httpStatus, status);
const method = document.createElement("span");
method.className = "opportunities-extension-debug-method";
method.textContent = entry.method || "GET";
method.setAttribute("data-method", method.textContent.toUpperCase());
summaryMeta.append(startedTime, method, httpStatus, status);
heading.append(name, summaryMeta);
@@ -200,7 +248,23 @@ function createDebugContentView({
});
panel.append(panelHeader, tabs, list);
panel.onkeydown = event => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
closeDebugPanel();
} else if (event.key === "Tab") {
const controls = [visibilityButton, closeButton, ...tabButtons.filter(button => button.tabIndex === 0), list, ...summaries];
const index = controls.indexOf(document.activeElement);
if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) {
event.preventDefault();
controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true });
}
}
};
if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true });
else if (focusedControl) focusTargets.get(focusedControl)?.focus({ preventScroll: true });
list.scrollTop = listScrollTop;
panel.scrollTop = scrollTop;
}
@@ -240,15 +304,41 @@ function createDebugContentView({
function appendDebugBlock(container, label, value) {
const block = document.createElement("section");
block.className = "opportunities-extension-debug-block";
block.setAttribute("data-direction", label.startsWith("Response") ? "response" : "request");
const heading = document.createElement("h3");
const content = document.createElement("pre");
heading.textContent = label;
content.textContent = value;
appendJsonTokens(content, value);
block.append(heading, content);
container.append(block);
}
function appendJsonTokens(content, value) {
const text = String(value ?? "");
try { JSON.parse(text); } catch { content.textContent = text; return; }
// Highlight only already-masked display text; never interpret response content as HTML.
const tokens = [...text.matchAll(/"(?:[^"\\]|\\.)*"\s*:|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g)];
let offset = 0;
const append = (textValue, kind) => {
if (!textValue) return;
const span = document.createElement("span");
span.textContent = textValue;
if (kind) span.className = `opportunities-extension-debug-json-${kind}`;
content.append(span);
};
tokens.forEach(token => {
append(text.slice(offset, token.index));
const value = token[0];
const kind = value.startsWith('"') ? value.endsWith(":") ? "key" : "string"
: /^(true|false|null)$/.test(value) ? "literal" : "number";
append(value, kind);
offset = token.index + value.length;
});
append(text.slice(offset));
}
function formatDebugTime(date) {
return date.toLocaleTimeString(undefined, {
hour: "2-digit",
@@ -257,5 +347,5 @@ function createDebugContentView({
});
}
return { toggleDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests };
return { toggleDebugPanel, closeDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests };
}

View File

@@ -1,4 +1,130 @@
/* Debug navigation only; preserve the extension's existing light/dark theme. */
/* Full-page diagnostics, scoped to the extension and its current theme. */
.opportunities-extension-modal .opportunities-extension-debug-panel {
--debug-surface: #fff;
--debug-code: #f5f6f7;
--debug-text: #312d2a;
--debug-muted: #5f5a55;
--debug-border: #dedbd7;
--debug-success: #22613b;
--debug-success-bg: #e1f1e5;
--debug-failed: #a12721;
--debug-failed-bg: #fce8e6;
--debug-pending: #805000;
--debug-pending-bg: #fff1ce;
--debug-info: #075e87;
--debug-info-bg: #e5f1fa;
--debug-key: #075e87;
--debug-string: #22613b;
--debug-number: #865000;
--debug-literal: #73429b;
position: fixed;
inset: 0;
z-index: 50;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
width: auto;
height: auto;
max-height: none;
min-width: 0;
box-sizing: border-box;
overflow: hidden;
margin: 0;
padding: 20px;
border: 0;
border-radius: 0;
background: var(--debug-surface);
color: var(--debug-text);
color-scheme: light;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel {
--debug-surface: #292827;
--debug-code: #222120;
--debug-text: #f6f4f2;
--debug-muted: #c9c5c1;
--debug-border: #514c47;
--debug-success: #a9e3bd;
--debug-success-bg: #214d34;
--debug-failed: #ffb4ab;
--debug-failed-bg: #582c2a;
--debug-pending: #ffe099;
--debug-pending-bg: #59441e;
--debug-info: #a8d9ff;
--debug-info-bg: #233f52;
--debug-key: #8ed6f0;
--debug-string: #a9e3bd;
--debug-number: #ffe099;
--debug-literal: #d6b7f1;
color: #f6f4f2;
color-scheme: dark;
}
.opportunities-extension-debug-actions { display: flex; gap: 8px; flex: 0 0 auto; }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-list {
overflow: auto;
min-height: 0;
min-width: 0;
align-content: start;
scrollbar-gutter: stable;
padding: 2px 8px 12px 2px;
}
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item {
--debug-state: var(--debug-info);
--debug-state-bg: var(--debug-info-bg);
border-color: var(--debug-border);
border-left-color: var(--debug-state);
background: var(--debug-surface);
min-width: 0;
}
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-success { --debug-state: var(--debug-success); --debug-state-bg: var(--debug-success-bg); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-failed { --debug-state: var(--debug-failed); --debug-state-bg: var(--debug-failed-bg); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-pending { --debug-state: var(--debug-pending); --debug-state-bg: var(--debug-pending-bg); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-status {
color: var(--debug-state);
background: var(--debug-state-bg);
}
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status,
.opportunities-extension-debug-method {
color: var(--debug-info);
background: var(--debug-info-bg);
}
.opportunities-extension-debug-method {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 4px;
font: 700 11px/1.2 Consolas, monospace;
}
.opportunities-extension-debug-method[data-method="POST"] { color: var(--debug-literal); background: var(--debug-code); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="2"] { color: var(--debug-success); background: var(--debug-success-bg); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="4"],
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="5"] { color: var(--debug-failed); background: var(--debug-failed-bg); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class="pending"] { color: var(--debug-pending); background: var(--debug-pending-bg); }
.opportunities-extension-debug-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding: 0 14px 16px; }
.opportunities-extension-debug-block { min-width: 0; border-left: 2px solid var(--debug-info); padding-left: 10px; }
.opportunities-extension-debug-block[data-direction="response"] { border-left-color: var(--debug-success); }
.opportunities-extension-modal .opportunities-extension-debug-block h3 { margin: 0 0 8px; color: var(--debug-info); font-size: 12px; }
.opportunities-extension-modal .opportunities-extension-debug-block[data-direction="response"] h3 { color: var(--debug-success); }
.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item pre {
min-width: 0;
margin: 0;
padding: 12px;
max-height: 380px;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--debug-text);
background: var(--debug-code);
line-height: 1.6;
tab-size: 2;
}
.opportunities-extension-debug-json-key { color: var(--debug-key); }
.opportunities-extension-debug-json-string { color: var(--debug-string); }
.opportunities-extension-debug-json-number { color: var(--debug-number); }
.opportunities-extension-debug-json-literal { color: var(--debug-literal); }
@media (max-width: 760px) {
.opportunities-extension-modal .opportunities-extension-debug-panel { padding: 12px; }
.opportunities-extension-debug-details { grid-template-columns: minmax(0, 1fr); }
}
.opportunities-extension-debug-tabs {
display: flex;
flex-wrap: wrap;
@@ -62,6 +188,14 @@
justify-content: flex-end;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs button {
color: #f6f4f2;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading::before {
color: #c9c5c1;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs {
border-bottom-color: #514c47;
}