A marketing system built to prove it works, not just to run once.
An end-to-end tracking, CRM, and reporting pipeline for a fictional product launch — instrumented with GTM and GA4, connected to HubSpot, automated with Make.com, and deliberately broken eight different ways to prove the debugging holds up. Built entirely on free-tier tools.
The problem
Marketing and CRM teams typically see only half of the customer journey each. Web analytics captures anonymous behavior; the CRM captures identified leads. Without a deliberate architecture connecting the two, neither side can reliably answer the question that actually matters: which campaign produced this lead. I built this system to design and implement that connective architecture end to end, entirely on free-tier tooling.
What I set out to build
- Governed GA4 event tracking through Google Tag Manager — not hard-coded scripts, not Enhanced-Measurement-only.
- A UTM naming convention that survives intact from an ad click through to a CRM record and a report.
- Leads captured in HubSpot with real source and campaign attribution, not just a name and an email.
- CRM-to-spreadsheet automation with Make.com, so nothing depends on manual copy-paste.
- One dashboard answering six specific business questions — and a system I could prove was actually working.
How the pieces fit together
The system runs as two deliberately separate tracks, meeting only at the reporting layer.
Track A — Reference
The real, live Google Merchandise Store GA4 demo account, used read-only to validate measurement-plan decisions against a mature, production ecommerce implementation. Nothing here was ever edited.
Track B — Owned & instrumented
Waypoint Supply Co., an original fictional brand built specifically so every layer — code, tags, CRM, automation — was mine to instrument, test, and break on purpose.
Campaign attribution is joined at the campaign level — shared UTM values — not the user level. GA4 and HubSpot never share a native user identity on this stack, and the architecture was built around that constraint rather than pretending it away.
Stack
What's actually running
Google Tag Manager
Seven tags across three folders. Native Click and Element Visibility triggers everywhere one existed, with one deliberate exception: a Custom HTML listener bridging HubSpot's cross-origin iframe submission events into the Data Layer, since no native GTM trigger can read postMessage traffic.
GA4
Enhanced Measurement audited, not just switched on — Form interactions deliberately disabled to avoid a second, unreliable count of form activity. Custom dimensions registered for every event parameter that needed to be reportable, one Key Event, one behavioral audience.
HubSpot
Free CRM, five custom properties mirroring the UTM taxonomy exactly, a two-field form kept deliberately short. Campaign attribution captured via HubSpot's native query-string auto-populate — chosen after confirming HubSpot's current default form architecture blocks the classic JavaScript approach most tutorials still describe.
Make.com
One scenario: a router that splits incoming leads by whether they carry campaign data, logs both outcomes, and includes an error handler I forced to fail on purpose to confirm it actually catches a failure instead of losing a lead silently.
Reporting
A six-tab Google Sheets operational layer feeding a twelve-chart Looker Studio dashboard, structured around six real business questions rather than built to look impressive.
Proving it works, not just building it
QA happened in two layers: a systematic pass across every system, then eight failures introduced on purpose and diagnosed from their actual symptoms — not from already knowing the cause.
- 01Mistyped event name — silently created an unrecognized event; caught via a funnel step reading zero. Fixed, verified.
- 02Duplicated trigger on one tag — one click fired the tag twice; caught by counting fires in Preview. Fixed, verified.
- 03Wrong event marked as a Key Event — inflated the conversion count with a funnel-stage signal, not an outcome. Fixed, verified.
- 04Inconsistent UTM casing — split one channel's data into two report rows. Confirmed unrecoverable retroactively; documented as a standing risk of manual tagging.
- 05HubSpot property internal name mismatch — a hidden field stayed silently blank with no error anywhere. Fixed, verified.
- 06Inverted Make.com filter — valid leads were dropped with nothing overtly labeled as an error. Fixed, verified.
- 07Forced automation failure — confirmed the error handler logs a failed run instead of losing the record.
- 08Broken dashboard formula — caught by two independently-sourced totals that should always agree, and didn't. Fixed, verified.
One item I haven't closed out
A portion of historical generate_lead events fired without a form ID attached. I ruled out GTM tag duplication and consent-mode interference, and narrowed it most likely to residual noise from an earlier troubleshooting session — but I didn't get a clean, isolated reproduction before mitigating it in the dashboard's reporting window. It's logged as an open item, not quietly patched over.
What's actually verifiable
Measured: every layer of the pipeline is independently checkable — GTM Preview and GA4 DebugView confirm correct event firing with correct parameters, HubSpot contact records show correctly populated campaign attribution, the Make.com scenario runs autonomously on schedule with a working error path, and the dashboard is live and public. All eight deliberately introduced failures were diagnosed and resolved.
This is a live environment rather than a fixed dataset, so exact lead-volume numbers are visible on the dashboard itself at any given time, rather than quoted here as a static figure.
What I'd tell a hiring manager directly
Challenges
The instructive ones weren't the ones I expected going in. HubSpot's current default form architecture broke the JavaScript approach I initially planned for capturing UTM data, forcing a redesign around a native auto-populate mechanism instead. A Make.com field-mapping issue meant most early test submissions logged only a timestamp, not full lead data, until I traced it to an unresolved mapping step.
Limitations
- GA4 and HubSpot share no native user-level identity — attribution works at the campaign level only.
- No true "form started" signal exists, since HubSpot's documented callbacks don't expose field-level interaction events.
- Campaign traffic is simulated — manually visited tagged links, not real ad spend.
- Free-tier ceilings are real: 1,000 HubSpot contacts, 15-minute minimum Make.com polling, ~15-minute Looker Studio data freshness.
What I'd build next
- Conclusively resolve the open
generate_leadanomaly with a clean, monitored reproduction. - Integrate a real ad platform to replace simulated UTM traffic with actual spend data.
- Move toward server-side tagging for genuine user-level attribution between GA4 and HubSpot.
- Turn the eight failure scenarios into a repeatable regression-test suite.