Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .cursor/rules/ai-assistant-workflow.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Guidelines for effective collaboration between developers and AI assistants on K
## Task Planning & Management

### Always Use Todo Lists

For any non-trivial task (3+ steps), AI assistants should create and maintain a todo list:

```markdown
Expand All @@ -24,18 +25,21 @@ For any non-trivial task (3+ steps), AI assistants should create and maintain a
```

### Task Breakdown Patterns

- **Feature Addition**: Research → Design → Implement → Test → Lint/Typecheck
- **Bug Fix**: Reproduce → Identify → Fix → Test → Verify
- **Refactoring**: Analyze → Plan → Execute → Test → Validate

## Code Understanding Workflow

### Before Making Changes

1. **Search and understand** - Use search tools to understand existing patterns
2. **Read related files** - Check imports, types, and similar components
3. **Follow project conventions** - Match existing code style and architecture

### Research Commands

```bash
# Find component patterns
find src/components -name "*.tsx" | head -10
Expand All @@ -50,42 +54,65 @@ find . -name "*.spec.tsx" -path "*/components/*"
## Development Best Practices

### Component Development

- **Check existing patterns** first (see `project-structure.mdc` for directory organization)
- **Follow project conventions** and existing code style

### Testing Requirements

- **Create unit tests** using patterns from `testing.mdc`
- **Test both success and error scenarios**

### Logger Usage

- **Never use `console.*` directly** — use the `logger` service from `src/monitoring/logger` instead.
```ts
import { logger } from '~/monitoring/logger';
logger.debug('message', { context }); // instead of console.log
logger.info('message', { context }); // instead of console.info
logger.warn('message', { context }); // instead of console.warn
logger.error('message', error, { context }); // instead of console.error
```
- When **modifying existing files** that contain `console.*` statements, replace them with the appropriate `logger` method.
- When **creating new files**, always use `logger` for any logging needs.
- **Do not** proactively search the codebase for `console.*` to replace — only update files you are already modifying as part of the current task.

### Code Quality Checks

Always run lint and typecheck commands after code changes (see `package-management.mdc` for yarn commands).

## File Navigation & References

### Provide Line References

When discussing code, always include file paths with line numbers:

```
The error handling is in src/hooks/useApplications.ts:45
```

## Communication Patterns

### Progress Updates

- Mark todos as `in_progress` when starting work
- Mark todos as `completed` immediately after finishing
- Update user on significant discoveries or blockers

### Code Explanations

- Focus on **why** rather than **what**
- Explain architectural decisions and trade-offs
- Reference existing patterns and conventions

### Error Handling

- Reproduce errors when possible
- Check common issues: imports, types, missing dependencies
- Provide specific file paths and line numbers for fixes

## Pull Request Checklist

See [CONTRIBUTING.md#pull-request-checklist](../../CONTRIBUTING.md) for the full, up-to-date list.

## Common Commands
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ tsconfig.tsbuildinfo
# Personal local claude settings
.claude/*
CLAUDE.local.md

# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
9 changes: 9 additions & 0 deletions src/consts/log-levels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';

/** Numeric severity values for each log level, used for level-based filtering */
export const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
237 changes: 237 additions & 0 deletions src/monitoring/__tests__/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { mockConsole, MockConsole } from '~/unit-test-utils';

const mockCaptureException = jest.fn();

jest.mock('../index', () => ({
monitoringService: {
captureException: (...args: unknown[]) => mockCaptureException(...args),
},
}));

describe('logger', () => {
let consoleMock: MockConsole;

beforeEach(() => {
consoleMock = mockConsole();
jest.clearAllMocks();
});

afterEach(() => {
consoleMock.restore();
});

describe('with default log level (debug in non-production)', () => {
// Default NODE_ENV in jest is 'test', so currentLevel = 'debug'
// Re-import logger for each describe block to capture env at module scope
let logger: typeof import('../logger').logger;

beforeEach(() => {
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
logger = require('../logger').logger;
});
});

describe('debug', () => {
it('should log with [DEBUG] prefix and context', () => {
const context = { key: 'value' };
logger.debug('test message', context);

expect(consoleMock.log).toHaveBeenCalledWith('[DEBUG] test message', context);
});

it('should log with empty string when context is not provided', () => {
logger.debug('test message');

expect(consoleMock.log).toHaveBeenCalledWith('[DEBUG] test message', '');
});
});

describe('info', () => {
it('should log with [INFO] prefix and context', () => {
const context = { user: 'admin' };
logger.info('info message', context);

expect(consoleMock.info).toHaveBeenCalledWith('[INFO] info message', context);
});

it('should log with empty string when context is not provided', () => {
logger.info('info message');

expect(consoleMock.info).toHaveBeenCalledWith('[INFO] info message', '');
});
});

describe('warn', () => {
it('should log with [WARN] prefix and context', () => {
const context = { severity: 'high' };
logger.warn('warning message', context);

expect(consoleMock.warn).toHaveBeenCalledWith('[WARN] warning message', context);
});

it('should log with empty string when context is not provided', () => {
logger.warn('warning message');

expect(consoleMock.warn).toHaveBeenCalledWith('[WARN] warning message', '');
});
});

describe('error', () => {
it('should log with [ERROR] prefix and call monitoringService.captureException', () => {
const error = new Error('boom');
const context = { action: 'submit' };
logger.error('error occurred', error, context);

expect(consoleMock.error).toHaveBeenCalledWith('[ERROR] error occurred', error, context);
expect(mockCaptureException).toHaveBeenCalledWith(error, context);
});

it('should handle missing error and context parameters', () => {
logger.error('error occurred');

expect(consoleMock.error).toHaveBeenCalledWith('[ERROR] error occurred', undefined, '');
expect(mockCaptureException).toHaveBeenCalledWith(undefined, undefined);
});

it('should handle error without context', () => {
const error = new Error('test');
logger.error('error occurred', error);

expect(consoleMock.error).toHaveBeenCalledWith('[ERROR] error occurred', error, '');
expect(mockCaptureException).toHaveBeenCalledWith(error, undefined);
});
});
});

describe('log level filtering', () => {
it('should not log debug messages in production environment', () => {
jest.isolateModules(() => {
process.env.NODE_ENV = 'production';
process.env.LOG_LEVEL = 'debug';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const prodLogger = require('../logger').logger;
prodLogger.debug('should not appear');

expect(consoleMock.log).not.toHaveBeenCalled();
process.env.NODE_ENV = 'test';
delete process.env.LOG_LEVEL;
});
});

it('should suppress debug and info when LOG_LEVEL is warn', () => {
jest.isolateModules(() => {
process.env.LOG_LEVEL = 'warn';
delete process.env.NODE_ENV;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const filteredLogger = require('../logger').logger;

filteredLogger.debug('should not appear');
filteredLogger.info('should not appear');
filteredLogger.warn('should appear');

expect(consoleMock.log).not.toHaveBeenCalled();
expect(consoleMock.info).not.toHaveBeenCalled();
expect(consoleMock.warn).toHaveBeenCalledWith('[WARN] should appear', '');

process.env.NODE_ENV = 'test';
delete process.env.LOG_LEVEL;
});
});

it('should only allow error when LOG_LEVEL is error', () => {
jest.isolateModules(() => {
process.env.LOG_LEVEL = 'error';
delete process.env.NODE_ENV;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const filteredLogger = require('../logger').logger;

filteredLogger.debug('no');
filteredLogger.info('no');
filteredLogger.warn('no');
filteredLogger.error('yes');

expect(consoleMock.log).not.toHaveBeenCalled();
expect(consoleMock.info).not.toHaveBeenCalled();
expect(consoleMock.warn).not.toHaveBeenCalled();
expect(consoleMock.error).toHaveBeenCalledWith('[ERROR] yes', undefined, '');

process.env.NODE_ENV = 'test';
delete process.env.LOG_LEVEL;
});
});

it('should default to warn level in production when LOG_LEVEL is not set', () => {
jest.isolateModules(() => {
delete process.env.LOG_LEVEL;
process.env.NODE_ENV = 'production';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const prodLogger = require('../logger').logger;

prodLogger.debug('no');
prodLogger.info('no');
prodLogger.warn('yes');

expect(consoleMock.log).not.toHaveBeenCalled();
expect(consoleMock.info).not.toHaveBeenCalled();
expect(consoleMock.warn).toHaveBeenCalledWith('[WARN] yes', '');

process.env.NODE_ENV = 'test';
});
});

it('should allow all levels when LOG_LEVEL is debug', () => {
jest.isolateModules(() => {
process.env.LOG_LEVEL = 'debug';
process.env.NODE_ENV = 'test';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const debugLogger = require('../logger').logger;

debugLogger.debug('d');
debugLogger.info('i');
debugLogger.warn('w');
debugLogger.error('e');

expect(consoleMock.log).toHaveBeenCalledTimes(1);
expect(consoleMock.info).toHaveBeenCalledTimes(1);
expect(consoleMock.warn).toHaveBeenCalledTimes(1);
expect(consoleMock.error).toHaveBeenCalledTimes(1);

delete process.env.LOG_LEVEL;
});
});
});

describe('LOG_LEVELS export', () => {
it('should export correct numeric values for each log level', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { LOG_LEVELS } = require('../../consts/log-levels');

expect(LOG_LEVELS).toEqual({
debug: 0,
info: 1,
warn: 2,
error: 3,
});
});
});

describe('monitoringService integration', () => {
it('should not call monitoringService when error level is suppressed', () => {
jest.isolateModules(() => {
// There's no level higher than error that would suppress it,
// but we can verify it IS called when error level is active
process.env.LOG_LEVEL = 'error';
process.env.NODE_ENV = 'test';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const errorLogger = require('../logger').logger;

errorLogger.error('critical', new Error('fail'));

expect(mockCaptureException).toHaveBeenCalledWith(new Error('fail'), undefined);

delete process.env.LOG_LEVEL;
});
});
Comment on lines +219 to +235
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading test name — it verifies the opposite of what it describes.

The test is named 'should not call monitoringService when error level is suppressed' but actually asserts that captureException is called. The inline comment (lines 222-223) acknowledges there's no level above error to suppress it, making this test effectively a duplicate of the error-level integration test.

Consider renaming to something like 'should call monitoringService.captureException when error level is active' or removing it if redundant with the existing error tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monitoring/__tests__/logger.spec.ts` around lines 219 - 235, The test
name is misleading: it currently reads 'should not call monitoringService when
error level is suppressed' but the body requires captureException to be called;
update the test to reflect its behavior by renaming the test description to
something like 'should call monitoringService.captureException when error level
is active' (or delete the test if it's a duplicate of the existing error-level
test). Locate the test within the describe('monitoringService integration')
block in src/monitoring/__tests__/logger.spec.ts and change the it(...)
description string to the new, accurate text (the assertions using
mockCaptureException and errorLogger.error remain unchanged).

});
});
7 changes: 0 additions & 7 deletions src/monitoring/const.ts

