← back to the analytics case study process · analytics · rebuilt on an invented company

How the measurement actually got built

nine steps · one conversation, one plan, eleven tags, one report that changed a decision

The case study says what I built. This says how, in order, with the screens — because "I built an analytics practice" is a claim, and a measurement plan with a tag behind it is evidence.

CambiumPak is not a real company. It is an invented maker of recycled and compostable B2B packaging supplies, built so I can show this process end to end. Every product, figure, document, certification mark, stakeholder and screen on this page is fabricated. No mark named here belongs to a real standards body, and no number here is any employer's number.

The real version of this work was done under NDA for a lighting manufacturer, so I cannot publish its data, its screens or its code. What is real is the sequence — the order of the steps, the reasoning at each one, and the artifacts each step produces. That is the transferable part, and it is the same shape I would build on any site.

Every reconstructed interface below is a hand-built HTML recreation of the layout of a working analytics tool, not a screenshot of one. It carries invented data.

The company has a real site. CambiumPak is published at cambiumpak.com — eleven product families, seventeen SKUs and forty-four downloadable documents, every one of them written for this. That site is the thing the measurement below was built against, so the events, the SKU codes and the four document types on this page all resolve to something you can go and click.

step 01

The conversation, which is where measurement plans actually start

before any tool is opened · the output is a list of questions, not a list of events

CambiumPak was six weeks from relaunching its site. Eleven product families, seventeen SKUs, forty-four downloadable documents, a distributor portal, and five interactive tools that had never existed before — a box configurator, a material comparison tool, a right-size calculator licensed from a third party, a recycled-content credit finder, and the portal itself. The comparison tool was billed monthly.

I did not start by opening a tag manager. I started by sitting down with Dana Whitfield, the marketing director, and asking her what she wished she knew and currently did not. Everything downstream is an answer to something she said in that hour.

Dana · marketing director

I can see sessions are up. I can see the top pages. I genuinely cannot tell you what to do with either of those.

me

Give me the question you get asked that you can't answer.

Dana

Which documents matter. We publish four sheets per product — spec, comparison, features and benefits, and the instructions. That's forty-four PDFs. Right now downloads come through as one number, so "downloads are up" is the entire insight. Up on what? For which product? Nobody can tell me.

Dana

And the portal. It looks busy. I don't know whether that's distributors, our own reps checking stock, or the fulfilment house. Those are three completely different businesses and I'm reporting them as one line.

Dana

Then the real one: where do people stop? Somebody lands on a family page and somebody else requests a sample, and I have no idea what happens in between or where we lose them.

me

And the five new tools?

Dana

Eight months of work between them, and on launch day I will have no way of knowing whether a single person used any of them. One of them we pay for every month and I'd have to renew it on a feeling.

Four frustrations. I wrote them down as questions rather than as features, because a question survives a tooling change and a feature request does not:

what she saidthe question underneath it
Downloads are one numberWhich documents, for which products, drive demand — and does that differ by document type?
The portal looks busyWho is actually logging in, and is it the audience the portal was designed for?
Where do people stop?Between landing on a product and requesting a sample, which step loses the most people?
Five new tools, no signalIs each tool used, by whom, and does the paid one earn its renewal?

the thing I would say to a hiring manager about this step

Nobody asks for an event. They describe a frustration. The skill is not tagging — the skill is hearing "the portal looks busy" and recognising it as a segmentation problem with a one-line fix, before anyone has scoped a project around it.

step 02

What the default install could and could not answer

an honest audit before proposing work · half of what people want is already there and free

Before proposing anything I checked what a standard analytics install gives you for nothing, because proposing work that the platform already does is the fastest way to lose credibility in the room. Enhanced measurement was already collecting page views, scrolls, outbound clicks, site search and file downloads. That last one is the interesting case, and it is the one that shows exactly where a default install stops being useful.

Here is the file-download report as it actually arrived, before I built anything. It is not empty. It is worse than empty — it is plausible.

Analytics Reports › Engagement › Events › file_download default install · no configuration

file_download

Last 28 days · 4,610 events · dimension: File name

File nameEvent count
CambiumPak-CP-MLR-spec-sheet.pdf412
CambiumPak-CP-CTN-spec-sheet.pdf338
CambiumPak-CP-MLR-assembly-instructions.pdf264
CambiumPak-CP-TP-sealing-instructions.pdf209
CambiumPak-CP-PPE-spec-sheet.pdf205
CambiumPak-CP-CTN-assembly-instructions.pdf190
…38 more rows2,992

