ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve extension error messaging

byarielm.fyi 7f2dcdab 3034db12

verified
+316 -18
+11 -3
packages/extension/src/background/service-worker.ts
··· 130 130 * Handle scrape error 131 131 */ 132 132 async function handleScrapeError(message: ScrapeErrorMessage): Promise<void> { 133 - const { error } = message.payload; 133 + const { error, category, userMessage, troubleshootingTips } = message.payload; 134 134 const currentState = await getState(); 135 135 136 136 const state: ExtensionState = { 137 137 ...currentState, 138 138 status: 'error', 139 139 error, 140 + errorCategory: category, 141 + errorUserMessage: userMessage, 142 + errorTroubleshootingTips: troubleshootingTips, 140 143 progress: { 141 144 count: currentState.progress?.count || 0, 142 145 status: 'error', 143 - message: `Error: ${error}` 146 + message: `Error: ${userMessage || error}` 144 147 } 145 148 }; 146 149 147 150 await setState(state); 148 - console.error('[Background] Scraping error:', error); 151 + console.error('[Background] Scraping error:', { 152 + technical: error, 153 + category, 154 + userMessage, 155 + tips: troubleshootingTips 156 + }); 149 157 } 150 158 151 159 /**
+27 -3
packages/extension/src/content/index.ts
··· 1 1 import { TwitterScraper } from './scrapers/twitter-scraper.js'; 2 - import type { BaseScraper } from './scrapers/base-scraper.js'; 2 + import type { BaseScraper, ScraperErrorContext } from './scrapers/base-scraper.js'; 3 3 import { 4 4 MessageType, 5 5 onMessage, ··· 10 10 type ScrapeCompleteMessage, 11 11 type ScrapeErrorMessage 12 12 } from '../lib/messaging.js'; 13 + import { categorizeError, detectCommonScenarios, type ErrorContext } from '../lib/errors.js'; 13 14 14 15 /** 15 16 * Platform configuration ··· 102 103 103 104 const scraper = config.createScraper(); 104 105 106 + // Track progress for error context 107 + let lastUsersFound = 0; 108 + 105 109 scraper.onProgress = (progress) => { 110 + lastUsersFound = progress.count; 106 111 const progressMessage: ScrapeProgressMessage = { 107 112 type: MessageType.SCRAPE_PROGRESS, 108 113 payload: progress ··· 120 125 currentScraper = null; 121 126 }; 122 127 123 - scraper.onError = (error) => { 128 + scraper.onError = (error, scraperContext?: ScraperErrorContext) => { 129 + // Build complete error context 130 + const errorContext: ErrorContext = { 131 + usersFound: lastUsersFound, 132 + scrollAttempts: scraperContext?.scrollAttempts || 0, 133 + timeElapsed: scraperContext?.timeElapsed || 0, 134 + pageUrl: scraperContext?.pageUrl || window.location.href 135 + }; 136 + 137 + // Check for common scenarios first 138 + let categorized = detectCommonScenarios(errorContext); 139 + 140 + // If no common scenario detected, categorize the error 141 + if (!categorized) { 142 + categorized = categorizeError(error, errorContext); 143 + } 144 + 124 145 const errorMessage: ScrapeErrorMessage = { 125 146 type: MessageType.SCRAPE_ERROR, 126 147 payload: { 127 - error: error.message 148 + error: categorized.technicalMessage, 149 + category: categorized.category, 150 + userMessage: categorized.userMessage, 151 + troubleshootingTips: categorized.troubleshootingTips 128 152 } 129 153 }; 130 154 sendToBackground(errorMessage);
+25 -3
packages/extension/src/content/scrapers/base-scraper.ts
··· 13 13 export interface ScraperCallbacks { 14 14 onProgress?: (progress: ScraperProgress) => void; 15 15 onComplete?: (result: ScraperResult) => void; 16 - onError?: (error: Error) => void; 16 + onError?: (error: Error, context?: ScraperErrorContext) => void; 17 + } 18 + 19 + export interface ScraperErrorContext { 20 + usersFound: number; 21 + scrollAttempts: number; 22 + timeElapsed: number; 23 + pageUrl: string; 17 24 } 18 25 19 26 export abstract class BaseScraper { 20 27 protected onProgress: (progress: ScraperProgress) => void; 21 28 protected onComplete: (result: ScraperResult) => void; 22 - protected onError: (error: Error) => void; 29 + protected onError: (error: Error, context?: ScraperErrorContext) => void; 23 30 24 31 constructor(callbacks: ScraperCallbacks = {}) { 25 32 this.onProgress = callbacks.onProgress || (() => {}); ··· 45 52 * Scrolls page until no new users found for 3 consecutive scrolls 46 53 */ 47 54 async scrape(): Promise<string[]> { 55 + const startTime = Date.now(); 56 + let scrollAttempts = 0; 57 + 48 58 try { 49 59 const usernames = new Set<string>(); 50 60 let stableCount = 0; ··· 54 64 this.onProgress({ count: 0, status: 'scraping', message: 'Starting scan...' }); 55 65 56 66 while (stableCount < maxStableCount) { 67 + scrollAttempts++; 68 + 57 69 // Collect visible usernames 58 70 const elements = document.querySelectorAll(this.getUsernameSelector()); 59 71 ··· 100 112 return result.usernames; 101 113 } catch (error) { 102 114 const err = error instanceof Error ? error : new Error(String(error)); 103 - this.onError(err); 115 + const timeElapsed = Date.now() - startTime; 116 + 117 + // Build error context for categorization 118 + const context: ScraperErrorContext = { 119 + usersFound: 0, // Will be set by content script if it has the info 120 + scrollAttempts, 121 + timeElapsed, 122 + pageUrl: window.location.href 123 + }; 124 + 125 + this.onError(err, context); 104 126 this.onProgress({ 105 127 count: 0, 106 128 status: 'error',
+193
packages/extension/src/lib/errors.ts
··· 1 + /** 2 + * Error categories for better user feedback 3 + */ 4 + export enum ErrorCategory { 5 + DOM_ERROR = 'DOM_ERROR', 6 + SCRAPING_ERROR = 'SCRAPING_ERROR', 7 + NETWORK_ERROR = 'NETWORK_ERROR', 8 + PERMISSION_ERROR = 'PERMISSION_ERROR', 9 + PAGE_STATE_ERROR = 'PAGE_STATE_ERROR', 10 + UNKNOWN_ERROR = 'UNKNOWN_ERROR' 11 + } 12 + 13 + /** 14 + * Enhanced error with category and user-friendly message 15 + */ 16 + export interface CategorizedError { 17 + category: ErrorCategory; 18 + technicalMessage: string; 19 + userMessage: string; 20 + troubleshootingTips?: string[]; 21 + } 22 + 23 + /** 24 + * Categorize an error and provide user-friendly messaging 25 + */ 26 + export function categorizeError(error: Error, context?: ErrorContext): CategorizedError { 27 + const message = error.message.toLowerCase(); 28 + const name = error.name.toLowerCase(); 29 + 30 + // DOM/Selector errors 31 + if ( 32 + message.includes('cannot read') || 33 + message.includes('null') || 34 + message.includes('undefined') || 35 + message.includes('queryselector') || 36 + message.includes('element') 37 + ) { 38 + return { 39 + category: ErrorCategory.DOM_ERROR, 40 + technicalMessage: error.message, 41 + userMessage: 'Page structure has changed', 42 + troubleshootingTips: [ 43 + 'Try refreshing the page', 44 + 'Make sure you\'re on the Following page', 45 + 'Extension may need an update' 46 + ] 47 + }; 48 + } 49 + 50 + // Network/fetch errors 51 + if ( 52 + message.includes('network') || 53 + message.includes('fetch') || 54 + message.includes('timeout') || 55 + message.includes('connection') || 56 + name.includes('network') 57 + ) { 58 + return { 59 + category: ErrorCategory.NETWORK_ERROR, 60 + technicalMessage: error.message, 61 + userMessage: 'Network connection issue', 62 + troubleshootingTips: [ 63 + 'Check your internet connection', 64 + 'Try again in a moment', 65 + 'Page may be loading slowly' 66 + ] 67 + }; 68 + } 69 + 70 + // Permission errors 71 + if ( 72 + message.includes('permission') || 73 + message.includes('denied') || 74 + message.includes('blocked') || 75 + message.includes('cors') 76 + ) { 77 + return { 78 + category: ErrorCategory.PERMISSION_ERROR, 79 + technicalMessage: error.message, 80 + userMessage: 'Extension permissions issue', 81 + troubleshootingTips: [ 82 + 'Reload the extension', 83 + 'Refresh the page', 84 + 'Check browser permissions' 85 + ] 86 + }; 87 + } 88 + 89 + // Use context to provide better categorization 90 + if (context) { 91 + // No users found - likely not logged in or on wrong page 92 + if (context.usersFound === 0 && context.scrollAttempts && context.scrollAttempts > 2) { 93 + return { 94 + category: ErrorCategory.PAGE_STATE_ERROR, 95 + technicalMessage: error.message, 96 + userMessage: 'No users found on page', 97 + troubleshootingTips: [ 98 + 'Make sure you\'re logged in to X/Twitter', 99 + 'Verify you\'re on the Following page', 100 + 'Check if you actually follow anyone' 101 + ] 102 + }; 103 + } 104 + 105 + // Scraping took too long or got stuck 106 + if (context.timeElapsed && context.timeElapsed > 120000) { 107 + return { 108 + category: ErrorCategory.SCRAPING_ERROR, 109 + technicalMessage: error.message, 110 + userMessage: 'Scanning took too long', 111 + troubleshootingTips: [ 112 + 'Page may have too many users', 113 + 'Try scrolling manually first', 114 + 'Refresh and try again' 115 + ] 116 + }; 117 + } 118 + } 119 + 120 + // Generic scraping error 121 + if ( 122 + message.includes('scrape') || 123 + message.includes('scan') || 124 + message.includes('extract') 125 + ) { 126 + return { 127 + category: ErrorCategory.SCRAPING_ERROR, 128 + technicalMessage: error.message, 129 + userMessage: 'Failed to scan page', 130 + troubleshootingTips: [ 131 + 'Refresh the page and try again', 132 + 'Make sure page is fully loaded', 133 + 'Check if you\'re logged in' 134 + ] 135 + }; 136 + } 137 + 138 + // Unknown error 139 + return { 140 + category: ErrorCategory.UNKNOWN_ERROR, 141 + technicalMessage: error.message, 142 + userMessage: 'An unexpected error occurred', 143 + troubleshootingTips: [ 144 + 'Refresh the page and try again', 145 + 'Reload the extension', 146 + 'Report this issue if it persists' 147 + ] 148 + }; 149 + } 150 + 151 + /** 152 + * Context information to help categorize errors 153 + */ 154 + export interface ErrorContext { 155 + usersFound?: number; 156 + scrollAttempts?: number; 157 + timeElapsed?: number; 158 + pageUrl?: string; 159 + } 160 + 161 + /** 162 + * Common scenario detection 163 + */ 164 + export function detectCommonScenarios(context: ErrorContext): CategorizedError | null { 165 + // Not logged in to X/Twitter 166 + if (context.usersFound === 0 && context.scrollAttempts && context.scrollAttempts >= 3) { 167 + return { 168 + category: ErrorCategory.PAGE_STATE_ERROR, 169 + technicalMessage: 'No users found after multiple scroll attempts', 170 + userMessage: 'No users found - are you logged in to X?', 171 + troubleshootingTips: [ 172 + 'Log in to your X/Twitter account', 173 + 'Navigate to your Following page', 174 + 'Make sure you follow at least one account' 175 + ] 176 + }; 177 + } 178 + 179 + // On wrong page 180 + if (context.pageUrl && !context.pageUrl.includes('/following')) { 181 + return { 182 + category: ErrorCategory.PAGE_STATE_ERROR, 183 + technicalMessage: 'Not on Following page', 184 + userMessage: 'Wrong page - must be on Following page', 185 + troubleshootingTips: [ 186 + 'Click "Open X Following" button', 187 + 'Or navigate to x.com/following manually' 188 + ] 189 + }; 190 + } 191 + 192 + return null; 193 + }
+6
packages/extension/src/lib/messaging.ts
··· 50 50 type: MessageType.SCRAPE_ERROR; 51 51 payload: { 52 52 error: string; 53 + category?: string; 54 + userMessage?: string; 55 + troubleshootingTips?: string[]; 53 56 }; 54 57 } 55 58 ··· 82 85 progress?: ScraperProgress; 83 86 result?: ScraperResult; 84 87 error?: string; 88 + errorCategory?: string; 89 + errorUserMessage?: string; 90 + errorTroubleshootingTips?: string[]; 85 91 } 86 92 87 93 /**
+21 -5
packages/extension/src/popup/popup.html
··· 131 131 class="w-full text-center flex flex-col justify-center" 132 132 > 133 133 <div class="text-5xl mb-4">⚠️</div> 134 - <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> 135 - Error 136 - </p> 137 134 <p 138 - id="error-message" 139 - class="text-sm text-purple-950 dark:text-cyan-50 mt-2 p-3 bg-white-50 dark:bg-slate-950/50 rounded border-l-[3px] border-red-600" 135 + id="error-user-message" 136 + class="text-base mb-3 text-purple-950 dark:text-cyan-50 font-semibold" 140 137 ></p> 138 + <div 139 + id="error-tips" 140 + class="text-left text-[13px] text-purple-900 dark:text-cyan-100 mt-2 p-3 bg-white/50 dark:bg-slate-900/50 rounded border-l-[3px] border-orange-600" 141 + > 142 + <p class="font-semibold mb-2">Try these steps:</p> 143 + <ul id="error-tips-list" class="list-disc pl-4 space-y-1"> 144 + </ul> 145 + </div> 146 + <details class="mt-3 text-left"> 147 + <summary 148 + class="text-xs text-slate-500 dark:text-slate-400 cursor-pointer hover:text-slate-700 dark:hover:text-slate-300" 149 + > 150 + Technical details 151 + </summary> 152 + <p 153 + id="error-technical" 154 + class="text-xs text-slate-600 dark:text-slate-400 mt-2 p-2 bg-slate-100 dark:bg-slate-950/50 rounded font-mono" 155 + ></p> 156 + </details> 141 157 <button 142 158 id="btn-retry" 143 159 class="w-full px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-bold shadow-md hover:shadow-lg transition-all mt-4"
+33 -4
packages/extension/src/popup/popup.ts
··· 28 28 platformName: document.getElementById("platform-name")!, 29 29 count: document.getElementById("count")!, 30 30 finalCount: document.getElementById("final-count")!, 31 - errorMessage: document.getElementById("error-message")!, 31 + errorUserMessage: document.getElementById("error-user-message")!, 32 + errorTechnical: document.getElementById("error-technical")!, 33 + errorTipsList: document.getElementById("error-tips-list")!, 32 34 serverUrl: document.getElementById("server-url")!, 33 35 devInstructions: document.getElementById("dev-instructions")!, 34 36 progressFill: document.getElementById("progress-fill")! as HTMLElement, ··· 100 102 101 103 case "error": 102 104 showState("error"); 103 - elements.errorMessage.textContent = 104 - state.error || "An unknown error occurred"; 105 + 106 + // Display user-friendly message 107 + elements.errorUserMessage.textContent = 108 + state.errorUserMessage || "An error occurred"; 109 + 110 + // Display troubleshooting tips 111 + elements.errorTipsList.innerHTML = ""; 112 + if (state.errorTroubleshootingTips && state.errorTroubleshootingTips.length > 0) { 113 + state.errorTroubleshootingTips.forEach(tip => { 114 + const li = document.createElement("li"); 115 + li.textContent = tip; 116 + elements.errorTipsList.appendChild(li); 117 + }); 118 + } else { 119 + const li = document.createElement("li"); 120 + li.textContent = "Try refreshing the page and scanning again"; 121 + elements.errorTipsList.appendChild(li); 122 + } 123 + 124 + // Display technical error message 125 + elements.errorTechnical.textContent = 126 + state.error || "Unknown error"; 105 127 break; 106 128 107 129 default: ··· 210 232 toolbar.appendChild( 211 233 createButton("Error", { 212 234 status: "error", 213 - error: "Failed to scrape page", 235 + error: "TypeError: Cannot read property 'textContent' of null at extractUsername", 236 + errorCategory: "DOM_ERROR", 237 + errorUserMessage: "Page structure has changed", 238 + errorTroubleshootingTips: [ 239 + "Try refreshing the page", 240 + "Make sure you're on the Following page", 241 + "Extension may need an update" 242 + ] 214 243 }), 215 244 ); 216 245