Skip to content

feat(site): utm persistence#7780

Merged
mhartington merged 8 commits intomainfrom
utm-persistence
Apr 7, 2026
Merged

feat(site): utm persistence#7780
mhartington merged 8 commits intomainfrom
utm-persistence

Conversation

@mhartington
Copy link
Copy Markdown
Member

@mhartington mhartington commented Apr 7, 2026

Summary by CodeRabbit

  • New Features
    • Site now persists UTM parameters site-wide and initializes persistence in the page shell on every route.
    • Clicked internal links are rewritten or navigated with merged/preserved UTM data to keep campaign context across pages.
    • Login/signup CTAs now include persisted UTM details; a dedicated CTA component builds destination links with active UTMs.
    • Navigation components resolve flexible UTM keys and indicate when exact URL UTMs should be preserved.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

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

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Apr 7, 2026 3:44pm
docs Ready Ready Preview, Comment Apr 7, 2026 3:44pm
eclipse Ready Ready Preview, Comment Apr 7, 2026 3:44pm
site Ready Ready Preview, Comment Apr 7, 2026 3:44pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds UTM plumbing across site and blog: new utm libs, client-only UtmPersistence components that persist/propagate UTM query params and rewrite/reroute qualifying links, NavigationWrapper components that resolve defaults vs. URL UTMs, WebNavigation prop changes, and Console CTA components/updates.

Changes

Cohort / File(s) Summary
Site UTM library
apps/site/src/lib/utm.ts
New UTM utilities and types: UTM_STORAGE_KEY, CONSOLE_HOST, UtmParams, sanitization, getUtmParams, hasUtmParams, syncUtmParams, readStoredUtmParams, writeStoredUtmParams, clearStoredUtmParams.
Site persistence component
apps/site/src/components/utm-persistence.tsx
New client-only UtmPersistence that stores UTMs on pathname change, installs a capturing click handler to merge/sync stored UTMs into clicked link targets, rewrites hrefs or routes via router.push for qualifying internal navigations.
Site layout mount
apps/site/src/app/layout.tsx
Mounts <UtmPersistence /> inside the app shell (ThemeProvider) so persistence runs on every route.
Site navigation wrapper
apps/site/src/components/navigation-wrapper.tsx
NavigationWrapper now reads URL UTMs after hydration, computes resolved UTM params (exact URL UTMs vs defaults), passes utm and preserveExactUtm to WebNavigation; widened utm.source type to string.
Site CTA & button
apps/site/src/app/(index)/page.tsx, apps/site/src/components/console-cta-button.tsx
Replaced hard-coded console links with ConsoleCtaButton; new ConsoleCtaButton computes destination href by preferring current-page UTMs (if present) or falling back to provided defaultUtm.
UI navigation changes
packages/ui/src/components/web-navigation.tsx
WebNavigationProps.utm changed from fixed shape to Record<string,string>; added preserveExactUtm?: boolean and buildConsoleHref helper that copies only utm_* keys and conditionally sets utm_campaign.
Blog UTM library
apps/blog/src/lib/utm.ts
Blog-scoped UTM utilities mirroring site: UTM_STORAGE_KEY ("blog_utm_params"), CONSOLE_HOST, UtmParams, sanitization, getUtmParams, hasUtmParams, syncUtmParams, readStoredUtmParams, writeStoredUtmParams, clearStoredUtmParams.
Blog persistence component
apps/blog/src/components/utm-persistence.tsx
New client-only UtmPersistence for blog with same behavior as site: persist on pathname changes and capture clicks to propagate UTMs into target URLs, rewriting or navigating as appropriate.
Blog navigation wrapper & layout
apps/blog/src/components/navigation-wrapper.tsx, apps/blog/src/app/(blog)/layout.tsx
Added NavigationWrapper component that resolves URL vs default UTMs and sets preserveExactUtm; blog layout now mounts UtmPersistence and uses NavigationWrapper passing utm_source/utm_medium defaults.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(site): utm persistence' directly and accurately describes the main feature added across the changeset—UTM parameter persistence logic—making it clear and specific.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@argos-ci
Copy link
Copy Markdown

