Fix silent failure when loading campaign metrics

carregarMetrics()/carregarMetricsData() swallowed fetch errors with a bare
catch, leaving the metrics card blank with no indication anything went
wrong (same class of issue already fixed for pending replies). Now shows
an explicit error message and toasts.

Found via a full-project audit.
This commit is contained in:
Gustavo
2026-07-31 22:22:51 -03:00
parent 3be769408c
commit b34d9294c6
2 changed files with 37 additions and 4 deletions
+11 -4
View File
@@ -267,10 +267,10 @@ window.campanhasInit = window.campanhasInit || (() => {
}
async function carregarMetrics() {
const filters = el('metrics-filters');
const display = el('campaign-metrics');
try {
const campaigns = await API.get('/api/campaigns').then((r) => r.campaigns).catch(() => []);
const filters = el('metrics-filters');
const display = el('campaign-metrics');
if (!campaigns.length) {
filters.replaceChildren(h('p', { class: 'muted' }, 'Sem campanhas para métricas.'));
@@ -297,7 +297,11 @@ window.campanhasInit = window.campanhasInit || (() => {
);
display.replaceChildren(h('p', { class: 'muted' }, 'Selecione uma campanha e clique em "Ver fila" para ver métricas detalhadas.'));
} catch { /* silêncio */ }
} catch (error) {
filters.replaceChildren(h('p', { class: 'muted' }, 'Erro ao carregar métricas.'));
display.replaceChildren();
toast(`Erro ao carregar métricas: ${error.message}`);
}
}
async function carregarMetricsData() {
@@ -336,7 +340,10 @@ window.campanhasInit = window.campanhasInit || (() => {
: null
].filter(Boolean)
);
} catch { /* silêncio */ }
} catch (error) {
el('campaign-metrics').replaceChildren(h('p', { class: 'muted' }, 'Erro ao carregar métricas.'));
toast(`Erro ao carregar métricas: ${error.message}`);
}
}
// Todos os listeners abaixo sao anexados UMA VEZ, aqui, sobre botoes fixos e
+26
View File
@@ -650,6 +650,32 @@ test('metrics section contains segmento and variant filters', async () => {
assert.match(t, /Variante/);
});
// Regressao: uma falha ao buscar /metrics nao pode deixar o card de metricas
// mudo sem nenhum indicio de que algo deu errado.
test('metrics fetch failure shows an error state and toasts, not a silent no-op', async () => {
const app = await executeApp();
const campaigns = [{ id: 1, code: 'gdelivery-food', name: 'G-Delivery', produto: 'G-Delivery', status: 'running', pause_reason: null, week_number: 1, approved_daily_limit: 10, segments: ['restaurantes'], manual_usage: 0, auto_usage: 0 }];
await renderCampaign(app, campaigns);
app.context.API.get = async (url) => {
if (url.includes('/metrics')) throw new Error('falha de rede');
if (url === '/api/campaigns') return { campaigns };
if (url.includes('/queue')) return { queue: [] };
throw new Error(`GET inesperado: ${url}`);
};
const verFilaBtns = Array.from(app.element('campaign-cards').children[0].children)
.flatMap((row) => row.children || [])
.filter((node) => /Ver fila/.test(nodeText(node)));
await verFilaBtns[0].click();
const segmentoSelect = app.element('filtro-metrics-segmento');
await segmentoSelect.listeners.change();
const t = nodeText(app.element('campaign-metrics'));
assert.match(t, /erro/i);
assert.ok(app.toasts.some((message) => /erro ao carregar m.tricas/i.test(message)), app.toasts.join(' | '));
});
test('metrics stats render using the real /metrics response shape ({ metrics: { previewed, approved, sent, replies, firstResponseTimeAvg } })', async () => {
const app = await executeApp();
const campaigns = [{ id: 1, code: 'gdelivery-food', name: 'G-Delivery', produto: 'G-Delivery', status: 'running', pause_reason: null, week_number: 1, approved_daily_limit: 10, segments: ['restaurantes'], manual_usage: 0, auto_usage: 0 }];