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 { QuizData } from '../interface/QuizData'
import { saveSessionData } from '../util/Load'
import { logError } from '../util/Logger'
export default class BrowserFunc {
@@ -114,7 +113,7 @@ export default class BrowserFunc {
} catch (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
}
}
@@ -179,7 +178,7 @@ export default class BrowserFunc {
} catch (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
}
@@ -246,7 +245,7 @@ export default class BrowserFunc {
/**
* 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> {
return await page.evaluate((scriptContent: string) => {
@@ -262,16 +261,27 @@ export default class BrowserFunc {
try {
const jsonStr = match[1]
// Validate basic JSON structure before parsing
if (!jsonStr.trim().startsWith('{') || !jsonStr.trim().endsWith('}')) {
const trimmed = jsonStr.trim()
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
continue
}
const parsed = JSON.parse(jsonStr)
// Validate it's actually an object
// Enhanced validation: check structure and type
if (typeof parsed !== 'object' || parsed === null) {
continue
}
// Validate essential dashboard properties exist
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
continue
}
// Successfully validated dashboard structure
return parsed
} catch (e) {
// JSON.parse failed or validation error - try next pattern
continue
}
}
@@ -339,7 +349,7 @@ export default class BrowserFunc {
}
} catch (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
}
}
@@ -404,7 +414,7 @@ export default class BrowserFunc {
return points
} catch (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
}
}
@@ -420,7 +430,7 @@ export default class BrowserFunc {
return data.userStatus.availablePoints
} catch (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
}
}
@@ -493,7 +503,7 @@ export default class BrowserFunc {
} catch (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
}
@@ -561,7 +571,7 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
} catch (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
}
}

View File

@@ -5,7 +5,7 @@
/**
* 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 defaultValue Default value if parsing fails or out of range
* @param min Minimum allowed value
@@ -18,13 +18,24 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
const parsed = Number(raw)
// 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 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 = {
SHORT: 500,
MEDIUM: 1500,
@@ -33,7 +44,7 @@ export const TIMEOUTS = {
VERY_LONG: 5000,
EXTRA_LONG: 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,
ONE_MINUTE: 60000,
ONE_HOUR: 3600000,

View File

@@ -29,8 +29,23 @@ export class DesktopFlow {
/**
* 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> {
this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow')
@@ -63,7 +78,10 @@ export class DesktopFlow {
undefined,
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
try {

View File

@@ -31,9 +31,24 @@ export class MobileFlow {
/**
* Execute the full mobile automation flow for an account
* @param account Account to process
* @param retryTracker Retry tracker for mobile search failures
* @returns Points collected during the flow
*
* Performs the following tasks in sequence:
* 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(
account: Account,
@@ -69,7 +84,10 @@ export class MobileFlow {
undefined,
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 {
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
/**
* 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> {
// First, try custom handlers (if any)
for (const h of this.handlers) {

View File

@@ -1,11 +1,11 @@
import { Page } from 'rebrowser-playwright'
import { platform } from 'os'
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { AxiosRequestConfig } from 'axios'
import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleSearch } from '../../interface/Search'
import { AxiosRequestConfig } from 'axios'
type GoogleTrendsResponse = [
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 {
private bingHome = 'https://bing.com'
private searchPageURL = ''
@@ -119,15 +123,15 @@ export class Search extends Workers {
if (missingPoints === 0) break
// Only for mobile searches
if (stagnation > 5 && this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
// Only for mobile searches - abort early if User-Agent is likely incorrect
if (stagnation > MOBILE_STAGNATION_LIMIT && this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${MOBILE_STAGNATION_LIMIT} iterations, likely bad User-Agent`, 'warn')
break
}
// If we didn't gain points for 10 iterations, assume it's stuck
if (stagnation > 10) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
// If we didn't gain points for many iterations, assume it's stuck
if (stagnation > DESKTOP_STAGNATION_LIMIT) {
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
break
}

View File

@@ -139,6 +139,17 @@ function determineColorFromContent(content: string): number {
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) {
const buf = getBuffer(url)
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 {
const configData = loadConfig()
// Access logging config with fallback for backward compatibility
const configAny = configData as unknown as Record<string, unknown>
const logging = configAny.logging as { excludeFunc?: string[]; logExcludeFunc?: string[] } | undefined
// Access logging config with type guard for safer access
const logging = hasValidLogging(configData) ? configData.logging : undefined
const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? []
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)
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg
const loggingCfg: LoggingCfg = logging || {}
const shouldRedact = !!loggingCfg.redactEmails
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||''}`
@@ -269,7 +279,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
// Webhook streaming (live logs)
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 liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : ''
const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '')

View File

@@ -123,6 +123,11 @@ export class Util {
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) {
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
}