feat: añadir métricas con labels y calcular alias_coverage_ratio

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent cbbadae965
commit d25efb097c

@ -282,6 +282,15 @@ export class WebhookServer {
try {
const nAlt = normalizeWhatsAppId(pAlt);
const n = normalizeWhatsAppId(p);
if (process.env.NODE_ENV !== 'test') {
console.log('[A0] message.key participants', {
participant: p,
participantAlt: pAlt,
normalized_participant: n,
normalized_participantAlt: nAlt,
alias_upsert: !!(nAlt && n && nAlt !== n)
});
}
if (nAlt && n && nAlt !== n) {
IdentityService.upsertAlias(p, pAlt, 'message.key');
}
@ -504,6 +513,12 @@ export class WebhookServer {
const PORT = process.env.PORT || '3007';
console.log('✅ Environment variables validated');
// A0: pre-crear contadores para que aparezcan en /metrics
try {
Metrics.inc('onboarding_prompts_sent_total', 0);
Metrics.inc('onboarding_prompts_skipped_total', 0);
Metrics.inc('onboarding_assign_failures_total', 0);
} catch {}
if (process.env.NODE_ENV !== 'test') {
try {

@ -1041,6 +1041,16 @@ export class CommandService {
...normalizedFromAtTokens
]));
if (process.env.NODE_ENV !== 'test') {
console.log('[A0] /t nueva menciones', {
context_mentions: context.mentions || [],
mentions_normalized: mentionsNormalizedFromContext,
at_tokens: atTokenCandidates,
at_normalized: normalizedFromAtTokens,
combined: combinedAssigneeCandidates
});
}
const { description, dueDate } = this.parseNueva(trimmed, mentionsNormalizedFromContext);
// Asegurar creador

@ -67,6 +67,9 @@ export class ContactsService {
const rawJid = typeof rec?.jid === 'string' ? rec.jid : null;
if (rawId && rawJid && rawId.includes('@lid') && rawJid.endsWith('@s.whatsapp.net')) {
IdentityService.upsertAlias(rawId, rawJid, 'contacts.update');
if (process.env.NODE_ENV !== 'test') {
console.log('[A0] contacts.update learned alias', { alias: rawId, jid: rawJid });
}
}
} catch {}

@ -629,9 +629,45 @@ export class GroupSyncService {
}
})();
try { this.computeAndPublishAliasCoverage(groupId); } catch {}
return { added, updated, deactivated };
}
private static computeAndPublishAliasCoverage(groupId: string): void {
try {
const rows = this.dbInstance.prepare(`
SELECT user_id
FROM group_members
WHERE group_id = ? AND is_active = 1
`).all(groupId) as Array<{ user_id: string }>;
const total = rows.length;
if (total === 0) {
try { Metrics.set('alias_coverage_ratio', 1, { group_id: groupId }); } catch {}
return;
}
let resolvable = 0;
for (const r of rows) {
const uid = String(r.user_id || '');
if (/^\d+$/.test(uid)) {
resolvable++;
continue;
}
try {
const resolved = IdentityService.resolveAliasOrNull(uid);
if (resolved && /^\d+$/.test(resolved)) {
resolvable++;
}
} catch {}
}
const ratio = Math.max(0, Math.min(1, total > 0 ? resolvable / total : 1));
try { Metrics.set('alias_coverage_ratio', ratio, { group_id: groupId }); } catch {}
} catch (e) {
console.warn('⚠️ No se pudo calcular alias_coverage_ratio para', groupId, e);
}
}
/**
* Sync members for all active groups by calling Evolution API and reconciling.
* Devuelve contadores agregados.

@ -1,6 +1,8 @@
export class Metrics {
private static counters = new Map<string, number>();
private static gauges = new Map<string, number>();
private static labeledCounters = new Map<string, Map<string, number>>();
private static labeledGauges = new Map<string, Map<string, number>>();
static enabled(): boolean {
if (typeof process !== 'undefined' && process.env) {
@ -13,14 +15,48 @@ export class Metrics {
return true;
}
static inc(name: string, value: number = 1): void {
private static serializeLabels(labels: Record<string, string> | undefined | null): string | null {
if (!labels) return null;
const keys = Object.keys(labels).sort();
const parts = keys.map(k => {
const val = String(labels[k] ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `${k}="${val}"`;
});
return parts.join(',');
}
private static ensureLabeledMap(map: Map<string, Map<string, number>>, name: string): Map<string, number> {
let inner = map.get(name);
if (!inner) {
inner = new Map<string, number>();
map.set(name, inner);
}
return inner;
}
static inc(name: string, value: number = 1, labels?: Record<string, string>): void {
if (!this.enabled()) return;
if (labels && Object.keys(labels).length > 0) {
const key = this.serializeLabels(labels);
if (!key) return;
const inner = this.ensureLabeledMap(this.labeledCounters, name);
const v = inner.get(key) || 0;
inner.set(key, v + value);
return;
}
const v = this.counters.get(name) || 0;
this.counters.set(name, v + value);
}
static set(name: string, value: number): void {
static set(name: string, value: number, labels?: Record<string, string>): void {
if (!this.enabled()) return;
if (labels && Object.keys(labels).length > 0) {
const key = this.serializeLabels(labels);
if (!key) return;
const inner = this.ensureLabeledMap(this.labeledGauges, name);
inner.set(key, value);
return;
}
this.gauges.set(name, value);
}
@ -35,6 +71,12 @@ export class Metrics {
const json = {
counters: Object.fromEntries(this.counters.entries()),
gauges: Object.fromEntries(this.gauges.entries()),
labeledCounters: Object.fromEntries(
Array.from(this.labeledCounters.entries()).map(([name, inner]) => [name, Object.fromEntries(inner.entries())])
),
labeledGauges: Object.fromEntries(
Array.from(this.labeledGauges.entries()).map(([name, inner]) => [name, Object.fromEntries(inner.entries())])
)
};
return JSON.stringify(json);
}
@ -43,15 +85,29 @@ export class Metrics {
lines.push(`# TYPE ${k} counter`);
lines.push(`${k} ${v}`);
}
for (const [name, inner] of this.labeledCounters.entries()) {
lines.push(`# TYPE ${name} counter`);
for (const [labelKey, v] of inner.entries()) {
lines.push(`${name}{${labelKey}} ${v}`);
}
}
for (const [k, v] of this.gauges.entries()) {
lines.push(`# TYPE ${k} gauge`);
lines.push(`${k} ${v}`);
}
for (const [name, inner] of this.labeledGauges.entries()) {
lines.push(`# TYPE ${name} gauge`);
for (const [labelKey, v] of inner.entries()) {
lines.push(`${name}{${labelKey}} ${v}`);
}
}
return lines.join('\n') + '\n';
}
static reset(): void {
this.counters.clear();
this.gauges.clear();
this.labeledCounters.clear();
this.labeledGauges.clear();
}
}

Loading…
Cancel
Save