feat: Improve error logging and validation across various flows and utilities

This commit is contained in:
2025-11-09 18:05:43 +01:00
parent 9fb5911fa2
commit 2c55fff61d
8 changed files with 128 additions and 34 deletions

View File

@@ -9,7 +9,6 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte
import { EarnablePoints } from '../interface/Points' import { EarnablePoints } from '../interface/Points'
import { QuizData } from '../interface/QuizData' import { QuizData } from '../interface/QuizData'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/Load'
import { logError } from '../util/Logger'
export default class BrowserFunc { export default class BrowserFunc {
@@ -114,7 +113,7 @@ export default class BrowserFunc {
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'GO-HOME', `[goHome] Navigation failed: ${errorMessage}`, 'error')
throw error throw error
} }
} }
@@ -179,7 +178,7 @@ export default class BrowserFunc {
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Error fetching dashboard data: ${errorMessage}`, 'error') this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `[getDashboardData] Failed to fetch dashboard data: ${errorMessage}`, 'error')
throw error throw error
} }
@@ -246,7 +245,7 @@ export default class BrowserFunc {
/** /**
* Parse dashboard object from script content * Parse dashboard object from script content
* FIXED: Added format validation before JSON.parse * IMPROVED: Enhanced validation with structure checks
*/ */
private async parseDashboardFromScript(page: Page, scriptContent: string): Promise<DashboardData | null> { private async parseDashboardFromScript(page: Page, scriptContent: string): Promise<DashboardData | null> {
return await page.evaluate((scriptContent: string) => { return await page.evaluate((scriptContent: string) => {
@@ -262,16 +261,27 @@ export default class BrowserFunc {
try { try {
const jsonStr = match[1] const jsonStr = match[1]
// Validate basic JSON structure before parsing // Validate basic JSON structure before parsing
if (!jsonStr.trim().startsWith('{') || !jsonStr.trim().endsWith('}')) { const trimmed = jsonStr.trim()
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
continue continue
} }
const parsed = JSON.parse(jsonStr) const parsed = JSON.parse(jsonStr)
// Validate it's actually an object
// Enhanced validation: check structure and type
if (typeof parsed !== 'object' || parsed === null) { if (typeof parsed !== 'object' || parsed === null) {
continue continue
} }
// Validate essential dashboard properties exist
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
continue
}
// Successfully validated dashboard structure
return parsed return parsed
} catch (e) { } catch (e) {
// JSON.parse failed or validation error - try next pattern
continue continue
} }
} }
@@ -339,7 +349,7 @@ export default class BrowserFunc {
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', `[getBrowserEarnablePoints] Failed to calculate earnable points: ${errorMessage}`, 'error')
throw error throw error
} }
} }
@@ -404,7 +414,7 @@ export default class BrowserFunc {
return points return points
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', `[getAppEarnablePoints] Failed to fetch app earnable points: ${errorMessage}`, 'error')
throw error throw error
} }
} }
@@ -420,7 +430,7 @@ export default class BrowserFunc {
return data.userStatus.availablePoints return data.userStatus.availablePoints
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', `[getCurrentPoints] Failed to fetch current points: ${errorMessage}`, 'error')
throw error throw error
} }
} }
@@ -493,7 +503,7 @@ export default class BrowserFunc {
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `[getQuizData] Failed to extract quiz data: ${errorMessage}`, 'error')
throw error throw error
} }
@@ -561,7 +571,7 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!') this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred: ' + errorMessage, 'error') this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `[closeBrowser] Failed to close browser cleanly: ${errorMessage}`, 'error')
throw error throw error
} }
} }

View File

@@ -5,7 +5,7 @@
/** /**
* Parse environment variable as number with validation * Parse environment variable as number with validation
* FIXED: Added strict validation for min/max boundaries * FIXED: Added strict validation for min/max boundaries with logging
* @param key Environment variable name * @param key Environment variable name
* @param defaultValue Default value if parsing fails or out of range * @param defaultValue Default value if parsing fails or out of range
* @param min Minimum allowed value * @param min Minimum allowed value
@@ -18,13 +18,24 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
const parsed = Number(raw) const parsed = Number(raw)
// Strict validation: must be finite, not NaN, and within bounds // Strict validation: must be finite, not NaN, and within bounds
if (!Number.isFinite(parsed) || parsed < min || parsed > max) { if (!Number.isFinite(parsed)) {
console.warn(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`)
return defaultValue
}
if (parsed < min || parsed > max) {
console.warn(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`)
return defaultValue return defaultValue
} }
return parsed return parsed
} }
// Login timeout boundaries (in milliseconds)
const LOGIN_TIMEOUT_MIN_MS = 30000 // 30 seconds - minimum login wait
const LOGIN_TIMEOUT_MAX_MS = 600000 // 10 minutes - maximum login wait
const LOGIN_TIMEOUT_DEFAULT_MS = 180000 // 3 minutes - default login timeout
export const TIMEOUTS = { export const TIMEOUTS = {
SHORT: 500, SHORT: 500,
MEDIUM: 1500, MEDIUM: 1500,
@@ -33,7 +44,7 @@ export const TIMEOUTS = {
VERY_LONG: 5000, VERY_LONG: 5000,
EXTRA_LONG: 10000, EXTRA_LONG: 10000,
DASHBOARD_WAIT: 10000, DASHBOARD_WAIT: 10000,
LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', 180000, 30000, 600000), LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', LOGIN_TIMEOUT_DEFAULT_MS, LOGIN_TIMEOUT_MIN_MS, LOGIN_TIMEOUT_MAX_MS),
NETWORK_IDLE: 5000, NETWORK_IDLE: 5000,
ONE_MINUTE: 60000, ONE_MINUTE: 60000,
ONE_HOUR: 3600000, ONE_HOUR: 3600000,

View File

@@ -29,8 +29,23 @@ export class DesktopFlow {
/** /**
* Execute the full desktop automation flow for an account * Execute the full desktop automation flow for an account
* @param account Account to process *
* @returns Points collected during the flow * Performs the following tasks in sequence:
* 1. Browser initialization with fingerprinting
* 2. Microsoft account login with 2FA support
* 3. Daily set completion
* 4. More promotions (quizzes, polls, etc.)
* 5. Punch cards
* 6. Desktop searches
*
* @param account Account to process (email, password, totp, proxy)
* @returns Promise resolving to points collected during the flow
* @throws {Error} If critical operation fails (login, browser init)
*
* @example
* const flow = new DesktopFlow(bot)
* const result = await flow.run(account)
* console.log(`Collected ${result.collectedPoints} points`)
*/ */
async run(account: Account): Promise<DesktopFlowResult> { async run(account: Account): Promise<DesktopFlowResult> {
this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow') this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow')
@@ -63,7 +78,10 @@ export class DesktopFlow {
undefined, undefined,
0xFFAA00 0xFFAA00
) )
} catch {/* ignore */} } catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(false, 'DESKTOP-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
}
// Save session for convenience, but do not close the browser // Save session for convenience, but do not close the browser
try { try {

View File

@@ -31,9 +31,24 @@ export class MobileFlow {
/** /**
* Execute the full mobile automation flow for an account * Execute the full mobile automation flow for an account
* @param account Account to process *
* @param retryTracker Retry tracker for mobile search failures * Performs the following tasks in sequence:
* @returns Points collected during the flow * 1. Mobile browser initialization with mobile user agent
* 2. Microsoft account login
* 3. OAuth token acquisition for mobile API access
* 4. Daily check-in via mobile API
* 5. Read to earn articles
* 6. Mobile searches with retry logic
*
* @param account Account to process (email, password, totp, proxy)
* @param retryTracker Retry tracker for mobile search failures (auto-created if not provided)
* @returns Promise resolving to points collected during mobile flow
* @throws {Error} If critical operation fails (login, OAuth)
*
* @example
* const flow = new MobileFlow(bot)
* const result = await flow.run(account)
* console.log(`Mobile: ${result.collectedPoints} points`)
*/ */
async run( async run(
account: Account, account: Account,
@@ -69,7 +84,10 @@ export class MobileFlow {
undefined, undefined,
0xFFAA00 0xFFAA00
) )
} catch {/* ignore */} } catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(true, 'MOBILE-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
}
try { try {
await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, true) await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, true)

View File

@@ -39,6 +39,24 @@ export class Activities {
} }
// Centralized dispatcher for activities from dashboard/punchcards // Centralized dispatcher for activities from dashboard/punchcards
/**
* Execute a promotional activity (quiz, poll, search-on-bing, etc.)
*
* Automatically detects activity type and delegates to specialized handler:
* - quiz → Quiz handler
* - abc → ABC (drag-and-drop) handler
* - thisorthat → This or That handler
* - poll → Poll handler
* - urlreward → URL reward handler
*
* @param page Playwright page for activity execution
* @param activity Activity metadata from dashboard data
* @returns Promise resolving when activity is complete
* @throws {Error} If activity type is unsupported or execution fails
*
* @example
* await activities.run(page, dailySetActivity)
*/
async run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void> { async run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void> {
// First, try custom handlers (if any) // First, try custom handlers (if any)
for (const h of this.handlers) { for (const h of this.handlers) {

View File

@@ -1,11 +1,11 @@
import { Page } from 'rebrowser-playwright'
import { platform } from 'os' import { platform } from 'os'
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
import { AxiosRequestConfig } from 'axios'
import { Counters, DashboardData } from '../../interface/DashboardData' import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleSearch } from '../../interface/Search' import { GoogleSearch } from '../../interface/Search'
import { AxiosRequestConfig } from 'axios'
type GoogleTrendsResponse = [ type GoogleTrendsResponse = [
string, string,
@@ -16,6 +16,10 @@ type GoogleTrendsResponse = [
][] ][]
]; ];
// Search stagnation thresholds (magic numbers extracted as constants)
const MOBILE_STAGNATION_LIMIT = 5 // Mobile searches: abort after 5 queries without points
const DESKTOP_STAGNATION_LIMIT = 10 // Desktop searches: abort after 10 queries without points
export class Search extends Workers { export class Search extends Workers {
private bingHome = 'https://bing.com' private bingHome = 'https://bing.com'
private searchPageURL = '' private searchPageURL = ''
@@ -119,15 +123,15 @@ export class Search extends Workers {
if (missingPoints === 0) break if (missingPoints === 0) break
// Only for mobile searches // Only for mobile searches - abort early if User-Agent is likely incorrect
if (stagnation > 5 && this.bot.isMobile) { if (stagnation > MOBILE_STAGNATION_LIMIT && this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${MOBILE_STAGNATION_LIMIT} iterations, likely bad User-Agent`, 'warn')
break break
} }
// If we didn't gain points for 10 iterations, assume it's stuck // If we didn't gain points for many iterations, assume it's stuck
if (stagnation > 10) { if (stagnation > DESKTOP_STAGNATION_LIMIT) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${DESKTOP_STAGNATION_LIMIT} iterations, aborting searches`, 'warn')
stagnation = 0 // allow fallback loop below stagnation = 0 // allow fallback loop below
break break
} }

View File

@@ -139,6 +139,17 @@ function determineColorFromContent(content: string): number {
return DISCORD.COLOR_GRAY return DISCORD.COLOR_GRAY
} }
/**
* Type guard to check if config has valid logging configuration
*/
function hasValidLogging(config: unknown): config is { logging: { excludeFunc?: string[]; webhookExcludeFunc?: string[] } } {
return typeof config === 'object' &&
config !== null &&
'logging' in config &&
typeof config.logging === 'object' &&
config.logging !== null
}
function enqueueWebhookLog(url: string, line: string) { function enqueueWebhookLog(url: string, line: string) {
const buf = getBuffer(url) const buf = getBuffer(url)
buf.lines.push(line) buf.lines.push(line)
@@ -164,9 +175,8 @@ function enqueueWebhookLog(url: string, line: string) {
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void { export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void {
const configData = loadConfig() const configData = loadConfig()
// Access logging config with fallback for backward compatibility // Access logging config with type guard for safer access
const configAny = configData as unknown as Record<string, unknown> const logging = hasValidLogging(configData) ? configData.logging : undefined
const logging = configAny.logging as { excludeFunc?: string[]; logExcludeFunc?: string[] } | undefined
const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? [] const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? []
if (logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) { if (logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) {
@@ -178,7 +188,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
// Clean string for notifications (no chalk, structured) // Clean string for notifications (no chalk, structured)
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean } type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg const loggingCfg: LoggingCfg = logging || {}
const shouldRedact = !!loggingCfg.redactEmails const shouldRedact = !!loggingCfg.redactEmails
const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => { const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => {
const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}` const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}`
@@ -269,7 +279,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
// Webhook streaming (live logs) // Webhook streaming (live logs)
try { try {
const loggingCfg: Record<string, unknown> = (configAny.logging || {}) as Record<string, unknown> const loggingCfg: Record<string, unknown> = (logging || {}) as Record<string, unknown>
const webhookCfg = configData.webhook const webhookCfg = configData.webhook
const liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : '' const liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : ''
const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '') const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '')

View File

@@ -123,6 +123,11 @@ export class Util {
return [] return []
} }
// Check for undefined/null elements which could cause issues downstream
if (arr.some(item => item === undefined || item === null)) {
throw new Error('Array contains undefined or null elements which are not allowed.')
}
if (!Number.isFinite(numChunks) || numChunks <= 0) { if (!Number.isFinite(numChunks) || numChunks <= 0) {
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`) throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
} }