Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ErrorBoundary } from './src/components/common/ErrorBoundary';
import AppNavigator from './src/navigation/AppNavigator';
import socketService from './src/services/socket';
import { useAppStore } from './src/store';

import logger from './src/utils/logger';
// Notification imports
import { setupNotificationNavigation } from './src/navigation/linking';
import apiClient from './src/services/api/axios.config';
Expand All @@ -18,16 +18,10 @@ import {
} from './src/services/pushNotifications';
import { handleNotificationReceived } from './src/utils/notificationHandlers';

// Enable error logging to console (visible in Metro bundler)
// Centralized logging is handled by src/utils/logger.
// Suppress known non-actionable navigation warnings in all environments.
if (__DEV__) {
// Log all errors to console
const originalError = console.error;
console.error = (...args) => {
originalError(...args);
// Errors will appear in Metro bundler terminal
};

// Show warnings in console but don't break the app
logger.debug('Development mode: centralized logger active');
LogBox.ignoreLogs([
'Non-serializable values were found in the navigation state',
]);
Expand All @@ -52,7 +46,7 @@ export default function App() {
// Check if app was launched from a notification
getLastNotificationResponse().then((response) => {
if (response) {
console.log('App launched from notification:', response);
logger.info('App launched from notification:', response);
}
});

Expand Down
150 changes: 150 additions & 0 deletions src/__tests__/utils/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { LogLevel } from '../../utils/logger';

describe('logger utility', () => {
describe('LogLevel enum', () => {
it('exports the correct string values for each level', () => {
expect(LogLevel.DEBUG).toBe('DEBUG');
expect(LogLevel.INFO).toBe('INFO');
expect(LogLevel.WARN).toBe('WARN');
expect(LogLevel.ERROR).toBe('ERROR');
});
});

describe('in development mode (__DEV__ = true)', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
let devLogger: typeof import('../../utils/logger').logger;

beforeAll(() => {
(global as any).__DEV__ = true;
jest.resetModules();
devLogger = require('../../utils/logger').logger;
});

afterAll(() => {
delete (global as any).__DEV__;
jest.resetModules();
});

it('info() forwards to console.log with the INFO prefix', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
devLogger.info('hello world');
expect(spy).toHaveBeenCalledWith('ℹ️ [INFO]', 'hello world');
spy.mockRestore();
});

it('info() forwards multiple arguments', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
devLogger.info('user:', { id: 1 });
expect(spy).toHaveBeenCalledWith('ℹ️ [INFO]', 'user:', { id: 1 });
spy.mockRestore();
});

it('warn() forwards to console.warn with the WARN prefix', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
devLogger.warn('low disk space');
expect(spy).toHaveBeenCalledWith('⚠️ [WARN]', 'low disk space');
spy.mockRestore();
});

it('debug() forwards to console.log with the DEBUG prefix', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
devLogger.debug('raw payload', { foo: 'bar' });
expect(spy).toHaveBeenCalledWith('🐛 [DEBUG]', 'raw payload', { foo: 'bar' });
spy.mockRestore();
});

it('component() includes the component name and action', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
devLogger.component('ProfileScreen', 'mounted');
expect(spy).toHaveBeenCalledWith('📱 [ProfileScreen] mounted', '');
spy.mockRestore();
});

it('component() includes optional data when provided', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
devLogger.component('CourseList', 'updated', { count: 5 });
expect(spy).toHaveBeenCalledWith('📱 [CourseList] updated', { count: 5 });
spy.mockRestore();
});
});

describe('error() — always logs regardless of environment', () => {
// Re-import logger once for these tests without resetting module state.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { logger } = require('../../utils/logger');

it('calls console.error with the ERROR prefix', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
logger.error('something went wrong');
expect(spy).toHaveBeenCalledWith('❌ [ERROR]', 'something went wrong');
spy.mockRestore();
});

it('prints the stack trace when an Error instance is the first argument', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
const err = new Error('boom');
logger.error(err);
expect(spy).toHaveBeenCalledWith('❌ [ERROR]', err);
expect(spy).toHaveBeenCalledWith('Stack:', err.stack);
spy.mockRestore();
});

it('does not print a stack trace for non-Error arguments', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
logger.error('plain string error');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
});

