Skip to content

NotificationService: add getHistory() dispatch audit trail (record each sent/failed/dry-run alert) #381

Description

@HanSur94

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)

  1. HistoryCapacity default and whether it's user-configurable.
  2. Confirm suppressed / no-rule / disabled paths get no record (proposed: correct).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions