Skip to content

fix(ai): attribute utm_term and utm_content like the rest of the UTM family - #744

Merged
izadoesdev merged 1 commit into
mainfrom
staging
Sep 7, 2026
Merged

fix(ai): attribute utm_term and utm_content like the rest of the UTM family#744
izadoesdev merged 1 commit into
mainfrom
staging

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Sep 7, 2026

Copy link
Copy Markdown
Member

Follow-up to #743, which surfaced this.

utm_terms and utm_content reported event-level UTM values while utm_sources, utm_mediums and utm_campaigns reported session-attributed ones. The same report family answered the same question two different ways.

Root cause

Not the builder config. needsSessionAttribution() only fires for columns listed in SESSION_ATTRIBUTION_FIELDS, and utm_term/utm_content were never in it — so setting the plugin on those builders was a silent no-op. I confirmed that by setting it and watching the generated SQL not change.

Cost to existing queries: zero

Adding both columns widens the shared session_attribution CTE from 8 argMins to 10, which touches every attributed query. Measured on the busiest client:

rows read bytes time
8 argMins 1,459,682 144.1 MiB ~123ms
10 argMins 1,459,682 144.1 MiB ~104ms

Byte-identical. ClickHouse prunes aggregates the outer query never references, which is the same behaviour that made an earlier column-pruning attempt a no-op.

What does change

The two reports gain the attribution join, 13ms → 27ms, and their numbers rise: +3.9% pageviews, +0.2% visitors on the highest-volume utm_content client. Pageviews after the landing one now count toward the value that acquired the session, which is what the other three already did.

Flagging that plainly: these two reports will show slightly higher pageviews after this ships. That is the intended correction, not a regression.

Also

Drops IS NOT NULL from the generated where. All five columns are Nullable(String), but NULL != '' is NULL and already discarded by WHERE — across 1,283,945 null rows both predicates return identical counts (11,881 and 6,910).

With both differences gone, utmDimension has no behavioural flags left; every call site is purely descriptive.

653 tests pass, typecheck and lint clean.


Summary by cubic

Fixes utm_terms and utm_content reports so they attribute UTM values to the session like the rest of the UTM family. Previously these two reported event-level values while utm_sources, utm_mediums, and utm_campaigns used session attribution.

Side effects

  • utm_terms and utm_content pageviews and visitors will rise slightly after this ships; this is the intended correction.
  • Existing session-attributed queries are unaffected; the added fields don't change rows read or bytes scanned.

Cleanup

  • Drops the IS NOT NULL filter from the generated where clause; results are identical.

Written for commit 22f4aaa. Summary will update on new commits.

Review in cubic

…est of the family

utm_terms and utm_content reported event-level UTM values while
utm_sources, utm_mediums and utm_campaigns reported session-attributed
ones, so the same report family answered the same question two different
ways. The cause was not the builder config: needsSessionAttribution only
fires for columns in SESSION_ATTRIBUTION_FIELDS, and utm_term and
utm_content were never in that list, so setting the plugin on them was a
silent no-op.

Adding both to the list costs existing attributed queries nothing.
Measured on the busiest client, 8 argMins vs 10 read an identical
1,459,682 rows and 144.1 MiB, because ClickHouse prunes aggregates the
outer query never references.

The two reports themselves gain the attribution join, measured at 13ms
to 27ms, and their numbers rise: +3.9% pageviews and +0.2% visitors on
the highest-volume utm_content client, since pageviews after the landing
one now count toward the value that acquired the session.

