Adjustment to the request debug view functionality
This commit is contained in:
@@ -60,6 +60,10 @@ function createBackgroundApplication(overrides = {}) {
|
||||
storageLocalSet: (...args) => platformBackgroundApi.storageLocalSet(...args)
|
||||
});
|
||||
const fusionProducts = createFusionProducts({
|
||||
createBackgroundDebugEntry: (...args) => httpBackgroundJson.createBackgroundDebugEntry(...args),
|
||||
completeBackgroundDebugEntry: (...args) => httpBackgroundJson.completeBackgroundDebugEntry(...args),
|
||||
createDebugBody: (...args) => httpBackgroundJson.createDebugBody(...args),
|
||||
headersToObject: (...args) => httpBackgroundJson.headersToObject(...args),
|
||||
OPPTY_PRODUCTS_FIELDS: contracts.OPPTY_PRODUCTS_FIELDS,
|
||||
OPPTY_PRODUCTS_MAX_PAGES: contracts.OPPTY_PRODUCTS_MAX_PAGES,
|
||||
OPPTY_PRODUCTS_PAGE_LIMIT: contracts.OPPTY_PRODUCTS_PAGE_LIMIT,
|
||||
|
||||
@@ -287,6 +287,7 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const productsContentJobs = createProductsContentJobs({
|
||||
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
|
||||
messageTypes,
|
||||
PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID,
|
||||
PRODUCTS_MESSAGE_LISTENER_KEY: contracts.PRODUCTS_MESSAGE_LISTENER_KEY,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
82
src/features/debug/styles.css
Normal file
82
src/features/debug/styles.css
Normal 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;
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,29 +3,67 @@ function createFusionProducts({
|
||||
OPPTY_PRODUCTS_MAX_PAGES,
|
||||
OPPTY_PRODUCTS_PAGE_LIMIT,
|
||||
ORACLE_DOMAIN,
|
||||
createBackgroundDebugEntry,
|
||||
completeBackgroundDebugEntry,
|
||||
createDebugBody,
|
||||
headersToObject,
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
async function fetchAllOpptyProducts(optyNumber, accessToken) {
|
||||
async function fetchAllOpptyProducts(optyNumber, accessToken, onDebug = () => {}) {
|
||||
let offset = 0;
|
||||
let page = 0;
|
||||
const items = [];
|
||||
|
||||
while (page < OPPTY_PRODUCTS_MAX_PAGES) {
|
||||
const response = await fetch(createOpptyProductsUrl(optyNumber, offset), {
|
||||
const url = createOpptyProductsUrl(optyNumber, offset);
|
||||
const options = {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`requestOpptyProducts failed with status ${response.status}.`);
|
||||
};
|
||||
const entry = createBackgroundDebugEntry("requestOpptyProducts", url, {
|
||||
...options, headers: { Accept: "application/json", Authorization: "(present)" }
|
||||
}, new Date());
|
||||
entry.metadata = { executionContext: "background", optyNumber, offset, page: page + 1 };
|
||||
const report = () => {
|
||||
// Diagnostics must not block requests or transmit the Bearer credential.
|
||||
try { onDebug({ ...entry, metadata: { ...entry.metadata } }); } catch (_) {}
|
||||
};
|
||||
let response;
|
||||
let responseBody = { value: "", truncated: false };
|
||||
let responseData;
|
||||
report();
|
||||
try {
|
||||
response = await fetch(url, options);
|
||||
entry.httpStatus = response.status;
|
||||
entry.statusText = response.statusText || "";
|
||||
entry.responseHeaders = headersToObject(response.headers);
|
||||
Object.keys(entry.responseHeaders).forEach(key => {
|
||||
if (/authorization|cookie|token/i.test(key)) entry.responseHeaders[key] = "(present)";
|
||||
});
|
||||
entry.metadata.phase = "reading-response";
|
||||
report();
|
||||
const text = await response.text();
|
||||
responseBody = createDebugBody(text);
|
||||
if (!response.ok) throw new Error(`requestOpptyProducts failed with status ${response.status}.`);
|
||||
responseData = JSON.parse(text);
|
||||
if (!responseData || typeof responseData !== "object") throw new Error("requestOpptyProducts returned an invalid response.");
|
||||
completeBackgroundDebugEntry(entry, response, responseBody, "");
|
||||
} catch (error) {
|
||||
completeBackgroundDebugEntry(entry, response, responseBody, error.message || "Unable to load products.");
|
||||
entry.status = "failed";
|
||||
throw error;
|
||||
} finally {
|
||||
Object.keys(entry.responseHeaders).forEach(key => {
|
||||
if (/authorization|cookie|token/i.test(key)) entry.responseHeaders[key] = "(present)";
|
||||
});
|
||||
entry.metadata.phase = "complete";
|
||||
report();
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const pageItems = Array.isArray(responseData.items) ? responseData.items : [];
|
||||
items.push(...pageItems);
|
||||
page += 1;
|
||||
|
||||
@@ -5,6 +5,7 @@ function createMessageTypes() {
|
||||
refreshXsrfCookie: "opportunitiesExtension.refreshXsrfCookie",
|
||||
requestOpptyProducts: "opportunitiesExtension.requestOpptyProducts",
|
||||
opptyProductsProgress: "opportunitiesExtension.opptyProductsProgress",
|
||||
opptyProductsRequestDebug: "opportunitiesExtension.opptyProductsRequestDebug",
|
||||
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
||||
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
||||
|
||||
Reference in New Issue
Block a user