All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 49m39s
243 lines
8.0 KiB
Markdown
243 lines
8.0 KiB
Markdown
# 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<LogLevel, number> = {
|
|
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/#/)
|