Skip to content

Commit fd5df02

Browse files
authored
fix: replace console statements with centralized logger utility (#1057)
* fix: replace console statements with centralized logger utility - Create environment-aware logger utility (dev/prod) - Migrate all 150 console.* statements to logger - Add Sentry-ready structure for future integration - Configure logger with category-based filtering - Document logger usage in frontend CLAUDE.md - Zero ESLint console warnings remaining Logger features: - Debug/info only in development - Warn/error always logged - Context support for errors - Performance timing utilities - TypeScript fully typed * style: apply prettier formatting to modified files * docs: fix duplicate logger declarations and remove obsolete TODO - Fix CLAUDE.md examples to avoid duplicate const declarations - Remove TODO comment about Sentry since logger is already Sentry-ready - Clarify logger usage patterns with proper variable names * refactor: polish Svelte logging patterns - Remove redundant import.meta.env.DEV checks (logger handles environment automatically) - Move template logging to proper functions using snippets - Add comprehensive component context to all error logs - Update documentation with Svelte-specific logging best practices Fixed issues: - NotificationBell: Improved error context for loadNotifications and markAsRead - DailySummaryCard: Moved template logging to snippet pattern - FilterSettingsPage: Simplified environment-aware logging - Added Svelte logging patterns to frontend CLAUDE.md * refactor: enhance logging practices and documentation - Standardized logger usage across components by replacing `getLogger('app')` with specific loggers like `loggers.analytics` and `loggers.auth`. - Improved error logging context in `DailySummaryCard` for better traceability. - Updated `CLAUDE.md` to include critical guidelines on PII protection and logging best practices. - Added security-aware logging patterns to prevent exposure of sensitive data. These changes aim to streamline logging practices and enhance security measures in the codebase. * feat: implement URL credential redaction in RTSPUrlInput component - Added a function to redact credentials from RTSP URLs for safer logging. - Enhanced error logging in the addUrl function to include redacted URLs, improving security and traceability. These changes aim to protect sensitive information during logging while maintaining functionality. * refactor: enhance logging context and improve logger utility - Updated logger usage in NotificationBell and DailySummaryCard components to include detailed context for warnings, improving traceability of issues. - Renamed logger instance in CLAUDE.md for clarity. - Enhanced logger interface to support throttling, preventing excessive log messages and improving performance. These changes aim to streamline logging practices and provide better insights during debugging.
1 parent de85d49 commit fd5df02

31 files changed

+889
-172
lines changed

frontend/CLAUDE.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,198 @@ function getCsrfToken(): string | null {
197197
headers.set('X-CSRF-Token', getCsrfToken());
198198
```
199199
200+
## Logging
201+
202+
Use the centralized logger utility instead of console statements:
203+
204+
```typescript
205+
import { getLogger, loggers } from '$lib/utils/logger';
206+
207+
// Option 1: Create a custom logger for your module
208+
const customLogger = getLogger('myModule');
209+
210+
// Option 2: Use predefined category loggers
211+
const apiLogger = loggers.api; // For API-related logging
212+
const authLogger = loggers.auth; // For authentication
213+
const sseLogger = loggers.sse; // For SSE connections
214+
const audioLogger = loggers.audio; // For audio components
215+
const uiLogger = loggers.ui; // For UI components
216+
const settingsLogger = loggers.settings; // For settings
217+
218+
// Most common pattern - choose one logger per file:
219+
const logger = loggers.ui; // For UI components
220+
```
221+
222+
**Important**: Call `getLogger()` once when the module loads and reuse the returned logger instance throughout the module. This prevents creating multiple logger instances inside functions, reducing unnecessary object allocation and ensuring consistent category scoping.
223+
224+
### Logger Methods
225+
226+
```typescript
227+
// Debug information (dev only)
228+
logger.debug('Component initialized', props);
229+
230+
// Informational messages (dev only)
231+
logger.info('Connection established');
232+
233+
// Warnings (always logged)
234+
logger.warn('Deprecated method used');
235+
236+
// Errors with context (always logged)
237+
logger.error('Failed to save', error, {
238+
component: 'SettingsPage',
239+
action: 'save',
240+
userId: user.id,
241+
});
242+
243+
// Performance timing (development only - no-op in production)
244+
logger.time('dataLoad');
245+
// ... expensive operation
246+
logger.timeEnd('dataLoad'); // Logs: [category] dataLoad: 123.45ms
247+
248+
// Grouping (development only - no-op in production)
249+
logger.group('Processing items');
250+
items.forEach(item => logger.debug('Item:', item));
251+
logger.groupEnd();
252+
```
253+
254+
### Key Features
255+
256+
- **Environment-aware**: Debug/info logs only in development
257+
- **Zero configuration**: Works immediately
258+
- **Category-based**: Helps identify log sources
259+
- **Sentry-ready**: Structured for future integration
260+
- **Type-safe**: Full TypeScript support
261+
- **No console warnings**: Properly configured for ESLint
262+
- **Security-aware**: Built-in PII protection guidelines
263+
264+
### Security and PII Protection
265+
266+
**CRITICAL**: Never log personally identifiable information (PII) or sensitive data:
267+
268+
```typescript
269+
// ❌ Don't log PII or sensitive data
270+
logger.error('Login failed', error, {
271+
username: 'john.doe@example.com', // PII
272+
password: 'secret123', // Sensitive
273+
sessionToken: 'abc123xyz', // Sensitive
274+
});
275+
276+
// ✅ Log safe identifiers and context
277+
logger.error('Login failed', error, {
278+
component: 'LoginForm',
279+
action: 'authenticate',
280+
userId: user.id, // Safe: non-PII identifier
281+
attemptCount: 3, // Safe: operational data
282+
});
283+
284+
// ✅ Sanitize or redact sensitive fields
285+
logger.debug('API request', {
286+
endpoint: '/api/user/profile',
287+
method: 'POST',
288+
headers: { 'content-type': 'application/json' }, // Safe headers only
289+
bodyFields: Object.keys(requestBody), // Field names, not values
290+
});
291+
```
292+
293+
**Safe to log**: Component names, action names, non-PII IDs, counts, timestamps, status codes, error types
294+
**Never log**: Emails, usernames, passwords, tokens, API keys, personal data, request/response bodies with PII
295+
296+
### Log Levels by Build Target
297+
298+
| Log Level | Development | Production | Purpose |
299+
| --------- | ----------- | ---------- | ------------------------------------------- |
300+
| `debug` | ✅ Logged | ❌ Silent | Development details, state changes |
301+
| `info` | ✅ Logged | ❌ Silent | Important flow information |
302+
| `warn` | ✅ Logged | ✅ Logged | Deprecations, fallbacks, recoverable issues |
303+
| `error` | ✅ Logged | ✅ Logged | Failures requiring attention |
304+
| `time` | ✅ Logged | ❌ No-op | Performance timing measurements |
305+
| `group` | ✅ Logged | ❌ No-op | Console grouping for organization |
306+
307+
**Key Points:**
308+
309+
- Only `warn` and `error` logs appear in production builds
310+
- Timing and grouping functions are no-op in production (zero overhead)
311+
- Development logs help with debugging but are stripped from production bundles
312+
313+
### Best Practices
314+
315+
1. Use appropriate log levels:
316+
- `debug`: Development details, state changes
317+
- `info`: Important flow information
318+
- `warn`: Deprecations, fallbacks
319+
- `error`: Failures requiring attention
320+
321+
2. Always provide context for errors:
322+
323+
```typescript
324+
logger.error('API request failed', error, {
325+
component: 'DetectionsPage',
326+
action: 'loadDetections',
327+
endpoint: '/api/v2/detections',
328+
});
329+
```
330+
331+
3. Use categories that match your module's purpose
332+
4. Keep production logs minimal (warn/error only)
333+
334+
### Svelte-Specific Patterns
335+
336+
**Avoid redundant environment checks** - Logger handles dev/prod automatically:
337+
338+
```svelte
339+
<!-- ✅ Do this - logger handles environment -->
340+
<script>
341+
$effect(() => {
342+
logger.debug('Component state updated', { state });
343+
});
344+
</script>
345+
346+
<!-- ❌ Don't do this - redundant check -->
347+
{#if import.meta.env.DEV}
348+
{logger.debug('Component state:', state)}
349+
{/if}
350+
```
351+
352+
**Move logging out of templates** - Use functions or effects:
353+
354+
```svelte
355+
<!-- ✅ Log in reactive statements -->
356+
<script>
357+
$effect(() => {
358+
if (someCondition) {
359+
logger.warn('Unexpected condition', { component: 'MyComponent' });
360+
}
361+
});
362+
</script>
363+
364+
<!-- ❌ Don't log in templates -->
365+
{#if someCondition}
366+
{logger.warn('Unexpected condition')}
367+
<div>Content</div>
368+
{/if}
369+
```
370+
371+
**Provide component context** - Always include component name and action:
372+
373+
```svelte
374+
<script>
375+
import { loggers } from '$lib/utils/logger';
376+
377+
const logger = loggers.ui;
378+
379+
async function handleSubmit() {
380+
try {
381+
await submitData();
382+
} catch (error) {
383+
logger.error('Form submission failed', error, {
384+
component: 'MyForm',
385+
action: 'handleSubmit',
386+
});
387+
}
388+
}
389+
</script>
390+
```
391+
200392
## Server-Sent Events (SSE)
201393
202394
Use `reconnecting-eventsource` package for real-time updates with automatic reconnection handling.

frontend/src/App.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import DashboardPage from './lib/desktop/features/dashboard/pages/DashboardPage.svelte'; // Keep dashboard for initial load
55
import type { Component } from 'svelte';
66
import type { BirdnetConfig } from './app.d.ts';
7+
import { getLogger } from './lib/utils/logger';
8+
9+
const logger = getLogger('app');
710
811
// Dynamic imports for heavy pages - properly typed component references
912
let Analytics = $state<Component | null>(null);
@@ -145,7 +148,11 @@
145148
break;
146149
}
147150
} catch (error) {
148-
console.error(`Failed to load component for route "${route}":`, error);
151+
logger.error(`Failed to load component for route "${route}"`, error, {
152+
component: 'App',
153+
action: 'loadComponent',
154+
route,
155+
});
149156
// Fall back to generic error page on component load failure
150157
currentRoute = 'error-generic';
151158
currentPage = 'error-generic';
@@ -157,7 +164,10 @@
157164
const module = await import('./lib/desktop/views/GenericErrorPage.svelte');
158165
GenericErrorPage = module.default;
159166
} catch (fallbackError) {
160-
console.error('Failed to load fallback error component:', fallbackError);
167+
logger.error('Failed to load fallback error component', fallbackError, {
168+
component: 'App',
169+
action: 'loadFallbackError',
170+
});
161171
}
162172
}
163173
} finally {

frontend/src/lib/desktop/components/forms/RTSPUrlInput.svelte

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script lang="ts">
22
import type { RTSPUrl } from '$lib/stores/settings';
3+
import { loggers } from '$lib/utils/logger';
4+
5+
const logger = loggers.ui;
36
47
interface Props {
58
urls: RTSPUrl[];
@@ -30,6 +33,21 @@
3033
}
3134
}
3235
36+
// Redact credentials from URL for safe logging
37+
function redactUrlCredentials(url: string): string {
38+
try {
39+
const urlObj = new URL(url);
40+
if (urlObj.username || urlObj.password) {
41+
// Replace credentials with [REDACTED]
42+
return url.replace(/(rtsp:\/\/)[^@]+(@)/, '$1[REDACTED]$2');
43+
}
44+
return url;
45+
} catch {
46+
// If URL parsing fails, redact anything that looks like credentials
47+
return url.replace(/(rtsp:\/\/)[^@]+(@)/, '$1[REDACTED]$2');
48+
}
49+
}
50+
3351
function addUrl() {
3452
const trimmedUrl = newUrl.trim();
3553
if (trimmedUrl && isValidRtspUrl(trimmedUrl)) {
@@ -38,7 +56,11 @@
3856
newUrl = '';
3957
} else if (trimmedUrl && !isValidRtspUrl(trimmedUrl)) {
4058
// URL is not empty but invalid - could add user feedback here
41-
console.error('Invalid RTSP URL format:', trimmedUrl); // TODO: Replace with Sentry.io logging
59+
logger.error('Invalid RTSP URL format', null, {
60+
url: redactUrlCredentials(trimmedUrl),
61+
component: 'RTSPUrlInput',
62+
action: 'addUrl',
63+
});
4264
}
4365
}
4466

frontend/src/lib/desktop/components/media/AudioPlayer.svelte

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import { cn } from '$lib/utils/cn.js';
3131
import { mediaIcons } from '$lib/utils/icons.js';
3232
import { t } from '$lib/i18n';
33+
import { loggers } from '$lib/utils/logger';
34+
35+
const logger = loggers.audio;
3336
3437
// Web Audio API types - these are built-in browser types
3538
/* global AudioContext, MediaElementAudioSourceNode, GainNode, DynamicsCompressorNode, BiquadFilterNode, EventListener, ResizeObserver */
@@ -202,8 +205,7 @@
202205
return audioContext;
203206
// eslint-disable-next-line no-unused-vars
204207
} catch (_e) {
205-
// eslint-disable-next-line no-console
206-
console.warn('Web Audio API is not supported in this browser');
208+
logger.warn('Web Audio API is not supported in this browser');
207209
audioContextAvailable = false;
208210
audioContextError =
209211
'Advanced audio features (volume control, filtering) are not available in this browser.';
@@ -264,7 +266,7 @@
264266
await audioElement.play();
265267
}
266268
} catch (err) {
267-
console.error('Error playing audio:', err);
269+
logger.error('Error playing audio:', err);
268270
error = 'Failed to play audio';
269271
}
270272
};
@@ -464,8 +466,8 @@
464466
// eslint-disable-next-line no-unused-vars
465467
} catch (_e) {
466468
// Nodes may already be disconnected, ignore errors
467-
// eslint-disable-next-line no-console
468-
console.warn('Error disconnecting audio nodes during cleanup');
469+
470+
logger.warn('Error disconnecting audio nodes during cleanup');
469471
}
470472
audioNodes = null;
471473
}
@@ -477,8 +479,8 @@
477479
// eslint-disable-next-line no-unused-vars
478480
} catch (_e) {
479481
// Context may already be closed, ignore errors
480-
// eslint-disable-next-line no-console
481-
console.warn('Error closing audio context during cleanup');
482+
483+
logger.warn('Error closing audio context during cleanup');
482484
}
483485
audioContext = null;
484486
}

frontend/src/lib/desktop/components/ui/AudioLevelIndicator.svelte

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import { cn } from '$lib/utils/cn';
33
import ReconnectingEventSource from 'reconnecting-eventsource';
44
import { mediaIcons } from '$lib/utils/icons';
5+
import { loggers } from '$lib/utils/logger';
6+
7+
const logger = loggers.audio;
58
69
interface AudioLevelData {
710
level: number;
@@ -180,7 +183,7 @@
180183
});
181184
182185
eventSource.onopen = () => {
183-
console.log('Audio level SSE connection opened');
186+
logger.debug('Audio level SSE connection opened');
184187
};
185188
186189
eventSource.onmessage = event => {
@@ -226,17 +229,17 @@
226229
});
227230
}
228231
} catch (error) {
229-
console.error('Failed to parse audio level data:', error);
232+
logger.error('Failed to parse audio level data:', error);
230233
}
231234
};
232235
233236
eventSource.onerror = (error: Event) => {
234-
console.error('Audio level SSE error:', error);
237+
logger.error('Audio level SSE error:', error);
235238
// ReconnectingEventSource handles reconnection automatically
236239
// No need for manual reconnection logic
237240
};
238241
} catch (error) {
239-
console.error('Failed to create ReconnectingEventSource:', error);
242+
logger.error('Failed to create ReconnectingEventSource:', error);
240243
// Try again in 5 seconds if initial setup fails
241244
globalThis.setTimeout(() => setupEventSource(), 5000);
242245
}

frontend/src/lib/desktop/components/ui/ErrorAlert.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import type { Snippet } from 'svelte';
55
import type { HTMLAttributes } from 'svelte/elements';
66
import { t } from '$lib/i18n';
7+
import { loggers } from '$lib/utils/logger';
8+
9+
const logger = loggers.ui;
710
811
type AlertType = AlertIconType;
912
@@ -44,7 +47,7 @@
4447
try {
4548
onDismiss();
4649
} catch (error) {
47-
console.error('Error occurred in ErrorAlert onDismiss callback:', error);
50+
logger.error('Error occurred in ErrorAlert onDismiss callback:', error);
4851
}
4952
}
5053
</script>

0 commit comments

Comments
 (0)