A CRM or analytics platform hands you a file once a day. Each row is a total. It looks exactly like a cumulative ledger, and every dashboard instinct you have says: chart it, sum it, show the headline number. On a recent pipeline the daily export turned out to be a trailing 31-day rolling window; each file reported what every donor had given in the preceding 31 days, not since the campaign began. That one property invalidates almost every metric you would reach for first.
Three failure modes fall out of it:
- “Total raised” is not computable from any single snapshot. The newest file knows about the last 31 days and nothing before it.
- Summing consecutive snapshots double-counts, badly. A gift made on the 1st reappears in all 31 following files, so summing a month or more of daily exports recounts each gift up to 31 times; the overstatement climbs toward ~31x and keeps growing the longer the campaign runs.
- A value can drop with nobody taking anything back. When a gift ages past 31 days it rolls off the back of the window, so a donor’s amount falls even though they did nothing. Naively diffing yesterday against today then reports a “lost donor” every single day.
The obvious build, treat the newest file as truth, sum it, chart it over time, produces a headline inflated toward ~31x (and still rising) and a trend line that slopes down for reasons no stakeholder can explain.
Step one is to name what the feed can and cannot support. A 31-day window genuinely tells you: the current 31-day total, the peak 31-day total and when it hit, the average and lowest 31-day totals, and, crucially, participation, because “how many distinct donors are in the current window” is a property of one snapshot. Day-over-day deltas are fair game too, as long as you interpret them against roll-off. What it cannot tell you is any cumulative-since-inception quantity. Full stop.
Step two is to make the refusal unmissable; in the type system, not a wiki page. The metric interface keeps the tempting field but deprecates it in place, and every surviving field carries the window in its name so no future reader can mistake what they hold:
interface WindowMetrics {
totalRaised: number; // DEPRECATED; misleading for rolling-window data
peakAmount: number; // Peak 31-day rolling total
currentAmount: number; // Current 31-day rolling total
average31DayTotal: number;
lowest31DayTotal: number;
}
totalRaised is still computed, as the sum of positive day-over-day deltas only, the least-wrong approximation available, but it is demoted, named, and flagged. There is even a display helper whose entire job is to refuse to add two peaks together, because peaks are snapshots and snapshots do not sum.
Step three: if you need events, reconstruct them by diffing, and model roll-off as its own case. Diffing consecutive snapshots recovers a daily event stream the source never emits. The subtlety is the decreased bucket: an amount that fell but stayed positive is old money aging out of the window, not a donor pulling back. A drop to exactly zero is a real stop; a drop that stays above zero is not. Collapse those two branches and you report a bereavement every day.
The general lesson has nothing to do with donations. Before you aggregate any feed, ask what each row actually measures over what interval. If it is a window, half your default metrics are lies, and the honest move is to delete them from the type, then publish only the ones the data can defend.