feat(error-tracking): add shared error tracking package

Add @manacore/shared-error-tracking package with:
- Frontend error tracker with batching and offline support
- SvelteKit integration with hooks handler
- Expo/React Native integration with global error handler
- NestJS module with exception filter and service
- Shared TypeScript types for error log entries
This commit is contained in:
Wuesteon 2025-12-19 02:17:36 +01:00
parent f834986a82
commit 5e1118b711
12 changed files with 1033 additions and 0 deletions

View file

@ -0,0 +1,67 @@
{
"name": "@manacore/shared-error-tracking",
"version": "1.0.0",
"description": "Centralized error tracking for ManaCore applications - NestJS and frontend clients",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./nestjs": {
"types": "./dist/nestjs/index.d.ts",
"default": "./dist/nestjs/index.js"
},
"./frontend": {
"types": "./dist/frontend/index.d.ts",
"default": "./dist/frontend/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
}
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"prepublishOnly": "pnpm build",
"lint": "eslint .",
"type-check": "tsc --noEmit"
},
"files": [
"dist"
],
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/config": "^3.0.0 || ^4.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0"
},
"peerDependenciesMeta": {
"@nestjs/common": {
"optional": true
},
"@nestjs/config": {
"optional": true
},
"@nestjs/core": {
"optional": true
}
},
"devDependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
},
"keywords": [
"nestjs",
"error-tracking",
"sveltekit",
"expo",
"manacore"
],
"author": "Mana Core Team",
"license": "MIT"
}

View file

