Skip to content

Add c15t to tanstack.com - #1364

Draft
KayleeWilliams wants to merge 5 commits into
TanStack:mainfrom
KayleeWilliams:kaylee/add-c15t-consent-integration
Draft

KayleeWilliams wants to merge 5 commits into
TanStack:mainfrom
KayleeWilliams:kaylee/add-c15t-consent-integration

Conversation

@KayleeWilliams

@KayleeWilliams KayleeWilliams commented Oct 12, 2026 •

Copy link
Copy Markdown

Visitors now choose what tanstack.com measures. Google Analytics, NameThatHost, Scarf and YouTube or Vimeo embeds wait for that choice. The banner comes in the server HTML, so it shows in the first frame and does not move the page.

c15t runs the banner and the consent records. The policy and the stored choices live in Inth, at tanstack-tanstack.inth.app.

Videos

Homepage bar on a large window. The bar covers the stats strip under the hero. When you scroll, it stops under the navbar until you choose.

sticky-bar-dark.mp4
sticky-bar-light.mp4

All window sizes. The homepage at 16 sizes from 390 × 844 to 3840 × 2160, then the floating banner on /blog at 5 sizes.

Too large will update later

What changes

Consent

  • ConsentRoot in src/routes/__root.tsx holds the visitor's consent state. The root loader gets that state on the server.
  • consentManifest() in vite.config.ts puts the Inth policy into the server build. The server does not ask Inth for the policy on each page. After a policy change in Inth, rebuild the site.
  • There are two categories. Measurement covers Analytics. Marketing covers the video embeds.
  • The homepage shows a bar (variant="bar"). All other pages show c15t's floating card.
  • Accept and Reject have the same button style, as the policy needs.
  • The footer has a "Privacy settings" link. It opens the consent dialog.
  • The dev tools get a c15t panel.

Scripts that wait for consent

  • Google Analytics loads through c15t's gtag helper with loadMode: 'after-consent', through the /_a proxy. Google gets no request, not even a cookieless Consent Mode ping, until the visitor allows Analytics. Page views from before the choice stay in a queue and go out only if the visitor accepts.
  • NameThatHost loads through c15t.
  • For a visitor who already accepted, c15t starts Google Analytics from the server HTML, before hydration. Traffic-quality tagging from Flag suspect direct sessions and log edge diagnostics #1399 wraps gtag before the first event, so every GA event still carries traffic_quality.
  • Live presence from Add live company dashboard and anonymous presence tracking #1442 starts only after the visitor allows Analytics, and stops if they withdraw it. Until then it sets no tanstack_live cookie and sends no heartbeats.
  • Server-side Scarf from Track server content requests separately in Scarf #1398 reads the stored c15t choice in the Worker. A browser request without Analytics consent is not sent to Scarf. Crawlers and agents send no Sec-Fetch-* headers, so they are still counted.
  • Scarf beacons and clipboard events stay in a queue until the choice. c15t adds no Scarf script. It only tells our Scarf code the decision.
  • Sentry still starts in src/router.tsx for all visitors, so it still catches hydration errors. Until a visitor allows Analytics, c15t removes user data, IP addresses and session IDs from the reports.
  • Markdown YouTube and Vimeo iframes show a placeholder until the visitor allows marketing. The placeholder has a "Privacy settings" button and a link to the video on YouTube or Vimeo.

