omnis-logger-sdk
Version:
Lightweight TypeScript SDK for error tracking and application monitoring in React Native and Web applications
806 lines • 25.4 kB
JavaScript
;
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