feat: implement error logging in catch blocks for improved error handling and debugging

This commit is contained in:
2025-11-04 22:04:20 +01:00
parent 03d94a0441
commit c0a868ff1f
7 changed files with 129 additions and 52 deletions

View File

@@ -9,6 +9,7 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte
import { QuizData } from '../interface/QuizData'
import { AppUserData } from '../interface/AppUserData'
import { EarnablePoints } from '../interface/Points'
import { logError } from '../util/Logger'
export default class BrowserFunc {
@@ -148,7 +149,7 @@ export default class BrowserFunc {
// Force a navigation retry once before failing hard
await this.goHome(target)
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(() => {})
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(logError('BROWSER-FUNC', 'Dashboard recovery load failed', this.bot.isMobile))
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
scriptContent = await this.extractDashboardScript(target)

View File

@@ -1,6 +1,7 @@
import { Page } from 'rebrowser-playwright'
import { load } from 'cheerio'
import { MicrosoftRewardsBot } from '../index'
import { logError } from '../util/Logger'
type DismissButton = { selector: string; label: string; isXPath?: boolean }
@@ -80,7 +81,7 @@ export default class BrowserUtil {
const visible = await loc.first().isVisible({ timeout: 200 }).catch(() => false)
if (!visible) return false
await loc.first().click({ timeout: 500 }).catch(() => {})
await loc.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', `Failed to click ${btn.label}`, this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
return true
} catch {
@@ -97,14 +98,14 @@ export default class BrowserUtil {
const rejectBtn = overlay.locator(reject)
if (await rejectBtn.first().isVisible().catch(() => false)) {
await rejectBtn.first().click({ timeout: 500 }).catch(() => {})
await rejectBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay reject click failed', this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
return 1
}
const acceptBtn = overlay.locator(accept)
if (await acceptBtn.first().isVisible().catch(() => false)) {
await acceptBtn.first().click({ timeout: 500 }).catch(() => {})
await acceptBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay accept click failed', this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
return 1
}
@@ -124,12 +125,12 @@ export default class BrowserUtil {
const closeBtn = dialog.locator(closeButtons).first()
if (await closeBtn.isVisible({ timeout: 200 }).catch(() => false)) {
await closeBtn.click({ timeout: 500 }).catch(() => {})
await closeBtn.click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Streak dialog close failed', this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Button')
return 1
}
await page.keyboard.press('Escape').catch(() => {})
await page.keyboard.press('Escape').catch(logError('BROWSER-UTIL', 'Streak dialog Escape failed', this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Escape')
return 1
} catch {
@@ -153,7 +154,7 @@ export default class BrowserUtil {
// Click the Next button
const nextBtn = page.locator(nextButton).first()
if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) {
await nextBtn.click({ timeout: 1000 }).catch(() => {})
await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile))
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)')
// Wait a bit for navigation
await page.waitForTimeout(1000)

View File

@@ -9,6 +9,7 @@ import { MicrosoftRewardsBot } from '../index'
import { OAuth } from '../interface/OAuth'
import { Retry } from '../util/Retry'
import { LoginState, LoginStateDetector } from '../util/LoginStateDetector'
import { logError } from '../util/Logger'
// -------------------------------
// Constants / Tunables
@@ -243,7 +244,7 @@ export class Login {
const homeUrl = 'https://rewards.bing.com/'
try {
await page.goto(homeUrl)
await page.waitForLoadState('domcontentloaded').catch(()=>{})
await page.waitForLoadState('domcontentloaded').catch(logError('LOGIN', 'DOMContentLoaded timeout', this.bot.isMobile))
await this.bot.browser.utils.reloadBadPage(page)
await this.bot.utils.wait(250)
@@ -384,7 +385,10 @@ export class Login {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled')
}
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
if (next) { await next.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
if (next) {
await next.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Email submit click failed: ${e}`, 'warn'))
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email')
}
}
private async inputPasswordOr2FA(page: Page, password: string) {
@@ -394,7 +398,10 @@ export class Login {
// Some flows require switching to password first
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null)
if (switchBtn) { await switchBtn.click().catch(()=>{}); await this.bot.utils.wait(1000) }
if (switchBtn) {
await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn'))
await this.bot.utils.wait(1000)
}
// Early TOTP check - if totpSecret is configured, check for TOTP challenge before password
if (this.currentTotpSecret) {
@@ -429,7 +436,10 @@ export class Login {
await page.fill(SELECTORS.passwordInput, '')
await page.fill(SELECTORS.passwordInput, password)
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
if (submit) {
await submit.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Password submit failed: ${e}`, 'warn'))
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted')
}
}
// --------------- 2FA Handling ---------------
@@ -462,10 +472,10 @@ export class Login {
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null)
if (!resend) break
await this.bot.utils.wait(60000)
await resend.click().catch(()=>{})
await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile))
}
}
await page.click('button[aria-describedby="confirmSendTitle"]').catch(()=>{})
await page.click('button[aria-describedby="confirmSendTitle"]').catch(logError('LOGIN', 'Confirm send click failed', this.bot.isMobile))
await this.bot.utils.wait(1500)
try {
const el = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { timeout: 2000 })
@@ -484,7 +494,7 @@ export class Login {
} catch {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired refreshing')
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null)
if (retryBtn) await retryBtn.click().catch(()=>{})
if (retryBtn) await retryBtn.click().catch(logError('LOGIN-AUTH', 'Refresh button click failed', this.bot.isMobile))
const refreshed = await this.fetchAuthenticatorNumber(page)
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
numberToPress = refreshed
@@ -584,9 +594,9 @@ export class Login {
// Use unified selector system
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
if (submit) {
await submit.click().catch(()=>{})
await submit.click().catch(logError('LOGIN-TOTP', 'Auto-submit click failed', this.bot.isMobile))
} else {
await page.keyboard.press('Enter').catch(()=>{})
await page.keyboard.press('Enter').catch(logError('LOGIN-TOTP', 'Auto-submit Enter failed', this.bot.isMobile))
}
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
} catch (error) {
@@ -747,7 +757,7 @@ export class Login {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
await loc.click().catch(()=>{})
await loc.click().catch(logError('LOGIN', `Click failed for selector: ${sel}`, this.bot.isMobile))
return true
}
}
@@ -1017,7 +1027,7 @@ export class Login {
const text = (await skipBtn.textContent() || '').trim()
// Check if it's actually a skip button (could be other secondary buttons)
if (/skip|later|not now|non merci|pas maintenant/i.test(text)) {
await skipBtn.click().catch(()=>{})
await skipBtn.click().catch(logError('LOGIN-PASSKEY', 'Skip button click failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('data-testid secondaryButton')
}
@@ -1028,7 +1038,11 @@ export class Login {
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null)
if (biometric) {
const btn = await page.$(SELECTORS.passkeySecondary)
if (btn) { await btn.click().catch(()=>{}); did = true; this.logPasskeyOnce('video heuristic') }
if (btn) {
await btn.click().catch(logError('LOGIN-PASSKEY', 'Video heuristic click failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('video heuristic')
}
}
}
@@ -1039,11 +1053,17 @@ export class Login {
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null)
const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title)
if (looksLike && secBtn) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('title heuristic '+title) }
if (looksLike && secBtn) {
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('title heuristic '+title)
}
else if (!did && secBtn && primBtn) {
const text = (await secBtn.textContent()||'').trim()
if (/skip for now|not now|later|passer|plus tard/i.test(text)) {
await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('secondary button text')
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('secondary button text')
}
}
}
@@ -1051,7 +1071,11 @@ export class Login {
// Priority 4: XPath fallback (includes Windows Hello specific patterns)
if (!did) {
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first()
if (await textBtn.isVisible().catch(()=>false)) { await textBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('xpath fallback') }
if (await textBtn.isVisible().catch(()=>false)) {
await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('xpath fallback')
}
}
// Priority 4.5: Windows Hello specific detection
@@ -1070,7 +1094,7 @@ export class Login {
for (const pattern of skipPatterns) {
const btn = await page.locator(pattern).first()
if (await btn.isVisible().catch(() => false)) {
await btn.click().catch(() => {})
await btn.click().catch(logError('LOGIN-PASSKEY', 'Windows Hello skip failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('Windows Hello skip')
break
@@ -1082,14 +1106,22 @@ export class Login {
// Priority 5: Close button fallback
if (!did) {
const close = await page.$('#close-button')
if (close) { await close.click().catch(()=>{}); did = true; this.logPasskeyOnce('close button') }
if (close) {
await close.click().catch(logError('LOGIN-PASSKEY', 'Close button fallback failed', this.bot.isMobile))
did = true
this.logPasskeyOnce('close button')
}
}
// KMSI prompt
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null)
if (kmsi) {
const yes = await page.$(SELECTORS.passkeyPrimary)
if (yes) { await yes.click().catch(()=>{}); did = true; this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') }
if (yes) {
await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile))
did = true
this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt')
}
}
if (!did && context === 'main') {
@@ -1140,9 +1172,9 @@ export class Login {
this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'sign-in-blocked'
this.startCompromisedInterval()
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(()=>{})
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(logError('LOGIN-SECURITY', 'Global standby engagement failed', this.bot.isMobile))
// Open security docs for immediate guidance (best-effort)
await this.openDocsTab(page, docsUrl).catch(()=>{})
await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-SECURITY', 'Failed to open docs tab', this.bot.isMobile))
return true
} catch { return false }
}
@@ -1250,8 +1282,8 @@ export class Login {
this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'recovery-mismatch'
this.startCompromisedInterval()
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(()=>{})
await this.openDocsTab(page, docsUrl).catch(()=>{})
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(logError('LOGIN-RECOVERY', 'Global standby failed', this.bot.isMobile))
await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-RECOVERY', 'Failed to open docs tab', this.bot.isMobile))
} else {
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
@@ -1263,7 +1295,7 @@ export class Login {
try {
const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first()
if (await link.isVisible().catch(()=>false)) {
await link.click().catch(()=>{})
await link.click().catch(logError('LOGIN', 'Use password link click failed', this.bot.isMobile))
await this.bot.utils.wait(800)
this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link')
}
@@ -1335,6 +1367,6 @@ export class Login {
body.isFidoSupported = false
route.continue({ postData: JSON.stringify(body) })
} catch { route.continue() }
}).catch(()=>{})
}).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile))
}
}

View File

@@ -6,6 +6,7 @@ import { MicrosoftRewardsBot } from '../index'
import JobState from '../util/JobState'
import { Retry } from '../util/Retry'
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
import { logError } from '../util/Logger'
export class Workers {
public bot: MicrosoftRewardsBot
@@ -204,7 +205,7 @@ export class Workers {
}
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile))
await this.bot.browser.utils.humanizePage(page)
await this.applyThrottle(throttle, 1200, 2600)
}

