mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
feat: Add automatic error reporting feature with configuration options and documentation
This commit is contained in:
@@ -85,6 +85,7 @@ For detailed configuration, advanced features, and troubleshooting, visit our co
|
|||||||
| **[Docker Deployment](docs/docker.md)** | Running in containers |
|
| **[Docker Deployment](docs/docker.md)** | Running in containers |
|
||||||
| **[Humanization](docs/humanization.md)** | Anti-detection and natural behavior |
|
| **[Humanization](docs/humanization.md)** | Anti-detection and natural behavior |
|
||||||
| **[Notifications](docs/conclusionwebhook.md)** | Discord webhooks and NTFY setup |
|
| **[Notifications](docs/conclusionwebhook.md)** | Discord webhooks and NTFY setup |
|
||||||
|
| **[Error Reporting](docs/ERROR_REPORTING.md)** | 🆕 Automatic error reporting to help improve the project |
|
||||||
| **[Proxy Setup](docs/proxy.md)** | Configuring proxies for privacy |
|
| **[Proxy Setup](docs/proxy.md)** | Configuring proxies for privacy |
|
||||||
| **[Troubleshooting](docs/diagnostics.md)** | Debug common issues and capture logs |
|
| **[Troubleshooting](docs/diagnostics.md)** | Debug common issues and capture logs |
|
||||||
|
|
||||||
|
|||||||
80
docs/ERROR_REPORTING.md
Normal file
80
docs/ERROR_REPORTING.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 🐛 Automatic Error Reporting
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The bot automatically reports errors to a community webhook to help identify and fix issues faster, without requiring manual bug reports from users.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
✅ **Privacy-First** - Only non-sensitive error information is sent
|
||||||
|
✅ **Opt-Out** - Can be disabled in configuration
|
||||||
|
✅ **Obfuscated Webhook** - URL is base64-encoded
|
||||||
|
✅ **Automatic Sanitization** - Removes emails, paths, tokens, and IPs
|
||||||
|
✅ **Intelligent Filtering** - Excludes user configuration errors and false positives
|
||||||
|
✅ **System Information** - Includes version, platform, architecture for debugging
|
||||||
|
|
||||||
|
## What's Sent
|
||||||
|
|
||||||
|
- Error message (sanitized)
|
||||||
|
- Stack trace (truncated, sanitized)
|
||||||
|
- Bot version
|
||||||
|
- Operating system and architecture
|
||||||
|
- Node.js version
|
||||||
|
- Timestamp
|
||||||
|
|
||||||
|
## What's NOT Sent
|
||||||
|
|
||||||
|
- ❌ Email addresses (redacted)
|
||||||
|
- ❌ File paths (redacted)
|
||||||
|
- ❌ IP addresses (redacted)
|
||||||
|
- ❌ Tokens/API keys (redacted)
|
||||||
|
- ❌ Account credentials
|
||||||
|
- ❌ Personal information
|
||||||
|
|
||||||
|
## Filtered Errors
|
||||||
|
|
||||||
|
The system intelligently filters out:
|
||||||
|
- User configuration errors (missing files, invalid credentials)
|
||||||
|
- Expected errors (no points, already completed)
|
||||||
|
- Network issues (proxy failures, port conflicts)
|
||||||
|
- Account-specific issues (banned accounts)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
In `config.jsonc`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"errorReporting": {
|
||||||
|
"enabled": true, // Set to false to disable
|
||||||
|
"webhookUrl": "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw=="
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable Error Reporting
|
||||||
|
|
||||||
|
Set `enabled` to `false`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"errorReporting": {
|
||||||
|
"enabled": false,
|
||||||
|
"webhookUrl": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Privacy & Security
|
||||||
|
|
||||||
|
- No PII is sent
|
||||||
|
- All sensitive data is automatically redacted
|
||||||
|
- Webhook URL is obfuscated (base64)
|
||||||
|
- Fire-and-forget (never blocks execution)
|
||||||
|
- Silent failure if webhook is unreachable
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- **File**: `src/util/ErrorReportingWebhook.ts`
|
||||||
|
- **Integration**: Automatic via `Logger.ts`
|
||||||
|
- **Method**: HTTP POST to Discord webhook
|
||||||
|
- **Timeout**: 10 seconds
|
||||||
|
- **Filtering**: Pattern-based false positive detection
|
||||||
|
|
||||||
|
Thank you for helping improve the bot! 🙏
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
| **[External Scheduling](schedule.md)** | Automate with cron or Task Scheduler |
|
| **[External Scheduling](schedule.md)** | Automate with cron or Task Scheduler |
|
||||||
| **[Humanization](humanization.md)** | Anti-detection system |
|
| **[Humanization](humanization.md)** | Anti-detection system |
|
||||||
| **[Webhooks](conclusionwebhook.md)** | Discord notifications |
|
| **[Webhooks](conclusionwebhook.md)** | Discord notifications |
|
||||||
|
| **[Error Reporting](ERROR_REPORTING.md)** | 🆕 Automatic error reporting |
|
||||||
| **[NTFY Alerts](ntfy.md)** | Mobile push notifications |
|
| **[NTFY Alerts](ntfy.md)** | Mobile push notifications |
|
||||||
| **[Proxy Setup](proxy.md)** | IP rotation (optional) |
|
| **[Proxy Setup](proxy.md)** | IP rotation (optional) |
|
||||||
| **[Docker](docker.md)** | Container deployment |
|
| **[Docker](docker.md)** | Container deployment |
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"enabled": true, // Auto-start dashboard with bot (default: false)
|
"enabled": false, // Auto-start dashboard with bot (default: false)
|
||||||
"port": 3000, // Dashboard port (default: 3000)
|
"port": 3000, // Dashboard port (default: 3000)
|
||||||
"host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security)
|
"host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security)
|
||||||
},
|
},
|
||||||
@@ -155,6 +155,12 @@
|
|||||||
"autoUpdateAccounts": false // Update accounts file from remote (NEVER recommended, keeps your accounts)
|
"autoUpdateAccounts": false // Update accounts file from remote (NEVER recommended, keeps your accounts)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Error Reporting (Community Contribution)
|
||||||
|
"errorReporting": {
|
||||||
|
"enabled": true, // Automatically report errors to help improve the project (no sensitive data sent)
|
||||||
|
"webhookUrl": "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw==" // Obfuscated webhook URL (base64 encoded)
|
||||||
|
},
|
||||||
|
|
||||||
// Scheduling (automatic task scheduling)
|
// Scheduling (automatic task scheduling)
|
||||||
// When enabled=true, the bot will automatically configure your system scheduler on first run.
|
// When enabled=true, the bot will automatically configure your system scheduler on first run.
|
||||||
// This works on Windows (Task Scheduler), Linux/Raspberry Pi (cron), and macOS (cron).
|
// This works on Windows (Task Scheduler), Linux/Raspberry Pi (cron), and macOS (cron).
|
||||||
|
|||||||
51
src/index.ts
51
src/index.ts
@@ -353,21 +353,13 @@ export class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
// Check if all workers have exited
|
// Check if all workers have exited
|
||||||
if (this.activeWorkers === 0) {
|
if (this.activeWorkers === 0) {
|
||||||
// All workers done -> send conclusion (if enabled), run optional auto-update, then exit
|
// All workers done -> send conclusion and exit (update check moved to startup)
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await this.sendConclusion(this.accountSummaries)
|
await this.sendConclusion(this.accountSummaries)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const updateCode = await this.runAutoUpdate()
|
|
||||||
if (updateCode === 0) {
|
|
||||||
log('main', 'UPDATE', '✅ Update successful - next run will use new version', 'log', 'green')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
|
||||||
}
|
|
||||||
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})()
|
})()
|
||||||
@@ -636,22 +628,8 @@ export class MicrosoftRewardsBot {
|
|||||||
process.send({ type: 'summary', data: this.accountSummaries })
|
process.send({ type: 'summary', data: this.accountSummaries })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single process mode -> build and send conclusion directly
|
// Single process mode -> build and send conclusion directly (update check moved to startup)
|
||||||
await this.sendConclusion(this.accountSummaries)
|
await this.sendConclusion(this.accountSummaries)
|
||||||
// After conclusion, run optional auto-update
|
|
||||||
const updateResult = await this.runAutoUpdate().catch((e) => {
|
|
||||||
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
|
||||||
return 1 // Error code
|
|
||||||
})
|
|
||||||
|
|
||||||
// If update was successful (code 0), restart the script to use the new version
|
|
||||||
// This is critical for cron jobs - they need to apply updates immediately
|
|
||||||
if (updateResult === 0) {
|
|
||||||
log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
|
|
||||||
// On Raspberry Pi/Linux with cron, just exit - cron will handle next run
|
|
||||||
// No need to restart immediately, next scheduled run will use new code
|
|
||||||
log('main', 'UPDATE', 'Next scheduled run will use the updated code', 'log')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
process.exit()
|
process.exit()
|
||||||
}
|
}
|
||||||
@@ -773,7 +751,7 @@ export class MicrosoftRewardsBot {
|
|||||||
*
|
*
|
||||||
* @returns Exit code (0 = success, non-zero = error)
|
* @returns Exit code (0 = success, non-zero = error)
|
||||||
*/
|
*/
|
||||||
private async runAutoUpdate(): Promise<number> {
|
async runAutoUpdate(): Promise<number> {
|
||||||
const upd = this.config.update
|
const upd = this.config.update
|
||||||
if (!upd) return 0
|
if (!upd) return 0
|
||||||
|
|
||||||
@@ -983,6 +961,29 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Check for updates BEFORE initializing and running tasks
|
||||||
|
try {
|
||||||
|
const updateResult = await rewardsBot.runAutoUpdate().catch((e) => {
|
||||||
|
log('main', 'UPDATE', `Auto-update check failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
return -1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updateResult === 0) {
|
||||||
|
log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
|
||||||
|
|
||||||
|
// Restart the process with the same arguments
|
||||||
|
const { spawn } = await import('child_process')
|
||||||
|
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'inherit'
|
||||||
|
})
|
||||||
|
child.unref()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
} catch (updateError) {
|
||||||
|
log('main', 'UPDATE', `Update check failed (continuing): ${updateError instanceof Error ? updateError.message : String(updateError)}`, 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
await rewardsBot.initialize()
|
await rewardsBot.initialize()
|
||||||
await rewardsBot.run()
|
await rewardsBot.run()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface Config {
|
|||||||
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
|
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
|
||||||
dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control
|
dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control
|
||||||
scheduling?: ConfigScheduling; // NEW: Automatic scheduler configuration (cron/Task Scheduler)
|
scheduling?: ConfigScheduling; // NEW: Automatic scheduler configuration (cron/Task Scheduler)
|
||||||
|
errorReporting?: ConfigErrorReporting; // NEW: Automatic error reporting to community webhook
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigSaveFingerprint {
|
export interface ConfigSaveFingerprint {
|
||||||
@@ -211,3 +212,8 @@ export interface ConfigScheduling {
|
|||||||
highestPrivileges?: boolean; // request highest privileges
|
highestPrivileges?: boolean; // request highest privileges
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigErrorReporting {
|
||||||
|
enabled?: boolean; // enable automatic error reporting to community webhook (default: true)
|
||||||
|
webhookUrl?: string; // obfuscated Discord webhook URL for error reports
|
||||||
|
}
|
||||||
|
|||||||
239
src/util/ErrorReportingWebhook.ts
Normal file
239
src/util/ErrorReportingWebhook.ts
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { DISCORD } from '../constants'
|
||||||
|
import { Config } from '../interface/Config'
|
||||||
|
|
||||||
|
interface ErrorReportPayload {
|
||||||
|
error: string
|
||||||
|
stack?: string
|
||||||
|
context: {
|
||||||
|
version: string
|
||||||
|
platform: string
|
||||||
|
arch: string
|
||||||
|
nodeVersion: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple obfuscation/deobfuscation for webhook URL
|
||||||
|
* Not for security, just to avoid easy scraping
|
||||||
|
*/
|
||||||
|
export function obfuscateWebhookUrl(url: string): string {
|
||||||
|
return Buffer.from(url).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deobfuscateWebhookUrl(encoded: string): string {
|
||||||
|
try {
|
||||||
|
return Buffer.from(encoded, 'base64').toString('utf-8')
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error should be reported (filter false positives and user configuration errors)
|
||||||
|
*/
|
||||||
|
function shouldReportError(errorMessage: string): boolean {
|
||||||
|
const lowerMessage = errorMessage.toLowerCase()
|
||||||
|
|
||||||
|
// List of patterns that indicate user configuration errors (not reportable bugs)
|
||||||
|
const userConfigPatterns = [
|
||||||
|
/accounts\.jsonc.*not found/i,
|
||||||
|
/config\.jsonc.*not found/i,
|
||||||
|
/invalid.*credentials/i,
|
||||||
|
/login.*failed/i,
|
||||||
|
/authentication.*failed/i,
|
||||||
|
/proxy.*connection.*failed/i,
|
||||||
|
/totp.*invalid/i,
|
||||||
|
/2fa.*failed/i,
|
||||||
|
/incorrect.*password/i,
|
||||||
|
/account.*suspended/i,
|
||||||
|
/account.*banned/i,
|
||||||
|
/no.*accounts.*enabled/i,
|
||||||
|
/invalid.*configuration/i,
|
||||||
|
/missing.*required.*field/i,
|
||||||
|
/port.*already.*in.*use/i,
|
||||||
|
/eaddrinuse/i
|
||||||
|
]
|
||||||
|
|
||||||
|
// Don't report user configuration errors
|
||||||
|
for (const pattern of userConfigPatterns) {
|
||||||
|
if (pattern.test(lowerMessage)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of patterns that indicate expected/handled errors (not bugs)
|
||||||
|
const expectedErrorPatterns = [
|
||||||
|
/no.*points.*to.*earn/i,
|
||||||
|
/already.*completed/i,
|
||||||
|
/activity.*not.*available/i,
|
||||||
|
/daily.*limit.*reached/i,
|
||||||
|
/quest.*not.*found/i,
|
||||||
|
/promotion.*expired/i
|
||||||
|
]
|
||||||
|
|
||||||
|
// Don't report expected/handled errors
|
||||||
|
for (const pattern of expectedErrorPatterns) {
|
||||||
|
if (pattern.test(lowerMessage)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report everything else (genuine bugs)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send error report to Discord webhook for community contribution
|
||||||
|
* Only sends non-sensitive error information to help improve the project
|
||||||
|
*/
|
||||||
|
export async function sendErrorReport(
|
||||||
|
config: Config,
|
||||||
|
error: Error | string,
|
||||||
|
additionalContext?: Record<string, unknown>
|
||||||
|
): Promise<void> {
|
||||||
|
// Check if error reporting is enabled and URL is configured
|
||||||
|
if (!config.errorReporting?.enabled) return
|
||||||
|
if (!config.errorReporting?.webhookUrl) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Deobfuscate webhook URL
|
||||||
|
const webhookUrl = deobfuscateWebhookUrl(config.errorReporting.webhookUrl)
|
||||||
|
if (!webhookUrl || !webhookUrl.startsWith('https://discord.com/api/webhooks/')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
// Filter out false positives and user configuration errors
|
||||||
|
if (!shouldReportError(errorMessage)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const errorStack = error instanceof Error ? error.stack : undefined
|
||||||
|
|
||||||
|
// Sanitize error message and stack - remove any potential sensitive data
|
||||||
|
const sanitize = (text: string): string => {
|
||||||
|
return text
|
||||||
|
// Remove email addresses
|
||||||
|
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[EMAIL_REDACTED]')
|
||||||
|
// Remove absolute paths (Windows and Unix)
|
||||||
|
.replace(/[A-Za-z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*/g, '[PATH_REDACTED]')
|
||||||
|
.replace(/\/(?:home|Users)\/[^/\s]+(?:\/[^/\s]+)*/g, '[PATH_REDACTED]')
|
||||||
|
// Remove IP addresses
|
||||||
|
.replace(/\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g, '[IP_REDACTED]')
|
||||||
|
// Remove potential tokens/keys (sequences of 20+ alphanumeric chars)
|
||||||
|
.replace(/\b[A-Za-z0-9_-]{20,}\b/g, '[TOKEN_REDACTED]')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedMessage = sanitize(errorMessage)
|
||||||
|
const sanitizedStack = errorStack ? sanitize(errorStack).split('\n').slice(0, 10).join('\n') : undefined
|
||||||
|
|
||||||
|
// Build context payload with system information
|
||||||
|
const payload: ErrorReportPayload = {
|
||||||
|
error: sanitizedMessage,
|
||||||
|
stack: sanitizedStack,
|
||||||
|
context: {
|
||||||
|
version: getProjectVersion(),
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
nodeVersion: process.version,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional context if provided (also sanitized)
|
||||||
|
if (additionalContext) {
|
||||||
|
const sanitizedContext: Record<string, unknown> = {}
|
||||||
|
for (const [key, value] of Object.entries(additionalContext)) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
sanitizedContext[key] = sanitize(value)
|
||||||
|
} else {
|
||||||
|
sanitizedContext[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(payload.context, sanitizedContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Discord embed
|
||||||
|
const embed = {
|
||||||
|
title: '🐛 Automatic Error Report',
|
||||||
|
description: `\`\`\`\n${sanitizedMessage.slice(0, 500)}\n\`\`\``,
|
||||||
|
color: DISCORD.COLOR_RED,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: '📦 Version',
|
||||||
|
value: payload.context.version,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '💻 Platform',
|
||||||
|
value: `${payload.context.platform} (${payload.context.arch})`,
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '⚙️ Node.js',
|
||||||
|
value: payload.context.nodeVersion,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timestamp: payload.context.timestamp,
|
||||||
|
footer: {
|
||||||
|
text: 'Automatic error reporting - Thank you for contributing!',
|
||||||
|
icon_url: DISCORD.AVATAR_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stack trace field if available (truncated)
|
||||||
|
if (sanitizedStack) {
|
||||||
|
embed.fields.push({
|
||||||
|
name: '📋 Stack Trace (truncated)',
|
||||||
|
value: `\`\`\`\n${sanitizedStack.slice(0, 800)}\n\`\`\``,
|
||||||
|
inline: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add additional context fields if provided
|
||||||
|
if (additionalContext) {
|
||||||
|
for (const [key, value] of Object.entries(additionalContext)) {
|
||||||
|
if (embed.fields.length < 25) { // Discord limit
|
||||||
|
embed.fields.push({
|
||||||
|
name: key,
|
||||||
|
value: String(value).slice(0, 1024),
|
||||||
|
inline: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const discordPayload = {
|
||||||
|
username: 'Microsoft-Rewards-Bot Error Reporter',
|
||||||
|
avatar_url: DISCORD.AVATAR_URL,
|
||||||
|
embeds: [embed]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to webhook with timeout
|
||||||
|
await axios.post(webhookUrl, discordPayload, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
// Silent fail - we don't want error reporting to break the application
|
||||||
|
// Only log to stderr to avoid recursion
|
||||||
|
process.stderr.write(`[ErrorReporting] Failed to send error report: ${webhookError}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project version from package.json
|
||||||
|
*/
|
||||||
|
function getProjectVersion(): string {
|
||||||
|
try {
|
||||||
|
// Dynamic import for package.json
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const packageJson = require('../../package.json') as { version?: string }
|
||||||
|
return packageJson.version || 'unknown'
|
||||||
|
} catch {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import axios from 'axios'
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
|
||||||
import { DISCORD } from '../constants'
|
import { DISCORD } from '../constants'
|
||||||
|
import { sendErrorReport } from './ErrorReportingWebhook'
|
||||||
import { loadConfig } from './Load'
|
import { loadConfig } from './Load'
|
||||||
import { Ntfy } from './Ntfy'
|
import { Ntfy } from './Ntfy'
|
||||||
|
|
||||||
@@ -282,8 +283,24 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
|||||||
process.stderr.write(`[Logger] Failed to enqueue webhook log: ${error}\n`)
|
process.stderr.write(`[Logger] Failed to enqueue webhook log: ${error}\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an Error when logging an error so callers can `throw log(...)`
|
// Automatic error reporting to community webhook (fire and forget)
|
||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
return new Error(cleanStr)
|
const errorObj = new Error(cleanStr)
|
||||||
|
|
||||||
|
// Send error report asynchronously without blocking
|
||||||
|
Promise.resolve().then(async () => {
|
||||||
|
try {
|
||||||
|
await sendErrorReport(configData, errorObj, {
|
||||||
|
title,
|
||||||
|
platform: platformText
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// Silent fail - error reporting should never break the application
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Catch any promise rejection silently
|
||||||
|
})
|
||||||
|
|
||||||
|
return errorObj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
38
tests/errorReporting.test.ts
Normal file
38
tests/errorReporting.test.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import assert from 'node:assert'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import { deobfuscateWebhookUrl, obfuscateWebhookUrl } from '../src/util/ErrorReportingWebhook'
|
||||||
|
|
||||||
|
describe('ErrorReportingWebhook', () => {
|
||||||
|
describe('URL obfuscation', () => {
|
||||||
|
it('should obfuscate and deobfuscate webhook URL correctly', () => {
|
||||||
|
const originalUrl = 'https://discord.com/api/webhooks/1234567890/test-webhook-token'
|
||||||
|
const obfuscated = obfuscateWebhookUrl(originalUrl)
|
||||||
|
const deobfuscated = deobfuscateWebhookUrl(obfuscated)
|
||||||
|
|
||||||
|
assert.notStrictEqual(obfuscated, originalUrl, 'Obfuscated URL should differ from original')
|
||||||
|
assert.strictEqual(deobfuscated, originalUrl, 'Deobfuscated URL should match original')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return empty string for invalid base64', () => {
|
||||||
|
const result = deobfuscateWebhookUrl('invalid!!!base64@@@')
|
||||||
|
assert.strictEqual(result, '', 'Invalid base64 should return empty string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
const obfuscated = obfuscateWebhookUrl('')
|
||||||
|
const deobfuscated = deobfuscateWebhookUrl(obfuscated)
|
||||||
|
assert.strictEqual(deobfuscated, '', 'Empty string should remain empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should verify project webhook URL', () => {
|
||||||
|
const projectWebhook = 'https://discord.com/api/webhooks/1437111962394689629/tlvGKZaH9-rJir4tnZKSZpRHS3YbeN4vZnuCv50k5MpADYRPnHnZ6MybAlgF5QFo6KH_'
|
||||||
|
const expectedObfuscated = 'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw=='
|
||||||
|
|
||||||
|
const obfuscated = obfuscateWebhookUrl(projectWebhook)
|
||||||
|
assert.strictEqual(obfuscated, expectedObfuscated, 'Project webhook should match expected obfuscation')
|
||||||
|
|
||||||
|
const deobfuscated = deobfuscateWebhookUrl(expectedObfuscated)
|
||||||
|
assert.strictEqual(deobfuscated, projectWebhook, 'Deobfuscated should match original project webhook')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user