describe('in production mode (__DEV__ = false)', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
let prodLogger: typeof import('../../utils/logger').logger;

beforeAll(() => {
(global as any).__DEV__ = false;
jest.resetModules();
prodLogger = require('../../utils/logger').logger;
});

afterAll(() => {
delete (global as any).__DEV__;
jest.resetModules();
});

it('info() is silent in production', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
prodLogger.info('should be silent');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

it('warn() is silent in production', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
prodLogger.warn('should be silent');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

it('debug() is silent in production', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
prodLogger.debug('should be silent');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

it('component() is silent in production', () => {
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
prodLogger.component('HomeScreen', 'mounted');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

it('error() still logs in production', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
prodLogger.error('prod error');
expect(spy).toHaveBeenCalledWith('❌ [ERROR]', 'prod error');
spy.mockRestore();
});
});
});
13 changes: 6 additions & 7 deletions src/services/api/axios.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
import logger from "../../utils/logger";
import { getAccessToken, getRefreshToken, saveTokens } from "../secureStorage";
import requestQueue from "./requestQueue";

Expand Down Expand Up @@ -50,13 +51,11 @@ apiClient.interceptors.response.use(
_retry?: boolean;
};

// ── Log non-network errors in dev ────────────────────────────────────
if (__DEV__) {
if (error.code === "ERR_NETWORK" || error.message === "Network Error") {
console.warn("⚠️ API not available (running in offline mode)");
} else if (error.response?.status !== 401) {
console.error("API Error:", error.response?.data || error.message);
}
// ── Log non-network errors ────────────────────────────────────────────
if (error.code === "ERR_NETWORK" || error.message === "Network Error") {
logger.warn("API not available (running in offline mode)");
} else if (error.response?.status !== 401) {
logger.error("API Error:", error.response?.data || error.message);
}

// ── Queue network errors for retry ───────────────────────────────────
Expand Down
7 changes: 4 additions & 3 deletions src/services/socket/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { io, Socket } from "socket.io-client";
import logger from "../../utils/logger";

class SocketService {
private socket: Socket | null = null;
Expand All @@ -15,15 +16,15 @@ class SocketService {
});

this.socket.on("connect", () => {
console.log("Socket connected:", this.socket?.id);
logger.info("Socket connected:", this.socket?.id);
});

this.socket.on("disconnect", () => {
console.log("Socket disconnected");
logger.info("Socket disconnected");
});

this.socket.on("error", (error) => {
console.error("Socket error:", error);
logger.error("Socket error:", error);
});
}
return this.socket;
Expand Down
36 changes: 25 additions & 11 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
/**
* Enhanced logging utility that ensures errors are visible in Metro bundler
* All logs appear in your PC terminal where npm start is running
* Centralized logging utility for TeachLink Mobile.
*
* - Development: all log levels are output to the Metro bundler terminal.
* - Production: only errors are logged; info/warn/debug are silenced.
*
* Import and use this instead of direct console.* calls throughout the codebase.
*/

const isDev = __DEV__;
/** Supported log severity levels. */
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}

// Safe __DEV__ check — falls back gracefully in non-RN environments (e.g. Jest).
const isDev =
typeof __DEV__ !== 'undefined' ? __DEV__ : process.env.NODE_ENV !== 'production';

export const logger = {
/**
* Log info messages (appears in Metro terminal)
* Log informational messages. Only emitted in development.
*/
info: (...args: any[]) => {
if (isDev) {
Expand All @@ -16,7 +30,7 @@ export const logger = {
},

/**
* Log warnings (appears in Metro terminal in YELLOW)
* Log warning messages. Only emitted in development.
*/
warn: (...args: any[]) => {
if (isDev) {
Expand All @@ -25,19 +39,19 @@ export const logger = {
},

/**
* Log errors (appears in Metro terminal in RED)
* Log error messages. Always emitted, including in production.
* Prints the stack trace when the first argument is an Error instance.
*/
error: (...args: any[]) => {
console.error('❌ [ERROR]', ...args);

// Also log stack trace if available

if (args[0] instanceof Error) {
console.error('Stack:', args[0].stack);
}
},

/**
* Log debug messages
* Log debug-level messages. Only emitted in development.
*/
debug: (...args: any[]) => {
if (isDev) {
Expand All @@ -46,11 +60,11 @@ export const logger = {
},

/**
* Log component lifecycle
* Log a component lifecycle event. Only emitted in development.
*/
component: (componentName: string, action: string, data?: any) => {
if (isDev) {
console.log(`📱 [${componentName}] ${action}`, data || '');
console.log(`📱 [${componentName}] ${action}`, data ?? '');
}
},
};
Expand Down