My dashboard said zero for two months, and the number was never wrong

6 min read AutomationPowerShellMetrics

A daily digest reported zero pins published, every day, for sixty one reports in a row. The weekly review escalated it to zero activity, stalled, and that reached my decision log. Pins were publishing the whole time. The counter was reading three spreadsheets that had finished two months earlier, so zero was the only answer it could ever have given.

TL;DR · THE FIX

A metric derived from a finite queue stops being a measurement the day that queue empties, and it fails silently because zero is a believable number. My pin counter filtered three static bulk-upload CSVs for rows dated today; those batches finished posting on 15 June, so the count was pinned at 0 by construction while the script that actually publishes wrote no record anywhere. The fix is not better counting, it is moving the count to the publish path: append one line at the moment a publish is confirmed, never on a dry run, and count those lines.

The symptom

Every morning a scheduled script mails me my numbers. One of them counts the pins I published:

pins_today: 0

Some days are like that. Except it said zero the next morning too, and the morning after that, and when I finally went looking, 61 daily reports carried that key and every one of them read 0.

The weekly review, which exists to catch this kind of thing, picked it up:

pins_today STALLED with zero activity across 14 days. Fourth run in a row.

That reading went into my decision log as evidence that distribution had stalled. I was one review away from cutting a channel for producing nothing, and it was producing. Three pins had gone live inside that review’s own window, each with a proof screenshot sitting in a folder on my disk.

What I tried first

The first question was whether the review was right. A dashboard saying zero and a person insisting things are fine is a common shape, and usually the dashboard wins. So I checked the boards before the code: three pins, published on 18, 20 and 23 August, all inside the 10 to 23 August window the review had called zero activity, each with a timestamped confirmation shot.

That settled the direction. The pins were real, so the number was false, and something between publishing and counting was broken. Only then was it worth opening the counter.

What was happening

The counter opened three spreadsheets and counted rows whose publish date was today:

$batchCsvs = [ordered]@{
    "batch 1" = Join-Path $pinBase "bulk-upload\pinterest-bulk.csv"
    "batch 2" = Join-Path $pinBase "bulk-upload\pinterest-bulk-b2.csv"
    "batch 3" = Join-Path $pinBase "bulk-upload\pinterest-bulk-b3.csv"
}

foreach ($row in (Import-Csv $csvPath)) {
    $pd = [datetime]::ParseExact($row."Publish date", "yyyy-MM-dd HH:mm", $ic)
    if ($pd.Date -eq $today.Date) { $pinsToday++ }
}

That is the whole measurement. The files it reads:

filerowsnewest publish date
pinterest-bulk.csv162026-06-15 17:00
pinterest-bulk-b2.csv142026-06-14 17:00
pinterest-bulk-b3.csv122026-06-13 17:00

Those three batches were finished. They were bulk-imported once, posted themselves out over a week in June, and nothing has been appended to them since. So on any day after 15 June the number of rows dated today is zero, because no other value is reachable. By the time I was reading that flatline, the newest row in the input was 71 days old.

Nothing else rescued it because the tool that publishes pins now, a Playwright script that drives the browser, wrote no record of a publish anywhere. The digest had no other source to count, so the work was invisible by construction.

The number was not wrong

“The metric lied” points at the wrong fix. The counter answered the question it was written to answer, correctly, every morning. What changed, on a specific date and without any announcement, was that the question had stopped being related to the thing I thought I was watching.

That is nastier than a wrong value, because a wrong value eventually contradicts something. Zero is believable. It is exactly what a quiet stretch looks like, so it survives every glance, and the longer it persists the more it looks like a real trend. Mine survived two months and then got promoted into a strategic conclusion.

The fix

Move the count to the publish path. The script that performs the publish now writes the record itself, at the point the publish is confirmed, past its own success check, and never on a dry run:

// The dry-run branch returns above this line and never reaches it.
// Best-effort: a logging failure must never turn a published pin into a
// failed run, because the pin is already live and a retry would duplicate it.
try {
  appendFileSync(
    PUBLISH_LOG,
    JSON.stringify({
      at: new Date().toISOString(),
      account: m.account,
      board: m.board,
      title: m.title,
      link: m.link ?? null,
      proof: basename(shot),
    }) + '\n',
    'utf8',
  );
} catch { /* the pin is live; the log is not worth failing the run over */ }

One JSONL line per pin that went live. The digest counts those lines instead of spreadsheet rows:

foreach ($line in (Get-Content $pinPublishLog -Encoding UTF8)) {
    if (-not $line.Trim()) { continue }
    $rec = $null
    try { $rec = $line | ConvertFrom-Json } catch { continue }
    $at = $null
    try { $at = [datetime]::Parse($rec.at, $ic).ToLocalTime() } catch { continue }
    if ($at.Date -eq $today.Date) { $pinsToday++ }
}

Three details in there are deliberate.

Timestamps are written in UTC and compared in local time, hence the .ToLocalTime(). A pin published at 01:20 local is stored as 23:20Z on the previous day, so comparing the UTC date against today files it under yesterday and it is never counted. Measured on a real timestamp:

local 2026-08-28 01:20   stored as 2026-08-27T23:20:00Z
  naive UTC date   -> 2026-08-27   counts as today? False
  ToLocalTime()    -> 2026-08-28   counts as today? True

A blank or malformed line is skipped. An append-only log written by a browser automation script will eventually catch a partial write, and one bad line must not take the whole number down with it.

A missing log reads 0 instead of throwing, because the first run after this shipped had no file yet.

The batch CSVs are still read, but only to list what is still scheduled. They no longer feed the number.

The check that proves it runs against a synthetic log rather than a live run, so the awkward cases are all present at once:

5 lines in:
  count: pin A, 09:00 local
  skip:  blank
  skip:  malformed
  skip:  not today (last week)
  count: pin B, 23:40 local
pinsToday = 2

The part worth keeping

A count taken from a finished list is only a measurement while the list is still filling. The day the queue drains, the same code changes meaning: it stops reporting on your work and starts reporting on the queue’s emptiness, and the number looks the same either way.

“Check your metrics” is advice nobody can act on, so here is the narrower version. Measure the action rather than the backlog: a number derived from a plan, a schedule, a queue or a spreadsheet is measuring intent, and intent and completion agree right up until the moment they part, which nothing announces. Have the code that performs the action write the record, because the publish call on its success path is the only place that knows a publish happened; a wrapper, a nightly reconciliation, or a scan of some artifact the action leaves behind all know less. And ask a zero what it would take to be non-zero. If you cannot answer that in one sentence, the number is not reporting on anything. Mine would have needed someone to hand-edit a two-month-old CSV, and I would have noticed that the first time I asked.

Related fixes

Discussion

Powered by GitHub. Sign in to leave a comment.