Skip to content
Merged
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
8 changes: 8 additions & 0 deletions apps/api/src/SDK-Debug/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './sdk-debug.constants';
export * from './sdk-debug.types';
export * from './sdk-debug.utils';
export * from './sdk-debug.service';
export * from './sdk-debug.interceptor';
export * from './sdk-debug.middleware';
export * from './sdk-debug.decorator';
export * from './sdk-debug.module';
69 changes: 69 additions & 0 deletions apps/api/src/SDK-Debug/sdk-debug.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export const SDK_DEBUG_MODULE_OPTIONS = 'SDK_DEBUG_MODULE_OPTIONS';
export const SDK_DEBUG_LOGGER = 'SDK_DEBUG_LOGGER';

export const SDK_DEBUG_LOG_LEVELS = {
VERBOSE: 'verbose',
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
ERROR: 'error',
} as const;

export const SDK_DEBUG_EVENTS = {
REQUEST_START: 'sdk.request.start',
REQUEST_END: 'sdk.request.end',
REQUEST_ERROR: 'sdk.request.error',
AUTH_ATTEMPT: 'sdk.auth.attempt',
AUTH_SUCCESS: 'sdk.auth.success',
AUTH_FAILURE: 'sdk.auth.failure',
RATE_LIMIT_HIT: 'sdk.rate_limit.hit',
RETRY_ATTEMPT: 'sdk.retry.attempt',
CACHE_HIT: 'sdk.cache.hit',
CACHE_MISS: 'sdk.cache.miss',
WEBHOOK_RECEIVED: 'sdk.webhook.received',
CIRCUIT_OPEN: 'sdk.circuit.open',
CIRCUIT_CLOSED: 'sdk.circuit.closed',
} as const;

export const SDK_DEBUG_TRANSPORT = {
CONSOLE: 'console',
FILE: 'file',
HTTP: 'http',
CUSTOM: 'custom',
} as const;

export const SDK_DEBUG_FORMAT = {
JSON: 'json',
PRETTY: 'pretty',
COMPACT: 'compact',
} as const;

export const REDACTED_PLACEHOLDER = '[REDACTED]';

export const DEFAULT_SENSITIVE_KEYS = [
'password',
'secret',
'token',
'apiKey',
'api_key',
'authorization',
'Authorization',
'x-api-key',
'privateKey',
'private_key',
'accessToken',
'access_token',
'refreshToken',
'refresh_token',
'clientSecret',
'client_secret',
'passphrase',
'mnemonic',
'seed',
'privateKeyBase58',
'secretKeyBase64',
];

export const SDK_DEBUG_METADATA_KEY = 'sdk:debug';
export const SDK_DEBUG_TRACE_ID_HEADER = 'x-sdk-trace-id';
export const SDK_DEBUG_REQUEST_ID_HEADER = 'x-sdk-request-id';
30 changes: 30 additions & 0 deletions apps/api/src/SDK-Debug/sdk-debug.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { SetMetadata, applyDecorators } from '@nestjs/common';
import { SDK_DEBUG_METADATA_KEY } from './sdk-debug.constants';

export interface SdkDebugDecoratorOptions {
event?: string;
label?: string;
meta?: Record<string, unknown>;
/** Suppress this particular call from debug output */
suppress?: boolean;
}

/**
* @SdkDebug() — attach debug metadata to a controller or route handler.
* The interceptor reads this metadata and enriches log entries accordingly.
*
* @example
* \@SdkDebug({ event: 'sdk.payment.create', label: 'CreatePayment' })
* \@Post('payments')
* createPayment() { ... }
*/
export function SdkDebug(options: SdkDebugDecoratorOptions = {}) {
return applyDecorators(SetMetadata(SDK_DEBUG_METADATA_KEY, options));
}

/**
* @SdkDebugSuppress() — completely suppress debug output for this route.
*/
export function SdkDebugSuppress() {
return SdkDebug({ suppress: true });
}
150 changes: 150 additions & 0 deletions apps/api/src/SDK-Debug/sdk-debug.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SdkDebugModule } from '../src/sdk-debug.module';
import { SdkDebugService } from '../src/sdk-debug.service';
import { SdkDebugInterceptor } from '../src/sdk-debug.interceptor';
import { SdkDebugMiddleware } from '../src/sdk-debug.middleware';
import { SdkDebugLogEntry, SdkDebugModuleOptions } from '../src/sdk-debug.types';
import { SDK_DEBUG_TRANSPORT } from '../src/sdk-debug.constants';

