Grain. The counting-bugs cheatsheet · print me

Free reference · yours to keep

The dozen counting bugs that quietly break content reporting

Symptom, cause, and the one-line fix for each. These are the defaults you get from wiring GA4 to a dashboard without a model in between — quiet, because the numbers still look plausible.

  1. Sessions inflate at the join

    Totals rise by a fixed multiple; the trend still looks right, so it survives every review.

    Fix: join traffic to pages at the grain it was measured at. Aggregate before the join, not after.

  2. One page becomes thousands of rows

    Query strings, tracking parameters and casing split a single page into many URLs; the dataset balloons.

    Fix: normalise the URL at ingest — strip query strings, lowercase, drop trailing slashes.

  3. Two people, two answers

    Someone exports to Excel and fixes numbers by hand; now last quarter has two versions.

    Fix: one source measure, published once. Everything downstream reads it; nobody re-derives.

  4. Totals need a spreadsheet

    You can't state last month's sessions without exporting first.

    Fix: the aggregation belongs in DAX, not in the export. Move it into a measure.

  5. Redirects snap the trend line

    History breaks every time a page moves, because the key is the URL.

    Fix: key on a stable page id and map redirects into it, so the series survives the move.

  6. Edited vs declining, indistinguishable

    Traffic drops after a rewrite and nobody can say if it's the content or the demand.

    Fix: join a content-change log (crawl history works) to the traffic table.

  7. The zero-traffic blind spot

    You can't say how many published pages got zero visits last quarter.

    Fix: full page inventory from a crawl, left-joined to traffic. The nulls are the answer.

  8. Broken totals on a date change

    A non-analyst changes the range and a total breaks.

    Fix: the measure depends on filter context it shouldn't. Rewrite it with the right context.

  9. Refresh is a row-count problem

    You optimise queries when the dataset is simply too big.

    Fix: normalise keys first, index second. Row count usually beats query complexity as the cause.

  10. Search and traffic in two files

    Impressions live apart from page traffic, so you never see the content gap.

    Fix: join on a cleaned page key. Impressions without traffic is your gap list.

  11. Date logic on the server clock

    Evening viewers see tomorrow's date because TODAY() runs on UTC.

    Fix: pin the boundary to your reporting timezone in one hidden measure; derive captions from it.

  12. The monthly manual step

    A report with a hand-edit every month has an expiry date the day its author leaves.

    Fix: written documentation and no manual steps. If there's a monthly hand-edit, it will break.

Three PBIP gotchas, while you're here

  1. Blank visuals from a misplaced block

    visualContainerObjects at the root of visual.json renders nothing, silently.

    Fix: it must sit inside the visual object — for every visual type, including textboxes.

  2. Schema rejects your relationships file

    Property 'description' is unknown on load.

    Fix: never use /// comments before a relationship — they create a description property relationships don't allow. Use //.

  3. Invalid indentation in TMDL

    TMDL won't parse; the error points at a comment line.

    Fix: tabs only, never spaces — including inside multi-line DAX and before /// comments.

This is the checklist. The audit is the proof.

Knowing the bugs is half of it; finding which ones are live in your numbers, and by how much, is the other half. The $400 Count Audit does exactly that on your data and hands you a written finding — and it credits against a pilot.

See the audit & pilot