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:
parent
5245e4b53b
commit
30583a1768
@ -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', () => {
|
||||
|
||||
@ -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'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user