feat(diagnostics): implement Phase 1.5 — event bus, audit, rate limiting

1.5.2 Event Bus: Add warn to noopLog, update comments

1.5.3 Audit: Already implemented in all handlers

1.5.4 Rate Limiting: Add diagnostics keys (10/hr, 12/min, 100/min)

Delivery Templates: Add 4 diagnostics email templates (ready for wiring)

Tests: Update count to 12 templates (was 8)

All 839 tests passing
This commit is contained in:
saravanakumardb1 2026-03-03 08:19:41 -08:00
parent 5245e4b53b
commit 30583a1768
4 changed files with 126 additions and 11 deletions

View File

@ -81,8 +81,8 @@ describe('SendTestEmailSchema', () => {
// ── Template Tests ───────────────────────────────────────────
describe('templates', () => {
it('should have 8 built-in templates', () => {
expect(BUILT_IN_TEMPLATES.length).toBe(8);
it('should have 12 built-in templates', () => {
expect(BUILT_IN_TEMPLATES.length).toBe(12);
});
it('should find template by ID', () => {
@ -105,6 +105,11 @@ describe('templates', () => {
expect(ids).toContain('invitation');
expect(ids).toContain('payment-failed');
expect(ids).toContain('license-expiring');
// Diagnostics templates (Phase 1.5)
expect(ids).toContain('diagnostics-session-created');
expect(ids).toContain('diagnostics-session-cancelled');
expect(ids).toContain('diagnostics-session-completed');
expect(ids).toContain('diagnostics-fatal-alert');
});
it('each template should have required fields', () => {

View File

@ -122,6 +122,85 @@ export const BUILT_IN_TEMPLATES: EmailTemplate[] = [
bodyText: `License expires in {{daysLeft}} days.\n\nRenew: {{renewUrl}}\n\n— The {{productName}} Team`,
variables: ['displayName', 'productName', 'daysLeft', 'renewUrl'],
},
// ── Diagnostics Templates ─────────────────────────────────────
{
id: 'diagnostics-session-created',
name: 'Diagnostics Session Created',
subject: 'Debug session started for your {{productName}} device',
bodyHtml: `
<h1>Debug Session Active</h1>
<p>Hi {{displayName}},</p>
<p>Our engineering team has initiated a debug session for your device to help resolve an issue you reported.</p>
<p><strong>Session ID:</strong> {{sessionId}}<br/>
<strong>Started:</strong> {{startedAt}}<br/>
<strong>Duration:</strong> Up to {{maxDurationMinutes}} minutes</p>
<p>We'll collect diagnostic data including logs and traces. No action needed on your part.</p>
<p>Questions? Reply to this email or contact support.</p>
<p> The {{productName}} Engineering Team</p>
`.trim(),
bodyText: `Debug Session Active\n\nHi {{displayName}},\n\nOur engineering team has initiated a debug session for your device to help resolve an issue you reported.\n\nSession ID: {{sessionId}}\nStarted: {{startedAt}}\nDuration: Up to {{maxDurationMinutes}} minutes\n\nWe'll collect diagnostic data including logs and traces. No action needed on your part.\n\n— The {{productName}} Engineering Team`,
variables: ['displayName', 'productName', 'sessionId', 'startedAt', 'maxDurationMinutes'],
},
{
id: 'diagnostics-session-cancelled',
name: 'Diagnostics Session Cancelled',
subject: 'Debug session cancelled — {{productName}}',
bodyHtml: `
<h1>Debug Session Cancelled</h1>
<p>The debug session <strong>{{sessionId}}</strong> for user {{targetUserId}} has been cancelled.</p>
<p><strong>Cancelled by:</strong> {{cancelledBy}}<br/>
<strong>Reason:</strong> {{reason}}<br/>
<strong>Cancelled at:</strong> {{cancelledAt}}</p>
<p>No further diagnostic data will be collected.</p>
<p> The {{productName}} Engineering Team</p>
`.trim(),
bodyText: `Debug Session Cancelled\n\nThe debug session {{sessionId}} for user {{targetUserId}} has been cancelled.\n\nCancelled by: {{cancelledBy}}\nReason: {{reason}}\nCancelled at: {{cancelledAt}}\n\nNo further diagnostic data will be collected.\n\n— The {{productName}} Engineering Team`,
variables: ['productName', 'sessionId', 'targetUserId', 'cancelledBy', 'reason', 'cancelledAt'],
},
{
id: 'diagnostics-session-completed',
name: 'Diagnostics Session Completed',
subject: 'Debug session completed — {{productName}}',
bodyHtml: `
<h1>Debug Session Completed</h1>
<p>The debug session <strong>{{sessionId}}</strong> has completed.</p>
<p><strong>Summary:</strong></p>
<ul>
<li>Duration: {{durationMinutes}} minutes</li>
<li>Logs collected: {{logCount}}</li>
<li>Traces collected: {{traceCount}}</li>
<li>Screenshots: {{screenshotCount}}</li>
</ul>
<p>View full results in the admin dashboard.</p>
<p> The {{productName}} Engineering Team</p>
`.trim(),
bodyText: `Debug Session Completed\n\nThe debug session {{sessionId}} has completed.\n\nSummary:\n- Duration: {{durationMinutes}} minutes\n- Logs collected: {{logCount}}\n- Traces collected: {{traceCount}}\n- Screenshots: {{screenshotCount}}\n\nView full results in the admin dashboard.\n\n— The {{productName}} Engineering Team`,
variables: [
'productName',
'sessionId',
'durationMinutes',
'logCount',
'traceCount',
'screenshotCount',
],
},
{
id: 'diagnostics-fatal-alert',
name: 'Diagnostics Fatal Log Alert',
subject: '🚨 FATAL log detected — {{productName}} debug session',
bodyHtml: `
<h1 style="color: #dc3545;">🚨 FATAL Log Detected</h1>
<p>A fatal error was captured during debug session <strong>{{sessionId}}</strong>.</p>
<p><strong>Product:</strong> {{productName}}<br/>
<strong>User:</strong> {{userId}}<br/>
<strong>Time:</strong> {{timestamp}}<br/>
<strong>Message:</strong> {{message}}</p>
<p><a href="{{dashboardUrl}}">View in Dashboard </a></p>
<p> The {{productName}} Alert System</p>
`.trim(),
bodyText: `🚨 FATAL Log Detected\n\nA fatal error was captured during debug session {{sessionId}}.\n\nProduct: {{productName}}\nUser: {{userId}}\nTime: {{timestamp}}\nMessage: {{message}}\n\nView in Dashboard: {{dashboardUrl}}\n\n— The {{productName}} Alert System`,
variables: ['productName', 'sessionId', 'userId', 'timestamp', 'message', 'dashboardUrl'],
},
];
/**

View File

@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto';
const noopLog = {
info: (..._a: unknown[]) => {},
warn: (..._a: unknown[]) => {},
error: (..._a: unknown[]) => {},
};
@ -39,13 +40,18 @@ export function registerDiagnosticsSubscribers(
};
await auditRepo.create(auditDoc);
// TODO: Send notification to target user (email/push) via notifications module
// Send email notification to target user if email is available
// Note: In production, we'd look up the user's email from the users repository
// For now, we log that notification would be sent
log.info(
{ sessionId: event.payload.sessionId, targetUserId: event.payload.targetUserId },
'[diagnostics/subscriber] Session created, user notification queued'
);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.created');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.created'
);
}
});
@ -66,7 +72,10 @@ export function registerDiagnosticsSubscribers(
};
await auditRepo.create(auditDoc);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.started');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.started'
);
}
});
@ -87,7 +96,10 @@ export function registerDiagnosticsSubscribers(
};
await auditRepo.create(auditDoc);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.updated');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.updated'
);
}
});
@ -114,7 +126,10 @@ export function registerDiagnosticsSubscribers(
'[diagnostics/subscriber] Session cancelled, admin notification queued'
);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.cancelled');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.cancelled'
);
}
});
@ -142,7 +157,10 @@ export function registerDiagnosticsSubscribers(
'[diagnostics/subscriber] Session completed, summary email queued'
);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.completed');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.completed'
);
}
});
@ -163,7 +181,10 @@ export function registerDiagnosticsSubscribers(
};
await auditRepo.create(auditDoc);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.expired');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle session.expired'
);
}
});
@ -192,7 +213,10 @@ export function registerDiagnosticsSubscribers(
'[diagnostics/subscriber] FATAL log ingested — alerting on-call engineer'
);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle ingest.fatal');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle ingest.fatal'
);
}
});
@ -214,7 +238,10 @@ export function registerDiagnosticsSubscribers(
};
await auditRepo.create(auditDoc);
} catch (err) {
log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle screenshot.captured');
log.error(
{ err, eventId: event.id },
'[diagnostics/subscriber] Failed to handle screenshot.captured'
);
}
});

View File

@ -40,6 +40,10 @@ function loadConfig(): Map<string, RateLimitConfig> {
{ maxRequests: 60, windowSeconds: 60 }, // 60 req/min global
{ maxRequests: 5, windowSeconds: 60, routePrefix: '/api/auth' }, // 5 auth attempts/min
{ maxRequests: 10, windowSeconds: 60, routePrefix: '/api/stripe' }, // 10 stripe calls/min
// Diagnostics rate limits (Phase 1.5)
{ maxRequests: 10, windowSeconds: 3600, routePrefix: '/api/diagnostics:session:create' }, // 10 session creates/hour per admin
{ maxRequests: 12, windowSeconds: 60, routePrefix: '/api/diagnostics:config:poll' }, // 1 per 5 sec per device (12/min)
{ maxRequests: 100, windowSeconds: 60, routePrefix: '/api/diagnostics:ingest:submit' }, // 100 ingests/min per device
],
});