4.4 KiB
ADR-026: Standardized Client-Side Structured Logging
Date: 2025-12-14
Status: Adopted
Context
Following the standardization of backend logging in ADR-004, 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:
- Effective Debugging: Understanding the state of a component or the sequence of user interactions that led to an error.
- Integration with Monitoring Tools: Sending structured logs to services like Datadog, Sentry, or LogRocket allows for powerful analysis and error tracking in production.
- 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 already 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.
Example Usage
Logging an Error in a Component:
// 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.');
}
};
Mocking the Logger in a Test File:
// 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) }), // Check for the error object
'Failed to fetch component data', // Check for the message
);
});
});
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.
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.