import { BrowserClient, defaultStackParser, getDefaultIntegrations, makeFetchTransport, Scope, } from "@sentry/browser"; // @ts-ignore - __VITE_ENVIRONMENT__ is injected by Vite at build time const isDevelopment = __VITE_ENVIRONMENT__ === "development"; // Cache navigator properties to avoid repeated access let cachedNavigatorInfo: { userAgent: string; platform: string; language: string; cookieEnabled: boolean; } | null = null; const getNavigatorInfo = () => { if (!cachedNavigatorInfo) { cachedNavigatorInfo = { userAgent: navigator.userAgent, platform: navigator.platform, language: navigator.language, cookieEnabled: navigator.cookieEnabled, }; } return cachedNavigatorInfo; }; const breadcrumbCategoriesToCapture = [ "api.", "widget", "user-interaction", "navigation", "error", ]; const shouldCaptureBreadcrumb = (breadcrumb: any) => { if (!breadcrumb.category) return false; return breadcrumbCategoriesToCapture.some( (category) => breadcrumb.category.startsWith(category) || breadcrumb.category === category ); }; const SAMPLE_RATE = 0.01; let client: BrowserClient | null = null; let scope: Scope; if (!isDevelopment) { const integrations = getDefaultIntegrations({}).filter((defaultIntegration) => { const shouldExclude = [ "BrowserApiErrors", "Breadcrumbs", ].includes(defaultIntegration.name); return !shouldExclude; }); client = new BrowserClient({ dsn: "https://d5fe437efbda5984e060ef9938343ae6@sentry.hamravesh.com/8920", transport: makeFetchTransport, stackParser: defaultStackParser, integrations: integrations, sampleRate: SAMPLE_RATE, tracesSampleRate: SAMPLE_RATE, enabled: true, tracePropagationTargets: [ "localhost", /^https:\/\/standardapi\.dori\.tech/, /^https:\/\/devapi\.dori\.tech/, ], debug: false, environment: "production", beforeBreadcrumb: (breadcrumb) => { if (!shouldCaptureBreadcrumb(breadcrumb)) { return null; } return breadcrumb; }, maxBreadcrumbs: 20, }); scope = new Scope(); scope.setClient(client); try { client.init(); } catch { } } else { scope = { captureException: (_error: Error, _options?: any) => "", setUser: (_user: any) => {}, setContext: (_key: string, _context: any) => {}, setTag: (_key: string, _value: any) => {}, addBreadcrumb: (_breadcrumb: any) => {}, setClient: (_client: any) => {}, } as Scope; } export const withErrorHandling = unknown>( context: string, fn: T ): T => { return ((...args: Parameters) => { try { return fn(...args); } catch (error) { scope.captureException(error as Error, { captureContext: { extra: { context } }, }); } }) as T; }; export const withAsyncErrorHandling = < T extends (...args: any[]) => Promise, >( context: string, fn: T ): T => { return ((...args: Parameters) => { try { return fn(...args).catch((error: Error) => { if (error.name !== "AbortError") { scope.captureException(error, { captureContext: { extra: { context } }, }); } throw error; }); } catch (error) { if ((error as Error).name !== "AbortError") { scope.captureException(error as Error, { captureContext: { extra: { context } }, }); } throw error; } }) as T; }; export const withAsyncErrorHandlingFilter = < T extends (...args: any[]) => Promise, >( context: string, shouldIgnoreError: (error: Error) => boolean, fn: T ): T => { return ((...args: Parameters) => { try { return fn(...args).catch((error: Error) => { if (error.name !== "AbortError" && !shouldIgnoreError(error)) { scope.captureException(error, { captureContext: { extra: { context } }, }); } throw error; }); } catch (error) { if ( (error as Error).name !== "AbortError" && !shouldIgnoreError(error as Error) ) { scope.captureException(error as Error, { captureContext: { extra: { context } }, }); } throw error; } }) as T; }; export const setSentryUserContext = ( sharingId: string, threadId?: string | null ) => { scope.setUser({ id: sharingId, username: sharingId, }); if (threadId) { scope.setContext("user_session", { thread_id: threadId, session_start: new Date().toISOString(), }); } }; export const setSentryWidgetMetadata = ( widgetType: "floating" | "embedded", installationSource: string, pluginVersion?: string ) => { scope.setContext("widget_metadata", { widget_type: widgetType, installation_source: installationSource, plugin_version: pluginVersion || "unknown", initialization_time: new Date().toISOString(), }); scope.setTag("widget_type", widgetType); scope.setTag("installation_source", installationSource); scope.setTag("environment", isDevelopment ? "development" : "production"); }; export const addBreadcrumb = ( message: string, category: string, level: "info" | "warning" | "error" = "info", data?: Record ) => { scope.addBreadcrumb({ message, category, level, data, timestamp: Date.now() / 1000, }); }; // Error categorization helpers export const categorizeError = (error: Error): string => { const errorName = error.name; const errorMessage = error.message.toLowerCase(); if (errorName === "TypeError" && errorMessage.includes("fetch")) { return "network"; } if (errorName === "AbortError") { return "aborted"; } if (errorMessage.includes("network") || errorMessage.includes("connection")) { return "network"; } if (errorMessage.includes("timeout")) { return "timeout"; } if ( errorMessage.includes("cors") || errorMessage.includes("access-control") ) { return "cors"; } if (errorMessage.includes("json") || errorMessage.includes("parse")) { return "json_parse"; } if (errorMessage.includes("404") || errorMessage.includes("not found")) { return "not_found"; } if (errorMessage.includes("403") || errorMessage.includes("forbidden")) { return "forbidden"; } if (errorMessage.includes("500") || errorMessage.includes("server error")) { return "server_error"; } if (errorMessage.includes("429") || errorMessage.includes("rate limit")) { return "rate_limit"; } if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) { return "unauthorized"; } return "unknown"; }; // Get environment context (optimized with caching) export const getEnvironmentContext = () => { const navInfo = getNavigatorInfo(); return { user_agent: navInfo.userAgent, online_status: navigator.onLine, connection_type: (navigator as any).connection?.effectiveType, network_downlink: (navigator as any).connection?.downlink, network_rtt: (navigator as any).connection?.rtt, viewport: { width: window.innerWidth, height: window.innerHeight, }, screen: { width: window.screen.width, height: window.screen.height, }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navInfo.language, platform: navInfo.platform, cookie_enabled: navInfo.cookieEnabled, memory_info: (performance as any).memory ? { used_js_heap_size: (performance as any).memory.usedJSHeapSize, total_js_heap_size: (performance as any).memory.totalJSHeapSize, js_heap_size_limit: (performance as any).memory.jsHeapSizeLimit, } : undefined, }; }; // Structured error capture with rich context export const captureStructuredError = ( error: Error, context: { operation: string; request?: { url: string; method: string; headers?: Record; body?: unknown; }; response?: { status?: number; statusText?: string; headers?: Record; body?: string; size?: number; duration?: number; }; additionalData?: Record; } ) => { const errorCategory = categorizeError(error); // Set tags on scope before capturing scope.setTag("error_category", errorCategory); scope.setTag("operation", context.operation); scope.setTag("environment", isDevelopment ? "development" : "production"); // Set contexts on scope scope.setContext("error_details", { category: errorCategory, name: error.name, message: error.message, stack: error.stack, }); if (context.request) { scope.setContext("request_context", { url: context.request.url, method: context.request.method, headers: context.request.headers, body_size: context.request.body ? JSON.stringify(context.request.body).length : 0, body_preview: context.request.body ? JSON.stringify(context.request.body).substring(0, 500) : undefined, }); } if (context.response) { scope.setContext("response_context", { status: context.response.status, status_text: context.response.statusText, headers: context.response.headers, body_size: context.response.size, body_preview: context.response.body?.substring(0, 500), duration_ms: context.response.duration, }); } scope.setContext("environment_context", getEnvironmentContext()); scope.setContext("timing_context", { timestamp: new Date().toISOString(), performance_timing: performance.timing ? { navigation_start: performance.timing.navigationStart, load_event_end: performance.timing.loadEventEnd, dom_content_loaded: performance.timing.domContentLoadedEventEnd, } : undefined, }); if (context.additionalData) { scope.setContext("additional_context", context.additionalData); } scope.captureException(error); // Clear tags and contexts to avoid affecting future events scope.setTag("error_category", undefined); scope.setTag("operation", undefined); scope.setTag("environment", undefined); scope.setContext("error_details", null); scope.setContext("request_context", null); scope.setContext("response_context", null); scope.setContext("environment_context", null); scope.setContext("timing_context", null); scope.setContext("additional_context", null); }; // Enhanced API error capture export const captureApiError = ( error: Error, operation: string, url: string, method: string = "GET", requestStartTime?: number, response?: Response, requestBody?: unknown ) => { const responseTime = requestStartTime ? Date.now() - requestStartTime : undefined; let responseBody = ""; let responseHeaders = {}; if (response) { responseHeaders = Object.fromEntries(response.headers.entries()); try { // Try to get response body for error context, but don't block on it response .clone() .text() .then((text) => { responseBody = text.substring(0, 1000); // Limit to first 1000 chars }) .catch((readError) => { responseBody = "Unable to read response body"; addBreadcrumb( "Failed to read response body for error context", "error.response_read_failed", "warning", { original_error: error.message, read_error: readError.message, operation, } ); }); } catch (cloneError) { responseBody = "Unable to clone response for body reading"; addBreadcrumb( "Failed to clone response for body reading", "error.response_clone_failed", "warning", { original_error: error.message, clone_error: (cloneError as Error).message, operation, } ); } } captureStructuredError(error, { operation, request: { url, method, body: requestBody, }, response: response ? { status: response.status, statusText: response.statusText, headers: responseHeaders as Record, body: responseBody, duration: responseTime, } : undefined, additionalData: { request_duration_ms: responseTime, online_status: navigator.onLine, connection_type: (navigator as any).connection?.effectiveType, }, }); }; // Optimized development logging utilities (no-op in production) export const devLog = isDevelopment ? { request: ( operation: string, url: string, method: string = "GET", data?: any ) => { console.group(`🚀 API Request: ${operation}`); console.log(`URL: ${url}`); console.log(`Method: ${method}`); if (data) console.log("Request Data:", data); console.log(`Timestamp: ${new Date().toISOString()}`); console.groupEnd(); }, success: ( operation: string, duration: number, responseSize?: number, status?: number ) => { console.group(`✅ API Success: ${operation}`); console.log(`Duration: ${duration}ms`); if (responseSize !== undefined) console.log(`Response Size: ${responseSize} bytes`); if (status !== undefined) console.log(`Status: ${status}`); console.log(`Timestamp: ${new Date().toISOString()}`); console.groupEnd(); }, error: (error: Error, operation: string, details?: any) => { const errorCategory = categorizeError(error); const emoji = getErrorEmoji(errorCategory); console.group(`${emoji} API Error: ${operation} (${errorCategory})`); console.error("Error:", error.message); console.error("Stack:", error.stack); if (details) console.log("Details:", details); console.log(`Timestamp: ${new Date().toISOString()}`); console.groupEnd(); }, networkInfo: () => { console.group("🌐 Network Info"); console.log("Online:", navigator.onLine); console.log( "Connection:", (navigator as any).connection?.effectiveType ); console.log( "Downlink:", (navigator as any).connection?.downlink, "Mbps" ); console.log("RTT:", (navigator as any).connection?.rtt, "ms"); console.log("User Agent:", navigator.userAgent); console.groupEnd(); }, } : { request: () => {}, success: () => {}, error: () => {}, networkInfo: () => {}, }; // Helper to get emoji for error category const getErrorEmoji = (category: string): string => { switch (category) { case "network": return "🌐"; case "timeout": return "⏱️"; case "cors": return "🚫"; case "json_parse": return "🔧"; case "not_found": return "🔍"; case "forbidden": return "🚷"; case "server_error": return "🔴"; case "rate_limit": return "🐌"; case "unauthorized": return "🔒"; case "aborted": return "🛑"; default: return "❌"; } }; // Enhanced error classes with rich context export class ApiError extends Error { public readonly category: string; public readonly operation: string; public readonly requestUrl: string; public readonly requestMethod: string; public readonly requestStartTime: number; public readonly responseTime?: number; public readonly responseStatus?: number; public readonly responseSize?: number; public readonly originalError?: Error; public readonly additionalContext?: Record; constructor( message: string, category: string, operation: string, requestUrl: string, requestMethod: string = "GET", requestStartTime: number = Date.now(), options: { responseTime?: number; responseStatus?: number; responseSize?: number; originalError?: Error; additionalContext?: Record; } = {} ) { super(message); this.name = "ApiError"; this.category = category; this.operation = operation; this.requestUrl = requestUrl; this.requestMethod = requestMethod; this.requestStartTime = requestStartTime; this.responseTime = options.responseTime; this.responseStatus = options.responseStatus; this.responseSize = options.responseSize; this.originalError = options.originalError; this.additionalContext = options.additionalContext; // Maintain proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, ApiError); } } // Computed properties for easy access get duration(): number { return this.responseTime || Date.now() - this.requestStartTime; } get requestAge(): number { return Date.now() - this.requestStartTime; } get hasResponse(): boolean { return this.responseStatus !== undefined; } // Convert to structured data for logging toStructuredData(): Record { return { error_name: this.name, error_message: this.message, error_category: this.category, operation: this.operation, request_url: this.requestUrl, request_method: this.requestMethod, request_start_time: new Date(this.requestStartTime).toISOString(), response_time_ms: this.responseTime, response_status: this.responseStatus, response_size_bytes: this.responseSize, duration_ms: this.duration, request_age_ms: this.requestAge, has_response: this.hasResponse, original_error: this.originalError ? { name: this.originalError.name, message: this.originalError.message, stack: this.originalError.stack, } : undefined, additional_context: this.additionalContext, timestamp: new Date().toISOString(), }; } } // Helper functions for creating enhanced errors export const createNetworkError = ( originalError: Error, operation: string, url: string, method: string = "GET", startTime: number ): ApiError => { return new ApiError( `Network error during ${operation}: ${originalError.message}`, "network", operation, url, method, startTime, { originalError, additionalContext: { network_online: navigator.onLine, connection_type: (navigator as any).connection?.effectiveType, network_downlink: (navigator as any).connection?.downlink, user_agent: navigator.userAgent, }, } ); }; export const createHttpError = ( status: number, statusText: string, operation: string, url: string, method: string = "GET", startTime: number, responseBody?: string ): ApiError => { return new ApiError( `HTTP ${status} ${statusText} for ${operation}`, categorizeHttpStatus(status), operation, url, method, startTime, { responseStatus: status, responseSize: responseBody?.length, additionalContext: { status_text: statusText, response_body_preview: responseBody?.substring(0, 500), network_online: navigator.onLine, }, } ); }; export const createParseError = ( originalError: Error, operation: string, url: string, method: string = "GET", startTime: number, responseSize?: number ): ApiError => { return new ApiError( `Parse error during ${operation}: ${originalError.message}`, "json_parse", operation, url, method, startTime, { originalError, responseSize, additionalContext: { parse_error_type: originalError.name, expected_content_type: "application/json", }, } ); }; export const createTimeoutError = ( operation: string, url: string, method: string = "GET", startTime: number, timeoutMs: number ): ApiError => { return new ApiError( `Request timeout after ${timeoutMs}ms for ${operation}`, "timeout", operation, url, method, startTime, { additionalContext: { timeout_ms: timeoutMs, network_online: navigator.onLine, connection_type: (navigator as any).connection?.effectiveType, }, } ); }; // Helper to categorize HTTP status codes const categorizeHttpStatus = (status: number): string => { if (status >= 200 && status < 300) return "success"; if (status === 400) return "bad_request"; if (status === 401) return "unauthorized"; if (status === 403) return "forbidden"; if (status === 404) return "not_found"; if (status === 408) return "timeout"; if (status === 429) return "rate_limit"; if (status >= 400 && status < 500) return "client_error"; if (status >= 500) return "server_error"; return "unknown"; }; // Helper to get minimal network info (for error cases only) export const getNetworkInfo = () => ({ online_status: navigator.onLine, connection_type: (navigator as any).connection?.effectiveType, network_downlink: (navigator as any).connection?.downlink, user_agent: getNavigatorInfo().userAgent, }); export { client, scope };