diff --git a/assets/README.md b/assets/README.md
deleted file mode 100644
index 8e9ad77..0000000
--- a/assets/README.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# 🎨 Assets Directory
-
-This folder contains visual assets for the Microsoft Rewards Script project.
-
-## 📁 Contents
-
-### `logo.png`
-**The official project mascot and logo**
-
-- **Dimensions:** Original size preserved
-- **Format:** PNG with transparency
-- **Usage:**
- - README.md header (200px width)
- - Documentation pages (120-150px width)
- - Social media and promotional materials
-
-## 📐 Logo Usage Guidelines
-
-### Recommended Sizes
-
-| Context | Recommended Width | Example |
-|---------|------------------|---------|
-| Main README | 200px | `
` |
-| Documentation pages | 120-150px | `
` |
-| GitHub social preview | 1200x630px | Resize as needed |
-| Favicon | 32x32px or 64x64px | Convert to ICO format |
-
-### Usage Examples
-
-**In README.md (root):**
-```markdown
-
-

-
-```
-
-**In docs/*.md files:**
-```markdown
-
-

-
-```
-
-**In HTML:**
-```html
-
-```
-
-## 🎨 Design Notes
-
-The logo represents the project's mascot and serves as the visual identity for:
-- Documentation headers
-- Community presence (Discord, etc.)
-- Project branding
-- Social media
-
-## 📝 Attribution
-
-Logo created for the Microsoft Rewards Script project.
-
-## 🔒 Usage Rights
-
-This logo is part of the Microsoft Rewards Script project and follows the same [LICENSE](../LICENSE) as the project code.
-
----
-
-## 🖼️ Future Assets
-
-This directory may be expanded to include:
-- Screenshots for documentation
-- Diagrams and flowcharts
-- Favicon files
-- Social media banners
-- Tutorial images
-
----
-
-
-
-**[← Back to README](../README.md)**
-
-
diff --git a/assets/banner.png b/assets/banner.png
index 9c2aa6a..bafd05f 100644
Binary files a/assets/banner.png and b/assets/banner.png differ
diff --git a/assets/logo.png b/assets/logo.png
index 9a7ead0..bafd05f 100644
Binary files a/assets/logo.png and b/assets/logo.png differ
diff --git a/src/flows/SummaryReporter.ts b/src/flows/SummaryReporter.ts
index 81e419c..0c93fec 100644
--- a/src/flows/SummaryReporter.ts
+++ b/src/flows/SummaryReporter.ts
@@ -13,6 +13,7 @@ import type { Config } from '../interface/Config'
import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
import { log } from '../util/notifications/Logger'
import { Ntfy } from '../util/notifications/Ntfy'
+import { getActivityStatsTracker, resetActivityStatsTracker } from '../util/state/ActivityStatsTracker'
import { JobState } from '../util/state/JobState'
export interface AccountResult {
@@ -258,12 +259,52 @@ export class SummaryReporter {
log('main', 'SUMMARY', '═'.repeat(80))
+ // Log activity statistics
+ this.logActivityStats()
+
// Send notifications
await Promise.all([
this.sendWebhookSummary(summary),
this.sendPushNotification(summary),
this.updateJobState(summary)
])
+
+ // Reset activity stats for next run
+ resetActivityStatsTracker()
+ }
+
+ /**
+ * Log activity success/failure statistics
+ */
+ private logActivityStats(): void {
+ const tracker = getActivityStatsTracker()
+ const summary = tracker.getSummary()
+
+ if (summary.totalAttempts === 0) {
+ return
+ }
+
+ log('main', 'SUMMARY', '─'.repeat(80))
+ log('main', 'SUMMARY', '📈 Activity Statistics:')
+ log('main', 'SUMMARY', ` Total: ${summary.totalSuccesses}/${summary.totalAttempts} succeeded (${(summary.overallSuccessRate * 100).toFixed(1)}%)`)
+
+ // Show per-activity breakdown if there are multiple activity types
+ if (summary.byActivity.length > 1) {
+ for (const activity of summary.byActivity) {
+ const rate = (activity.successRate * 100).toFixed(0)
+ const avgTime = (activity.avgDurationMs / 1000).toFixed(1)
+ log('main', 'SUMMARY', ` ${activity.type}: ${activity.successes}/${activity.attempts} (${rate}%) avg ${avgTime}s`)
+ }
+ }
+
+ // Warn about problematic activities
+ const problematic = tracker.getProblematicActivities()
+ if (problematic.length > 0) {
+ log('main', 'SUMMARY', '⚠️ High Failure Activities:', 'warn')
+ for (const p of problematic) {
+ log('main', 'SUMMARY', ` ${p.type}: ${(p.failureRate * 100).toFixed(0)}% failure rate (${p.attempts} attempts)`, 'warn')
+ }
+ }
}
/**
diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts
index cb1b4d5..8faa29e 100644
--- a/src/functions/Workers.ts
+++ b/src/functions/Workers.ts
@@ -8,6 +8,7 @@ import { waitForElementSmart, waitForNetworkIdle } from '../util/browser/SmartWa
import { Retry } from '../util/core/Retry'
import { AdaptiveThrottler } from '../util/notifications/AdaptiveThrottler'
import { logError } from '../util/notifications/Logger'
+import { getActivityStatsTracker } from '../util/state/ActivityStatsTracker'
import JobState from '../util/state/JobState'
// Selector patterns (extracted to avoid magic strings)
@@ -267,7 +268,11 @@ export class Workers {
}
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise {
- this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
+ const activityType = this.bot.activities.getTypeLabel(activity)
+ const statsTracker = getActivityStatsTracker()
+ const startTime = statsTracker.startActivity(activityType)
+
+ this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${activityType}" title: "${activity.title}"`)
// IMPROVED: Fast-fail for unavailable activities (1s+3s instead of 2s+5s)
const elementResult = await waitForElementSmart(page, selector, {
@@ -279,6 +284,7 @@ export class Workers {
if (!elementResult.found) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `[SKIP] Activity not available: "${activity.title}" (already completed or not offered today)`)
+ statsTracker.recordSuccess(activityType, startTime) // Count as success (nothing to do)
return // Skip this activity gracefully
}
@@ -294,6 +300,7 @@ export class Workers {
} catch (clickError) {
const errMsg = clickError instanceof Error ? clickError.message : String(clickError)
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Failed to click activity: ${errMsg}`, 'error')
+ statsTracker.recordFailure(activityType, startTime, clickError instanceof Error ? clickError : new Error(errMsg))
throw new Error(`Activity click failed: ${errMsg}`)
}
@@ -302,24 +309,31 @@ export class Workers {
// Execute activity with timeout protection using Promise.race
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
- await retry.run(async () => {
- const activityPromise = this.bot.activities.run(page, activity)
- const timeoutPromise = new Promise((_, reject) => {
- const timer = setTimeout(() => {
- reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))
- }, timeoutMs)
- // Clean up timer if activity completes first
- activityPromise.finally(() => clearTimeout(timer))
- })
+ try {
+ await retry.run(async () => {
+ const activityPromise = this.bot.activities.run(page, activity)
+ const timeoutPromise = new Promise((_, reject) => {
+ const timer = setTimeout(() => {
+ reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))
+ }, timeoutMs)
+ // Clean up timer if activity completes first
+ activityPromise.finally(() => clearTimeout(timer))
+ })
- try {
- await Promise.race([activityPromise, timeoutPromise])
- throttle.record(true)
- } catch (e) {
- throttle.record(false)
- throw e
- }
- }, () => true)
+ try {
+ await Promise.race([activityPromise, timeoutPromise])
+ throttle.record(true)
+ } catch (e) {
+ throttle.record(false)
+ throw e
+ }
+ }, () => true)
+
+ statsTracker.recordSuccess(activityType, startTime)
+ } catch (activityError) {
+ statsTracker.recordFailure(activityType, startTime, activityError instanceof Error ? activityError : undefined)
+ throw activityError
+ }
await this.bot.browser.utils.humanizePage(page)
}
diff --git a/src/index.ts b/src/index.ts
index 9067a0d..736b3e3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,7 @@ import { createInterface } from 'readline'
import BrowserFunc from './browser/BrowserFunc'
import BrowserUtil from './browser/BrowserUtil'
import Humanizer from './util/browser/Humanizer'
+import { getMemoryMonitor, stopMemoryMonitor } from './util/core/MemoryMonitor'
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils'
import Axios from './util/network/Axios'
import { QueryDiversityEngine } from './util/network/QueryDiversityEngine'
@@ -1059,18 +1060,21 @@ async function main(): Promise {
log('main', 'FATAL', `UncaughtException: ${err.message}${err.stack ? `\nStack: ${err.stack.split('\n').slice(0, 3).join(' | ')}` : ''}`, 'error')
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
+ stopMemoryMonitor() // Stop memory monitoring before exit
gracefulExit(1)
})
process.on('SIGTERM', () => {
log('main', 'SHUTDOWN', 'Received SIGTERM, shutting down gracefully...', 'log')
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
+ stopMemoryMonitor() // Stop memory monitoring before exit
gracefulExit(0)
})
process.on('SIGINT', () => {
log('main', 'SHUTDOWN', 'Received SIGINT (Ctrl+C), shutting down gracefully...', 'log')
scheduler?.stop() // Stop scheduler before exit
stopWebhookCleanup()
+ stopMemoryMonitor() // Stop memory monitoring before exit
gracefulExit(0)
})
}
@@ -1178,6 +1182,15 @@ async function main(): Promise {
// This gives users instant confirmation of the cron schedule without waiting for long execution
log('main', 'MAIN', 'Scheduling enabled - activating scheduler, then executing immediate run', 'log', 'cyan')
+ // Start memory monitoring for long-running scheduled sessions
+ const memoryMonitor = getMemoryMonitor({
+ warningThresholdMB: 500,
+ criticalThresholdMB: 1024,
+ leakRateMBPerHour: 50,
+ samplingIntervalMs: 60000 // Sample every minute
+ })
+ memoryMonitor.start()
+
// Initialize and start scheduler first
scheduler = new InternalScheduler(config, async () => {
try {
diff --git a/src/util/core/MemoryMonitor.ts b/src/util/core/MemoryMonitor.ts
new file mode 100644
index 0000000..7f49345
--- /dev/null
+++ b/src/util/core/MemoryMonitor.ts
@@ -0,0 +1,268 @@
+/**
+ * Memory monitoring utility for long-running bot sessions
+ * Tracks heap usage and warns on potential memory leaks
+ */
+
+import { log } from '../notifications/Logger'
+
+interface MemorySnapshot {
+ timestamp: number
+ heapUsedMB: number
+ heapTotalMB: number
+ externalMB: number
+ rssMB: number
+}
+
+interface MemoryConfig {
+ /** Threshold in MB to trigger warning (default: 500MB) */
+ warningThresholdMB?: number
+ /** Threshold in MB to trigger critical alert (default: 1024MB) */
+ criticalThresholdMB?: number
+ /** Minimum growth rate (MB/hour) to consider a leak (default: 50MB/hour) */
+ leakRateMBPerHour?: number
+ /** Sampling interval in milliseconds (default: 60000 = 1 minute) */
+ samplingIntervalMs?: number
+ /** Number of samples to keep for trend analysis (default: 60) */
+ maxSamples?: number
+}
+
+const DEFAULT_CONFIG: Required = {
+ warningThresholdMB: 500,
+ criticalThresholdMB: 1024,
+ leakRateMBPerHour: 50,
+ samplingIntervalMs: 60000,
+ maxSamples: 60
+}
+
+export class MemoryMonitor {
+ private config: Required
+ private samples: MemorySnapshot[] = []
+ private intervalId: NodeJS.Timeout | null = null
+ private startTime: number = Date.now()
+ private warningEmitted: boolean = false
+ private criticalEmitted: boolean = false
+
+ constructor(config?: MemoryConfig) {
+ this.config = { ...DEFAULT_CONFIG, ...config }
+ }
+
+ /**
+ * Start monitoring memory usage at configured interval
+ */
+ start(): void {
+ if (this.intervalId) {
+ return // Already running
+ }
+
+ this.startTime = Date.now()
+ this.samples = []
+ this.warningEmitted = false
+ this.criticalEmitted = false
+
+ // Take initial sample
+ this.takeSample()
+
+ // Schedule periodic sampling
+ this.intervalId = setInterval(() => {
+ this.takeSample()
+ this.analyzeMemory()
+ }, this.config.samplingIntervalMs)
+
+ log('main', 'MEMORY', `Memory monitoring started (warning: ${this.config.warningThresholdMB}MB, critical: ${this.config.criticalThresholdMB}MB)`)
+ }
+
+ /**
+ * Stop monitoring and clear resources
+ */
+ stop(): void {
+ if (this.intervalId) {
+ clearInterval(this.intervalId)
+ this.intervalId = null
+ }
+
+ // Log final summary
+ if (this.samples.length > 0) {
+ const summary = this.getSummary()
+ log('main', 'MEMORY', `Monitor stopped. Peak: ${summary.peakHeapMB.toFixed(1)}MB, Avg: ${summary.avgHeapMB.toFixed(1)}MB, Growth: ${summary.growthRateMBPerHour.toFixed(2)}MB/h`)
+ }
+ }
+
+ /**
+ * Take a memory usage sample
+ */
+ private takeSample(): void {
+ const usage = process.memoryUsage()
+
+ const snapshot: MemorySnapshot = {
+ timestamp: Date.now(),
+ heapUsedMB: usage.heapUsed / (1024 * 1024),
+ heapTotalMB: usage.heapTotal / (1024 * 1024),
+ externalMB: usage.external / (1024 * 1024),
+ rssMB: usage.rss / (1024 * 1024)
+ }
+
+ this.samples.push(snapshot)
+
+ // Keep only recent samples
+ if (this.samples.length > this.config.maxSamples) {
+ this.samples.shift()
+ }
+ }
+
+ /**
+ * Analyze memory trends and emit warnings if needed
+ */
+ private analyzeMemory(): void {
+ if (this.samples.length < 2) {
+ return
+ }
+
+ const latest = this.samples[this.samples.length - 1]
+ if (!latest) {
+ return
+ }
+
+ // Check absolute thresholds
+ if (latest.heapUsedMB >= this.config.criticalThresholdMB && !this.criticalEmitted) {
+ this.criticalEmitted = true
+ log('main', 'MEMORY', `CRITICAL: Heap usage (${latest.heapUsedMB.toFixed(1)}MB) exceeds critical threshold (${this.config.criticalThresholdMB}MB)`, 'error')
+ this.logDetailedUsage()
+ } else if (latest.heapUsedMB >= this.config.warningThresholdMB && !this.warningEmitted) {
+ this.warningEmitted = true
+ log('main', 'MEMORY', `WARNING: Heap usage (${latest.heapUsedMB.toFixed(1)}MB) exceeds warning threshold (${this.config.warningThresholdMB}MB)`, 'warn')
+ }
+
+ // Reset warning flags if memory drops below thresholds (allows re-warning)
+ if (latest.heapUsedMB < this.config.warningThresholdMB * 0.8) {
+ this.warningEmitted = false
+ }
+ if (latest.heapUsedMB < this.config.criticalThresholdMB * 0.8) {
+ this.criticalEmitted = false
+ }
+
+ // Check for memory leak pattern (sustained growth)
+ const growthRate = this.calculateGrowthRate()
+ if (growthRate > this.config.leakRateMBPerHour && this.samples.length >= 10) {
+ log('main', 'MEMORY', `Potential memory leak detected: ${growthRate.toFixed(2)}MB/hour growth rate`, 'warn')
+ }
+ }
+
+ /**
+ * Calculate memory growth rate in MB/hour using linear regression
+ */
+ private calculateGrowthRate(): number {
+ if (this.samples.length < 5) {
+ return 0
+ }
+
+ // Simple linear regression on recent samples
+ const recentSamples = this.samples.slice(-Math.min(30, this.samples.length))
+ const n = recentSamples.length
+
+ if (n === 0) {
+ return 0
+ }
+
+ const firstSample = recentSamples[0]
+ if (!firstSample) {
+ return 0
+ }
+
+ let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0
+ const firstTimestamp = firstSample.timestamp
+
+ for (let i = 0; i < n; i++) {
+ const sample = recentSamples[i]
+ if (!sample) continue
+ const x = (sample.timestamp - firstTimestamp) / 3600000 // Hours
+ const y = sample.heapUsedMB
+ sumX += x
+ sumY += y
+ sumXY += x * y
+ sumX2 += x * x
+ }
+
+ const denominator = n * sumX2 - sumX * sumX
+ if (denominator === 0) {
+ return 0
+ }
+
+ // Slope = growth rate in MB/hour
+ return (n * sumXY - sumX * sumY) / denominator
+ }
+
+ /**
+ * Log detailed memory usage breakdown
+ */
+ private logDetailedUsage(): void {
+ const usage = process.memoryUsage()
+ log('main', 'MEMORY', `Detailed: Heap ${(usage.heapUsed / 1024 / 1024).toFixed(1)}/${(usage.heapTotal / 1024 / 1024).toFixed(1)}MB, RSS ${(usage.rss / 1024 / 1024).toFixed(1)}MB, External ${(usage.external / 1024 / 1024).toFixed(1)}MB`)
+ }
+
+ /**
+ * Get current memory usage summary
+ */
+ getSummary(): { currentHeapMB: number; peakHeapMB: number; avgHeapMB: number; growthRateMBPerHour: number; uptimeHours: number } {
+ const lastSample = this.samples.length > 0 ? this.samples[this.samples.length - 1] : undefined
+ const current = lastSample?.heapUsedMB ?? 0
+ const peak = this.samples.reduce((max, s) => Math.max(max, s.heapUsedMB), 0)
+ const avg = this.samples.length > 0 ? this.samples.reduce((sum, s) => sum + s.heapUsedMB, 0) / this.samples.length : 0
+ const growthRate = this.calculateGrowthRate()
+ const uptimeHours = (Date.now() - this.startTime) / 3600000
+
+ return {
+ currentHeapMB: current,
+ peakHeapMB: peak,
+ avgHeapMB: avg,
+ growthRateMBPerHour: growthRate,
+ uptimeHours
+ }
+ }
+
+ /**
+ * Force garbage collection if available (requires --expose-gc flag)
+ */
+ forceGC(): boolean {
+ if (typeof global.gc === 'function') {
+ global.gc()
+ log('main', 'MEMORY', 'Forced garbage collection executed')
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Get instant memory snapshot without modifying samples
+ */
+ static getInstantUsage(): { heapUsedMB: number; heapTotalMB: number; rssMB: number } {
+ const usage = process.memoryUsage()
+ return {
+ heapUsedMB: usage.heapUsed / (1024 * 1024),
+ heapTotalMB: usage.heapTotal / (1024 * 1024),
+ rssMB: usage.rss / (1024 * 1024)
+ }
+ }
+}
+
+// Singleton instance for global access
+let globalMonitor: MemoryMonitor | null = null
+
+/**
+ * Get or create the global memory monitor instance
+ */
+export function getMemoryMonitor(config?: MemoryConfig): MemoryMonitor {
+ if (!globalMonitor) {
+ globalMonitor = new MemoryMonitor(config)
+ }
+ return globalMonitor
+}
+
+/**
+ * Stop and release the global memory monitor
+ */
+export function stopMemoryMonitor(): void {
+ if (globalMonitor) {
+ globalMonitor.stop()
+ globalMonitor = null
+ }
+}
diff --git a/src/util/core/Utils.ts b/src/util/core/Utils.ts
index db93e3f..dd4959f 100644
--- a/src/util/core/Utils.ts
+++ b/src/util/core/Utils.ts
@@ -241,7 +241,7 @@ export function normalizeRecoveryEmail(recoveryEmail: unknown): string | undefin
export function replaceUntilStable(
input: string,
pattern: RegExp,
- replacement: string | ((substring: string, ...args: any[]) => string),
+ replacement: string | ((substring: string, ...args: string[]) => string),
maxPasses: number = 1000
): string {
if (!(pattern instanceof RegExp)) {
@@ -254,7 +254,11 @@ export function replaceUntilStable(
let previous = input
for (let i = 0; i < maxPasses; i++) {
- const next = previous.replace(globalPattern, replacement as any)
+ // Type assertion needed for union type compatibility with String.prototype.replace
+ const next = previous.replace(
+ globalPattern,
+ replacement as (substring: string, ...args: string[]) => string
+ )
if (next === previous) return next
previous = next
}
diff --git a/src/util/state/ActivityStatsTracker.ts b/src/util/state/ActivityStatsTracker.ts
new file mode 100644
index 0000000..0b1fefe
--- /dev/null
+++ b/src/util/state/ActivityStatsTracker.ts
@@ -0,0 +1,262 @@
+/**
+ * Activity Statistics Tracker
+ * Collects and reports statistics on activity success/failure rates
+ * Useful for identifying problematic activities and improving bot reliability
+ */
+
+import { log } from '../notifications/Logger'
+
+interface ActivityStat {
+ attempts: number
+ successes: number
+ failures: number
+ totalDurationMs: number
+ lastError?: string
+ lastAttemptTime: number
+}
+
+interface ActivityStatsConfig {
+ /** Whether to log periodic summaries (default: true) */
+ logSummaries?: boolean
+ /** Minimum failure rate (0-1) to trigger warnings (default: 0.5) */
+ warningFailureRate?: number
+}
+
+const DEFAULT_CONFIG: Required = {
+ logSummaries: true,
+ warningFailureRate: 0.5
+}
+
+export class ActivityStatsTracker {
+ private stats: Map = new Map()
+ private config: Required
+ private sessionStartTime: number = Date.now()
+
+ constructor(config?: ActivityStatsConfig) {
+ this.config = { ...DEFAULT_CONFIG, ...config }
+ }
+
+ /**
+ * Record the start of an activity attempt
+ * @param activityType Activity type identifier (e.g., 'SEARCH', 'QUIZ', 'POLL')
+ * @returns Start timestamp for duration calculation
+ */
+ startActivity(activityType: string): number {
+ const normalizedType = activityType.toUpperCase()
+ const startTime = Date.now()
+
+ let stat = this.stats.get(normalizedType)
+ if (!stat) {
+ stat = {
+ attempts: 0,
+ successes: 0,
+ failures: 0,
+ totalDurationMs: 0,
+ lastAttemptTime: startTime
+ }
+ this.stats.set(normalizedType, stat)
+ }
+
+ stat.attempts++
+ stat.lastAttemptTime = startTime
+
+ return startTime
+ }
+
+ /**
+ * Record a successful activity completion
+ * @param activityType Activity type identifier
+ * @param startTime Start timestamp from startActivity()
+ */
+ recordSuccess(activityType: string, startTime: number): void {
+ const normalizedType = activityType.toUpperCase()
+ const duration = Date.now() - startTime
+
+ const stat = this.stats.get(normalizedType)
+ if (stat) {
+ stat.successes++
+ stat.totalDurationMs += duration
+ }
+ }
+
+ /**
+ * Record a failed activity attempt
+ * @param activityType Activity type identifier
+ * @param startTime Start timestamp from startActivity()
+ * @param error Error that caused the failure
+ */
+ recordFailure(activityType: string, startTime: number, error?: Error | string): void {
+ const normalizedType = activityType.toUpperCase()
+ const duration = Date.now() - startTime
+
+ const stat = this.stats.get(normalizedType)
+ if (stat) {
+ stat.failures++
+ stat.totalDurationMs += duration
+ if (error) {
+ stat.lastError = error instanceof Error ? error.message : String(error)
+ }
+ }
+ }
+
+ /**
+ * Get statistics for a specific activity type
+ */
+ getActivityStats(activityType: string): ActivityStat | undefined {
+ return this.stats.get(activityType.toUpperCase())
+ }
+
+ /**
+ * Get failure rate for a specific activity type (0-1)
+ */
+ getFailureRate(activityType: string): number {
+ const stat = this.stats.get(activityType.toUpperCase())
+ if (!stat || stat.attempts === 0) {
+ return 0
+ }
+ return stat.failures / stat.attempts
+ }
+
+ /**
+ * Get average duration for a specific activity type in milliseconds
+ */
+ getAverageDuration(activityType: string): number {
+ const stat = this.stats.get(activityType.toUpperCase())
+ if (!stat || stat.attempts === 0) {
+ return 0
+ }
+ return stat.totalDurationMs / stat.attempts
+ }
+
+ /**
+ * Get all activity types that exceed the warning failure rate
+ */
+ getProblematicActivities(): Array<{ type: string; failureRate: number; attempts: number; lastError?: string }> {
+ const problematic: Array<{ type: string; failureRate: number; attempts: number; lastError?: string }> = []
+
+ for (const [type, stat] of this.stats) {
+ const failureRate = stat.attempts > 0 ? stat.failures / stat.attempts : 0
+ if (failureRate >= this.config.warningFailureRate && stat.attempts >= 2) {
+ problematic.push({
+ type,
+ failureRate,
+ attempts: stat.attempts,
+ lastError: stat.lastError
+ })
+ }
+ }
+
+ return problematic.sort((a, b) => b.failureRate - a.failureRate)
+ }
+
+ /**
+ * Get comprehensive summary of all activity statistics
+ */
+ getSummary(): {
+ totalAttempts: number
+ totalSuccesses: number
+ totalFailures: number
+ overallSuccessRate: number
+ sessionDurationHours: number
+ byActivity: Array<{
+ type: string
+ attempts: number
+ successes: number
+ failures: number
+ successRate: number
+ avgDurationMs: number
+ }>
+ } {
+ let totalAttempts = 0
+ let totalSuccesses = 0
+ let totalFailures = 0
+ const byActivity: Array<{
+ type: string
+ attempts: number
+ successes: number
+ failures: number
+ successRate: number
+ avgDurationMs: number
+ }> = []
+
+ for (const [type, stat] of this.stats) {
+ totalAttempts += stat.attempts
+ totalSuccesses += stat.successes
+ totalFailures += stat.failures
+
+ byActivity.push({
+ type,
+ attempts: stat.attempts,
+ successes: stat.successes,
+ failures: stat.failures,
+ successRate: stat.attempts > 0 ? stat.successes / stat.attempts : 0,
+ avgDurationMs: stat.attempts > 0 ? stat.totalDurationMs / stat.attempts : 0
+ })
+ }
+
+ return {
+ totalAttempts,
+ totalSuccesses,
+ totalFailures,
+ overallSuccessRate: totalAttempts > 0 ? totalSuccesses / totalAttempts : 0,
+ sessionDurationHours: (Date.now() - this.sessionStartTime) / 3600000,
+ byActivity: byActivity.sort((a, b) => b.attempts - a.attempts)
+ }
+ }
+
+ /**
+ * Log summary to console/webhook
+ */
+ logSummary(): void {
+ if (!this.config.logSummaries) {
+ return
+ }
+
+ const summary = this.getSummary()
+
+ if (summary.totalAttempts === 0) {
+ return
+ }
+
+ log('main', 'ACTIVITY-STATS', `Session summary: ${summary.totalSuccesses}/${summary.totalAttempts} activities succeeded (${(summary.overallSuccessRate * 100).toFixed(1)}%)`)
+
+ // Log problematic activities
+ const problematic = this.getProblematicActivities()
+ if (problematic.length > 0) {
+ for (const activity of problematic) {
+ log('main', 'ACTIVITY-STATS', `High failure rate: ${activity.type} (${(activity.failureRate * 100).toFixed(0)}% failed, ${activity.attempts} attempts)${activity.lastError ? ` - Last error: ${activity.lastError.substring(0, 80)}` : ''}`, 'warn')
+ }
+ }
+ }
+
+ /**
+ * Reset all statistics (call between bot runs)
+ */
+ reset(): void {
+ this.stats.clear()
+ this.sessionStartTime = Date.now()
+ }
+}
+
+// Singleton instance for global access
+let globalTracker: ActivityStatsTracker | null = null
+
+/**
+ * Get or create the global activity stats tracker instance
+ */
+export function getActivityStatsTracker(config?: ActivityStatsConfig): ActivityStatsTracker {
+ if (!globalTracker) {
+ globalTracker = new ActivityStatsTracker(config)
+ }
+ return globalTracker
+}
+
+/**
+ * Reset and release the global tracker
+ */
+export function resetActivityStatsTracker(): void {
+ if (globalTracker) {
+ globalTracker.logSummary()
+ globalTracker.reset()
+ }
+}