Fonts

  • Bricolage Grotesque, IBM Plex Mono, DM Sans, Manrope, JetBrains Mono and the game's Inter now come from public/fonts. Before, Google Fonts got each visitor's IP address before any choice.
  • This also removes a render-blocking stylesheet from Google. Most of the LCP gain on desktop below comes from this change.
  • public/_headers caches /fonts/* for one year. If a font file changes, give it a new name.

Other

  • c15t is on 3.2.2, the stable release.
  • @sentry/* goes from 10.47 to ^10.76.1. The c15t integration for Sentry needs 10.67 or later.
  • src/utils/analytics/events.ts is now registry.ts. Lists in tracker blockers match the dev module URL /analytics/events.ts. With a blocker on, the page did not hydrate in dev.
  • A Vite plugin gives the Cloudflare Worker the real @c15t/tanstack-start/server entry. The browser export condition gives the Worker a stub that throws.
  • Commit 0a61c04 tells shared caches not to store server-rendered HTML. That HTML now holds each visitor's consent state, so a cached copy could show the banner again. A shared cache could also give one visitor's state to a different visitor.

Performance

Lab test, median of 7 runs, main at fe0b1ba5 against this branch. Both are production builds served locally behind a gzip proxy, as Cloudflare compresses responses. Requests carry cf-ipcountry: DE, so the visitor gets an opt-in policy and the banner. Mobile is 412 × 823 with the CPU slowed 4× on a 1.6 Mbps link with 150 ms latency. Desktop is 1350 × 940 with no throttle. Analytics beacons are blocked in both builds.

main Branch
Mobile /, LCP 1952 ms 1996 ms
Mobile /, Total Blocking Time 369 ms 404 ms
Mobile /, first paint / banner paint 1844 ms / — 1884 ms / 1954 ms
Mobile /, Reject All response (INP) — 104 ms
Mobile /blog, LCP 3816 ms 3592 ms
Desktop /, LCP 316 ms 148 ms
Desktop /, Reject All response (INP) — 56 ms
Desktop /blog, LCP 320 ms 308 ms
Mobile /, gtag.js request after first paint, visitor who accepted 134 ms 125 ms
CLS, / 0.0002 0.0002
CLS, desktop /blog 0.079 0.079
First-load JavaScript, gzipped 452 KB 587 KB
  • The banner is in the server HTML. On desktop it paints with the first paint in 7 of 7 runs. On throttled mobile it paints about 70 ms after first paint.
  • First load adds 135 KB of gzipped JavaScript: 101 KB in the entry chunk and 33 KB in c15t chunks that load in the first seconds. c15t 3.2.2 adds about 6 KB of that compared with the 3.0 alpha this branch used before. Total Blocking Time on mobile goes up by about 35 ms.
  • Desktop LCP drops by half because Google Fonts' render-blocking stylesheet is gone. Mobile LCP is within noise.
  • /blog has the same 0.079 CLS on main, so the floating banner adds none.
  • For a visitor who already accepted, Google Analytics starts as early as on main.
  • The sticky hook numbers from the first version of this PR were not measured again.

Checks

  • pnpm test
  • Lab test above, on a production build
  • Videos: homepage at 16 window sizes and /blog at 5, in light and dark mode
  • Accept, Reject and Customize on a preview deploy, with each choice checked in Inth

@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 3e0cfe9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@socket-security

socket-security Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​sentry/​tanstackstart-react@​10.47.0 ⏵ 10.78.099 +110087 +196 +1100
Updated@​sentry/​browser@​10.47.0 ⏵ 10.78.098 +191009196 +1100
Updated@​sentry/​node@​10.47.0 ⏵ 10.78.099 +210095 +196 +1100
Added@​c15t/​integrations@​3.2.29910010096100
Added@​c15t/​core@​3.2.210010010096100
Added@​c15t/​tanstack-start@​3.2.29710010096100
Added@​c15t/​react@​3.2.210010010097100

View full report

@KayleeWilliams KayleeWilliams changed the title Ask visitors for consent before loading trackers Add c15t to tanstack.com Oct 12, 2026
Loaders and server functions set Cache-Control on the whole response.
getOSSStats sets "public, max-age=300, stale-while-revalidate=3600", so
any page that loads stats, including the homepage, told browsers and
proxies they could reuse its HTML for 5 minutes and serve it stale for
up to an hour.

That was harmless while every visitor got the same HTML. With the
consent banner, the server renders the visitor's own consent state from
their c15t cookie: whether to show the banner, their recorded choices
and their subject ID. A visitor who accepted and opened the homepage in
a new tab got the cached pre-choice HTML from the browser's disk cache,
so the banner came back until c15t re-read the cookie on the next focus.
A shared cache could hand one visitor's consent state to another.

The hosting layer now sets "Cache-Control: private, no-cache" on every
HTML response and drops Cloudflare-CDN-Cache-Control. Browsers still
keep the page, but revalidate it on each visit. JSON, images and other
non-HTML responses keep the headers their routes set.
Add a c15t consent banner and hold Google Analytics, NameThatHost, Scarf
and video embeds until the visitor chooses. Sentry strips user data until
Analytics is allowed. Self-host fonts so Google Fonts no longer sees each
visitor's IP before a choice.
main (TanStack#1399) added traffic-quality tagging to the inline GA bootstrap, which
this branch replaces with c15t's gtag helper. Install the same wrapper in the
helper's onBeforeLoad, after gtag and config are queued and before held page
views flush, so every event still carries traffic_quality.
@KayleeWilliams
KayleeWilliams force-pushed the kaylee/add-c15t-consent-integration branch from d8e90e1 to a3102ce Compare November 13, 2026 17:00
main added two trackers after this branch began. Live presence (TanStack#1442) sets
the tanstack_live cookie and sends heartbeats, and server-side Scarf (TanStack#1398)
forwards the visitor's IP and user agent on every HTML request. Both
checked only DNT and GPC.

Live presence now starts when the visitor allows Analytics and stops when
they withdraw it. The Worker reads the stored c15t choice and skips server
Scarf for browser requests without Analytics consent. Only browsers send
Sec-Fetch-* headers, so crawler and agent requests, which carry no consent
cookie, stay in the server request dataset.
c15t 3.2.2 is the stable release after the 3.0 alphas this branch pinned.

- Google Analytics used the gtag helper's default loadMode, 'always', which
  loads gtag.js for every visitor and sends cookieless Consent Mode pings
  before they choose. It now uses 'after-consent', so no request reaches
  Google until Analytics is allowed, as the banner promises.
- c15t 3.2 starts helper scripts for visitors who already consented from the
  server HTML, before hydration. It does this only when the entry keeps the
  helper's own onBeforeLoad, so the override that installed traffic quality
  and flushed held events is gone. The first page view now does both, and
  onLoad flushes for a visitor who sends none.
- transport_url is a fixed https://tanstack.com/_a. The server renders the
  config, and window.location does not exist there.
- vendorDetails: undefined is gone. Since 3.1 the dialog shows vendor
  switches only when the site declares vendors.

This branch has not been deployed

No deployments
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