Home Blog Resume Contact Ask AI About Me
Home/Blog/The Lead-Quality Report HubSpot Cannot Build …
ArticleHubSpotHubSpotReportingAttribution

The Lead-Quality Report HubSpot Cannot Build for You

8 min readBy Miloš Mitrović

Your Google Ads report says a keyword drives leads. Finance says those leads never close. HubSpot will not join those two facts for you out of the box, so the keyword keeps its budget and the pipeline keeps filling with contacts that die at the same stage every month. A lead-quality report closes that gap by tying the source of a contact to the revenue of the deal it became, and it takes deliberate assembly because the platform scatters those fields across three objects and two subscription tiers.

Key takeaways

  • Deal create and revenue attribution reports require Marketing Hub Enterprise; Professional attributes only contact creation, so most portals cannot natively map revenue back to a source.
  • A deal inherits the Original Source of its earliest-activity associated contact, so any report keyed on deal source quietly hides the touch that generated the lead.
  • The custom report builder caps non-table charts at 1,000 unique rows and joins only pre-defined object relationships, which is why a keyword-by-revenue view rarely survives inside one native report.
  • GCLID lands on the contact automatically, but Google retains it for only 90 days, so revenue that closes later needs an interim conversion event to stay attributable.
  • The version of this report you can trust usually lives outside the native builder, either as a Google Ads offline conversion feed or a warehouse join keyed on the contact.

What does lead quality actually mean in a HubSpot report?

Lead quality is the join between where a contact came from and what the deal it became was worth, measured per source rather than per lead. A lead-count report tells you a keyword produced 40 form fills. A lead-quality report tells you those 40 fills produced two deals worth 12,000 dollars while a different keyword produced eight fills and 60,000 dollars in closed revenue.

That distinction changes budget decisions. Cost per lead rewards the keyword that fills the top of the funnel; cost per closed dollar rewards the keyword that fills the bank. The report you need carries at least four facts on one row: the acquisition source, the form the contact submitted, the deal stage history, and the closed amount.

None of those four live on the same object. Source and form data sit on the contact, amount and close date sit on the deal, and the timestamps that prove a stage transition sit in the deal's property history. Assembling them is the entire job.

Why can't the HubSpot report builder produce this on its own?

Because the builder is gated by tier and bounded by row and relationship limits that a source-to-revenue view runs straight into. The custom report builder is available only on Professional and Enterprise plans, and non-table charts are limited to 1,000 unique rows of data, so any keyword-level breakdown with real traffic overflows the moment you chart it rather than table it.

The harder ceiling is attribution. HubSpot's own documentation confirms that contact create attribution runs on Marketing Hub Professional, but deal create and revenue attribution reports are Marketing Hub Enterprise only. A Professional portal can prove a keyword created a contact and can prove nothing about whether that contact became money.

Report typeMarketing Hub ProfessionalMarketing Hub Enterprise
Contact create attributionYesYes
Deal create attributionNoYes
Revenue (multi-touch) attributionNoYes
Data-driven attribution modelNoYes

The custom report builder also joins only pre-defined object relationships, not any arbitrary combination of properties you want on one row. You can build contact-to-deal reports, but the moment your quality definition needs a stage timestamp alongside a source drill-down and a closed amount, the native builder starts fighting you.

Where does the source data actually break?

It breaks at the contact-to-deal handoff, because a deal does not carry its own independent source. HubSpot sets a deal's Original Source from the associated contact with the earliest activity, and if no contact is associated it falls back to the associated company. So a deal created by a sales rep, or one where a later-added stakeholder became the primary contact, can report a source that never touched an ad.

The drill-down properties that hold the keyword-level detail are fragile in a second way. They are set automatically by HubSpot, cannot be edited manually, and are cleared entirely if a record's traffic source changes. An enrichment tool or a re-import that rewrites the source silently wipes the exact fields your report keys on.

GCLID behaves the same way. HubSpot captures the Google ad click ID automatically onto the contact when the visit came from a Google ad, but it stays a contact property. Nothing propagates it to the deal, so tying a specific click to a specific closed amount means reaching back across the association, not reading a single object.

How do you tie a keyword to closed revenue?