Showing 6 of 44 rows.

reconstruction · the default file-download report · invented company, fabricated figures. forty-four rows of filenames. technically itemised, analytically useless.

Everything you would want to ask of that table, it cannot answer. You cannot total spec sheets against instructions, because document type only exists inside a filename. You cannot roll up to a product family, because family only exists as three letters in the middle of a string. And the whole thing breaks silently the day someone renames a file — the row simply stops appearing and no error is ever raised.

free, already working, leave it alone

  • Page views, sessions, users, channels
  • Scroll depth and outbound clicks
  • Site search terms
  • That a PDF was downloaded

needs building, and only ever by hand

  • Which document, as a type rather than a filename
  • Which product and family it belonged to
  • Who logged into the portal, by customer type
  • Whether each of the five tools was used, and whether use succeeded
  • Every step between landing and a sample request

That right-hand column is the project. It is also, precisely, Dana's four questions — which is how I knew the scope was right rather than invented.

step 03

The measurement plan, written before a single tag existed

one document · signed off by marketing and IT before build · it is an intake form, not documentation

This is the artifact I care most about and the one most analytics work skips. It is a single document with one row per event, and the leftmost column is the business question. If a row cannot name the decision it informs, it does not get built — which is how you end up with a property that answers questions instead of one that collects everything and answers nothing.

It also names the parameters up front, and that matters more than it looks: a parameter you did not think of on day one is not recoverable on day ninety. You cannot backfill it.

CambiumPak — web measurement plan v1.0 · pre-relaunch · owner: marketing approved · marketing + IT
Business questionEvent Key parametersRegistered asPri
Which documents, for which products, drive demand? document_download doc_type · product_sku · product_family · source_page 4 dimensionsP1
Who is actually logging into the portal? portal_login customer_type · account_tier · first_login 3 dimensionsP1
Does the configurator get used, and does it succeed? configurator_start
configurator_complete
box_style · material · recycled_pct (banded) · outcome 4 dimensionsP1
Is the paid comparison tool worth renewing? material_compare materials_compared · compare_count 2 dimensionsP1
The conversion everything else predicts. sample_request product_context · form_type 2 dimensionsP1
Do buyers care about recycled-content credits, or do we only think so? credit_finder_search credit_scheme · region 2 dimensionsP2
Is anyone reaching the calculator we license? rightsize_launch source_page 1 dimensionP2 · limited

P2 · limited — the right-size calculator is a third-party embed. Launches are measurable from our side; what happens inside it is not. Recorded as a known boundary rather than a gap to be closed later.

reconstruction · the measurement plan · the leftmost column is the entry requirement. an event with an empty question column does not get built.

the judgment call in this table

That last row is a decision not to build something. The calculator is a vendor component that regenerates its element IDs on every render — DOM-based tracking against it would appear to work perfectly and then break silently on the vendor's next release, at which point the report keeps showing a number and the number is wrong. Silent wrong data is worse than no data. I shipped a launch count, wrote the boundary into the plan, and said so out loud. Declining to build fragile tracking is a harder call than building it, and nobody thanks you for it until eighteen months later.

step 04

The dataLayer contract, agreed with the web vendor

the step people skip · if the push is ad hoc, every later step inherits the mess

Some of this could have been scraped off the DOM. Most of it should not be. Anything read out of markup is a promise the front end never made, and the front end will break it the first time someone reorders a template. So before the vendor wrote a line, I gave them the exact shape of what to push, and it went into the build as a requirement rather than a favour.

// Fired when a document is downloaded, from anywhere on the site.
// Values come from the product record, never from the filename.
window.dataLayer.push({
  event: 'document_download',
  doc_type: 'spec',              // spec | comparison | features | instructions
  product_sku: 'CP-MLR-1208',
  product_family: 'mailer',     // so roll-ups need no lookup table
  source_page: 'family'          // family | compare | portal | search
});

// Fired once, after authentication resolves. Never on page load.
window.dataLayer.push({
  event: 'portal_login',
  customer_type: 'distributor_rep',  // internal | distributor_rep | fulfilment | brand
  account_tier: 'tier_2',
  first_login: false                // true once, ever — activation, not traffic
});

why product_family is in there

  • Without it, every roll-up needs a join against a SKU list somebody has to maintain.
  • Nobody maintains it.
  • Within a year the reporting quietly stops matching the catalogue and no one notices which one is wrong.
  • One extra key at push time costs nothing and removes that failure mode permanently.