argos-ci bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Apr 7, 2026, 3:50 PM

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/components/navigation-wrapper.tsx`:
- Around line 60-79: The initial useState for storedUtmParams only sets
utm_source, causing other UTM fields to be undefined on first render; change the
initial state to synchronously read persisted values by calling
readStoredUtmParams() (and merging fallbacks from utm.source/utm.medium) so
storedUtmParams is fully populated immediately, then keep the existing useEffect
to update/write via getUtmParams and writeStoredUtmParams; apply the same
synchronous initialization fix to the other storedUtmParams instance referenced
later (the one updated in the second block using setStoredUtmParams).

In `@apps/site/src/lib/utm.ts`:
- Around line 34-50: readStoredUtmParams currently reads
window.sessionStorage.getItem outside a try/catch and casts parsed JSON to
UtmParams without validation; update readStoredUtmParams to wrap sessionStorage
access (getItem) and JSON.parse in try/catch, and validate/filter the parsed
object so it only returns keys that match the utm_* pattern with string values
before returning {}; for writeStoredUtmParams wrap sessionStorage.setItem in
try/catch so failures in restricted environments are swallowed gracefully, and
ensure you only stringify and store a validated object (use hasUtmParams as a
guard) to avoid persisting malformed data.

In `@packages/ui/src/components/web-navigation.tsx`:
- Around line 40-45: The utm prop in WebNavigation (and the forwarding helper
around it) currently narrows the persisted UTM payload to only source, medium,
campaign, content, and term which drops any other utm_* keys; change the
prop/type and forwarding logic to accept a generic record of arbitrary utm_*
entries (e.g., Record<string,string | undefined> or a generic object) and
forward all keys through, and only synthesize/default utm_campaign when that
specific key is absent (preserving other keys like utm_id or
utm_source_platform); update the component/utility signatures that reference utm
(the utm prop and the helper that builds the login/get-started URL) to use the
broader type and pass through the entire map.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d96bc111-8286-4eef-bbec-fe1b9aafb530

📥 Commits

Reviewing files that changed from the base of the PR and between 051e12b and 560fe89.

📒 Files selected for processing (5)
  • apps/site/src/app/layout.tsx
  • apps/site/src/components/navigation-wrapper.tsx
  • apps/site/src/components/utm-persistence.tsx
  • apps/site/src/lib/utm.ts
  • packages/ui/src/components/web-navigation.tsx

Preserve arbitrary persisted UTM fields across site and console navigation while making storage access safer and available on first render.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/components/navigation-wrapper.tsx`:
- Around line 7-12: The current logic in UtmPersistence (and in the consumers in
navigation-wrapper.tsx) only treats payloads with utm_source as valid, causing
stored single-field params like utm_campaign or utm_content to be dropped;
update the acceptance/normalization logic to consider any non-empty UtmParams
(i.e., any utm_* field present) as valid rather than requiring utm_source, and
ensure getUtmParams/readStoredUtmParams/writeStoredUtmParams and the
WebNavigation prop forwarding preserve all present optional fields (UtmParams)
when merging or persisting. Ensure checks use a generic "hasAnyUtmField"
predicate and update places referencing UtmPersistence, getUtmParams, and
WebNavigation to use that predicate so partial UTM objects are kept.

In `@apps/site/src/components/utm-persistence.tsx`:
- Around line 45-91: The click handler mutates the DOM href which Next.js Link
ignores; update handleClick (inside the useEffect) to, after computing targetUrl
and merging UTM params via mergeUtmParams/readStoredUtmParams, prevent the
default navigation and perform client-side navigation using Next's router (e.g.,
call router.push with
`${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`) for same-origin
internal links instead of setting anchor.setAttribute, while preserving existing
guards for blank/modified/new-tab clicks and download/mailto/tel; ensure you
import/use Next's useRouter or next/router and only call router.push when
anchor.target is not "_blank" and no modifier keys are pressed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68430cc9-93ea-4362-8458-246c74aa941f

📥 Commits

Reviewing files that changed from the base of the PR and between 560fe89 and fdad040.

📒 Files selected for processing (2)
  • apps/site/src/components/navigation-wrapper.tsx
  • apps/site/src/components/utm-persistence.tsx

Extend the site UTM persistence handler to rewrite console.prisma.io links and simplify nav UTM resolution so page-level CTAs inherit the stored campaign parameters.

Made-with: Cursor
Use the incoming UTM set exactly when one is present, fall back to defaults only when no UTM exists, and bring the same persistence behavior to the blog shell.

Made-with: Cursor
Avoid nav hydration mismatches by deferring exact UTM resolution until after mount and apply the same exact-or-default UTM handling to the homepage create database CTAs.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/blog/src/components/navigation-wrapper.tsx (1)

6-20: Consider extracting shared Link interface to reduce duplication.

This interface is duplicated from packages/ui/src/components/web-navigation.tsx. While keeping it local avoids cross-package type dependencies, it creates a maintenance burden where changes must be synchronized manually.

