Adjustment to the request debug view functionality

This commit is contained in:
2026-09-18 16:46:47 -03:00
parent ac817a69d4
commit eed4189a07
20 changed files with 945 additions and 87 deletions

View File

@@ -56,8 +56,11 @@ function createDebugContentLog({
}
entries.forEach((entry) => {
debugState.requestLog.push({
id: debugState.requestLog.length + 1,
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 || "",
@@ -75,7 +78,9 @@ function createDebugContentLog({
responseBody: entry.responseBody || "",
responseBodyTruncated: Boolean(entry.responseBodyTruncated),
error: entry.error || ""
});
};
if (existing) Object.assign(existing, normalized);
else debugState.requestLog.push(normalized);
});
renderDebugPanel();
}

View File

@@ -8,6 +8,27 @@ function createDebugContentView({
environment
}) {
const { document } = environment;
const categories = [
["all", "All"], ["authentication", "Authentication"],
["opportunities", "Opportunities"], ["products", "Products"],
["ai", "AI"], ["consumption", "Consumption"], ["other", "Other"]
];
let selectedCategory = "all";
const expandedIds = new Set();
function getRequestCategory(entry) {
if (/^request(?:TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication";
if (entry.name === "requestOpportunities") return "opportunities";
if (entry.name === "requestOpptyProducts") return "products";
if (entry.name === "requestGenerateAiSummary") return "ai";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService)/.test(entry.name)) return "consumption";
return "other";
}
function getChronologicalRequests() {
return [...debugState.requestLog].sort((a, b) =>
(new Date(a.startedAt).getTime() || 0) - (new Date(b.startedAt).getTime() || 0) || a.id - b.id);
}
function toggleDebugPanel() {
const modal = document.getElementById(MODAL_ID);
@@ -32,6 +53,13 @@ function createDebugContentView({
return;
}
for (const item of panel.querySelectorAll("details[data-request-id]")) {
const id = item.getAttribute("data-request-id");
if (item.open) expandedIds.add(id);
else expandedIds.delete(id);
}
const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category");
const scrollTop = panel.scrollTop;
panel.textContent = "";
const panelHeader = document.createElement("header");
@@ -43,6 +71,8 @@ function createDebugContentView({
const summary = document.createElement("p");
summary.textContent = `${debugState.requestLog.length} request${debugState.requestLog.length === 1 ? "" : "s"} since modal opened.`;
const running = debugState.requestLog.filter(entry => entry.status === "pending").length;
if (running) summary.textContent += ` ${running} running.`;
heading.append(title, summary);
const visibilityButton = document.createElement("button");
@@ -58,8 +88,51 @@ function createDebugContentView({
});
panelHeader.append(heading, visibilityButton);
const entries = getChronologicalRequests();
const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key));
if (!availableCategories.some(([key]) => key === selectedCategory)) selectedCategory = "all";
const tabs = document.createElement("div");
tabs.className = "opportunities-extension-debug-tabs";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Request categories");
const tabButtons = [];
availableCategories.forEach(([key, label], index) => {
const categoryEntries = key === "all" ? entries : entries.filter(entry => getRequestCategory(entry) === key);
const pending = categoryEntries.filter(entry => entry.status === "pending").length;
const button = document.createElement("button");
button.type = "button";
button.id = `${DEBUG_PANEL_ID}-tab-${key}`;
button.setAttribute("role", "tab");
button.setAttribute("data-debug-category", key);
button.setAttribute("aria-selected", String(key === selectedCategory));
button.setAttribute("aria-controls", `${DEBUG_PANEL_ID}-requests`);
button.tabIndex = key === selectedCategory ? 0 : -1;
button.textContent = `${label} (${categoryEntries.length})${pending ? ` / ${pending} running` : ""}`;
button.addEventListener("click", () => {
selectedCategory = key;
renderDebugPanel();
});
button.addEventListener("keydown", event => {
let nextIndex;
if (event.key === "ArrowRight") nextIndex = (index + 1) % availableCategories.length;
else if (event.key === "ArrowLeft") nextIndex = (index + availableCategories.length - 1) % availableCategories.length;
else if (event.key === "Home") nextIndex = 0;
else if (event.key === "End") nextIndex = availableCategories.length - 1;
else return;
event.preventDefault();
selectedCategory = availableCategories[nextIndex][0];
renderDebugPanel();
});
tabs.append(button);
tabButtons.push(button);
});
const list = document.createElement("div");
list.className = "opportunities-extension-debug-list";
list.id = `${DEBUG_PANEL_ID}-requests`;
list.setAttribute("role", "tabpanel");
list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`);
list.tabIndex = 0;
if (debugState.requestLog.length === 0) {
const empty = document.createElement("div");
@@ -68,15 +141,20 @@ function createDebugContentView({
list.append(empty);
}
debugState.requestLog.forEach((entry) => {
entries.filter(entry => selectedCategory === "all" || getRequestCategory(entry) === selectedCategory).forEach((entry) => {
const item = document.createElement("details");
item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`;
item.setAttribute("data-request-id", String(entry.id));
item.open = expandedIds.has(String(entry.id));
const heading = document.createElement("summary");
heading.className = "opportunities-extension-debug-heading";
const name = document.createElement("strong");
name.textContent = `${entry.id}. ${entry.name}`;
if (entry.name === "requestOpptyProducts" && entry.metadata.optyNumber) {
name.textContent += ` (${entry.metadata.optyNumber}, offset ${entry.metadata.offset})`;
}
const httpStatus = document.createElement("span");
httpStatus.className = "opportunities-extension-debug-http-status";
@@ -84,11 +162,15 @@ function createDebugContentView({
const status = document.createElement("span");
status.className = "opportunities-extension-debug-status";
status.textContent = entry.status;
status.textContent = entry.status === "pending" ? "running" : entry.status;
const summaryMeta = document.createElement("span");
summaryMeta.className = "opportunities-extension-debug-summary-meta";
summaryMeta.append(httpStatus, status);
const startedTime = document.createElement("time");
startedTime.className = "opportunities-extension-debug-time";
startedTime.textContent = formatDebugTime(entry.startedAt);
startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`;
summaryMeta.append(startedTime, httpStatus, status);
heading.append(name, summaryMeta);
@@ -114,7 +196,9 @@ function createDebugContentView({
list.append(item);
});
panel.append(panelHeader, list);
panel.append(panelHeader, tabs, list);
if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true });
panel.scrollTop = scrollTop;
}
function createDebugVisibilityIcon(secretsVisible) {
@@ -170,5 +254,5 @@ function createDebugContentView({
});
}
return { toggleDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime };
return { toggleDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests };
}

View File

@@ -0,0 +1,82 @@
/* Debug navigation only; preserve the extension's existing light/dark theme. */
.opportunities-extension-debug-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #dedbd7;
}
.opportunities-extension-debug-tabs button {
max-width: 100%;
min-height: 32px;
padding: 6px 8px;
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 12px;
line-height: 1.35;
overflow-wrap: anywhere;
cursor: pointer;
}
.opportunities-extension-debug-tabs button:hover {
background: #f5f4f2;
}
.opportunities-extension-debug-tabs button[aria-selected="true"] {
border-bottom-color: #00758f;
background: #e8f4f6;
color: #006b84;
font-weight: 700;
}
.opportunities-extension-debug-tabs button:focus-visible,
.opportunities-extension-debug-list[role="tabpanel"]:focus-visible {
outline: 2px solid #00758f;
outline-offset: -2px;
}
.opportunities-extension-debug-time {
font-size: 11px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.opportunities-extension-debug-heading {
flex-wrap: wrap;
}
.opportunities-extension-debug-heading strong {
flex: 1 1 180px;
min-width: 0;
overflow-wrap: anywhere;
}
.opportunities-extension-debug-summary-meta {
flex-wrap: wrap;
justify-content: flex-end;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs {
border-bottom-color: #514c47;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs button:hover {
background: #383633;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs button[aria-selected="true"] {
background: #173f48;
border-bottom-color: #8cc8d3;
color: #bde7ee;
}
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-tabs button:focus-visible,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-list[role="tabpanel"]:focus-visible {
outline-color: #8cc8d3;
}

View File

@@ -23,6 +23,15 @@ function createProductsBackgroundJobs({
let completed = 0;
let failed = 0;
let cacheHits = 0;
let debugDelivery = Promise.resolve();
const reportRequest = entry => {
const message = {
type: messageTypes.opptyProductsRequestDebug,
requestId,
entry: { ...entry, debugKey: JSON.stringify([requestId, entry.metadata.optyNumber, entry.metadata.offset]) }
};
debugDelivery = debugDelivery.catch(() => {}).then(() => sendProductsProgress(sender, message));
};
const runWorker = async () => {
while (nextIndex < optyNumbers.length) {
@@ -31,7 +40,7 @@ function createProductsBackgroundJobs({
const optyNumber = optyNumbers[index];
try {
const result = await getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs);
const result = await getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs, reportRequest);
completed += 1;
if (result.fromCache) {
@@ -72,6 +81,7 @@ function createProductsBackgroundJobs({
const workerCount = Math.min(OPPTY_PRODUCTS_CONCURRENCY, Math.max(optyNumbers.length, 1));
await Promise.all(Array.from({ length: workerCount }, runWorker));
await debugDelivery.catch(() => {});
const state = failed > 0 ? "complete-with-errors" : "complete";
await sendProductsProgress(sender, {

View File

@@ -5,7 +5,7 @@ function createProductsCache({
storageLocalGet,
storageLocalSet
}) {
async function getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs) {
async function getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs, onDebug = () => {}) {
const cache = await getProductsCache();
const cachedEntry = cache[optyNumber];
@@ -14,7 +14,14 @@ function createProductsCache({
}
if (!productsCacheState.inFlightProducts.has(optyNumber)) {
const request = fetchAllOpptyProducts(optyNumber, accessToken)
const flight = { listeners: new Set(), entries: new Map(), promise: null };
productsCacheState.inFlightProducts.set(optyNumber, flight);
flight.promise = fetchAllOpptyProducts(optyNumber, accessToken, entry => {
flight.entries.set(entry.metadata.offset, entry);
for (const listener of flight.listeners) {
try { listener(entry); } catch (_) {}
}
})
.then(async (items) => {
const fetchedAt = Date.now();
cache[optyNumber] = {
@@ -27,15 +34,20 @@ function createProductsCache({
.finally(() => {
productsCacheState.inFlightProducts.delete(optyNumber);
});
productsCacheState.inFlightProducts.set(optyNumber, request);
}
const fetchedResult = await productsCacheState.inFlightProducts.get(optyNumber);
return {
items: fetchedResult.items,
cachedAt: fetchedResult.fetchedAt,
fromCache: false
};
const flight = productsCacheState.inFlightProducts.get(optyNumber);
// A second tab/job shares the fetch but still receives its current diagnostics.
flight.listeners.add(onDebug);
for (const entry of flight.entries.values()) {
try { onDebug(entry); } catch (_) {}
}
try {
const fetchedResult = await flight.promise;
return { items: fetchedResult.items, cachedAt: fetchedResult.fetchedAt, fromCache: false };
} finally {
flight.listeners.delete(onDebug);
}
}
function normalizeCacheMaxAge(value) {

View File

@@ -3,6 +3,7 @@ function createProductsContentJobs({
PRODUCTS_CACHE_TIMESTAMP_ID,
PRODUCTS_MESSAGE_LISTENER_KEY,
PRODUCTS_PROGRESS_ID,
appendBackgroundRequestLog,
preferencesState,
productsContentState,
renderOpportunitiesTable,
@@ -67,7 +68,14 @@ function createProductsContentJobs({
window[PRODUCTS_MESSAGE_LISTENER_KEY] = true;
runtimeApi.runtime.onMessage.addListener((message) => {
if (!message || message.type !== messageTypes.opptyProductsProgress || message.requestId !== productsContentState.currentProductsRequestId) {
if (!message || message.requestId !== productsContentState.currentProductsRequestId) {
return false;
}
if (message.type === messageTypes.opptyProductsRequestDebug) {
if (message.entry && typeof message.entry.debugKey === "string") appendBackgroundRequestLog([message.entry]);
return false;
}
if (message.type !== messageTypes.opptyProductsProgress) {
return false;
}