Problem / motivation
NotificationService dispatches alerts but keeps no record of what it sent. notify(event, sensorData) (libs/EventDetection/NotificationService.m:160) evaluates rules, sends a real email or logs a dry-run line, stamps the per-key cooldown map, and increments NotificationCount — but the service tracks only aggregate counters (NotificationCount, SuppressedCount) plus cooldown timestamps. There is no per-dispatch record and no accessor to answer the basic operational question:
Which alerts went out, for which events, to whom, when, in what mode, and did they actually succeed?
Smoking gun — silent failures. On a real-send error the code does
catch ex
fprintf('[NOTIFY ERROR] Email failed: %s\n', ex.message); % NotificationService.m:218-220
end
i.e. it prints and swallows the exception. A failed alert therefore leaves no durable, queryable trace — an operator cannot later discover that a fault-alert never reached anyone. And NotificationCount counts proceeds, not successes, so it can't distinguish sent-vs-failed either. For a monitoring/alerting system this is a real observability hole.
Proposed feature
Add a bounded in-memory dispatch audit trail to NotificationService, with a public accessor:
records = svc.getHistory(); % all retained records (oldest -> newest)
records = svc.getHistory(n); % just the last n
Each record is one dispatch attempt (real or dry-run), including whether it succeeded.
Rough sketch
- Lib/class:
libs/EventDetection/NotificationService.m (single file).
- New private props:
History_ (struct array, ring buffer) and HistoryCapacity (cap, default e.g. 500).
- Append point: inside
notify(), immediately after the send/dry-run step. Wrap the existing sendEmail_ try/catch to capture Success/Error, then append a record and trim to capacity.
- Record fields:
Time (datenum), EventId, SensorName, ThresholdLabel, Severity, Subject, Recipients, Mode ('real'/'dryrun'), Success (logical), Error (message on failure, else ''), NumSnapshots.
- Accessor:
records = getHistory(obj) / getHistory(obj, n) returning the struct array (empty struct array when nothing recorded).
Public-API shape:
% properties (Access = public)
HistoryCapacity = 500
% methods (Access = public)
function records = getHistory(obj, n) % n optional -> last n
Semantics: record only actual dispatch attempts. Cooldown-suppressed / no-rule / disabled paths do not produce a record (suppression stays with the existing SuppressedCount).
Value
Constraints check
- Toolbox-free: plain struct array + ring-buffer trim. No toolboxes. ✅
- Pure MATLAB / Octave: no MATLAB-only types (no
table/timetable); Octave-safe. ✅
- Backward-compatible: default behavior byte-for-byte unchanged — history is populated but nobody must read it; counters, cooldown, send path untouched; no serialization change. ✅
- Contracts: no change to any
Tag / DashboardWidget / DataSource base contract. ✅
Effort estimate
S–M — one file: two props, an append+trim helper, one accessor, and wrapping the existing sendEmail_ call to capture success/error. Plus a test (real send -> one OK record; forced send failure -> one FAILED record with error text; dry-run -> one dryrun record; cooldown-suppressed -> no record; capacity cap trims oldest; empty history -> empty struct).
Open product micro-decisions (for the human)
HistoryCapacity default and whether it's user-configurable.
- Confirm suppressed / no-rule / disabled paths get no record (proposed: correct).
- Whether an
exportHistory(path) CSV sibling ships now or as a follow-on.
AI-proposed via /feature-scout — needs a human product decision before implementation.
Problem / motivation
NotificationServicedispatches alerts but keeps no record of what it sent.notify(event, sensorData)(libs/EventDetection/NotificationService.m:160) evaluates rules, sends a real email or logs a dry-run line, stamps the per-key cooldown map, and incrementsNotificationCount— but the service tracks only aggregate counters (NotificationCount,SuppressedCount) plus cooldown timestamps. There is no per-dispatch record and no accessor to answer the basic operational question:Smoking gun — silent failures. On a real-send error the code does
i.e. it prints and swallows the exception. A failed alert therefore leaves no durable, queryable trace — an operator cannot later discover that a fault-alert never reached anyone. And
NotificationCountcounts proceeds, not successes, so it can't distinguish sent-vs-failed either. For a monitoring/alerting system this is a real observability hole.Proposed feature
Add a bounded in-memory dispatch audit trail to
NotificationService, with a public accessor:Each record is one dispatch attempt (real or dry-run), including whether it succeeded.
Rough sketch
libs/EventDetection/NotificationService.m(single file).History_(struct array, ring buffer) andHistoryCapacity(cap, default e.g.500).notify(), immediately after the send/dry-run step. Wrap the existingsendEmail_try/catch to captureSuccess/Error, then append a record and trim to capacity.Time(datenum),EventId,SensorName,ThresholdLabel,Severity,Subject,Recipients,Mode('real'/'dryrun'),Success(logical),Error(message on failure, else''),NumSnapshots.records = getHistory(obj)/getHistory(obj, n)returning the struct array (empty struct array when nothing recorded).Public-API shape:
Semantics: record only actual dispatch attempts. Cooldown-suppressed / no-rule / disabled paths do not produce a record (suppression stays with the existing
SuppressedCount).Value
[NOTIFY ERROR]fprintfbecomes aSuccess=falserecord with the error text.Constraints check
table/timetable); Octave-safe. ✅Tag/DashboardWidget/DataSourcebase contract. ✅Effort estimate
S–M — one file: two props, an append+trim helper, one accessor, and wrapping the existing
sendEmail_call to capture success/error. Plus a test (real send -> one OK record; forced send failure -> one FAILED record with error text; dry-run -> one dryrun record; cooldown-suppressed -> no record; capacity cap trims oldest; empty history -> empty struct).Open product micro-decisions (for the human)
HistoryCapacitydefault and whether it's user-configurable.exportHistory(path)CSV sibling ships now or as a follow-on.AI-proposed via
/feature-scout— needs a human product decision before implementation.