Consider exporting the Link type from @prisma-docs/ui alongside WebNavigation to ensure type consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blog/src/components/navigation-wrapper.tsx` around lines 6 - 20, The
local Link interface in navigation-wrapper.tsx duplicates the Link type defined
in WebNavigation (packages/ui/src/components/web-navigation.tsx); export the
shared Link type from `@prisma-docs/ui` and import it here instead of redefining
it. Update the UI package to export the Link type alongside the WebNavigation
component, then replace the local interface in navigation-wrapper.tsx with an
import of Link from '@prisma-docs/ui' and remove the duplicated declaration so
both packages share a single source of truth.
apps/site/src/lib/utm.ts (1)

1-101: Consider extracting this shared UTM helper instead of maintaining a second copy.

This is almost a verbatim copy of apps/blog/src/lib/utm.ts aside from the storage key. Any fix to sanitization, sync behavior, or storage semantics now has to land twice, and the two apps can drift silently. Pulling the shared logic into one helper and injecting the key only where it truly differs would make future changes safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/lib/utm.ts` around lines 1 - 101, Duplicate UTM logic exists
(UTM_STORAGE_KEY, sanitizeUtmParams, getUtmParams, syncUtmParams,
readStoredUtmParams, writeStoredUtmParams, clearStoredUtmParams) and should be
extracted into a shared helper module to avoid drift; create a single shared utm
utility (exporting sanitizeUtmParams, getUtmParams, hasUtmParams, syncUtmParams,
readStoredUtmParams, writeStoredUtmParams, clearStoredUtmParams) in a common
package or lib and update this file to import those functions, keeping only the
app-specific storage key injection (UTM_STORAGE_KEY) or a small wrapper that
passes the key into the shared write/read functions so storage key remains
configurable without duplicating logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/components/utm-persistence.tsx`:
- Around line 18-29: The effect currently clears stored UTMs whenever the
current route lacks utm_* which loses the original campaign; instead, when
getUtmParams(...) yields no utm params, call readStoredUtmParams() and if that
returns a stored value, keep/use it (do not call clearStoredUtmParams()); only
clear stored UTMs when explicitly needed (e.g., when a new campaign is detected
or on session end). Update the logic around getUtmParams, hasUtmParams,
writeStoredUtmParams, clearStoredUtmParams and the other block that currently
returns without reading readStoredUtmParams() so it falls back to
readStoredUtmParams() before deciding to drop UTMs.

---

Nitpick comments:
In `@apps/blog/src/components/navigation-wrapper.tsx`:
- Around line 6-20: The local Link interface in navigation-wrapper.tsx
duplicates the Link type defined in WebNavigation
(packages/ui/src/components/web-navigation.tsx); export the shared Link type
from `@prisma-docs/ui` and import it here instead of redefining it. Update the UI
package to export the Link type alongside the WebNavigation component, then
replace the local interface in navigation-wrapper.tsx with an import of Link
from '@prisma-docs/ui' and remove the duplicated declaration so both packages
share a single source of truth.

In `@apps/site/src/lib/utm.ts`:
- Around line 1-101: Duplicate UTM logic exists (UTM_STORAGE_KEY,
sanitizeUtmParams, getUtmParams, syncUtmParams, readStoredUtmParams,
writeStoredUtmParams, clearStoredUtmParams) and should be extracted into a
shared helper module to avoid drift; create a single shared utm utility
(exporting sanitizeUtmParams, getUtmParams, hasUtmParams, syncUtmParams,
readStoredUtmParams, writeStoredUtmParams, clearStoredUtmParams) in a common
package or lib and update this file to import those functions, keeping only the
app-specific storage key injection (UTM_STORAGE_KEY) or a small wrapper that
passes the key into the shared write/read functions so storage key remains
configurable without duplicating logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85815350-a1ad-4e02-a806-630a507b1303

📥 Commits

Reviewing files that changed from the base of the PR and between 1d45680 and 8e586db.

📒 Files selected for processing (8)
  • apps/blog/src/app/(blog)/layout.tsx
  • apps/blog/src/components/navigation-wrapper.tsx
  • apps/blog/src/components/utm-persistence.tsx
  • apps/blog/src/lib/utm.ts
  • apps/site/src/components/navigation-wrapper.tsx
  • apps/site/src/components/utm-persistence.tsx
  • apps/site/src/lib/utm.ts
  • packages/ui/src/components/web-navigation.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/blog/src/app/(blog)/layout.tsx
  • apps/site/src/components/navigation-wrapper.tsx

Comment on lines +18 to +29
useEffect(() => {
const currentUtmParams = getUtmParams(
new URLSearchParams(window.location.search),
);

if (hasUtmParams(currentUtmParams)) {
writeStoredUtmParams(currentUtmParams);
return;
}

clearStoredUtmParams();
}, [pathname]);
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.

⚠️ Potential issue | 🟠 Major

Fall back to stored UTMs instead of clearing them on the first clean URL.

Lines 18-29 clear the session as soon as the next route lacks utm_*, and Lines 57-63 return without reading readStoredUtmParams(). A redirect, router.push(), or any internal navigation this listener doesn't rewrite will drop the original campaign for the rest of the session, so this behaves like link propagation rather than persistence.

Also applies to: 57-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/components/utm-persistence.tsx` around lines 18 - 29, The
effect currently clears stored UTMs whenever the current route lacks utm_* which
loses the original campaign; instead, when getUtmParams(...) yields no utm
params, call readStoredUtmParams() and if that returns a stored value, keep/use
it (do not call clearStoredUtmParams()); only clear stored UTMs when explicitly
needed (e.g., when a new campaign is detected or on session end). Update the
logic around getUtmParams, hasUtmParams, writeStoredUtmParams,
clearStoredUtmParams and the other block that currently returns without reading
readStoredUtmParams() so it falls back to readStoredUtmParams() before deciding
to drop UTMs.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/site/src/app/(index)/page.tsx (1)