View File

@@ -120,10 +120,16 @@ export class MicrosoftRewardsBot {
// Run comprehensive startup validation
const validator = new StartupValidator()
await validator.validate(this.config, this.accounts)
try {
await validator.validate(this.config, this.accounts)
} catch (error) {
// Critical validation errors prevent startup
const errorMsg = error instanceof Error ? error.message : String(error)
log('main', 'VALIDATION', `Fatal validation error: ${errorMsg}`, 'error')
throw error // Re-throw to stop execution
}
// Always continue - validation is informative, not blocking
// This allows users to proceed even with warnings or minor issues
// Validation passed - continue with initialization
// Initialize job state
if (this.config.jobState?.enabled !== false) {

View File

@@ -7,6 +7,18 @@ import { DISCORD } from '../constants'
const WEBHOOK_USERNAME = 'MS Rewards - Live Logs'
/**
* Safe error logger for catch blocks
* Use in .catch() to log errors without breaking flow
* @example await action().catch(logError('ACTION', 'Failed to do something'))
*/
export function logError(title: string, message: string, isMobile: boolean | 'main' = 'main') {
return (error: unknown) => {
const errMsg = error instanceof Error ? error.message : String(error)
log(isMobile, title, `${message}: ${errMsg}`, 'warn')
}
}
type WebhookBuffer = {
lines: string[]
sending: boolean

View File

@@ -11,6 +11,7 @@ interface ValidationError {
message: string
fix?: string
docsLink?: string
blocking?: boolean // If true, prevents bot startup
}
export class StartupValidator {
@@ -19,8 +20,8 @@ export class StartupValidator {
/**
* Run all validation checks before starting the bot.
* Always returns true - validation is informative, not blocking.
* Displays errors and warnings but lets execution continue.
* Throws ValidationError if critical (blocking) errors are found.
* Displays errors and warnings to help users fix configuration issues.
*/
async validate(config: Config, accounts: Account[]): Promise<boolean> {
log('main', 'STARTUP', 'Running configuration validation...')
@@ -40,8 +41,15 @@ export class StartupValidator {
// Display results (await to respect the delay)
await this.displayResults()
// Always return true - validation is informative only
// Users can proceed even with errors (they might be false positives)
// Check for blocking errors
const blockingErrors = this.errors.filter(e => e.blocking === true)
if (blockingErrors.length > 0) {
const errorMsg = `Validation failed with ${blockingErrors.length} critical error(s). Fix configuration before proceeding.`
log('main', 'VALIDATION', errorMsg, 'error')
throw new Error(errorMsg)
}
// Non-blocking errors and warnings allow execution to continue
return true
}
@@ -51,7 +59,8 @@ export class StartupValidator {
'accounts',
'No accounts found in accounts.json',
'Add at least one account to src/accounts.json or src/accounts.jsonc',
'docs/accounts.md'
'docs/accounts.md',
true // blocking: no accounts = nothing to run
)
return
}
@@ -64,13 +73,17 @@ export class StartupValidator {
this.addError(
'accounts',
`${prefix}: Missing or invalid email address`,
'Add a valid email address in the "email" field'
'Add a valid email address in the "email" field',
undefined,
true // blocking: email is required
)
} else if (!/@/.test(account.email)) {
this.addError(
'accounts',
`${prefix}: Email format is invalid`,
'Email must contain @ symbol (e.g., user@example.com)'
'Email must contain @ symbol (e.g., user@example.com)',
undefined,
true // blocking: invalid email = cannot login
)
}
@@ -79,7 +92,9 @@ export class StartupValidator {
this.addError(
'accounts',
`${prefix}: Missing or invalid password`,
'Add your Microsoft account password in the "password" field'
'Add your Microsoft account password in the "password" field',
undefined,
true // blocking: password is required
)
} else if (account.password.length < 4) {
this.addWarning(
@@ -218,7 +233,9 @@ export class StartupValidator {
this.addError(
'config',
'Global timeout is set to 0',
'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs'
'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs',
undefined,
true // blocking: 0 timeout = infinite hangs guaranteed
)
}
@@ -368,13 +385,16 @@ export class StartupValidator {
'network',
'Webhook enabled but URL is missing',
'Add webhook URL or set webhook.enabled=false',
'docs/config.md'
'docs/config.md',
true // blocking: enabled but no URL = will crash
)
} else if (!config.webhook.url.startsWith('http')) {
this.addError(
'network',
`Invalid webhook URL: ${config.webhook.url}`,
'Webhook URL must start with http:// or https://'
'Webhook URL must start with http:// or https://',
undefined,
true // blocking: invalid URL = will crash
)
}
}
@@ -385,7 +405,9 @@ export class StartupValidator {
this.addError(
'network',
'Conclusion webhook enabled but URL is missing',
'Add conclusion webhook URL or disable it'
'Add conclusion webhook URL or disable it',
undefined,
true // blocking: enabled but no URL = will crash
)
}
}
@@ -397,7 +419,8 @@ export class StartupValidator {
'network',
'NTFY enabled but URL is missing',
'Add NTFY server URL or set ntfy.enabled=false',
'docs/ntfy.md'
'docs/ntfy.md',
true // blocking: enabled but no URL = will crash
)
}
if (!config.ntfy.topic || config.ntfy.topic.trim() === '') {
@@ -405,7 +428,8 @@ export class StartupValidator {
'network',
'NTFY enabled but topic is missing',
'Add NTFY topic name',
'docs/ntfy.md'
'docs/ntfy.md',
true // blocking: enabled but no topic = will crash
)
}
}
@@ -611,12 +635,12 @@ export class StartupValidator {
}
}
private addError(category: string, message: string, fix?: string, docsLink?: string): void {
this.errors.push({ severity: 'error', category, message, fix, docsLink })
private addError(category: string, message: string, fix?: string, docsLink?: string, blocking = false): void {
this.errors.push({ severity: 'error', category, message, fix, docsLink, blocking })
}
private addWarning(category: string, message: string, fix?: string, docsLink?: string): void {
this.warnings.push({ severity: 'warning', category, message, fix, docsLink })
this.warnings.push({ severity: 'warning', category, message, fix, docsLink, blocking: false })
}
private async displayResults(): Promise<void> {