refactor(logging): streamline whatsapp console output

This commit is contained in:
Peter Steinberger
2025-12-21 17:36:24 +00:00
parent f1202ff152
commit 52e7a4456a
5 changed files with 132 additions and 96 deletions

View File

@@ -63,9 +63,7 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
resolved, resolved,
}; };
logServer.info( logServer.info(`Browser control listening on http://127.0.0.1:${port}/`);
`🦞 clawd browser control listening on http://127.0.0.1:${port}/`,
);
return state; return state;
} }

View File

@@ -253,6 +253,14 @@ export function routeLogsToStderr(): void {
forceConsoleToStderr = true; forceConsoleToStderr = true;
} }
const SUPPRESSED_CONSOLE_PREFIXES = ["Closing session:"] as const;
function shouldSuppressConsoleMessage(message: string): boolean {
return SUPPRESSED_CONSOLE_PREFIXES.some((prefix) =>
message.startsWith(prefix),
);
}
/** /**
* Route console.* calls through pino while still emitting to stdout/stderr. * Route console.* calls through pino while still emitting to stdout/stderr.
* This keeps user-facing output unchanged but guarantees every console call is captured in log files. * This keeps user-facing output unchanged but guarantees every console call is captured in log files.
@@ -282,6 +290,7 @@ export function enableConsoleCapture(): void {
(level: Level, orig: (...args: unknown[]) => void) => (level: Level, orig: (...args: unknown[]) => void) =>
(...args: unknown[]) => { (...args: unknown[]) => {
const formatted = util.format(...args); const formatted = util.format(...args);
if (shouldSuppressConsoleMessage(formatted)) return;
try { try {
// Map console levels to pino // Map console levels to pino
if (level === "trace") { if (level === "trace") {
@@ -339,10 +348,18 @@ function shouldLogToConsole(level: Level, settings: ConsoleSettings): boolean {
type ChalkInstance = InstanceType<typeof Chalk>; type ChalkInstance = InstanceType<typeof Chalk>;
function isRichConsoleEnv(): boolean {
const term = (process.env.TERM ?? "").toLowerCase();
if (process.env.COLORTERM || process.env.TERM_PROGRAM) return true;
return term.length > 0 && term !== "dumb";
}
function getColorForConsole(): ChalkInstance { function getColorForConsole(): ChalkInstance {
if (process.env.NO_COLOR) return new Chalk({ level: 0 }); if (process.env.NO_COLOR) return new Chalk({ level: 0 });
const hasTty = Boolean(process.stdout.isTTY || process.stderr.isTTY); const hasTty = Boolean(process.stdout.isTTY || process.stderr.isTTY);
return hasTty ? new Chalk({ level: 1 }) : new Chalk({ level: 0 }); return hasTty || isRichConsoleEnv()
? new Chalk({ level: 1 })
: new Chalk({ level: 0 });
} }
const SUBSYSTEM_COLORS = [ const SUBSYSTEM_COLORS = [

View File

@@ -12,11 +12,10 @@ import {
saveSessionStore, saveSessionStore,
updateLastRoute, updateLastRoute,
} from "../config/sessions.js"; } from "../config/sessions.js";
import { danger, isVerbose, logVerbose, success } from "../globals.js"; import { isVerbose, logVerbose } from "../globals.js";
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js"; import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
import { enqueueSystemEvent } from "../infra/system-events.js"; import { enqueueSystemEvent } from "../infra/system-events.js";
import { logInfo } from "../logger.js"; import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { getChildLogger } from "../logging.js";
import { getQueueSize } from "../process/command-queue.js"; import { getQueueSize } from "../process/command-queue.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js"; import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js";
@@ -38,6 +37,10 @@ import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
const WEB_TEXT_LIMIT = 4000; const WEB_TEXT_LIMIT = 4000;
const DEFAULT_GROUP_HISTORY_LIMIT = 50; const DEFAULT_GROUP_HISTORY_LIMIT = 50;
const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
const whatsappInboundLog = whatsappLog.child("inbound");
const whatsappOutboundLog = whatsappLog.child("outbound");
const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
let heartbeatsEnabled = true; let heartbeatsEnabled = true;
export function setHeartbeatsEnabled(enabled: boolean) { export function setHeartbeatsEnabled(enabled: boolean) {
@@ -209,7 +212,6 @@ export async function runWebHeartbeatOnce(opts: {
to: string; to: string;
verbose?: boolean; verbose?: boolean;
replyResolver?: typeof getReplyFromConfig; replyResolver?: typeof getReplyFromConfig;
runtime?: RuntimeEnv;
sender?: typeof sendMessageWhatsApp; sender?: typeof sendMessageWhatsApp;
sessionId?: string; sessionId?: string;
overrideBody?: string; overrideBody?: string;
@@ -223,7 +225,6 @@ export async function runWebHeartbeatOnce(opts: {
overrideBody, overrideBody,
dryRun = false, dryRun = false,
} = opts; } = opts;
const _runtime = opts.runtime ?? defaultRuntime;
const replyResolver = opts.replyResolver ?? getReplyFromConfig; const replyResolver = opts.replyResolver ?? getReplyFromConfig;
const sender = opts.sender ?? sendWithIpcFallback; const sender = opts.sender ?? sendWithIpcFallback;
const runId = newConnectionId(); const runId = newConnectionId();
@@ -269,10 +270,8 @@ export async function runWebHeartbeatOnce(opts: {
try { try {
if (overrideBody) { if (overrideBody) {
if (dryRun) { if (dryRun) {
console.log( whatsappHeartbeatLog.info(
success( `[dry-run] web send -> ${to}: ${elide(overrideBody.trim(), 200)} (manual message)`,
`[dry-run] web send -> ${to}: ${overrideBody.trim()} (manual message)`,
),
); );
return; return;
} }
@@ -292,10 +291,8 @@ export async function runWebHeartbeatOnce(opts: {
}, },
"manual heartbeat message sent", "manual heartbeat message sent",
); );
console.log( whatsappHeartbeatLog.info(
success( `manual heartbeat sent to ${to} (id ${sendResult.messageId})`,
`sent manual message to ${to} (web), id ${sendResult.messageId}`,
),
); );
return; return;
} }
@@ -328,7 +325,9 @@ export async function runWebHeartbeatOnce(opts: {
}, },
"heartbeat skipped", "heartbeat skipped",
); );
if (verbose) console.log(success("heartbeat: ok (empty reply)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (empty reply)");
}
emitHeartbeatEvent({ status: "ok-empty", to }); emitHeartbeatEvent({ status: "ok-empty", to });
return; return;
} }
@@ -350,7 +349,9 @@ export async function runWebHeartbeatOnce(opts: {
{ to, reason: "heartbeat-token", rawLength: replyPayload.text?.length }, { to, reason: "heartbeat-token", rawLength: replyPayload.text?.length },
"heartbeat skipped", "heartbeat skipped",
); );
console.log(success("heartbeat: ok (HEARTBEAT_OK)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (HEARTBEAT_OK)");
}
emitHeartbeatEvent({ status: "ok-token", to }); emitHeartbeatEvent({ status: "ok-token", to });
return; return;
} }
@@ -368,8 +369,8 @@ export async function runWebHeartbeatOnce(opts: {
{ to, reason: "dry-run", chars: finalText.length }, { to, reason: "dry-run", chars: finalText.length },
"heartbeat dry-run", "heartbeat dry-run",
); );
console.log( whatsappHeartbeatLog.info(
success(`[dry-run] heartbeat -> ${to}: ${finalText.slice(0, 200)}`), `[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`,
); );
return; return;
} }
@@ -390,11 +391,11 @@ export async function runWebHeartbeatOnce(opts: {
}, },
"heartbeat sent", "heartbeat sent",
); );
console.log(success(`heartbeat: alert sent to ${to}`)); whatsappHeartbeatLog.info(`heartbeat alert sent to ${to}`);
} catch (err) { } catch (err) {
const reason = String(err); const reason = String(err);
heartbeatLogger.warn({ to, error: reason }, "heartbeat failed"); heartbeatLogger.warn({ to, error: reason }, "heartbeat failed");
console.log(danger(`heartbeat: failed - ${reason}`)); whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
emitHeartbeatEvent({ status: "failed", to, reason: String(err) }); emitHeartbeatEvent({ status: "failed", to, reason: String(err) });
throw err; throw err;
} }
@@ -516,7 +517,6 @@ async function deliverWebReply(params: {
msg: WebInboundMsg; msg: WebInboundMsg;
maxMediaBytes: number; maxMediaBytes: number;
replyLogger: ReturnType<typeof getChildLogger>; replyLogger: ReturnType<typeof getChildLogger>;
runtime: RuntimeEnv;
connectionId?: string; connectionId?: string;
skipLog?: boolean; skipLog?: boolean;
}) { }) {
@@ -525,7 +525,6 @@ async function deliverWebReply(params: {
msg, msg,
maxMediaBytes, maxMediaBytes,
replyLogger, replyLogger,
runtime,
connectionId, connectionId,
skipLog, skipLog,
} = params; } = params;
@@ -576,9 +575,9 @@ async function deliverWebReply(params: {
await sendWithRetry(() => msg.reply(chunk), "text"); await sendWithRetry(() => msg.reply(chunk), "text");
} }
if (!skipLog) { if (!skipLog) {
logInfo( const durationMs = Date.now() - replyStarted;
`✅ Sent web reply to ${msg.from} (${(Date.now() - replyStarted).toFixed(0)}ms)`, whatsappOutboundLog.info(
runtime, `Sent reply to ${msg.from} (${durationMs.toFixed(0)}ms)`,
); );
} }
replyLogger.info( replyLogger.info(
@@ -659,9 +658,8 @@ async function deliverWebReply(params: {
"media:document", "media:document",
); );
} }
logInfo( whatsappOutboundLog.info(
`Sent web media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`, `Sent media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`,
runtime,
); );
replyLogger.info( replyLogger.info(
{ {
@@ -678,8 +676,8 @@ async function deliverWebReply(params: {
"auto-reply sent (media)", "auto-reply sent (media)",
); );
} catch (err) { } catch (err) {
console.error( whatsappOutboundLog.error(
danger(`Failed sending web media to ${msg.from}: ${String(err)}`), `Failed sending web media to ${msg.from}: ${String(err)}`,
); );
replyLogger.warn({ err, mediaUrl }, "failed to send web media reply"); replyLogger.warn({ err, mediaUrl }, "failed to send web media reply");
if (index === 0) { if (index === 0) {
@@ -693,7 +691,9 @@ async function deliverWebReply(params: {
].filter(Boolean); ].filter(Boolean);
const fallbackText = fallbackTextParts.join("\n"); const fallbackText = fallbackTextParts.join("\n");
if (fallbackText) { if (fallbackText) {
console.log(`⚠️ Media skipped; sent text-only to ${msg.from}`); whatsappOutboundLog.warn(
`Media skipped; sent text-only to ${msg.from}`,
);
await msg.reply(fallbackText); await msg.reply(fallbackText);
} }
} }
@@ -892,13 +892,14 @@ export async function monitorWebProvider(
"inbound web message", "inbound web message",
); );
const tsDisplay = msg.timestamp
? new Date(msg.timestamp).toISOString()
: new Date().toISOString();
const fromDisplay = msg.chatType === "group" ? conversationId : msg.from; const fromDisplay = msg.chatType === "group" ? conversationId : msg.from;
console.log( const kindLabel = msg.mediaType ? `, ${msg.mediaType}` : "";
`\n[${tsDisplay}] ${fromDisplay} -> ${msg.to}: ${combinedBody}`, whatsappInboundLog.info(
`Inbound message ${fromDisplay} -> ${msg.to} (${msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
); );
if (isVerbose()) {
whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
}
if (msg.chatType !== "group") { if (msg.chatType !== "group") {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.inbound?.session;
@@ -957,7 +958,6 @@ export async function monitorWebProvider(
msg, msg,
maxMediaBytes, maxMediaBytes,
replyLogger, replyLogger,
runtime,
connectionId, connectionId,
skipLog: true, skipLog: true,
}); });
@@ -970,12 +970,8 @@ export async function monitorWebProvider(
} }
}) })
.catch((err) => { .catch((err) => {
console.error( whatsappOutboundLog.error(
danger( `Failed sending web tool update to ${msg.from ?? conversationId}: ${String(err)}`,
`Failed sending web tool update to ${msg.from ?? conversationId}: ${String(
err,
)}`,
),
); );
}); });
}; };
@@ -1031,7 +1027,6 @@ export async function monitorWebProvider(
msg, msg,
maxMediaBytes, maxMediaBytes,
replyLogger, replyLogger,
runtime,
connectionId, connectionId,
}); });
@@ -1049,24 +1044,24 @@ export async function monitorWebProvider(
const fromDisplay = const fromDisplay =
msg.chatType === "group" ? conversationId : (msg.from ?? "unknown"); msg.chatType === "group" ? conversationId : (msg.from ?? "unknown");
const hasMedia = Boolean(
replyPayload.mediaUrl || replyPayload.mediaUrls?.length,
);
whatsappOutboundLog.info(
`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`,
);
if (isVerbose()) { if (isVerbose()) {
console.log( const preview =
success( replyPayload.text != null
`↩️ Auto-replied to ${fromDisplay} (web${replyPayload.mediaUrl || replyPayload.mediaUrls?.length ? ", media" : ""})`, ? elide(replyPayload.text, 400)
), : "<media>";
); whatsappOutboundLog.debug(
} else { `Reply body: ${preview}${hasMedia ? " (media)" : ""}`,
console.log(
success(
`↩️ ${replyPayload.text ?? "<media>"}${replyPayload.mediaUrl || replyPayload.mediaUrls?.length ? " (media)" : ""}`,
),
); );
} }
} catch (err) { } catch (err) {
console.error( whatsappOutboundLog.error(
danger( `Failed sending web auto-reply to ${msg.from ?? conversationId}: ${String(err)}`,
`Failed sending web auto-reply to ${msg.from ?? conversationId}: ${String(err)}`,
),
); );
} }
} }
@@ -1090,7 +1085,9 @@ export async function monitorWebProvider(
// Skip if this is a message we just sent (echo detection) // Skip if this is a message we just sent (echo detection)
if (recentlySent.has(msg.body)) { if (recentlySent.has(msg.body)) {
console.log(`⏭️ Skipping echo: detected recently sent message`); whatsappInboundLog.debug(
"Skipping echo: detected recently sent message",
);
logVerbose( logVerbose(
`Skipping auto-reply: detected echo (message matches recently sent text)`, `Skipping auto-reply: detected echo (message matches recently sent text)`,
); );
@@ -1211,8 +1208,8 @@ export async function monitorWebProvider(
}, },
"Message timeout detected - forcing reconnect", "Message timeout detected - forcing reconnect",
); );
console.error( whatsappHeartbeatLog.warn(
`⚠️ No messages received in ${minutesSinceLastMessage}m - restarting connection`, `No messages received in ${minutesSinceLastMessage}m - restarting connection`,
); );
closeListener(); // Trigger reconnect closeListener(); // Trigger reconnect
} }
@@ -1231,7 +1228,9 @@ export async function monitorWebProvider(
{ connectionId, reason: "requests-in-flight", queued }, { connectionId, reason: "requests-in-flight", queued },
"reply heartbeat skipped", "reply heartbeat skipped",
); );
console.log(success("heartbeat: skipped (requests in flight)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat skipped (requests in flight)");
}
return { status: "skipped", reason: "requests-in-flight" }; return { status: "skipped", reason: "requests-in-flight" };
} }
if (!replyHeartbeatMinutes) { if (!replyHeartbeatMinutes) {
@@ -1260,7 +1259,9 @@ export async function monitorWebProvider(
}, },
"reply heartbeat skipped", "reply heartbeat skipped",
); );
console.log(success("heartbeat: skipped (no recent inbound)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat skipped (no recent inbound)");
}
return { status: "skipped", reason: "no-recent-inbound" }; return { status: "skipped", reason: "no-recent-inbound" };
} }
const snapshot = getSessionSnapshot(cfg, fallbackTo, true); const snapshot = getSessionSnapshot(cfg, fallbackTo, true);
@@ -1269,7 +1270,11 @@ export async function monitorWebProvider(
{ connectionId, to: fallbackTo, reason: "no-session-for-fallback" }, { connectionId, to: fallbackTo, reason: "no-session-for-fallback" },
"reply heartbeat skipped", "reply heartbeat skipped",
); );
console.log(success("heartbeat: skipped (no session to resume)")); if (isVerbose()) {
whatsappHeartbeatLog.debug(
"heartbeat skipped (no session to resume)",
);
}
return { status: "skipped", reason: "no-session-for-fallback" }; return { status: "skipped", reason: "no-session-for-fallback" };
} }
if (isVerbose()) { if (isVerbose()) {
@@ -1289,7 +1294,6 @@ export async function monitorWebProvider(
to: fallbackTo, to: fallbackTo,
verbose, verbose,
replyResolver, replyResolver,
runtime,
sessionId: snapshot.entry.sessionId, sessionId: snapshot.entry.sessionId,
}); });
heartbeatLogger.info( heartbeatLogger.info(
@@ -1353,7 +1357,9 @@ export async function monitorWebProvider(
}, },
"reply heartbeat skipped", "reply heartbeat skipped",
); );
console.log(success("heartbeat: ok (empty reply)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (empty reply)");
}
return { status: "ran", durationMs: Date.now() - started }; return { status: "ran", durationMs: Date.now() - started };
} }
@@ -1371,7 +1377,9 @@ export async function monitorWebProvider(
}, },
"reply heartbeat skipped", "reply heartbeat skipped",
); );
console.log(success("heartbeat: ok (HEARTBEAT_OK)")); if (isVerbose()) {
whatsappHeartbeatLog.debug("heartbeat ok (HEARTBEAT_OK)");
}
return { status: "ran", durationMs: Date.now() - started }; return { status: "ran", durationMs: Date.now() - started };
} }
@@ -1396,13 +1404,13 @@ export async function monitorWebProvider(
msg: heartbeatInboundMsg, msg: heartbeatInboundMsg,
maxMediaBytes, maxMediaBytes,
replyLogger, replyLogger,
runtime,
connectionId, connectionId,
}); });
const durationMs = Date.now() - tickStart; const durationMs = Date.now() - tickStart;
const summary = `heartbeat: alert sent (${formatDuration(durationMs)})`; whatsappHeartbeatLog.info(
console.log(summary); `heartbeat alert sent (${formatDuration(durationMs)})`,
);
heartbeatLogger.info( heartbeatLogger.info(
{ {
connectionId, connectionId,
@@ -1423,8 +1431,8 @@ export async function monitorWebProvider(
}, },
"reply heartbeat failed", "reply heartbeat failed",
); );
console.log( whatsappHeartbeatLog.warn(
danger(`heartbeat: failed (${formatDuration(durationMs)})`), `heartbeat failed (${formatDuration(durationMs)})`,
); );
return { status: "failed", reason: String(err) }; return { status: "failed", reason: String(err) };
} }
@@ -1443,9 +1451,8 @@ export async function monitorWebProvider(
} }
} }
logInfo( whatsappLog.info(
"📡 Listening for personal WhatsApp Web inbound messages. Leave this running; Ctrl+C to stop.", "Listening for personal WhatsApp inbound messages. Ctrl+C to stop.",
runtime,
); );
if (!keepAlive) { if (!keepAlive) {
@@ -1516,9 +1523,7 @@ export async function monitorWebProvider(
if (loggedOut) { if (loggedOut) {
runtime.error( runtime.error(
danger( "WhatsApp session logged out. Run `clawdis login --provider web` to relink.",
"WhatsApp session logged out. Run `clawdis login --provider web` to relink.",
),
); );
await closeListener(); await closeListener();
break; break;
@@ -1541,9 +1546,7 @@ export async function monitorWebProvider(
"web reconnect: max attempts reached; continuing in degraded mode", "web reconnect: max attempts reached; continuing in degraded mode",
); );
runtime.error( runtime.error(
danger( `WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`,
`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`,
),
); );
await closeListener(); await closeListener();
break; break;
@@ -1561,9 +1564,7 @@ export async function monitorWebProvider(
"web reconnect: scheduling retry", "web reconnect: scheduling retry",
); );
runtime.error( runtime.error(
danger( `WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDuration(delay)}… (${errorStr})`,
`WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDuration(delay)}… (${errorStr})`,
),
); );
await closeListener(); await closeListener();
try { try {

View File

@@ -11,7 +11,7 @@ import {
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { isVerbose, logVerbose } from "../globals.js"; import { isVerbose, logVerbose } from "../globals.js";
import { getChildLogger } from "../logging.js"; import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { saveMediaBuffer } from "../media/store.js"; import { saveMediaBuffer } from "../media/store.js";
import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js"; import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js";
import { import {
@@ -57,6 +57,9 @@ export async function monitorWebInbox(options: {
onMessage: (msg: WebInboundMessage) => Promise<void>; onMessage: (msg: WebInboundMessage) => Promise<void>;
}) { }) {
const inboundLogger = getChildLogger({ module: "web-inbound" }); const inboundLogger = getChildLogger({ module: "web-inbound" });
const inboundConsoleLog = createSubsystemLogger(
"gateway/providers/whatsapp",
).child("inbound");
const sock = await createWaSocket(false, options.verbose); const sock = await createWaSocket(false, options.verbose);
await waitForWaConnection(sock); await waitForWaConnection(sock);
let onCloseResolve: ((reason: WebListenerCloseReason) => void) | null = null; let onCloseResolve: ((reason: WebListenerCloseReason) => void) | null = null;
@@ -259,10 +262,22 @@ export async function monitorWebInbox(options: {
}), }),
); );
void task.catch((err) => { void task.catch((err) => {
console.error("Failed handling inbound web message:", String(err)); inboundLogger.error(
{ error: String(err) },
"failed handling inbound web message",
);
inboundConsoleLog.error(
`Failed handling inbound web message: ${String(err)}`,
);
}); });
} catch (err) { } catch (err) {
console.error("Failed handling inbound web message:", String(err)); inboundLogger.error(
{ error: String(err) },
"failed handling inbound web message",
);
inboundConsoleLog.error(
`Failed handling inbound web message: ${String(err)}`,
);
} }
} }
}); });

View File

@@ -1,11 +1,14 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { logInfo } from "../logger.js"; import { createSubsystemLogger, getChildLogger } from "../logging.js";
import { getChildLogger } from "../logging.js";
import { toWhatsappJid } from "../utils.js"; import { toWhatsappJid } from "../utils.js";
import { getActiveWebListener } from "./active-listener.js"; import { getActiveWebListener } from "./active-listener.js";
import { loadWebMedia } from "./media.js"; import { loadWebMedia } from "./media.js";
const outboundLog = createSubsystemLogger("gateway/providers/whatsapp").child(
"outbound",
);
export async function sendMessageWhatsApp( export async function sendMessageWhatsApp(
to: string, to: string,
body: string, body: string,
@@ -13,6 +16,7 @@ export async function sendMessageWhatsApp(
): Promise<{ messageId: string; toJid: string }> { ): Promise<{ messageId: string; toJid: string }> {
let text = body; let text = body;
const correlationId = randomUUID(); const correlationId = randomUUID();
const startedAt = Date.now();
const active = getActiveWebListener(); const active = getActiveWebListener();
if (!active) { if (!active) {
throw new Error( throw new Error(
@@ -47,8 +51,8 @@ export async function sendMessageWhatsApp(
text = caption ?? ""; text = caption ?? "";
} }
} }
logInfo( outboundLog.info(
`📤 Sending via web session -> ${jid}${options.mediaUrl ? " (media)" : ""}`, `Sending message -> ${jid}${options.mediaUrl ? " (media)" : ""}`,
); );
logger.info( logger.info(
{ jid, hasMedia: Boolean(options.mediaUrl) }, { jid, hasMedia: Boolean(options.mediaUrl) },
@@ -59,8 +63,9 @@ export async function sendMessageWhatsApp(
const result = await active.sendMessage(to, text, mediaBuffer, mediaType); const result = await active.sendMessage(to, text, mediaBuffer, mediaType);
const messageId = const messageId =
(result as { messageId?: string })?.messageId ?? "unknown"; (result as { messageId?: string })?.messageId ?? "unknown";
logInfo( const durationMs = Date.now() - startedAt;
`✅ Sent via web session. Message ID: ${messageId} -> ${jid}${options.mediaUrl ? " (media)" : ""}`, outboundLog.info(
`Sent message ${messageId} -> ${jid}${options.mediaUrl ? " (media)" : ""} (${durationMs}ms)`,
); );
logger.info({ jid, messageId }, "sent message"); logger.info({ jid, messageId }, "sent message");
return { messageId, toJid: jid }; return { messageId, toJid: jid };