UNPKG

omnis-logger-sdk

Version:

Lightweight TypeScript SDK for error tracking and application monitoring in React Native and Web applications

806 lines 25.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.init = init; exports.log = log; exports.setUserId = setUserId; exports.updateSession = updateSession; exports.setContext = setContext; exports.setEnabled = setEnabled; exports.getConfig = getConfig; exports.setAppContext = setAppContext; exports.getAppContext = getAppContext; exports.clearAppContext = clearAppContext; exports.leaveBreadcrumb = leaveBreadcrumb; exports.trackNavigation = trackNavigation; exports.trackUserAction = trackUserAction; exports.trackStateChange = trackStateChange; exports.trackUIEvent = trackUIEvent; exports.getBreadcrumbsDebug = getBreadcrumbsDebug; exports.testBreadcrumbs = testBreadcrumbs; exports.startFlow = startFlow; exports.logFlow = logFlow; exports.endFlow = endFlow; exports.getCurrentFlow = getCurrentFlow; exports.useComponentTracking = useComponentTracking; exports.captureReactError = captureReactError; const DEFAULT_API_URL = 'https://api.omnis-cloud.com'; let config = { apiKey: '', apiUrl: DEFAULT_API_URL, environment: 'production', enabled: true, maxBreadcrumbs: 50, sensitiveKeys: [ 'password', 'token', 'apiKey', 'api_key', 'authorization', 'secret', 'creditCard', 'credit_card', 'cvv', 'ssn', 'pin', ], }; let originalFetch; const logQueue = []; const breadcrumbs = []; let appContext = {}; let isInitialized = false; let currentFlowId = null; let currentFlowName = null; const flowBreadcrumbs = []; function init(userConfig) { const apiUrl = userConfig.apiUrl || DEFAULT_API_URL; config = { ...config, ...userConfig, apiUrl }; if (!config.apiKey) { return; } if (!config.sessionId) { config.sessionId = generateSessionId(); } isInitialized = true; setupFetchInterceptor(); setupErrorHandler(); } function maskSensitiveData(obj, depth = 0) { var _a; if (!obj || depth > 10) return obj; if (typeof obj === 'string') { obj = obj.replace(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi, '***@***.***'); obj = obj.replace(/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g, '***-***-****'); return obj; } if (Array.isArray(obj)) { return obj.map(item => maskSensitiveData(item, depth + 1)); } if (typeof obj === 'object') { const masked = {}; for (const key in obj) { const lowerKey = key.toLowerCase(); const isSensitive = (_a = config.sensitiveKeys) === null || _a === void 0 ? void 0 : _a.some(sensitiveKey => lowerKey.includes(sensitiveKey.toLowerCase())); if (isSensitive) { masked[key] = '***MASKED***'; } else { masked[key] = maskSensitiveData(obj[key], depth + 1); } } return masked; } return obj; } function addBreadcrumb(breadcrumb) { if (!config.enabled || !isInitialized) { return; } const fullBreadcrumb = { timestamp: Date.now(), ...breadcrumb, }; breadcrumbs.push(fullBreadcrumb); const maxBreadcrumbs = config.maxBreadcrumbs || 50; if (breadcrumbs.length > maxBreadcrumbs) { breadcrumbs.shift(); } } function parseStackTrace(stack) { if (!stack) return {}; const lines = stack.split('\n'); for (let i = 1; i < Math.min(lines.length, 5); i++) { const line = lines[i]; const rnMatch = line.match(/at\s+([^\s(]+)\s*\(([^:]+):(\d+):\d+\)/); if (rnMatch) { return { function: rnMatch[1], component: rnMatch[1], file: rnMatch[2].split('/').pop(), line: parseInt(rnMatch[3], 10), }; } const webMatch = line.match(/at\s+([^\s(]+)\s*\(/); if (webMatch && webMatch[1] !== 'Object' && webMatch[1] !== 'Anonymous') { return { function: webMatch[1], component: webMatch[1], }; } const webpackMatch = line.match(/([^@\s]+)@/); if (webpackMatch && webpackMatch[1] !== 'Object') { return { function: webpackMatch[1], component: webpackMatch[1], }; } } return {}; } function getBreadcrumbs() { return [...breadcrumbs]; } function clearBreadcrumbs() { breadcrumbs.length = 0; } function extractUrlString(url) { if (typeof url === 'string') { return url; } if (url instanceof URL) { return url.href; } if (typeof Request !== 'undefined' && url instanceof Request) { return url.url; } if (typeof url === 'object' && url !== null) { if ('url' in url) return url.url; if ('href' in url) return url.href; } return String(url); } function extractRequestMetadata(url, options, clonedRequest) { let method = options.method || 'GET'; let requestHeaders = {}; const requestSource = clonedRequest || (typeof Request !== 'undefined' && url instanceof Request ? url : null); if (requestSource) { method = requestSource.method || method; if (requestSource.headers) { requestSource.headers.forEach((value, key) => { requestHeaders[key] = value; }); } } if (options.headers) { if (options.headers instanceof Headers) { options.headers.forEach((value, key) => { requestHeaders[key] = value; }); } else if (typeof options.headers === 'object') { requestHeaders = { ...requestHeaders, ...options.headers }; } } return { method, requestHeaders }; } async function extractRequestBody(options, clonedRequest) { let bodySource = options.body; if (clonedRequest && !bodySource) { try { const bodyText = await clonedRequest.text(); if (bodyText && bodyText.length > 0) { bodySource = bodyText; } } catch (err) { } } if (!bodySource) return null; try { if (typeof bodySource === 'string') { if (bodySource.length === 0) return null; try { return JSON.parse(bodySource); } catch (_a) { return bodySource.slice(0, 2000); } } if (bodySource instanceof FormData) return '[FormData]'; if (bodySource instanceof Blob) return '[Blob]'; if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) return '[Binary Data]'; if (typeof bodySource === 'object') return bodySource; return String(bodySource).slice(0, 2000); } catch (err) { return null; } } async function extractResponseData(response) { if (!response) { return { responseData: null, responseHeaders: null, httpStatus: undefined }; } const httpStatus = response.status; const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); let responseData = null; try { const clonedResponse = response.clone(); const text = await clonedResponse.text(); try { responseData = JSON.parse(text); } catch (_a) { responseData = text.slice(0, 2000); } } catch (err) { } return { responseData, responseHeaders, httpStatus }; } function setupFetchInterceptor() { if (!originalFetch && global.fetch) { originalFetch = global.fetch; } if (!global.fetch) { return; } global.fetch = async (url, options = {}) => { const startTime = Date.now(); let clonedRequest; const urlString = extractUrlString(url); addBreadcrumb({ category: 'network', message: `🌐 ${options.method || 'GET'} ${urlString}`, level: 'info', data: { url: urlString, method: options.method || 'GET', }, }); if (typeof Request !== 'undefined' && url instanceof Request) { try { clonedRequest = url.clone(); } catch (err) { } } try { const response = await originalFetch(url, options); const duration = Date.now() - startTime; if (response.status >= 400) { await logNetworkError(url, options, response, duration, undefined, clonedRequest); } return response; } catch (err) { const duration = Date.now() - startTime; await logNetworkError(url, options, undefined, duration, err, clonedRequest); throw err; } }; } let pendingNetworkErrors = []; let networkErrorTimeout = null; const NETWORK_ERROR_DELAY = 300; async function logNetworkError(url, options, response, duration, error, clonedRequest) { try { const urlString = extractUrlString(url); const { method, requestHeaders } = extractRequestMetadata(url, options, clonedRequest); const requestData = await extractRequestBody(options, clonedRequest); const { responseData, responseHeaders, httpStatus } = await extractResponseData(response); const stackInfo = parseStackTrace(error === null || error === void 0 ? void 0 : error.stack); const logData = { level: 'error', message: error ? `Network Error: ${error.message}` : `HTTP ${httpStatus}: ${method} ${urlString}`, stack: error === null || error === void 0 ? void 0 : error.stack, context: { type: error ? 'network_error' : 'http_error', url: urlString, method, duration, ...stackInfo, }, url: urlString, httpStatus, requestHeaders: maskSensitiveData(requestHeaders), responseHeaders: maskSensitiveData(responseHeaders), requestData: maskSensitiveData(requestData), responseData: maskSensitiveData(responseData), userAgent: getUserAgent(), userId: config.userId, sessionId: config.sessionId, platform: config.platform, version: config.version, environment: config.environment, deviceInfo: config.deviceInfo || getDeviceInfo(), navigationInfo: getNavigationInfo(), appContext: { ...appContext }, }; pendingNetworkErrors.push({ logData, timestamp: Date.now() }); if (networkErrorTimeout) { clearTimeout(networkErrorTimeout); } networkErrorTimeout = setTimeout(async () => { const errorsToSend = [...pendingNetworkErrors]; pendingNetworkErrors = []; networkErrorTimeout = null; const currentBreadcrumbs = getBreadcrumbs(); for (const { logData } of errorsToSend) { const fullLogData = { ...logData, breadcrumbs: currentBreadcrumbs, }; await sendLog(fullLogData); } }, NETWORK_ERROR_DELAY); } catch (err) { } } function setupErrorHandler() { var _a, _b; const globalAny = global; if (globalAny.ErrorUtils && typeof globalAny.ErrorUtils.setGlobalHandler === 'function') { const originalHandler = (_b = (_a = globalAny.ErrorUtils).getGlobalHandler) === null || _b === void 0 ? void 0 : _b.call(_a); globalAny.ErrorUtils.setGlobalHandler((error, isFatal) => { logJsError(error, isFatal); if (originalHandler) { originalHandler(error, isFatal); } }); } if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { const originalOnError = window.onerror; window.onerror = (message, source, lineno, colno, error) => { if (error) { logJsError(error, false); } else { logJsError(new Error(String(message)), false); } if (originalOnError) { return originalOnError(message, source, lineno, colno, error); } return false; }; window.addEventListener('unhandledrejection', (event) => { const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason)); logJsError(error, false); }); } } function logJsError(error, isFatal) { try { addBreadcrumb({ category: 'error', message: `❌ ${error.message || 'Unknown error'}`, level: 'error', data: { name: error.name, isFatal, }, }); const stackInfo = parseStackTrace(error.stack); const logData = { level: 'error', message: error.message || 'Unknown error', stack: error.stack, context: { type: 'js_error', isFatal, errorName: error.name, ...stackInfo, }, userAgent: getUserAgent(), userId: config.userId, sessionId: config.sessionId, platform: config.platform, version: config.version, environment: config.environment, deviceInfo: config.deviceInfo || getDeviceInfo(), navigationInfo: getNavigationInfo(), breadcrumbs: getBreadcrumbs(), appContext: { ...appContext }, }; sendLog(logData); } catch (err) { } } async function sendLog(logData) { if (!config.enabled || !isInitialized) { return; } if (!originalFetch) { logQueue.push(logData); return; } try { const logDataToSend = { ...logData, userId: config.userId || logData.userId, sessionId: config.sessionId || logData.sessionId, }; await originalFetch(`${config.apiUrl}/api/logs`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': config.apiKey, }, body: JSON.stringify({ data: logDataToSend }), }); } catch (err) { logQueue.push(logData); } } function getUserAgent() { if (typeof navigator !== 'undefined') { return navigator.userAgent || 'Unknown'; } return 'React Native'; } function getDeviceInfo() { const info = {}; if (typeof navigator !== 'undefined') { info.platform = navigator.platform; info.language = navigator.language; info.cookieEnabled = navigator.cookieEnabled; } if (typeof window !== 'undefined' && window.screen) { info.screenWidth = window.screen.width; info.screenHeight = window.screen.height; info.screenPixelDepth = window.screen.pixelDepth; } return info; } function getNavigationInfo() { var _a, _b, _c, _d; const info = {}; if (typeof window !== 'undefined') { info.url = (_a = window.location) === null || _a === void 0 ? void 0 : _a.href; info.pathname = (_b = window.location) === null || _b === void 0 ? void 0 : _b.pathname; info.search = (_c = window.location) === null || _c === void 0 ? void 0 : _c.search; info.hash = (_d = window.location) === null || _d === void 0 ? void 0 : _d.hash; if (typeof document !== 'undefined') { info.referrer = document.referrer; } } return info; } function generateSessionId() { return `session_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } function log(level, message, context) { if (!config.enabled || !isInitialized) { return; } const breadcrumbLevel = level === 'error' ? 'error' : level === 'warn' ? 'warning' : 'info'; addBreadcrumb({ category: 'console', message, level: breadcrumbLevel, data: context, }); const logData = { level, message, context: maskSensitiveData(context), userAgent: getUserAgent(), userId: config.userId, sessionId: config.sessionId, platform: config.platform, version: config.version, environment: config.environment, deviceInfo: config.deviceInfo || getDeviceInfo(), navigationInfo: getNavigationInfo(), breadcrumbs: getBreadcrumbs(), appContext: { ...appContext }, }; sendLog(logData); } function setUserId(userId, syncWithServer = true) { config.userId = userId; if (syncWithServer && isInitialized) { updateSession({ userId }); } } async function updateSession(updates) { if (!config.enabled || !isInitialized) { return; } if (!originalFetch) { return; } const requestBody = { sessionId: config.sessionId, ...updates, }; try { await originalFetch(`${config.apiUrl}/api/logs/update-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': config.apiKey, }, body: JSON.stringify(requestBody), }); } catch (err) { } } function setContext(context) { if (context.userId) config.userId = context.userId; if (context.sessionId) config.sessionId = context.sessionId; } function setEnabled(enabled) { config.enabled = enabled; } function getConfig() { const { apiKey, ...safeConfig } = config; return safeConfig; } function setAppContext(context, skipBreadcrumb = false) { appContext = { ...appContext, ...context }; if (!skipBreadcrumb && (context.route || context.screen || context.component)) { const displayName = context.route || context.screen || context.component || 'Unknown'; addBreadcrumb({ category: context.route ? 'navigation' : 'ui', message: context.route ? `📍 ${displayName}` : `🎨 ${displayName}`, level: 'info', data: context, }); } } function getAppContext() { return { ...appContext }; } function clearAppContext() { appContext = {}; } function leaveBreadcrumb(category, message, data) { addBreadcrumb({ category, message, level: 'info', data, }); } let lastTrackedRoute = null; function trackNavigation(route, params) { if (!route) { return; } if (route === lastTrackedRoute) { return; } lastTrackedRoute = route; setAppContext({ route }, true); addBreadcrumb({ category: 'navigation', message: `📍 ${route}`, level: 'info', data: { route, params, timestamp: Date.now() }, }); } function trackUserAction(action, data) { addBreadcrumb({ category: 'user-action', message: `👆 ${action}`, level: 'info', data, }); } function trackStateChange(description, data) { addBreadcrumb({ category: 'state-change', message: `🔄 ${description}`, level: 'info', data, }); } function trackUIEvent(event, data) { addBreadcrumb({ category: 'ui', message: `🎨 ${event}`, level: 'info', data, }); } function getBreadcrumbsDebug() { const currentBreadcrumbs = getBreadcrumbs(); return currentBreadcrumbs; } function testBreadcrumbs() { log('info', '[TEST] Breadcrumbs test', { breadcrumbsCount: breadcrumbs.length, appContext, }); } function generateFlowId() { return `flow_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } function startFlow(flowName, flowId) { if (currentFlowId) { console.warn(`[OmnisLogger] Flow "${currentFlowName}" уже активен. Завершите его перед началом нового.`); } currentFlowId = flowId || generateFlowId(); currentFlowName = flowName; flowBreadcrumbs.length = 0; addFlowBreadcrumb({ category: 'state-change', message: `🚀 Начало флоу: ${flowName}`, level: 'info', data: { flowId: currentFlowId, flowName }, }); return currentFlowId; } function addFlowBreadcrumb(breadcrumb) { const fullBreadcrumb = { timestamp: Date.now(), ...breadcrumb, }; flowBreadcrumbs.push(fullBreadcrumb); breadcrumbs.push(fullBreadcrumb); const maxBreadcrumbs = config.maxBreadcrumbs || 50; if (breadcrumbs.length > maxBreadcrumbs) { breadcrumbs.shift(); } } function logFlow(level, message, context) { if (!currentFlowId) { console.warn('[OmnisLogger] Нет активного флоу. Используйте startFlow() перед logFlow() или используйте обычный log()'); log(level, message, context); return; } const breadcrumbLevel = level === 'error' ? 'error' : level === 'warn' ? 'warning' : 'info'; const icon = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : '✓'; addFlowBreadcrumb({ category: level === 'error' ? 'error' : 'console', message: `${icon} ${message}`, level: breadcrumbLevel, data: context, }); } function endFlow(level = 'info', summaryMessage) { if (!currentFlowId) { console.warn('[OmnisLogger] Нет активного флоу для завершения'); return; } if (!config.enabled || !isInitialized) { currentFlowId = null; currentFlowName = null; flowBreadcrumbs.length = 0; return; } addFlowBreadcrumb({ category: 'state-change', message: `🏁 Завершение флоу: ${currentFlowName}`, level: level === 'error' ? 'error' : level === 'warn' ? 'warning' : 'info', }); const message = summaryMessage || `Флоу: ${currentFlowName}`; const logData = { level, message, context: maskSensitiveData({ flowCompleted: true, eventsCount: flowBreadcrumbs.length, }), userAgent: getUserAgent(), userId: config.userId, sessionId: config.sessionId, platform: config.platform, version: config.version, environment: config.environment, deviceInfo: config.deviceInfo || getDeviceInfo(), navigationInfo: getNavigationInfo(), breadcrumbs: [...flowBreadcrumbs], appContext: { ...appContext }, }; logData.flowId = currentFlowId; logData.flowName = currentFlowName; sendLog(logData); currentFlowId = null; currentFlowName = null; flowBreadcrumbs.length = 0; } function getCurrentFlow() { if (!currentFlowId) { return null; } return { flowId: currentFlowId, flowName: currentFlowName || '', eventsCount: flowBreadcrumbs.length, }; } function useComponentTracking(componentName) { setAppContext({ component: componentName }); addBreadcrumb({ category: 'ui', message: `${componentName} rendered`, level: 'info', }); } function captureReactError(error, errorInfo) { addBreadcrumb({ category: 'error', message: `React Error: ${error.message}`, level: 'error', data: { componentStack: errorInfo === null || errorInfo === void 0 ? void 0 : errorInfo.componentStack, }, }); const logData = { level: 'error', message: error.message || 'React Error Boundary caught an error', stack: error.stack, componentStack: errorInfo === null || errorInfo === void 0 ? void 0 : errorInfo.componentStack, context: { type: 'react_error', errorName: error.name, componentStack: errorInfo === null || errorInfo === void 0 ? void 0 : errorInfo.componentStack, }, userAgent: getUserAgent(), userId: config.userId, sessionId: config.sessionId, platform: config.platform, version: config.version, environment: config.environment, deviceInfo: config.deviceInfo || getDeviceInfo(), navigationInfo: getNavigationInfo(), breadcrumbs: getBreadcrumbs(), appContext: { ...appContext }, }; sendLog(logData); } exports.default = { init, log, setUserId, setContext, setEnabled, getConfig, updateSession, setAppContext, getAppContext, clearAppContext, leaveBreadcrumb, trackNavigation, trackUserAction, trackStateChange, trackUIEvent, clearBreadcrumbs, useComponentTracking, captureReactError, getBreadcrumbsDebug, testBreadcrumbs, startFlow, logFlow, endFlow, getCurrentFlow, }; //# sourceMappingURL=index.js.map