This file was deleted.

38 changes: 38 additions & 0 deletions src/monitoring/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LOG_LEVELS, type LogLevel } from '../consts/log-levels';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use absolute path alias for the import.

Per project conventions, imports from src/ should use the configured ~/ alias.

Suggested fix
-import { LOG_LEVELS, type LogLevel } from '../consts/log-levels';
+import { LOG_LEVELS, type LogLevel } from '~/consts/log-levels';

As per coding guidelines: "Use absolute imports with configured path aliases: ~/components, ~/types, ~/k8s, ~/utils, ~/models, @routes".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { LOG_LEVELS, type LogLevel } from '../consts/log-levels';
import { LOG_LEVELS, type LogLevel } from '~/consts/log-levels';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monitoring/logger.ts` at line 1, The import in logger.ts uses a relative
path; change it to the project's path-alias form by importing LOG_LEVELS and
LogLevel from the aliased module (e.g. replace the relative import with import {
LOG_LEVELS, type LogLevel } from '~/consts/log-levels') so LOG_LEVELS and
LogLevel resolve via the configured ~/ alias and conform to project conventions;
no runtime logic changes required.

import { monitoringService } from './index';

const currentLevel: LogLevel =
(process.env.LOG_LEVEL as LogLevel) || (process.env.NODE_ENV === 'production' ? 'warn' : 'debug');
Comment on lines +4 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Invalid LOG_LEVEL values silently suppress all logging.

If process.env.LOG_LEVEL is set to an unrecognized value (e.g., 'verbose'), the cast as LogLevel won't fail at runtime. LOG_LEVELS[currentLevel] will return undefined, causing shouldLog to evaluate number >= undefinedfalse for every level, silently dropping all logs with no indication of misconfiguration.

Consider validating or falling back:

Suggested fix
-const currentLevel: LogLevel =
-  (process.env.LOG_LEVEL as LogLevel) || (process.env.NODE_ENV === 'production' ? 'warn' : 'debug');
+const defaultLevel: LogLevel = process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
+const envLevel = process.env.LOG_LEVEL as string | undefined;
+const currentLevel: LogLevel =
+  envLevel && envLevel in LOG_LEVELS ? (envLevel as LogLevel) : defaultLevel;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentLevel: LogLevel =
(process.env.LOG_LEVEL as LogLevel) || (process.env.NODE_ENV === 'production' ? 'warn' : 'debug');
const defaultLevel: LogLevel = process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
const envLevel = process.env.LOG_LEVEL as string | undefined;
const currentLevel: LogLevel =
envLevel && envLevel in LOG_LEVELS ? (envLevel as LogLevel) : defaultLevel;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/monitoring/logger.ts` around lines 4 - 5, The code currently casts
process.env.LOG_LEVEL to LogLevel and assigns it to currentLevel which can be an
invalid value and cause LOG_LEVELS[currentLevel] to be undefined; update the
logic around currentLevel (and related shouldLog/LOG_LEVELS usage) to validate
that process.env.LOG_LEVEL is one of the known keys in LOG_LEVELS, and if not,
fall back to the default computed level (NODE_ENV-based) and emit a warning
(e.g., via console.warn or the module logger) that the provided LOG_LEVEL was
invalid; ensure subsequent checks in shouldLog use the validated level so
comparisons against LOG_LEVELS[currentLevel] never encounter undefined.


function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
}

export const logger = {
debug(message: string, context?: Record<string, unknown>) {
if (!shouldLog('debug')) return;
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`[DEBUG] ${message}`, context ?? '');
}
},

info(message: string, context?: Record<string, unknown>) {
if (!shouldLog('info')) return;
// eslint-disable-next-line no-console
console.info(`[INFO] ${message}`, context ?? '');
},

warn(message: string, context?: Record<string, unknown>) {
if (!shouldLog('warn')) return;
// eslint-disable-next-line no-console
console.warn(`[WARN] ${message}`, context ?? '');
},

error(message: string, error?: Error, context?: Record<string, unknown>) {
if (!shouldLog('error')) return;
// eslint-disable-next-line no-console
console.error(`[ERROR] ${message}`, error, context ?? '');
monitoringService?.captureException(error, context);
},
};
Loading
Loading