@ -0,0 +1,257 @@
import type {
ErrorTrackingConfig,
ErrorLogPayload,
ErrorContext,
CreateErrorLogResponse,
ErrorSourceType,
} from '../types';
/**
* Frontend error tracker client
*/
export class ErrorTracker {
private config: ErrorTrackingConfig;
private queue: ErrorLogPayload[] = [];
private isFlushing = false;
constructor(config: ErrorTrackingConfig) {
this.config = config;
}
/**
* Capture an error and send it to the tracking service
*/
async captureError(
error: Error | unknown,
context?: ErrorContext
): Promise<CreateErrorLogResponse> {
const payload = this.buildPayload(error, context);
// Log locally if enabled
if (this.config.enableLocalLogging !== false) {
console.error(`[${payload.errorCode}] ${payload.message}`, error);
}
return this.sendError(payload);
}
/**
* Capture a message as an error
*/
async captureMessage(
message: string,
severity: 'debug' | 'info' | 'warning' | 'error' | 'critical' = 'info',
context?: ErrorContext
): Promise<CreateErrorLogResponse> {
const payload: ErrorLogPayload = {
errorCode: 'MESSAGE',
errorType: 'CapturedMessage',
message,
severity,
context,
appId: this.config.appId,
serviceName: this.config.serviceName,
sourceType: this.detectSourceType(),
environment: this.config.environment || this.detectEnvironment(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
browserInfo: this.getBrowserInfo(),
occurredAt: new Date().toISOString(),
};
return this.sendError(payload);
}
/**
* Queue an error for batch sending (useful for offline scenarios)
*/
queueError(error: Error | unknown, context?: ErrorContext): void {
const payload = this.buildPayload(error, context);
this.queue.push(payload);
}
/**
* Flush queued errors to the tracking service
*/
async flushQueue(): Promise<void> {
if (this.isFlushing || this.queue.length === 0) return;
this.isFlushing = true;
const errors = [...this.queue];
this.queue = [];
try {
await this.sendBatch(errors);
} catch {
// Re-queue failed errors
this.queue.unshift(...errors);
} finally {
this.isFlushing = false;
}
}
/**
* Build error payload from error object
*/
private buildPayload(error: Error | unknown, context?: ErrorContext): ErrorLogPayload {
const err = error instanceof Error ? error : new Error(String(error));
return {
errorCode: this.extractErrorCode(err),
errorType: err.constructor.name,
message: err.message,
stackTrace: err.stack,
severity: 'error',
context,
appId: this.config.appId,
serviceName: this.config.serviceName,
sourceType: this.detectSourceType(),
environment: this.config.environment || this.detectEnvironment(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
browserInfo: this.getBrowserInfo(),
occurredAt: new Date().toISOString(),
};
}
/**
* Send a single error to the tracking service
*/
private async sendError(payload: ErrorLogPayload): Promise<CreateErrorLogResponse> {
try {
const url = `${this.config.errorTrackingUrl}/api/v1/errors`;
const headers = await this.buildHeaders();
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` };
}
return (await response.json()) as CreateErrorLogResponse;
} catch (err) {
console.warn('Failed to send error to tracking service', err);
return { success: false, error: 'Network error' };
}
}
/**
* Send batch errors to the tracking service
*/
private async sendBatch(errors: ErrorLogPayload[]): Promise<void> {
const url = `${this.config.errorTrackingUrl}/api/v1/errors/batch`;
const headers = await this.buildHeaders();
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ errors }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
}
/**
* Build request headers
*/
private async buildHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-App-Id': this.config.appId,
...this.config.customHeaders,
};
if (this.config.getAuthToken) {
const token = await this.config.getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
/**
* Detect source type based on environment
*/
private detectSourceType(): ErrorSourceType {
if (typeof window === 'undefined') {
return 'backend'; // SSR or Node.js
}
// Check for React Native
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
return 'frontend_mobile';
}
return 'frontend_web';
}
/**
* Detect current environment
*/
private detectEnvironment(): 'development' | 'staging' | 'production' {
if (typeof process !== 'undefined' && process.env) {
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'production') return 'production';
if (nodeEnv === 'staging') return 'staging';
}
// Check for common dev indicators
if (typeof window !== 'undefined') {
const hostname = window.location?.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'development';
}
if (hostname?.includes('staging') || hostname?.includes('stage')) {
return 'staging';
}
}
return 'production';
}
/**
* Extract error code from error object
*/
private extractErrorCode(error: Error): string {
const anyError = error as unknown as Record<string, unknown>;
if (anyError.code && typeof anyError.code === 'string') {
return anyError.code;
}
if (anyError.name && typeof anyError.name === 'string') {
return anyError.name;
}
return 'UNKNOWN_ERROR';
}
/**
* Get browser information
*/
private getBrowserInfo(): Record<string, unknown> | undefined {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return undefined;
}
return {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
url: window.location?.href,
referrer: document?.referrer,
screenWidth: window.screen?.width,
screenHeight: window.screen?.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
};
}
}
/**
* Create an error tracker instance
*/
export function createErrorTracker(config: ErrorTrackingConfig): ErrorTracker {
return new ErrorTracker(config);
}

View file

@ -0,0 +1,107 @@
import type { ErrorTracker } from './error-tracker';
/**
* Create an Expo/React Native error handler for react-native-error-boundary
*
* Usage:
* ```typescript
* // App.tsx
* import ErrorBoundary from 'react-native-error-boundary';
* import { createExpoErrorHandler } from '@manacore/shared-error-tracking/frontend';
* import { errorTracker } from '@/lib/error-tracking';
*
* const { errorHandler, ErrorFallback } = createExpoErrorHandler(errorTracker);
*
* export default function App() {
* return (
* <ErrorBoundary onError={errorHandler} FallbackComponent={ErrorFallback}>
* <RootNavigator />
* </ErrorBoundary>
* );
* }
* ```
*/
export function createExpoErrorHandler(errorTracker: ErrorTracker) {
const errorHandler = (error: Error, stackTrace: string) => {
void errorTracker.captureError(error, {
type: 'error_boundary',
stackTrace,
});
};
return {
errorHandler,
};
}
/**
* Get device info for React Native
* This is a simple implementation - for more detailed info,
* consider using react-native-device-info package
*/
export function getReactNativeDeviceInfo(): Record<string, unknown> {
const info: Record<string, unknown> = {
platform: 'react-native',
};
// Add Platform info if available
try {
// Dynamic import to avoid issues in non-RN environments
const Platform = require('react-native').Platform;
info.os = Platform.OS;
info.version = Platform.Version;
info.isTV = Platform.isTV;
} catch {
// Platform not available
}
// Add Dimensions if available
try {
const Dimensions = require('react-native').Dimensions;
const { width, height } = Dimensions.get('window');
info.screenWidth = width;
info.screenHeight = height;
} catch {
// Dimensions not available
}
return info;
}
/**
* Setup global error handler for React Native
* Call this in your app entry point
*
* Usage:
* ```typescript
* // index.js or App.tsx
* import { setupReactNativeErrorHandler } from '@manacore/shared-error-tracking/frontend';
* import { errorTracker } from '@/lib/error-tracking';
*
* setupReactNativeErrorHandler(errorTracker);
* ```
*/
export function setupReactNativeErrorHandler(errorTracker: ErrorTracker): void {
// Override the default error handler
const originalHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
// Capture the error
void errorTracker.captureError(error, {
type: 'global_error',
isFatal,
deviceInfo: getReactNativeDeviceInfo(),
});
// Call the original handler
if (originalHandler) {
originalHandler(error, isFatal);
}
});
}
// Type declaration for React Native's ErrorUtils
declare const ErrorUtils: {
getGlobalHandler: () => ((error: Error, isFatal?: boolean) => void) | undefined;
setGlobalHandler: (handler: (error: Error, isFatal?: boolean) => void) => void;
};

View file

@ -0,0 +1,7 @@
export { ErrorTracker, createErrorTracker } from './error-tracker';
export { createSvelteErrorHandler, setupGlobalErrorHandler } from './sveltekit';
export {
createExpoErrorHandler,
getReactNativeDeviceInfo,
setupReactNativeErrorHandler,
} from './expo';

View file

@ -0,0 +1,79 @@
import type { ErrorTracker } from './error-tracker';
/**
* Create a SvelteKit error handler for hooks.client.ts
*
* Usage:
* ```typescript
* // src/hooks.client.ts
* import { createSvelteErrorHandler } from '@manacore/shared-error-tracking/frontend';
* import { errorTracker } from '$lib/error-tracking';
*
* export const handleError = createSvelteErrorHandler(errorTracker);
* ```
*/
export function createSvelteErrorHandler(errorTracker: ErrorTracker) {
return async ({
error,
event,
status,
message,
}: {
error: unknown;
event: { url: URL; params: Record<string, string>; route: { id: string | null } };
status: number;
message: string;
}) => {
// Capture the error
await errorTracker.captureError(error, {
status,
message,
url: event.url.toString(),
routeId: event.route.id,
params: event.params,
});
// Return standard SvelteKit error response
return {
message: message || 'An unexpected error occurred',
};
};
}
/**
* Setup global error handler for unhandled errors and promise rejections
* Call this in hooks.client.ts
*
* Usage:
* ```typescript
* // src/hooks.client.ts
* import { setupGlobalErrorHandler } from '@manacore/shared-error-tracking/frontend';
* import { errorTracker } from '$lib/error-tracking';
*
* if (typeof window !== 'undefined') {
* setupGlobalErrorHandler(errorTracker);
* }
* ```
*/
export function setupGlobalErrorHandler(errorTracker: ErrorTracker): void {
if (typeof window === 'undefined') {
return;
}
// Handle unhandled errors
window.addEventListener('error', (event) => {
void errorTracker.captureError(event.error || new Error(event.message), {
type: 'unhandled_error',
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
void errorTracker.captureError(event.reason || new Error('Unhandled promise rejection'), {
type: 'unhandled_rejection',
});
});
}

View file

@ -0,0 +1,4 @@
// Re-export everything for convenience
export * from './types';
export * from './nestjs';
export * from './frontend';

View file

@ -0,0 +1,118 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { ErrorTrackingService } from './error-tracking.service';
// Sensitive header keys to sanitize before logging
const SENSITIVE_HEADERS = ['authorization', 'cookie', 'x-api-key', 'api-key'];
// Sensitive body field keys to sanitize
const SENSITIVE_BODY_FIELDS = ['password', 'token', 'secret', 'apikey', 'api_key'];
@Injectable()
@Catch()
export class ErrorTrackingFilter implements ExceptionFilter {
private readonly logger = new Logger(ErrorTrackingFilter.name);
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
private readonly errorTrackingService: ErrorTrackingService
) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
// Determine status code
const httpStatus =
exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
// Build error message
const message =
exception instanceof Error
? exception.message
: typeof exception === 'string'
? exception
: 'Internal server error';
// Build response body
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(request),
message,
};
// Report error to tracking service (fire and forget)
this.trackError(exception, request, httpStatus).catch((err) => {
this.logger.warn('Failed to track error', err);
});
// Send response
httpAdapter.reply(response, responseBody, httpStatus);
}
private async trackError(
exception: unknown,
request: Record<string, unknown>,
statusCode: number
): Promise<void> {
// Don't track 4xx client errors below 500 by default (optional)
// You can customize this based on your needs
const error = exception instanceof Error ? exception : new Error(String(exception));
const sanitizedHeaders = this.sanitizeHeaders(request.headers as Record<string, unknown>);
const sanitizedBody = this.sanitizeBody(request.body as Record<string, unknown>);
await this.errorTrackingService.reportHttpException(
error,
{
url: request.url as string,
method: request.method as string,
headers: sanitizedHeaders,
body: sanitizedBody,
user: request.user as { userId?: string; sessionId?: string },
},
statusCode
);
}
private sanitizeHeaders(headers?: Record<string, unknown>): Record<string, unknown> | undefined {
if (!headers) return undefined;
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(headers)) {
if (SENSITIVE_HEADERS.includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = value;
}
}
return sanitized;
}
private sanitizeBody(body?: Record<string, unknown>): Record<string, unknown> | undefined {
if (!body) return undefined;
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(body)) {
if (SENSITIVE_BODY_FIELDS.includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitizeBody(value as Record<string, unknown>);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}

View file

@ -0,0 +1,58 @@
import { Module, DynamicModule } from '@nestjs/common';
import type {
InjectionToken,
OptionalFactoryDependency,
Type,
ForwardReference,
} from '@nestjs/common';
import { ErrorTrackingService, ERROR_TRACKING_CONFIG } from './error-tracking.service';
import type { ErrorTrackingConfig } from '../types';
export type ErrorTrackingModuleOptions = ErrorTrackingConfig;
export interface ErrorTrackingModuleAsyncOptions {
useFactory: (...args: unknown[]) => Promise<ErrorTrackingConfig> | ErrorTrackingConfig;
inject?: (InjectionToken | OptionalFactoryDependency)[];
imports?: (Type | DynamicModule | Promise<DynamicModule> | ForwardReference)[];
}
@Module({})
export class ErrorTrackingModule {
/**
* Register the error tracking module with static configuration
*/
static forRoot(options: ErrorTrackingModuleOptions): DynamicModule {
return {
module: ErrorTrackingModule,
providers: [
{
provide: ERROR_TRACKING_CONFIG,
useValue: options,
},
ErrorTrackingService,
],
exports: [ErrorTrackingService],
global: true,
};
}
/**
* Register the error tracking module with async configuration
*/
static forRootAsync(options: ErrorTrackingModuleAsyncOptions): DynamicModule {
return {
module: ErrorTrackingModule,
imports: options.imports,
providers: [
{
provide: ERROR_TRACKING_CONFIG,
useFactory: options.useFactory,
inject: options.inject,
},
ErrorTrackingService,
],
exports: [ErrorTrackingService],
global: true,
};
}
}

View file

@ -0,0 +1,194 @@
import { Injectable, Logger, Inject, Optional } from '@nestjs/common';
import type {
ErrorTrackingConfig,
ErrorLogPayload,
ReportErrorOptions,
CreateErrorLogResponse,
} from '../types';
export const ERROR_TRACKING_CONFIG = 'ERROR_TRACKING_CONFIG';
@Injectable()
export class ErrorTrackingService {
private readonly logger = new Logger(ErrorTrackingService.name);
private readonly config: ErrorTrackingConfig;
constructor(
@Inject(ERROR_TRACKING_CONFIG)
@Optional()
config?: ErrorTrackingConfig
) {
this.config = config || {
errorTrackingUrl: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001',
appId: 'unknown',
};
}
/**
* Report an error to the error tracking service
*/
async reportError(options: ReportErrorOptions): Promise<CreateErrorLogResponse> {
const payload: ErrorLogPayload = {
errorCode: options.errorCode,
errorType: options.errorType,
message: options.message,
severity: options.severity || 'error',
context: options.context,
stackTrace: options.stackTrace,
appId: this.config.appId,
serviceName: this.config.serviceName,
sourceType: 'backend',
environment: this.config.environment || this.detectEnvironment(),
occurredAt: new Date().toISOString(),
};
return this.sendErrorLog(payload);
}
/**
* Report an exception (Error object) to the error tracking service
*/
async reportException(
error: Error,
context?: Record<string, unknown>
): Promise<CreateErrorLogResponse> {
const payload: ErrorLogPayload = {
errorCode: this.extractErrorCode(error),
errorType: error.constructor.name,
message: error.message,
stackTrace: error.stack,
severity: 'error',
context,
appId: this.config.appId,
serviceName: this.config.serviceName,
sourceType: 'backend',
environment: this.config.environment || this.detectEnvironment(),
occurredAt: new Date().toISOString(),
};
return this.sendErrorLog(payload);
}
/**
* Report an HTTP exception with request details
*/
async reportHttpException(
error: Error,
request: {
url?: string;
method?: string;
headers?: Record<string, unknown>;
body?: Record<string, unknown>;
user?: { userId?: string; sessionId?: string };
},
statusCode?: number
): Promise<CreateErrorLogResponse> {
const payload: ErrorLogPayload = {
errorCode: this.extractErrorCode(error),
errorType: error.constructor.name,
message: error.message,
stackTrace: error.stack,
severity: this.getSeverityFromStatusCode(statusCode),
appId: this.config.appId,
serviceName: this.config.serviceName,
sourceType: 'backend',
environment: this.config.environment || this.detectEnvironment(),
requestUrl: request.url,
requestMethod: request.method,
requestHeaders: request.headers,
requestBody: request.body,
responseStatusCode: statusCode,
userId: request.user?.userId,
sessionId: request.user?.sessionId,
occurredAt: new Date().toISOString(),
};
return this.sendErrorLog(payload);
}
/**
* Send error log to the tracking endpoint
*/
private async sendErrorLog(payload: ErrorLogPayload): Promise<CreateErrorLogResponse> {
// Log locally if enabled
if (this.config.enableLocalLogging !== false) {
this.logger.error(`[${payload.errorCode}] ${payload.message}`, payload.stackTrace);
}
// Skip sending to remote in development by default
if (this.detectEnvironment() === 'development' && !this.config.errorTrackingUrl) {
return { success: true, id: 'local-only' };
}
try {
const url = `${this.config.errorTrackingUrl}/api/v1/errors`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-App-Id': this.config.appId,
...this.config.customHeaders,
};
// Add auth token if available
if (this.config.getAuthToken) {
const token = await this.config.getAuthToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!response.ok) {
this.logger.warn(`Failed to send error log: HTTP ${response.status}`);
return { success: false, error: `HTTP ${response.status}` };
}
const result = (await response.json()) as CreateErrorLogResponse;
return result;
} catch (err) {
this.logger.warn('Failed to send error log to tracking service', err);
return { success: false, error: 'Network error' };
}
}
/**
* Detect current environment
*/
private detectEnvironment(): 'development' | 'staging' | 'production' {
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'production') return 'production';
if (nodeEnv === 'staging') return 'staging';
return 'development';
}
/**
* Extract error code from error object
*/
private extractErrorCode(error: Error): string {
// Check for common NestJS exception properties
const anyError = error as unknown as Record<string, unknown>;
if (anyError.code && typeof anyError.code === 'string') {
return anyError.code;
}
if (anyError.name && typeof anyError.name === 'string') {
return anyError.name;
}
return 'UNKNOWN_ERROR';
}
/**
* Get severity level from HTTP status code
*/
private getSeverityFromStatusCode(
statusCode?: number
): 'debug' | 'info' | 'warning' | 'error' | 'critical' {
if (!statusCode) return 'error';
if (statusCode >= 500) return 'critical';
if (statusCode >= 400) return 'warning';
return 'info';
}
}

View file

@ -0,0 +1,7 @@
export { ErrorTrackingModule } from './error-tracking.module';
export type {
ErrorTrackingModuleOptions,
ErrorTrackingModuleAsyncOptions,
} from './error-tracking.module';
export { ErrorTrackingService, ERROR_TRACKING_CONFIG } from './error-tracking.service';
export { ErrorTrackingFilter } from './error-tracking.filter';

View file

@ -0,0 +1,111 @@
/**
* Error tracking configuration options
*/
export interface ErrorTrackingConfig {
/** URL of mana-core-auth service */
errorTrackingUrl: string;
/** App identifier (e.g., 'chat', 'picture') */
appId: string;
/** Service name for identification */
serviceName?: string;
/** Default environment if not detected */
environment?: 'development' | 'staging' | 'production';
/** Log errors locally as well (default: true in dev) */
enableLocalLogging?: boolean;
/** Custom headers for requests */
customHeaders?: Record<string, string>;
/** Function to get auth token (optional) */
getAuthToken?: () => Promise<string | null>;
}
/**
* Error source types
*/
export type ErrorSourceType = 'backend' | 'frontend_web' | 'frontend_mobile';
/**
* Error environments
*/
export type ErrorEnvironment = 'development' | 'staging' | 'production';
/**
* Error severity levels
*/
export type ErrorSeverity = 'debug' | 'info' | 'warning' | 'error' | 'critical';
/**
* Error log payload sent to the API
*/
export interface ErrorLogPayload {
// Required
errorCode: string;
errorType: string;
message: string;
// Optional
stackTrace?: string;
appId?: string;
sourceType?: ErrorSourceType;
serviceName?: string;
userId?: string;
sessionId?: string;
requestUrl?: string;
requestMethod?: string;
requestHeaders?: Record<string, unknown>;
requestBody?: Record<string, unknown>;
responseStatusCode?: number;
environment?: ErrorEnvironment;
severity?: ErrorSeverity;
context?: Record<string, unknown>;
fingerprint?: string;
browserInfo?: Record<string, unknown>;
deviceInfo?: Record<string, unknown>;
userAgent?: string;
occurredAt?: string;
}
/**
* Response from creating a single error log
*/
export interface CreateErrorLogResponse {
success: boolean;
id?: string;
error?: string;
}
/**
* Response from creating batch error logs
*/
export interface BatchErrorLogResponse {
success: boolean;
total: number;
succeeded: number;
failed: number;
}
/**
* Manual error report options
*/
export interface ReportErrorOptions {
errorCode: string;
errorType: string;
message: string;
severity?: ErrorSeverity;
context?: Record<string, unknown>;
stackTrace?: string;
}
/**
* Context for error capture in frontends
*/
export interface ErrorContext {
component?: string;
action?: string;
[key: string]: unknown;
}

View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}