why recycled_pct is banded before it is sent

  • The brand system states recycled content as a band — 0–39 / 40–69 / 70–100 — never as an implied precision.
  • Sending the raw figure would let a report make a claim the certification does not support.
  • Banding at the push is the only place it can be enforced for everyone downstream.
  • Governance you have to remember is governance that fails. Put it in the contract.
step 05

Building the tags, by hand, one at a time

no auto-events, no plugins · every tag specified, built, then tested before the next one starts

Eleven tags, nine variables, seven triggers. Three are worth showing because each solves a different class of problem: mapping a messy input to a clean one, passing structured parameters through, and reading something the page already knows but never told anyone.

First, the one that fixes Dana's forty-four rows. A lookup table maps a filename pattern to a document type. This is the fallback path — the dataLayer contract from step 04 is the primary source — but legacy links, emailed PDFs and links inside the portal do not always go through it, and a fallback that produces a clean value beats a fallback that produces a filename.

Tag manager Variables › User-defined › RegEx table workspace: measurement-v1
var — doc type from filename
RegEx table
{{Click URL}}

Pattern → output

PatternOutput
-spec-sheet\.pdf$spec
-comparison-sheet\.pdf$comparison
-features-and-benefits\.pdf$features
-(assembly|use|handling|fitting|wrapping|filling|sealing)-instructions\.pdf$instructions
-document-map\.pdf$internal
unclassified

Ignore case: off · Full matches only: off

reconstruction · the lookup that collapses 44 filename rows into 4 reportable types.

Two details in that table are the whole difference between a variable that works and one that rots. The fourth row alternates seven verbs, because the fourth document in each product pack is not always called the same thing — four families assemble, the rest use, handle, fit, wrap, fill or seal, because that is what the product actually asks of the packer. A pattern matching only "assembly" would have silently dropped seven families out of the report.

And the default is unclassified, not empty. An unclassified row is visible in a report and someone investigates it. An empty value looks like nothing happened, and nobody investigates nothing.

Second, the tag itself, passing the parameters through as named fields rather than jamming them into the event name:

Tag manager Tags › GA4 event › document_download tested in preview
GA4 — document_download
{{const — GA4 measurement ID}}
document_download

Event parameters

ParameterValue
doc_type{{dlv — doc_type}} || {{var — doc type from filename}}
product_sku{{dlv — product_sku}}
product_family{{dlv — product_family}}
source_page{{dlv — source_page}}

Triggering

Custom event · event equals document_download
Click — link · Click URL ends with .pdf fallback

Blocking trigger: none. Deduplication handled by the fallback trigger excluding pages where the dataLayer push is present.

reconstruction · the event tag · one event name, four parameters. never download_spec_sheet_pdf.

The naming rule underneath this is worth stating because it is the single most common way an analytics property becomes unusable: events are things a person did, parameters are the details. The moment you write download_spec_sheet_pdf you have put a parameter into the event name, and you will never be able to total downloads again without a regular expression and an apology.

Third, the one that turned a cross-vendor project into six lines. Portal login segmentation was scoped as backend work with an external development partner — expose the user role through an API, wire it in, timeline in months. Before agreeing to that, I opened the post-login page and read the markup. The role was already there, on the body element, put there by the theme so it could style the navigation.

Tag manager Variables › User-defined › Custom JavaScript var — customer type
function() {
  // The portal theme already writes the role onto <body> to style the nav.
  // Read it rather than commissioning an API to tell us what the page knows.
  var m = document.body.className.match(/\brole-([a-z_]+)\b/);
  if (!m) return 'unresolved';   // never '' — an empty value hides itself

  var map = {
    staff: 'internal', rep: 'distributor_rep',
    threepl: 'fulfilment', brand: 'brand'
  };
  return map[m[1]] || 'unmapped_' + m[1];  // a new role shows up named, not as noise
}

reconstruction · six lines, no backend ticket, no vendor dependency.

The unmapped_ prefix is deliberate. When someone adds a role in eighteen months — and they will, without telling marketing — it appears in the report as unmapped_auditor rather than being quietly folded into a bucket where it corrupts a segment nobody thinks to re-check.

the pattern behind all three

Every one of these fails loudly rather than quietly. unclassified, unresolved, unmapped_*. Analytics does not usually break with an error — it breaks by continuing to produce a number that has stopped being true. Designing the failure states is most of the engineering.

step 06

