# ADR-028: Standardized Client-Side Structured Logging **Date**: 2026-02-10 **Status**: Accepted **Source**: Imported from flyer-crawler project (ADR-026) **Related**: [ADR-027](ADR-027-application-wide-structured-logging.md), [ADR-029](ADR-029-error-tracking-with-bugsink.md) ## Context Following the standardization of backend logging in ADR-027, it is clear that our frontend components also require a consistent logging strategy. Currently, components either use `console.log` directly or a simple wrapper, but without a formal standard, this can lead to inconsistent log formats and difficulty in debugging user-facing issues. While the frontend does not have the concept of a "request-scoped" logger, the principles of structured, context-rich logging are equally important for: 1. **Effective Debugging**: Understanding the state of a component or the sequence of user interactions that led to an error 2. **Integration with Monitoring Tools**: Sending structured logs to services like Sentry/Bugsink or LogRocket allows for powerful analysis and error tracking in production 3. **Clean Test Outputs**: Uncontrolled logging can pollute test runner output, making it difficult to spot actual test failures An existing client-side logger at `src/services/logger.client.ts` provides a simple, structured logging interface. This ADR formalizes its use as the application standard. ## Decision We will adopt a standardized, application-wide structured logging policy for all client-side (React) code. ### 1. Mandatory Use of the Global Client Logger All frontend components, hooks, and services **MUST** use the global logger singleton exported from `src/services/logger.client.ts`. Direct use of `console.log`, `console.error`, etc., is discouraged. ### 2. Pino-like API for Structured Logging The client logger mimics the `pino` API, which is the standard on the backend. It supports two primary call signatures: - `logger.info('A simple message');` - `logger.info({ key: 'value' }, 'A message with a structured data payload');` The second signature, which includes a data object as the first argument, is **strongly preferred**, especially for logging errors or complex state. ### 3. Mocking in Tests All Jest/Vitest tests for components or hooks that use the logger **MUST** mock the `src/services/logger.client.ts` module. This prevents logs from appearing in test output and allows for assertions that the logger was called correctly. ## Implementation ### Client Logger Service Located in `src/services/logger.client.ts`: ```typescript type LogLevel = 'debug' | 'info' | 'warn' | 'error'; interface LoggerOptions { level?: LogLevel; enabled?: boolean; } const LOG_LEVELS: Record = { debug: 0, info: 1, warn: 2, error: 3, }; class ClientLogger { private level: LogLevel; private enabled: boolean; constructor(options: LoggerOptions = {}) { this.level = options.level ?? 'info'; this.enabled = options.enabled ?? import.meta.env.DEV; } private shouldLog(level: LogLevel): boolean { return this.enabled && LOG_LEVELS[level] >= LOG_LEVELS[this.level]; } private formatMessage(data: object | string, message?: string): string { if (typeof data === 'string') { return data; } const payload = JSON.stringify(data, null, 2); return message ? `${message}\n${payload}` : payload; } debug(data: object | string, message?: string): void { if (this.shouldLog('debug')) { console.debug(`[DEBUG] ${this.formatMessage(data, message)}`); } } info(data: object | string, message?: string): void { if (this.shouldLog('info')) { console.info(`[INFO] ${this.formatMessage(data, message)}`); } } warn(data: object | string, message?: string): void { if (this.shouldLog('warn')) { console.warn(`[WARN] ${this.formatMessage(data, message)}`); } } error(data: object | string, message?: string): void { if (this.shouldLog('error')) { console.error(`[ERROR] ${this.formatMessage(data, message)}`); } } } export const logger = new ClientLogger({ level: import.meta.env.DEV ? 'debug' : 'warn', enabled: true, }); ``` ### Example Usage **Logging an Error in a Component:** ```typescript // In a React component or hook import { logger } from '../services/logger.client'; import { notifyError } from '../services/notificationService'; const fetchData = async () => { try { const data = await apiClient.getData(); return data; } catch (err) { // Log the full error object for context, along with a descriptive message. logger.error({ err }, 'Failed to fetch component data'); notifyError('Something went wrong. Please try again.'); } }; ``` **Logging State Changes:** ```typescript // In a Zustand store or state hook import { logger } from '../services/logger.client'; const useAuthStore = create((set) => ({ login: async (credentials) => { logger.info({ email: credentials.email }, 'User login attempt'); try { const user = await authService.login(credentials); logger.info({ userId: user.id }, 'User logged in successfully'); set({ user, isAuthenticated: true }); } catch (error) { logger.error({ error }, 'Login failed'); throw error; } }, })); ``` ### Mocking the Logger in Tests ```typescript // In a *.test.tsx file import { vi } from 'vitest'; // Mock the logger at the top of the test file vi.mock('../services/logger.client', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); describe('MyComponent', () => { beforeEach(() => { vi.clearAllMocks(); // Clear mocks between tests }); it('should log an error when fetching fails', async () => { // ... test setup to make fetch fail ... // Assert that the logger was called with the expected structure expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ err: expect.any(Error) }), 'Failed to fetch component data', ); }); }); ``` ## Integration with Error Tracking When using Sentry/Bugsink for error tracking (see ADR-029), the client logger can be extended to send logs as breadcrumbs: ```typescript import * as Sentry from '@sentry/react'; class ClientLogger { // ... existing implementation error(data: object | string, message?: string): void { if (this.shouldLog('error')) { console.error(`[ERROR] ${this.formatMessage(data, message)}`); } // Add to Sentry breadcrumbs for error context Sentry.addBreadcrumb({ category: 'log', level: 'error', message: typeof data === 'string' ? data : message, data: typeof data === 'object' ? data : undefined, }); } } ``` ## Consequences ### Positive - **Consistency**: All client-side logs will have a predictable structure, making them easier to read and parse - **Debuggability**: Errors logged with a full object (`{ err }`) capture the stack trace and other properties, which is invaluable for debugging - **Testability**: Components that log are easier to test without polluting CI/CD output. We can also assert that logging occurs when expected - **Future-Proof**: If we later decide to send client-side logs to a remote service, we only need to modify the central `logger.client.ts` file instead of every component - **Error Tracking Integration**: Logs can be used as breadcrumbs in Sentry/Bugsink for better error context ### Negative - **Minor Boilerplate**: Requires importing the logger in every file that needs it and mocking it in every corresponding test file. However, this is a small and consistent effort - **Production Noise**: Care must be taken to configure appropriate log levels in production to avoid performance impact ## Key Files - `src/services/logger.client.ts` - Client-side logger implementation - `src/services/logger.server.ts` - Backend logger (for reference) ## References - [ADR-027: Application-Wide Structured Logging](ADR-027-application-wide-structured-logging.md) - [ADR-029: Error Tracking with Bugsink](ADR-029-error-tracking-with-bugsink.md) - [Pino Documentation](https://getpino.io/#/)