The universal logger provides centralized, structured logging for the Sobers application. All logs are sent to Sentry as breadcrumbs and output to the console in development.
import { logger, LogCategory } from '@/lib/logger';
// Error logging
try {
await supabase.from('tasks').insert(data);
} catch (error) {
logger.error('Task creation failed', error as Error, {
category: LogCategory.DATABASE,
userId: user.id,
});
}
// Info logging
logger.info('User logged in successfully', {
category: LogCategory.AUTH,
userId: user.id,
});
// Debug logging
logger.debug('Component rendered', {
category: LogCategory.UI,
componentName: 'TaskList',
});The logger provides five log levels:
Log errors that need attention. Always include the Error object.
logger.error('Profile fetch failed', error as Error, {
category: LogCategory.DATABASE,
userId: profile.id,
});Parameters:
message(string): Human-readable error descriptionerror(Error): The error object to logmetadata(object, optional): Additional context
When to use:
- Database operation failures
- API request failures
- Authentication errors
- Any exceptional condition that prevents normal operation
Log warnings that should be investigated but don't prevent operation.
logger.warn('Cache miss, fetching from database', {
category: LogCategory.DATABASE,
key: cacheKey,
});Parameters:
message(string): Warning descriptionmetadata(object, optional): Additional context
When to use:
- Deprecated feature usage
- Fallback behavior triggered
- Missing optional data
- Performance concerns
Log significant events in the application flow.
logger.info('OAuth session created', {
category: LogCategory.AUTH,
provider: 'google',
});Parameters:
message(string): Event descriptionmetadata(object, optional): Additional context
When to use:
- Successful authentication
- Important state changes
- Milestone completions
- Feature activation
Log detailed information for debugging.
logger.debug('Steps content loaded', {
category: LogCategory.DATABASE,
count: steps.length,
});Parameters:
message(string): Debug informationmetadata(object, optional): Additional context
When to use:
- Development debugging
- Detailed flow tracking
- Data transformation details
- Only active in development (
__DEV__= true)
Log very detailed execution traces (rarely needed).
logger.trace('Entering function', {
category: LogCategory.UI,
functionName: 'calculateTimeline',
});Parameters:
message(string): Trace informationmetadata(object, optional): Additional context
When to use:
- Very detailed debugging
- Performance profiling
- Rarely needed in application code
Use LogCategory enum to categorize logs for easier filtering in Sentry.
export enum LogCategory {
AUTH = 'auth', // Authentication and authorization
NAVIGATION = 'navigation', // Routing and navigation
DATABASE = 'database', // Supabase operations
API = 'http', // API requests
UI = 'ui', // UI interactions and rendering
STORAGE = 'storage', // Local storage operations
NOTIFICATION = 'notification', // Push notifications
SYNC = 'sync', // Data synchronization
ERROR = 'error', // General errors
ANALYTICS = 'analytics', // Analytics and tracking events
}Usage:
logger.error('Database query failed', error as Error, {
category: LogCategory.DATABASE,
table: 'profiles',
});❌ Don't:
console.log('User logged in');
console.error('Error:', error);✅ Do:
logger.info('User logged in', { category: LogCategory.AUTH });
logger.error('Login failed', error as Error, { category: LogCategory.AUTH });Why: The logger provides:
- Centralized tracking in Sentry
- Structured metadata
- Privacy scrubbing
- Development console output
- Consistent formatting
❌ Don't:
logger.error('Task creation failed', error as Error, {
category: LogCategory.DATABASE,
});✅ Do:
logger.error('Task creation failed', error as Error, {
category: LogCategory.DATABASE,
userId: user.id,
taskTitle: taskData.title,
assignedTo: taskData.assigned_to,
});Why: Metadata helps with debugging and understanding context in Sentry.
❌ Don't:
logger.error('Profile update failed', error as Error, {
category: LogCategory.UI,
});✅ Do:
logger.error('Profile update failed', error as Error, {
category: LogCategory.DATABASE,
profileId: profile.id,
});Why: Correct categories enable effective filtering in Sentry.
❌ Don't:
logger.error('Error', error as Error);
logger.info('Success');✅ Do:
logger.error('Sponsor-sponsee relationship creation failed', error as Error, {
category: LogCategory.DATABASE,
});
logger.info('Google Auth session created successfully', {
category: LogCategory.AUTH,
});Why: Clear messages make logs searchable and understandable.
❌ Don't:
logger.error('Something failed', undefined, {
category: LogCategory.DATABASE,
errorMessage: error.message,
});✅ Do:
logger.error('Database query failed', error as Error, {
category: LogCategory.DATABASE,
query: 'select * from profiles',
});Why: Error objects include stack traces and error details.
❌ Don't:
logger.debug('x = 1');
logger.debug('y = 2');
logger.debug('z = 3');✅ Do:
logger.debug('Timeline calculation complete', {
category: LogCategory.UI,
eventCount: events.length,
calculationTime: Date.now() - startTime,
});Why: Debug logs should provide meaningful insights, not clutter.
// Successful operation with debug logging
try {
const { data, error } = await supabase.from('steps_content').select('*').order('step_number');
if (error) throw error;
logger.debug('Steps content loaded successfully', {
category: LogCategory.DATABASE,
count: data.length,
});
setSteps(data);
} catch (err) {
logger.error('Steps content fetch failed', err as Error, {
category: LogCategory.DATABASE,
});
setError('Failed to load steps');
}// OAuth flow
logger.debug('Google Auth redirect URL configured', {
category: LogCategory.AUTH,
redirectUrl,
});
try {
const { data, error } = await supabase.auth.setSession({
access_token,
refresh_token,
});
if (error) {
logger.error('Google Auth session creation failed', error, {
category: LogCategory.AUTH,
});
throw error;
}
logger.info('Google Auth session created successfully', {
category: LogCategory.AUTH,
});
} catch (error) {
// Handle error
}// Button press in demo component
const handlePress = () => {
logger.debug('Demo nav item pressed', {
category: LogCategory.UI,
item: 'Home',
});
// Handle navigation
};The app uses a layered storage approach:
- AsyncStorage: Non-sensitive data (theme preferences)
- SecureStore: Sensitive data like auth tokens (mobile only, with chunking for large values)
- localStorage: Web platform storage
// Theme preference loading (AsyncStorage - non-sensitive)
try {
const saved = await AsyncStorage.getItem('theme_mode');
if (saved) {
setThemeMode(saved as ThemeMode);
}
} catch (error) {
logger.error('Failed to load theme preference', error as Error, {
category: LogCategory.STORAGE,
});
}
// Auth token storage errors (SecureStore - sensitive)
// Note: The SupabaseStorageAdapter handles this automatically
try {
await SecureStore.setItemAsync(key, token);
} catch (error) {
logger.error('Failed to save session to SecureStore', error as Error, {
category: LogCategory.AUTH,
});
}
// Storage migration logging (legacy AsyncStorage → SecureStore)
logger.error(
'Session migration to secure storage failed - session will remain functional but may use less secure storage until next login',
error as Error,
{ category: LogCategory.AUTH }
);The logger automatically scrubs personally identifiable information (PII) through Sentry's beforeBreadcrumb hook configured in lib/sentry-privacy.ts.
Scrubbed fields:
- Email addresses
- Passwords
- Access tokens
- Refresh tokens
- API keys
- Credit card numbers
Example:
// This metadata will be automatically scrubbed
logger.info('User registered', {
category: LogCategory.AUTH,
email: 'user@example.com', // Scrubbed to '[email]'
password: 'secret123', // Scrubbed to '[password]'
});Even with automatic scrubbing, avoid logging:
- ❌ Full names (use user IDs instead)
- ❌ Phone numbers
- ❌ Addresses
- ❌ Social security numbers
- ❌ Medical information
- ❌ Financial data
- All logs output to console
- Debug and trace logs are active
- Full metadata visible
- Logs sent to Sentry only (no console output)
- Debug and trace logs still create breadcrumbs
- Privacy scrubbing active
All logger calls create Sentry breadcrumbs that appear in error reports.
Breadcrumb structure:
{
level: 'info', // or 'error', 'warning', 'debug'
category: 'database', // from LogCategory
message: 'Task created',
data: {
category: 'database',
userId: '123',
taskId: '456',
},
timestamp: 1234567890,
}- Open Sentry dashboard
- Navigate to an issue
- View "Breadcrumbs" tab
- Filter by category to find relevant logs
The no-console ESLint rule is enforced across the codebase to prevent direct console usage.
Exceptions:
lib/logger.ts- The logger implementation itselflib/sentry.ts- Sentry initialization (uses console to avoid circular dependency)jest.setup.js- Test configuration
To use console in a new file:
Don't. Use the logger instead. If you absolutely must use console (e.g., for a new logging system), add the file to the exception list in eslint.config.js.
Problem: Import path not resolving
Solution: Ensure @/ path alias is configured in tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}Problem: Breadcrumbs not visible in Sentry dashboard
Possible causes:
- Sentry not initialized - Check
EXPO_PUBLIC_SENTRY_DSNenv var - Privacy scrubbing too aggressive - Review
lib/sentry-privacy.ts - Error not captured - Breadcrumbs only appear with errors
Solution: Test by triggering an error:
logger.info('Test breadcrumb', { category: LogCategory.AUTH });
throw new Error('Test error to see breadcrumbs');Problem: ESLint catching console usage
Solution: Use the logger instead:
// ❌ This will error
console.log('Hello');
// ✅ Use logger
logger.debug('Hello', { category: LogCategory.UI });If you're migrating existing console calls:
-
Import the logger:
import { logger, LogCategory } from '@/lib/logger';
-
Replace console calls:
// Before console.error('Error:', error); // After logger.error('Descriptive message', error as Error, { category: LogCategory.DATABASE, });
-
Choose appropriate category:
- Database operations →
LogCategory.DATABASE - Auth operations →
LogCategory.AUTH - UI interactions →
LogCategory.UI - Storage operations →
LogCategory.STORAGE
- Database operations →
-
Add contextual metadata:
logger.error('Task creation failed', error as Error, { category: LogCategory.DATABASE, userId: user.id, taskTitle: data.title, });
-
Test the change:
pnpm format && pnpm lint && pnpm typecheck
lib/logger.ts- Logger implementationlib/logger.test.ts- Logger testslib/sentry.ts- Sentry initializationlib/sentry-privacy.ts- Privacy scrubbing ruleslib/supabase.ts- SupabaseStorageAdapter with auth loggingcontexts/ThemeContext.tsx- Theme storage logging exampleeslint.config.js- ESLint no-console rulejest.setup.js- Test mocks for Sentry