// suppress console noise
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);

describe('SdkDebugModule — integration', () => {
let module: TestingModule;
let service: SdkDebugService;
const captured: SdkDebugLogEntry[] = [];

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
SdkDebugModule.forRoot({
enabled: true,
level: 'verbose',
namespace: 'BridgeWise-Test',
colorize: false,
includeSystemInfo: true,
includeMemoryUsage: true,
includeStackTrace: true,
globalMeta: { env: 'test' },
sensitiveKeys: ['password', 'apiKey'],
transports: [
{
type: SDK_DEBUG_TRANSPORT.CUSTOM,
handler: (e) => captured.push(e),
},
],
}),
],
}).compile();

service = module.get(SdkDebugService);
});

afterAll(() => module.close());
afterEach(() => captured.splice(0));

it('provides SdkDebugService', () => {
expect(service).toBeInstanceOf(SdkDebugService);
});

it('provides SdkDebugInterceptor', () => {
expect(module.get(SdkDebugInterceptor)).toBeInstanceOf(SdkDebugInterceptor);
});

it('provides SdkDebugMiddleware', () => {
expect(module.get(SdkDebugMiddleware)).toBeInstanceOf(SdkDebugMiddleware);
});

it('writes logs to custom transport', () => {
service.info('test.integration', 'hello from integration test');
expect(captured).toHaveLength(1);
expect(captured[0].namespace).toBe('BridgeWise-Test');
expect(captured[0].event).toBe('test.integration');
});

it('redacts sensitive keys in integration context', () => {
service.debug('test.auth', 'login attempt', {
username: 'alice',
password: 'hunter2',
});
expect(captured[0].meta?.password).toBe('[REDACTED]');
expect(captured[0].meta?.username).toBe('alice');
});

it('attaches globalMeta to every entry', () => {
service.debug('x', 'msg');
expect(captured[0].meta?.env).toBe('test');
});

it('attaches systemInfo when configured', () => {
service.debug('x', 'msg');
expect(captured[0].system).toBeDefined();
expect(captured[0].system?.pid).toBe(process.pid);
});

it('attaches memoryInfo when configured', () => {
service.debug('x', 'msg');
expect(captured[0].memory).toBeDefined();
expect(captured[0].memory?.heapUsedMB).toBeGreaterThanOrEqual(0);
});

it('trace() captures success result', async () => {
const result = await service.trace('test.trace', 'AddNumbers', async () => 1 + 1);
expect(result).toBe(2);
// 2 entries: start + end
expect(captured).toHaveLength(2);
const endEntry = captured[1];
expect(endEntry.meta?.success).toBe(true);
});

it('trace() captures error and rethrows', async () => {
await expect(
service.trace('test.trace.fail', 'BadOp', async () => {
throw new Error('intentional');
}),
).rejects.toThrow('intentional');
// 3 entries: start + end + error
expect(captured.length).toBeGreaterThanOrEqual(2);
expect(captured.some((e) => e.error !== undefined)).toBe(true);
});

it('getStats() aggregates across multiple calls', () => {
service.info('a', '1');
service.info('b', '2');
service.error('c', '3');
const stats = service.getStats();
expect(stats.totalLogs).toBeGreaterThanOrEqual(3);
expect(stats.logsByLevel.info).toBeGreaterThanOrEqual(2);
expect(stats.logsByLevel.error).toBeGreaterThanOrEqual(1);
});
});

// ---------------------------------------------------------------------------
// forRootAsync test
// ---------------------------------------------------------------------------
describe('SdkDebugModule.forRootAsync', () => {
it('registers via useFactory', async () => {
const mod = await Test.createTestingModule({
imports: [
SdkDebugModule.forRootAsync({
useFactory: (): SdkDebugModuleOptions => ({
enabled: true,
level: 'info',
namespace: 'async-test',
}),
}),
],
}).compile();

const svc = mod.get(SdkDebugService);
expect(svc).toBeInstanceOf(SdkDebugService);
expect(svc.isEnabled).toBe(true);
await mod.close();
});

it('throws if no factory/class/existing provided', () => {
expect(() =>
SdkDebugModule.forRootAsync({} as never),
).toThrow();
});
});
Loading
Loading