You carry the click identifier forward and report revenue against it, rather than asking HubSpot to model attribution it was never given the data to model. The cleanest mechanism is a Google Ads offline conversion feed: store the GCLID on the contact, then when the deal closes, send the closed amount back to Google keyed on that GCLID.

The constraint that catches teams is time. Google keeps a GCLID for only 90 days, so a conversion that happens later than that cannot be uploaded against the original click and the sale falls out of keyword-level reporting entirely. For any sales cycle longer than a quarter, you upload an interim conversion (a qualified-lead event) inside the window and reconcile revenue separately.

Google's offline import also deduplicates on the combination of GCLID, conversion name, and timestamp, per the offline conversion import guidance, so a retry that resends the same row is ignored rather than double-counted. Build the feed to be idempotent and a failed batch is safe to replay.

What does the report look like when you build it correctly?

One row per source, with the count and the money side by side, so the two never get read in isolation. The minimum column set is source or keyword, contacts created, deals created, deals won, closed-won revenue, and a derived cost per closed dollar if you can join spend.

The grain matters. Report at the contact grain and join deal revenue up, rather than starting from deals, because starting from deals inherits the earliest-activity source problem above. Deduplicate deals that carry multiple associated contacts so one 60,000 dollar deal with three contacts does not post 180,000 dollars of phantom revenue against three keywords.

Before trusting any of it, confirm the underlying properties are clean. A source field that half the records populate and half leave blank produces a report that looks precise and lies by omission, which is the first thing to check when you audit a HubSpot portal you have just inherited.

What silently corrupts this report, and how do you catch it?

Three things break it without throwing an error: property renames, association drift, and source overwrites. A renamed or retyped custom property that fed the source dimension does not error, it just stops matching, and the affected rows quietly collapse into an unknown bucket.

Association drift is subtler. When a deal gains a new primary contact, or a contact is merged, the earliest-activity logic can reassign the deal's inherited source, so last month's report and this month's report disagree about the same historical deal. Snapshot the source at deal creation into a static custom property if you need the number to hold still.

Source overwrites are the quiet revenue killer. If your report and your Google Ads spend keep drifting apart even after you fix the joins, the mismatch usually sits in how the two systems date and model the same touch, the same class of problem behind HubSpot and Google Ads attribution mismatches. Reconcile on a shared identifier, not on totals.

Should you build it in HubSpot, Google Ads, or a warehouse?

Build it wherever the join can hold the click identifier and the closed amount on the same row without a tier upgrade you do not need. Each option trades effort against fidelity.

ApproachKeyword-level revenueEffortMain limitation
Native HubSpot report builderOnly with Marketing Enterprise revenue attributionLowTier gate, 1,000-row chart cap, pre-defined joins
Google Ads offline conversionsYes, per keyword in the ads UIMedium90-day GCLID window, needs a feed from CRM to Google
Warehouse join (contact grain)Yes, any grain you modelHighRequires a pipeline, schema ownership, and maintenance

For a Professional portal that already runs Google Ads, the offline conversion feed is the highest return, because it puts closed revenue next to the keyword inside the tool where the bidding decision actually gets made. Reserve the warehouse when you need blended reporting across more than one ad platform, or when the sales cycle regularly exceeds Google's retention window and you need to own the reconciliation logic yourself.

What to watch before you trust the number

Watch three ratios over the first month. Contacts with a populated source versus total contacts tells you how much of the funnel the report can even see; anything below the high 90s percent means the report is blind to a chunk of spend.

Deals with a preserved creation-time source versus deals with a drifted source tells you how much your revenue figures move under you. And GCLID coverage on paid contacts, measured as clicks stored versus clicks Google reports, tells you whether the feed is losing rows before they ever reach the join. Fix those three and the keyword-to-revenue number stops being a guess.

Sources

M
Miloš Mitrović
Revenue Operations & AI Automation

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Ask AI About Me
Clicking an assistant copies the prompt and opens it: ready to run in ChatGPT, Perplexity, and Grok; in Claude, Gemini, or Copilot press Ctrl+V (Cmd+V on Mac) to paste. Use Copy prompt for any other AI. The assistant reads my site, so it needs web access.
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.