Contact age tracked per record
Why it matters
A contact record with no engagement timestamp is indistinguishable from one acquired yesterday versus three years ago. Without per-record age tracking, list hygiene jobs cannot identify stale contacts for re-verification, re-engagement campaigns cannot target the right cohort, and sender reputation metrics cannot explain why open rates are declining. A generic updated_at column conflates admin edits (which don't signal engagement) with recipient activity, making it useless for deliverability decisions. Many ESPs now enforce inactivity-based suppression at their end — without local engagement timestamps, your system cannot anticipate or mirror those suppressions.
Severity rationale
High because contacts without engagement timestamps cannot be triaged for stale-list cleanup, causing disengaged addresses to accumulate on the active send list and silently drag down open rates and sender reputation.
Remediation
Add a dedicated engagement timestamp column separate from the system updated_at:
ALTER TABLE contacts
ADD COLUMN last_engaged_at TIMESTAMPTZ,
ADD COLUMN engagement_score INTEGER NOT NULL DEFAULT 0;
Write to last_engaged_at only from ESP webhook events (opens, clicks), not from admin UI actions:
// In your ESP webhook handler:
if (['open', 'click'].includes(event.event_type)) {
await db.contact.updateMany({
where: { email: event.email.toLowerCase().trim() },
data: { last_engaged_at: new Date(event.timestamp) }
})
}
Keep updated_at for record-level change tracking and last_engaged_at for deliverability decisions — the two serve different purposes and must not be merged into one column.
Detection
-
ID:
contact-age-tracked -
Severity:
high -
What to look for: Examine the contact table schema. Count all timestamp columns on the contact record — list all by name. Verify that at minimum
created_atandupdated_attimestamps are present per record. Preferably, check for alast_engaged_atorlast_activity_atcolumn that is updated whenever the contact opens, clicks, or otherwise interacts — distinct from system-levelupdated_at(which changes on any field update, including admin edits). A genericupdated_atthat conflates admin edits with recipient engagement does not count as pass. -
Pass criteria: Contacts have at least 2 timestamp columns: a
created_attimestamp and a separate engagement-based timestamp (last_engaged_ator equivalent) that updates only on recipient activity, not on admin edits. Quote the actual column names found. -
Fail criteria: Contacts have no timestamp, only a generic
updated_atthat conflates admin edits with recipient engagement, or contacts havecreated_atbut no engagement-based timestamp. -
Skip (N/A) when: The system sends only transactional email to authenticated users; engagement tracking is managed externally by the ESP.
-
Cross-reference: Check
data-quality-list-hygiene.data-decay.last-engagement-maintained— the timestamp column is useless if nothing writes to it from ESP webhook events. -
Detail on fail: Example:
"Contacts table has updated_at only — no distinction between admin edits and recipient engagement"or"No timestamp columns on contact records" -
Remediation: Add engagement-specific timestamp columns:
ALTER TABLE contacts ADD COLUMN last_engaged_at TIMESTAMPTZ, ADD COLUMN engagement_score INTEGER NOT NULL DEFAULT 0; -- Update engagement timestamp on open/click events: UPDATE contacts SET last_engaged_at = now(), engagement_score = engagement_score + 1 WHERE id = $1;Ensure this update is triggered by webhook events from your ESP (open, click) — not by internal admin updates to the contact record.
External references
- iso-25010:2011 · functional-correctness — Functional Correctness (functional suitability)
Taxons
History
- 2026-04-18·v1.0.0·Initial import from data-quality-list-hygiene·automated