Verifying before publish, and keeping the receipt

nothing is published on the strength of "the tag went green" · fired ≠ arrived ≠ correct

A tag firing tells you a tag fired. It does not tell you the parameters resolved, and it certainly does not tell you the platform accepted them. Those are three separate claims and I check them separately. Preview mode first — trigger the interaction on staging, then read the resolved values, not the tag list.

Tag Assistant — preview connected to staging.cambiumpak.example ● connected

Event stream

Container Loaded
DOM Ready
Window Loaded
document_download
Click
TagsVariablesData LayerErrors

Variables set on document_download — the values as the tag will actually send them.

VariableResolved value
dlv — doc_typespec
dlv — product_skuCP-MLR-1208
dlv — product_familymailer
dlv — source_pagefamily
var — doc type from filenamespec fallback, unused
Click URL/assets/docs/CambiumPak-CP-MLR-spec-sheet.pdf

Tags fired on this event

GA4 — document_downloadsucceeded

reconstruction · preview mode, variables tab. i read this tab, not the tags tab — a tag can fire perfectly and send an empty string.

Note what the fallback row proves: the lookup table resolved to the same value the dataLayer supplied. Agreement between the primary path and the fallback is a much stronger signal than either one working alone, and it takes no extra effort to check while you are already looking at the pane.

Then Realtime, because preview proves the browser sent something and Realtime proves the platform received and parsed it. Between those two claims sits every consent tool, ad blocker, and dropped parameter I have ever had to debug.

Analytics Reports › Realtime ● 1 user in last 30 min

Event count by event name

document_download1
page_view3
session_start1

Parameter: doc_type

spec1

Parameter: product_family

mailer1

QA pass — 30 July

Tag firespass
Params resolvepass
Event receivedpass
Params receivedpass
No duplicate firepass
Fires on all 11 familiespass
Screenshot fileddone

reconstruction · realtime confirming the parameters arrived, alongside the pass list run against every tag before the container version was published.

why the screenshot row exists

Every verified tag was screenshotted and filed before publish. Not for me — for the conversation eleven weeks later that starts "was this tracking live at launch?" On a relaunch you are one party among several, and the difference between "I'm fairly sure we tested that" and a dated screenshot sent to the web vendor at the time is the difference between a discussion and an argument. It costs four seconds per tag.

step 07

Registering the dimensions, which is not optional and is not retroactive

the quiet step that decides whether any of the previous six were worth doing

A parameter that is collected but not registered as a custom dimension is invisible in reporting. The data is arriving, it is in the payload, and it does not appear in a single report or exploration. People discover this months later, register the dimension, and find it populates from that day forward and not one day earlier. Registration is not retroactive. There is no backfill, no support ticket, no recovery.

So this happened on the same day the tags were built, not after the first month of data.

Analytics Admin › Data display › Custom definitions 12 event-scoped · 2 user-scoped
Dimension nameScopeParameterCreatedStatus
Document typeEventdoc_type30 Julcollecting
Product SKUEventproduct_sku30 Julcollecting
Product familyEventproduct_family30 Julcollecting
Source pageEventsource_page30 Julcollecting
Customer typeUsercustomer_type30 Julcollecting
Account tierUseraccount_tier30 Julcollecting
Configurator outcomeEventoutcome30 Julcollecting
Box styleEventbox_style30 Julcollecting
…6 moreEvent30 Julcollecting

reconstruction · custom definitions, registered the same day the tags were built. customer_type is user-scoped, not event-scoped — see below.

The highlighted row is a real decision, not a checkbox. customer_type is registered at user scope rather than event scope. Event scope would tell you the customer type of the login event; user scope attaches it to everything that person subsequently does in the session. That is the difference between "1,240 logins, 58% distributor reps" and being able to ask which documents distributor reps download — which is the question Dana was actually circling when she said the portal looked busy. One radio button, and it determines whether half the interesting questions are answerable at all.

step 08

The Explore that answered her question, and one it answered by accident

free-form exploration · rows, columns, one metric, and reading the totals carefully

Once the dimensions were populating I built the report Dana actually asked for: which documents, for which products. A free-form exploration — product family down the side, document type across the top, sessions as the metric. Deliberately sessions rather than event count, for a reason that turns out to matter enormously.

Analytics — Explore Free-form · Documents by family and type last 90 days

Variables

Product family Document type Product SKU Source page Customer type

Metrics

Sessions Event count Total users

Tab settings

Technique

Free form

