magically-sdk
Version:
Official SDK for Magically - Build mobile apps with AI
304 lines (253 loc) âĸ 8.84 kB
text/typescript
/**
* Simple, formatted logger for Magically SDK
* Provides clean, readable debug output
*/
export class Logger {
private isDebugEnabled: boolean;
private prefix: string;
private requestCounter = 0;
constructor(isDebugEnabled: boolean = false, prefix: string = 'MagicallySDK') {
this.isDebugEnabled = isDebugEnabled;
this.prefix = prefix;
}
/**
* Log debug information (only when debug is enabled)
*/
debug(step: string, data?: any): void {
if (!this.isDebugEnabled) return;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console.log(`đ§ [${this.prefix}] ${timestamp} - ${step}`);
if (data) {
console.log(' đ Data:', this.sanitizeData(data));
}
}
/**
* Log success steps
*/
success(step: string, data?: any): void {
if (!this.isDebugEnabled) return;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console.log(`â
[${this.prefix}] ${timestamp} - ${step}`);
if (data) {
console.log(' đ Data:', this.sanitizeData(data));
}
}
/**
* Log errors (always shown, regardless of debug flag)
*/
error(step: string, error?: any): void {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console.error(`â [${this.prefix}] ${timestamp} - ${step}`);
console.error(' đ¨ Error:', error);
}
/**
* Log warnings
*/
warn(step: string, data?: any): void {
if (!this.isDebugEnabled) return;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console.warn(`â ī¸ [${this.prefix}] ${timestamp} - ${step}`);
if (data) {
console.warn(' đ Data:', this.sanitizeData(data));
}
}
/**
* Log info (always shown)
*/
info(step: string, data?: any): void {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console.log(`âšī¸ [${this.prefix}] ${timestamp} - ${step}`);
if (data) {
console.log(' đ Data:', this.sanitizeData(data));
}
}
/**
* Sanitize sensitive data for logging
*/
private sanitizeData(data: any): any {
if (typeof data !== 'object' || data === null) {
return data;
}
const sanitized = { ...data };
// Remove sensitive fields
const sensitiveFields = [
'password', 'token', 'accessToken', 'refreshToken',
'access_token', 'refresh_token', 'client_secret',
'authorization', 'auth', 'secret'
];
sensitiveFields.forEach(field => {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
});
// Truncate long URLs but keep important parts
if (sanitized.url && sanitized.url.length > 100) {
const url = new URL(sanitized.url);
sanitized.url = `${url.origin}${url.pathname}...`;
}
return sanitized;
}
/**
* Create a scoped logger for a specific component
*/
scope(scopeName: string): Logger {
return new Logger(this.isDebugEnabled, `${this.prefix}:${scopeName}`);
}
/**
* Generate unique request ID
*/
private generateRequestId(): string {
return `req-${++this.requestCounter}-${Date.now()}`;
}
/**
* Sanitize URLs for logging
*/
private sanitizeUrl(url: string): string {
try {
const urlObj = new URL(url);
// Keep the important parts but remove query params that might contain sensitive data
return `${urlObj.origin}${urlObj.pathname}`;
} catch {
return url.substring(0, 100) + '...';
}
}
/**
* Sanitize headers for logging
*/
private sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
const sanitized = { ...headers };
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'set-cookie'];
Object.keys(sanitized).forEach(key => {
if (sensitiveHeaders.includes(key.toLowerCase())) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
/**
* Log network request
*/
networkRequest(method: string, url: string, options?: {
headers?: Record<string, string>;
body?: any;
requestId?: string;
operation?: string;
}): string {
const requestId = options?.requestId || this.generateRequestId();
const startTime = Date.now();
if (!this.isDebugEnabled) return requestId;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const operation = options?.operation ? `:${options.operation}` : '';
// Send BOTH formats for compatibility
// 1. Original string format (for backward compatibility)
let message = `[Network] ${this.prefix}${operation} ${timestamp} - Request [${requestId}]\n`;
message += ` ${method} ${this.sanitizeUrl(url)}`;
if (options?.headers) {
message += `\n Headers: ${JSON.stringify(this.sanitizeHeaders(options.headers))}`;
}
if (options?.body) {
message += `\n Body: ${JSON.stringify(this.sanitizeData(options.body))}`;
}
console.log(message);
// 2. Also send structured data with __raw prefix
const structuredData = {
__raw: true,
type: 'network',
data: {
requestId,
method: method as any,
url,
startTime,
operation: options?.operation,
requestHeaders: options?.headers ? this.sanitizeHeaders(options.headers) : undefined,
requestBody: options?.body ? this.sanitizeData(options.body) : undefined,
}
};
console.log(JSON.stringify(structuredData));
return requestId;
}
/**
* Log network response
*/
networkResponse(requestId: string, response: {
status: number;
statusText?: string;
duration?: number;
data?: any;
headers?: Record<string, string>;
operation?: string;
}): void {
if (!this.isDebugEnabled) return;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const durationText = response.duration ? ` (${response.duration}ms)` : '';
const operation = response.operation ? `:${response.operation}` : '';
const endTime = Date.now();
// 1. Original string format
let message = `[Network] ${this.prefix}${operation} ${timestamp} - Response [${requestId}]${durationText}\n`;
message += ` Status: ${response.status} ${response.statusText || ''}`;
if (response.headers) {
message += `\n Headers: ${JSON.stringify(this.sanitizeHeaders(response.headers))}`;
}
if (response.data !== undefined) {
message += `\n Data: ${JSON.stringify(this.sanitizeData(response.data))}`;
}
// Use appropriate console method based on status
if (response.status >= 400) {
console.error(message);
} else {
console.log(message);
}
// 2. Also send structured data with __raw prefix
const structuredData = {
__raw: true,
type: 'network_response',
data: {
requestId,
status: response.status,
statusText: response.statusText,
endTime,
duration: response.duration,
operation: response.operation,
responseHeaders: response.headers ? this.sanitizeHeaders(response.headers) : undefined,
responseBody: response.data !== undefined ? this.sanitizeData(response.data) : undefined,
responseSize: response.data ? JSON.stringify(response.data).length : 0,
}
};
// Use appropriate console method based on status
if (response.status >= 400) {
console.error(JSON.stringify(structuredData));
} else {
console.log(JSON.stringify(structuredData));
}
}
/**
* Log network error
*/
networkError(requestId: string, error: any, options?: {
duration?: number;
operation?: string;
}): void {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const durationText = options?.duration ? ` (${options.duration}ms)` : '';
const operation = options?.operation ? `:${options.operation}` : '';
const endTime = Date.now();
const errorMessage = error?.message || error?.toString() || 'Unknown error';
// 1. Original string format
const message = `[Network] ${this.prefix}${operation} ${timestamp} - Error [${requestId}]${durationText}\n Error: ${errorMessage}`;
console.error(message);
// 2. Also send structured data with __raw prefix
const structuredData = {
__raw: true,
type: 'network_error',
data: {
requestId,
error: error?.responseData || error, // Pass the full error/response data
errorStack: error?.stack,
endTime,
duration: options?.duration,
operation: options?.operation,
}
};
console.error(JSON.stringify(structuredData));
}
}