# Per-campaign health metrics tracked

- **Pattern:** `ab-001980` (`operational-resilience-email.monitoring-alerting.per-campaign-health-tracked`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001980
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001980)

## Why it matters

Aggregate email metrics hide which campaign is hurting your sender reputation. A bounce rate of 4% looks alarming in aggregate but harmless if it comes entirely from one poorly-segmented cold campaign — and fixable once you can isolate it. Without `campaign_id` on individual event records, triage requires reconstructing send state from logs and timestamps, which takes hours during an active incident. The Campaign Analytics & Attribution Audit validates attribution accuracy; this check verifies that the raw event storage layer records the campaign context in the first place.

## Severity rationale

Medium because missing campaign attribution makes deliverability triage time-consuming and imprecise, slowing incident response during bounce rate spikes.

## Remediation

Include `campaign_id` on every email event record in your webhook handler (e.g., `src/app/api/webhooks/email/route.ts`):

```ts
await db.emailEvent.create({
  data: {
    type: 'bounce',
    campaign_id: send.campaignId,
    contact_id: send.contactId,
    recorded_at: new Date()
  }
})
```

All five event types — bounce, complaint, open, click, and unsubscribe — must include campaign attribution at the individual event level. Aggregate-only storage does not satisfy this check.

## Detection

- **ID:** `per-campaign-health-tracked`
- **Severity:** `medium`
- **What to look for:** Enumerate all email event recording locations (bounce handler, complaint handler, open tracker, click tracker, unsubscribe handler). For each, check whether a `campaign_id` or equivalent foreign key is included in the event record. Count the number of event types that include campaign attribution versus total event types. Report: X of Y event types include campaign_id.
- **Pass criteria:** At least 4 of 5 email event types (bounce, complaint, open, click, unsubscribe) include a campaign identifier in the event record that allows filtering and aggregation per campaign. Do NOT pass when events are recorded in aggregate only — campaign attribution must be present at the individual event level.
- **Fail criteria:** Events are recorded in aggregate only with no campaign attribution — you cannot tell which campaign is causing a deliverability problem without manual investigation. Or fewer than 4 of 5 event types include a campaign_id.
- **Skip (N/A) when:** The system sends only transactional emails with no campaign concept — confirmed by the absence of campaign models, campaign tables, or campaign-related code.
- **Detail on fail:** `"Email events are stored without campaign attribution — bounce rates cannot be segmented by campaign"` or `"Metrics are emitted without campaign_id tag — all sends appear as one aggregate pool"`
- **Cross-reference:** The Campaign Analytics & Attribution Audit verifies that attribution data is accurate and complete — this check verifies that the event recording layer includes the campaign context needed for per-campaign health segmentation.
- **Remediation:** Include campaign ID in all event records in your webhook handler (e.g., `src/app/api/webhooks/email/route.ts`):

  ```ts
  await db.emailEvent.create({
    data: {
      type: 'bounce',
      campaign_id: send.campaignId,
      contact_id: send.contactId,
      recorded_at: new Date()
    }
  })
  ```

---

## External references

- iso-25010 reliability.operability

Taxons: observability

HTML version: https://auditbuffet.com/patterns/ab-001980