Also drops IS NOT NULL from the generated where clause. All five columns
are Nullable, but NULL != '' is already NULL and discarded by WHERE:
across 1,283,945 null rows the two predicates return identical counts
(11,881 and 6,910). With both differences gone utmDimension has no
behavioural flags left.
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
databuddy-status Ready Ready Preview Sep 7, 2026 9:32am UTC
2 Skipped Deployments
Project Deployment Actions Updated
dashboard (staging) Skipped Skipped Sep 7, 2026 9:32am UTC
documentation (staging) Skipped Skipped Sep 7, 2026 9:32am UTC

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 81dbf0eb-a15d-43ba-af60-e954800799da

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes utm_term and utm_content use the same session-attribution query path as the other UTM dimensions and removes a redundant nullable predicate.

  • Moves the session-attribution plugin configuration into the shared UTM builder.
  • Adds term and content to the shared session-attribution field list.
  • However, the nullable aggregate currently allows a later UTM value to replace a missing acquisition value.

Confidence Score: 4/5

The PR is not safe to merge until nullable UTM values are made to preserve the first event rather than allowing a later event to acquire the session retroactively.

The new fields flow through plain argMin despite being nullable, so a realistic session with an untagged landing event and a later tagged event is attributed to the later value, contradicting the established first-touch contract.

Files Needing Attention: packages/ai/src/query/expressions.ts

Important Files Changed

Filename Overview
packages/ai/src/query/builders/traffic.ts Consolidates UTM reports onto the session-attribution plugin and removes the redundant explicit null predicate.
packages/ai/src/query/expressions.ts Adds nullable term and content fields to an argMin attribution path that can select a later non-null event instead of the acquisition event.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Landing event<br/>utm_term = NULL] --> B[Later event<br/>utm_term = paid-keyword]
  A --> C[argMin nullable field by time]
  B --> C
  C --> D[NULL argument skipped]
  D --> E[paid-keyword selected]
  E --> F[Entire session retroactively attributed]
Loading

Reviews (1): Last reviewed commit: "fix(ai): attribute utm_term and utm_cont..." | Re-trigger Greptile

Comment on lines +111 to +112
"utm_term",
"utm_content",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Later Values Acquire Sessions

When a landing event has no utm_term or utm_content but a later event does, the new fields pass through argMin(field, time). Because these fields are nullable, ClickHouse skips the landing row's null argument and selects the later value. This retroactively credits every pageview in the session to a value that did not acquire it, contrary to the existing first-touch behavior. Preserve the first event by coalescing nullable values before argMin, as the revenue attribution path does.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Report as Traffic Reports (utm_terms, utm_content)
    participant Builder as utmDimension Builder
    participant CTE as session_attribution CTE
    participant Expression as SESSION_ATTRIBUTION_FIELDS
    participant DB as ClickHouse Database

    Note over Report,DB: UTM Term and Content Attribution Flow

    Report->>Builder: Get report config for utm_term/utm_content
    Builder->>Builder: Build query with sessionAttribution plugin enabled
    Builder->>Builder: Set WHERE clause (column != '' AND event_name = 'screen_view')
    Builder->>CTE: Request session-attributed UTM values
    
    CTE->>Expression: Check if utm_term/utm_content need attribution
    Expression-->>CTE: Include both fields in attribution set
    
    CTE->>CTE: Apply 10 argMin aggregations (was 8)
    CTE->>DB: Execute attribution query
    
    alt Session attribution join
        DB-->>CTE: Session-attributed UTM term/content values
        CTE-->>Builder: Return attributed dimensions
        Builder-->>Report: Pageviews + visitors (slightly higher)
    else Event-level fallback
        DB-->>CTE: Event-level UTM values only
        CTE-->>Builder: Return event-scoped dimensions
        Builder-->>Report: Pageviews + visitors (original counts)
    end

    Note over CTE,DB: NULL and '' values filtered by WHERE clause
    Note over Builder: All five UTM dimensions now use same attribution path
Loading

Shadow auto-approve: would require human review. Aligns utm_terms/utm_content with session attribution, and this metric-affecting change alters customer-visible metrics and requires human sign-off.

Re-trigger cubic

@izadoesdev
izadoesdev merged commit d7f55e2 into main Sep 7, 2026
28 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant