refactor: improve libsignal error logging and deduplication

refactor: improve libsignal error logging and deduplication
This commit is contained in:
Renato Alcara
2026-02-12 18:33:30 -03:00
committed by GitHub
2 changed files with 34 additions and 69 deletions
+14 -9
View File
@@ -425,10 +425,18 @@ export const decryptMessageNode = (
// Automatic cleanup of corrupted session (if enabled)
if (autoCleanCorrupted) {
try {
await cleanupCorruptedSession(decryptionJid, repository, logger)
const deletedCount = await cleanupCorruptedSession(decryptionJid, repository, logger)
// Mask only user portion of JID for privacy (preserve domain info)
const { user, server } = jidDecode(decryptionJid) || {}
const maskedUser = user && user.length > 8
? `${user.substring(0, 4)}****${user.substring(user.length - 4)}`
: user
const maskedJid = maskedUser && server ? `${maskedUser}@${server}` : decryptionJid
logger.info(
{ decryptionJid, author },
'✅ Corrupted session cleaned up. New session will be created on next message.'
{ jid: decryptionJid, maskedJid, targetedDevices: deletedCount },
`🔄 Session Reset | JID: ${maskedJid} | Targeted: ${deletedCount} devices | Will recreate on next message`
)
} catch (cleanupErr) {
logger.error(
@@ -500,11 +508,11 @@ async function cleanupCorruptedSession(
jid: string,
repository: SignalRepositoryWithLIDStore,
logger: ILogger
): Promise<void> {
): Promise<number> {
const { user, device } = jidDecode(jid) || {}
if (!user) {
logger.warn({ jid }, 'Cannot cleanup session - invalid JID')
return
return 0
}
// Build list of JIDs to delete (primary + secondary devices)
@@ -541,8 +549,5 @@ async function cleanupCorruptedSession(
await repository.deleteSession(jidsToDelete)
logger.debug(
{ jid, user, device, deletedSessions: jidsToDelete },
'Deleted corrupted sessions for user'
)
return jidsToDelete.length
}
+20 -60
View File
@@ -2,73 +2,24 @@
// Format session error logs from libsignal BEFORE any imports
// This MUST be at the very top to intercept console before libsignal loads
// ============================================
const _origConsoleLog = console.log
const _origConsoleError = console.error
// Track last error to avoid duplicates
let _lastErrorMsg = ''
let _lastErrorTime = 0
// Track errors by type + JID to avoid duplicates (using Map for better performance)
const _errorTimestamps = new Map<string, number>()
const DEDUP_WINDOW_MS = 150
console.log = function(...args: unknown[]) {
console.error = function(...args: unknown[]) {
if (args.length > 0 && typeof args[0] === 'string') {
const msg = args[0]
const stack = new Error().stack || ''
const isFromLibsignal = stack.includes('libsignal') || stack.includes('session_cipher')
if (isFromLibsignal) {
// Suppress session lifecycle dumps
// Suppress session lifecycle logs
if (msg.startsWith('Closing session')) {
return
}
// Format session errors cleanly
if (
msg.includes('Session error') ||
msg.includes('Bad MAC') ||
msg.includes('MessageCounterError') ||
msg.includes('Key used already') ||
msg.includes('Failed to decrypt')
) {
// Extract error type
let errorType = '⚠️ Session Error'
if (msg.includes('Bad MAC')) errorType = '🔐 Bad MAC Error'
else if (msg.includes('MessageCounterError') || msg.includes('Key used already')) errorType = '🔢 Counter Error'
else if (msg.includes('Failed to decrypt')) errorType = '🔌 Decryption Failed'
// Extract JID if present (format: 46802258641027_1.0 or similar)
const jidMatch = msg.match(/(\d{10,}(?:_\d+\.\d+)?)/);
const jid = jidMatch ? jidMatch[1] : null
const maskedJid = jid && jid.length > 8 ? `${jid.substring(0, 4)}****${jid.substring(jid.length - 4)}` : jid
// Format clean message
const cleanMsg = maskedJid
? `${errorType} | JID: ${maskedJid}`
: errorType
// Avoid duplicate logs (within 100ms)
const now = Date.now()
if (cleanMsg === _lastErrorMsg && now - _lastErrorTime < 100) {
return
}
_lastErrorMsg = cleanMsg
_lastErrorTime = now
_origConsoleError(cleanMsg)
return
}
}
}
_origConsoleLog.apply(console, args)
}
console.error = function(...args: unknown[]) {
if (args.length > 0 && typeof args[0] === 'string') {
const msg = args[0]
const stack = new Error().stack || ''
const isFromLibsignal = stack.includes('libsignal') || stack.includes('session_cipher')
if (isFromLibsignal) {
// Format session errors cleanly
if (
msg.includes('Session error') ||
@@ -84,7 +35,7 @@ console.error = function(...args: unknown[]) {
else if (msg.includes('Failed to decrypt')) errorType = '🔌 Decryption Failed'
// Extract JID from stack trace or message
const jidMatch = (msg + String(args[1] ?? '')).match(/(\d{10,}(?:_\d+\.\d+)?)/);
const jidMatch = (msg + String(args[1] ?? '')).match(/(\d{10,}(?:_\d+\.\d+)?)/)
const jid = jidMatch ? jidMatch[1] : null
const maskedJid = jid && jid.length > 8 ? `${jid.substring(0, 4)}****${jid.substring(jid.length - 4)}` : jid
@@ -93,13 +44,22 @@ console.error = function(...args: unknown[]) {
? `${errorType} | JID: ${maskedJid}`
: errorType
// Avoid duplicate logs (within 100ms)
// Deduplication key: type + ORIGINAL JID (use unmasked to prevent collisions)
const dedupeKey = `${errorType}:${jid || 'unknown'}`
const now = Date.now()
if (cleanMsg === _lastErrorMsg && now - _lastErrorTime < 100) {
return
const lastTime = _errorTimestamps.get(dedupeKey)
if (lastTime && now - lastTime < DEDUP_WINDOW_MS) {
return // Skip duplicate within 150ms window
}
_errorTimestamps.set(dedupeKey, now)
// Cleanup old entries (keep only last 50 to prevent memory leak)
if (_errorTimestamps.size > 50) {
const oldestKey = _errorTimestamps.keys().next().value
if (oldestKey) _errorTimestamps.delete(oldestKey)
}
_lastErrorMsg = cleanMsg
_lastErrorTime = now
_origConsoleError(cleanMsg)
return