Rows

Product family

Columns

Document type

Values

Sessions

Filters

event = document_download
Product familySpecComp. Feat.Instr.Sessions
Corrugated mailer41296118264561
Shipping carton, RSC3387184190447
Water-activated tape1743847209341
Paper-padded envelope2054461152318
Moulded pulp tray, 6 cav.121523988214
…6 more families402119137278
Totals1,6524204861,1811,742

Sessions is de-duplicated. A session appearing in two columns is counted once in the Totals column.

reconstruction · the free-form exploration · invented figures. the answer to her question is the body of the table. the more interesting finding is in the last column.

Her question, answered. Spec sheets and instructions dominate. The comparison sheet is the least-downloaded document in every single family, without exception — and the comparison sheet is the one whose entire job is to help somebody decide whether this is the right format. The document built to win the specification decision was the one nobody was finding. That is a navigation problem wearing a content problem's clothes, and it went straight onto the website roadmap.

Then the last column. Because the metric is sessions and sessions de-duplicate, the row cells and the row total say two different things — and the gap between them is a finding nobody asked for.

3,739document-type downloads across all families
1,742distinct sessions that downloaded anything
2.15sheets per downloading session, on average
4separate PDFs to answer one specification

The cells sum to 3,739. The de-duplicated session total is 1,742. So the average session that downloaded anything took 2.15 of the four sheets — and since no session can take more than four, at least 38% of them provably took more than one. The true figure is certainly higher; 38% is only the floor the arithmetic guarantees, and I quoted the floor rather than the estimate because the floor is the part I can defend.

That is not a documentation preference. It is the four-sheet structure working exactly as designed and being, for the buyer, exactly as painful as it looks: the board grade is on the spec sheet, the warranty term is on the features sheet, the make-up time is on the instructions, and the recycled band is on the comparison sheet. Nobody set out to make a buyer open four PDFs. It is what happens when four documents each have one owner and nobody measures the seam between them.

honest limitation, and it belongs on the page

I also used free-form to answer "where do people stop?" — event names down the rows in buying order, sessions as the metric, and read the drop between steps. That is a step comparison, not a funnel. It tells you how many sessions reached each step; it does not prove the same sessions moved between them in that order, and it will not show you someone who arrived at step three from a search result without ever seeing step one. It was enough to point at the largest gap, which was the question being asked. It would not be enough to claim a conversion rate, and I did not claim one.

step 09

Handing it over as something she opens herself

the last five percent · a report nobody opens has not changed a decision

An exploration is a thing an analyst opens. Dana is not an analyst, and a report that requires me to run it is a report that stops existing the week I am busy. So the last step is the handover: the same queries, standing, behind a login, with a date range and an export — a dashboard she opens on a Monday without asking anyone.

That build is its own story — the API auth, the caching, the query batching — and it is written up separately, with a working version you can click through.

what the nine steps produced

  • Four questions in, four questions answerable out. Every event traces to a line Dana said in step 01, and nothing was built that could not name the decision it informed.
  • Forty-four filename rows became four document types that roll up by family without a lookup table anyone has to maintain.
  • A portal login became a segment — six lines of JavaScript instead of a cross-vendor backend project, registered at user scope so it colours everything else that person does.
  • Five new tools went from no measurable signal to individually reportable on day one of the relaunch, including the one billed monthly, whose renewal now rests on usage rather than on a feeling.
  • One finding nobody asked for — the average downloading session needed 2.15 of the four sheets — which is now the business case for a website change, and came out of choosing sessions over event count.

the part I would want asked about in an interview

Step 01 and step 07. The conversation, because that is where the scope is set and everything downstream is either an answer to it or waste. And the dimension registration, because it is a five-minute admin screen that silently determines whether the previous six steps produced anything usable at all — and it is the mistake I have most often been called in to find, months after it stopped being fixable.

ga4google tag managermeasurement planning datalayer contractsregex table variablescustom javascript variables custom dimensionsevent & user scopepreview & realtime qa free-form explorationsstakeholder interviewinganalytics governance

The real version of this work was done for my employer, under NDA, on a lighting manufacturer's site. Its data, screens and code stay private — so this page rebuilds the process on an invented company instead, with fabricated figures throughout. I am glad to walk through the real event schema, the QA process and the architecture in a conversation.

open to marketing operations, analytics and martech roles

Let's talk.

Remote, or Albuquerque and hybrid. If you want the architecture behind any of the work here, ask me and I will walk you through it.