Status: ✅ BUG ALREADY FIXED (but external integrations may still be affected)
This codebase has already implemented proper deduplication logic for retry metrics. However, the issue description suggests external monitoring systems (Prometheus, Datadog, CloudWatch, or custom dashboards) may be consuming raw execution logs and double-counting successful retries.
- Language: Node.js with TypeScript
- Database: SQLite3 with custom repository pattern
- Job Queue: Custom polling-based scheduler (no BullMQ/Celery)
- Testing: Jest
- Logging: Winston
- Metrics API: Custom REST endpoints
Scenario: A notification fails twice, then succeeds on the 3rd attempt.
notification_execution_log table:
| id | scheduled_notification_id | execution_attempt | status |
|----|---------------------------|-------------------|---------|
| 1 | 100 | 1 | RETRY |
| 2 | 100 | 2 | RETRY |
| 3 | 100 | 3 | SUCCESS |
External monitoring counting all rows with status='SUCCESS' → 1 success
But if counting all log entries for successful notifications → 3 events ❌
GET /api/schedule/execution-metrics returns:
{
"totalNotifications": 1,
"successfulFirstAttempt": 0,
"successfulAfterRetry": 1, ← Counted exactly once
"totalRetryAttempts": 2
}
File: listener/src/services/notification-scheduler.ts (lines 125-213)
The Issue: The processNotification() method calls logExecution() on every retry attempt:
private async processNotification(notification: ScheduledNotification): Promise<void> {
const executionAttempt = notification.retryCount + 1;
try {
const success = await this.executeNotification(notification);
if (success) {
// ✅ Marks notification as completed (status update)
await this.repository.markAsCompleted(notification.id!);
// ⚠️ Logs this attempt in execution_log table
await this.repository.logExecution({
scheduledNotificationId: notification.id!,
executionAttempt,
status: 'SUCCESS',
durationMs: duration,
});
}
} catch (error) {
// ⚠️ Also logs retry/failure attempts
await this.repository.logExecution({
scheduledNotificationId: notification.id!,
executionAttempt,
status: notification.retryCount >= notification.maxRetries ? 'FAILED' : 'RETRY',
errorMessage: (error as Error).message,
});
}
}Result: Multiple log entries per notification, creating the potential for double-counting if external systems query the notification_execution_log table directly.
File: listener/src/services/scheduled-notification-repository.ts (lines 297-370)
The getExecutionMetrics() method implements SQL-based deduplication using a CTE (Common Table Expression):
WITH final_outcomes AS (
SELECT
sn.id,
sn.status,
sn.retry_count,
log.status as final_execution_status,
log.duration_ms
FROM scheduled_notifications sn
LEFT JOIN notification_execution_log log
ON log.scheduled_notification_id = sn.id
AND log.execution_attempt = (
SELECT MAX(execution_attempt) ← KEY: Only gets final attempt
FROM notification_execution_log
WHERE scheduled_notification_id = sn.id
)
WHERE sn.status IN ('COMPLETED', 'FAILED')
)
SELECT
COUNT(*) as total_notifications,
SUM(CASE WHEN final_execution_status = 'SUCCESS' AND retry_count = 0 THEN 1 ELSE 0 END) as success_first_attempt,
SUM(CASE WHEN final_execution_status = 'SUCCESS' AND retry_count > 0 THEN 1 ELSE 0 END) as success_after_retry,
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) as permanent_failures,
SUM(retry_count) as total_retry_attempts
FROM final_outcomesHow it works:
- For each notification, selects only the final execution attempt using
MAX(execution_attempt) - Groups by notification ID (implicit through the subquery)
- Counts each notification exactly once regardless of retry attempts
- Separates first-attempt successes from retry successes
- Accurately counts total retry attempts without inflating success count
File: listener/src/services/execution-metrics.test.ts (lines 55-102)
The test suite includes a critical regression test:
it('should count a notification with 2 failures + 1 success as exactly 1 successful notification', async () => {
const notificationId = await repository.create({...});
// Simulate first attempt: RETRY (failure)
await repository.logExecution({
scheduledNotificationId: notificationId,
executionAttempt: 1,
status: 'RETRY',
errorMessage: 'Network timeout',
});
// Simulate second attempt: RETRY (failure)
await repository.logExecution({
scheduledNotificationId: notificationId,
executionAttempt: 2,
status: 'RETRY',
errorMessage: 'Service unavailable',
});
// Simulate third attempt: SUCCESS
await repository.logExecution({
scheduledNotificationId: notificationId,
executionAttempt: 3,
status: 'SUCCESS',
});
const metrics = await repository.getExecutionMetrics();
// CRITICAL ASSERTIONS
expect(metrics.totalNotifications).toBe(1);
expect(metrics.successfulFirstAttempt).toBe(0);
expect(metrics.successfulAfterRetry).toBe(1); // ← EXACTLY 1 SUCCESS
expect(metrics.totalRetryAttempts).toBe(2); // ← 2 RETRIES COUNTED CORRECTLY
});Test Coverage:
- ✅ Single retry sequence (2 failures + 1 success)
- ✅ Multiple notifications with different retry patterns
- ✅ Retry distribution breakdown
- ✅ Average duration calculations
- ✅ Empty database edge case
- ✅ Filtering PENDING notifications (don't count incomplete jobs)
File: listener/src/api/events-server.ts (lines 253-268)
The API exposes the deduplicated metrics endpoint:
// Get execution metrics with deduplication (prevents double-counting)
if (req.method === 'GET' && req.url === '/api/schedule/execution-metrics') {
options.notificationAPI.getExecutionMetrics()
.then((metrics) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(metrics));
})
}Available Endpoints:
/api/schedule/stats- Notification counts by status (PENDING, PROCESSING, COMPLETED, FAILED)/api/schedule/execution-metrics- Deduplicated metrics (use this!)/api/schedule/retry-distribution- Retry pattern analysis
File: dashboard/src/services/eventsApi.ts
The dashboard currently only fetches event data, not execution metrics:
// Only has event-related endpoints, no metrics consumption
export const eventsApi = {
fetchEvents: async (params) => { /* ... */ },
// ❌ No getExecutionMetrics() call
};Impact: If the dashboard or external monitoring queries notification_execution_log directly, it will double-count retries.
- Status: ACHIEVED
- Evidence: SQL query uses
MAX(execution_attempt)to get only final outcome - Test:
execution-metrics.test.tsline 91 asserts exactly 1 success for 3 attempts
- Status: API READY (but dashboard needs integration)
- Evidence:
/api/schedule/execution-metricsendpoint exists - Action Needed: Update dashboard to consume this endpoint
- Status: COMPREHENSIVE
- Evidence: 6 test cases covering all retry scenarios
- Coverage: Single retries, multiple notifications, distributions, edge cases
If using Prometheus/Datadog/CloudWatch:
❌ DO NOT query notification_execution_log directly:
-- WRONG: This will count retries multiple times
SELECT COUNT(*) FROM notification_execution_log WHERE status = 'SUCCESS'✅ DO use the API endpoint:
curl http://localhost:3000/api/schedule/execution-metrics✅ OR replicate the deduplication query:
-- Use this pattern in your monitoring queries
WITH final_outcomes AS (
SELECT
sn.id,
log.status as final_status
FROM scheduled_notifications sn
LEFT JOIN notification_execution_log log
ON log.scheduled_notification_id = sn.id
AND log.execution_attempt = (
SELECT MAX(execution_attempt)
FROM notification_execution_log
WHERE scheduled_notification_id = sn.id
)
WHERE sn.status IN ('COMPLETED', 'FAILED')
)
SELECT COUNT(*) FROM final_outcomes WHERE final_status = 'SUCCESS'File: listener/src/database/schema.sql (lines 85-95)
The template_usage_log table may have similar issues:
CREATE TABLE IF NOT EXISTS template_usage_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_id INTEGER NOT NULL,
context_hash VARCHAR(64) NOT NULL,
notification_type VARCHAR(50) NOT NULL,
event_id TEXT,
contract_address TEXT,
used_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE,
UNIQUE(template_id, context_hash) ← Uses deduplication via UNIQUE constraint
);Good: Uses UNIQUE(template_id, context_hash) to prevent duplicate logging.
If using log aggregation tools (ELK, Splunk, Loki) that parse Winston logs:
❌ Avoid counting these log messages multiple times:
logger.info('Notification delivered successfully', {
id: notification.id,
type: notification.notificationType,
duration,
});This log appears on every successful attempt, including retries.
✅ Use structured query filters:
# Logstash/Elasticsearch query
# Count notifications by final status change, not log messages
message:"Notification marked as completed"
-
Audit External Integrations ✋
- Identify all systems consuming notification metrics
- Verify they use
/api/schedule/execution-metricsendpoint - Update any direct database queries to use deduplication pattern
-
Update Dashboard 🎯
// File: dashboard/src/services/eventsApi.ts // Add this method: export const getExecutionMetrics = async () => { const response = await fetch('/api/schedule/execution-metrics'); return await response.json(); };
-
Add Monitoring Endpoint Documentation 📝 Create
docs/MONITORING.mdwith:- Correct API endpoints to use
- Example Prometheus/Datadog queries
- Warning about direct database queries
-
Add API Response Examples 📊
// Example response from /api/schedule/execution-metrics { "totalNotifications": 1000, "successfulFirstAttempt": 850, "successfulAfterRetry": 120, "permanentFailures": 30, "totalRetryAttempts": 180, "averageRetriesPerNotification": 0.18, "averageSuccessDurationMs": 750, "averageFailureDurationMs": 2000 }
-
Add Prometheus Exporter 📈
// File: listener/src/services/prometheus-exporter.ts import promClient from 'prom-client'; const notificationSuccessCounter = new promClient.Gauge({ name: 'notifications_successful_total', help: 'Total successful notifications (deduplicated)', async collect() { const metrics = await repository.getExecutionMetrics(); this.set(metrics.successfulFirstAttempt + metrics.successfulAfterRetry); } });
-
Add Alerting 🚨
// Alert if retry rate exceeds threshold if (metrics.totalRetryAttempts / metrics.totalNotifications > 0.5) { logger.error('High retry rate detected', { metrics }); // Send alert to PagerDuty/Slack }
-
Add Idempotency Keys 🔑
// Ensure external webhooks are idempotent const idempotencyKey = `${notification.id}-${executionAttempt}`; headers['Idempotency-Key'] = idempotencyKey;
| Acceptance Criteria | Status | Evidence |
|---|---|---|
| Zero Duplicate Counting | ✅ PASS | SQL deduplication implemented |
| Dashboard Data Integrity | Endpoint exists, dashboard needs integration | |
| Regression Test Suite | ✅ PASS | Comprehensive tests in place |
For Internal Systems: ✅ Already fixed - use /api/schedule/execution-metrics
For External Systems:
For Dashboard: 🔧 Integration needed - connect to metrics API
Run the regression tests to verify the fix:
cd listener
npm test -- execution-metrics.test.tsExpected Output:
PASS src/services/execution-metrics.test.ts
Execution Metrics Deduplication
✓ should count a notification with 2 failures + 1 success as exactly 1 successful notification
✓ should correctly count multiple notifications with different retry patterns
✓ should return retry distribution breakdown
✓ should calculate accurate average durations
✓ should handle empty database gracefully
✓ should only count COMPLETED and FAILED notifications, not PENDING
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Document Version: 1.0
Date: June 20, 2026
Author: Senior Backend Engineer / SRE Analysis