121-125: Extract repeated default UTM config to a shared constant.

The same defaultUtm object appears twice. Pulling it into one constant reduces drift and keeps campaign edits one-touch.

Proposed refactor
+const INDEX_CTA_DEFAULT_UTM = {
+  utm_source: "website",
+  utm_medium: "index",
+  utm_campaign: "cta",
+} as const;
+
 export default function SiteHome() {
   return (
@@
-              defaultUtm={{
-                utm_source: "website",
-                utm_medium: "index",
-                utm_campaign: "cta",
-              }}
+              defaultUtm={INDEX_CTA_DEFAULT_UTM}
@@
-                  defaultUtm={{
-                    utm_source: "website",
-                    utm_medium: "index",
-                    utm_campaign: "cta",
-                  }}
+                  defaultUtm={INDEX_CTA_DEFAULT_UTM}

Also applies to: 259-263

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/`(index)/page.tsx around lines 121 - 125, Duplicate inline
defaultUtm objects are used; extract them into a single shared constant (e.g.
const DEFAULT_UTM = { utm_source: "website", utm_medium: "index", utm_campaign:
"cta" }) and replace both inline usages of defaultUtm with DEFAULT_UTM; update
any components or props that currently pass the inline object (the places where
defaultUtm: {...} is provided) to reference DEFAULT_UTM so edits to
campaign/medium/source are centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/site/src/components/console-cta-button.tsx`:
- Around line 38-42: The code currently replaces defaultUtm with
currentUtmParams when hasUtmParams(...) is true, causing missing keys; instead
merge defaults with incoming UTMs: create a mergedUtm object that starts from
defaultUtm and overlays any non-empty properties from currentUtmParams, then
pass mergedUtm to buildConsoleHref (change call sites around
setHref/buildConsoleHref and reference hasUtmParams, currentUtmParams,
defaultUtm). Ensure you only override when the URL value is present (non-empty)
so unspecified defaults are preserved.

---

Nitpick comments:
In `@apps/site/src/app/`(index)/page.tsx:
- Around line 121-125: Duplicate inline defaultUtm objects are used; extract
them into a single shared constant (e.g. const DEFAULT_UTM = { utm_source:
"website", utm_medium: "index", utm_campaign: "cta" }) and replace both inline
usages of defaultUtm with DEFAULT_UTM; update any components or props that
currently pass the inline object (the places where defaultUtm: {...} is
provided) to reference DEFAULT_UTM so edits to campaign/medium/source are
centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 58bbda39-8891-4c9b-af6d-86846e75bd4a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e586db and c0a1870.

📒 Files selected for processing (4)
  • apps/blog/src/components/navigation-wrapper.tsx
  • apps/site/src/app/(index)/page.tsx
  • apps/site/src/components/console-cta-button.tsx
  • apps/site/src/components/navigation-wrapper.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/site/src/components/navigation-wrapper.tsx

Use the anchor variant of the shared Button props so homepage console CTAs accept link-only attributes like target and rel during type checking.

Made-with: Cursor
Extract the shared homepage CTA UTM payload into a single constant so the create database buttons stay aligned when campaign defaults change.

Made-with: Cursor
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