Skip to main content
Contact us
Revere Health responsive website

Project Story

Tracking on a healthcare website that waits for consent and keeps health details out of error reports

Revere Health's website runs on Next.js with Google Analytics, Google Tag Manager, Sentry, Google Maps and YouTube. In May and June 2026 we rebuilt how every one of them loads. Nothing optional runs until a visitor opts in, search terms and condition page paths are scrubbed out of every error report, declining clears the cookies already set, and maps load from Google only after a visitor turns them on.

Client
Revere Health
Industry
Healthcare
When
2026
Status
Live since May 2026
  • Next.js (Pages Router)
  • TypeScript
  • Google Tag Manager
  • Google Analytics 4
  • Sentry
  • Algolia
  • Google Maps Platform
  • Vercel

On a health site, the address bar is health information

Revere Health’s site has a page for every specialty, every clinic and each of its 400-plus providers, 2,861 pages at the September 2026 crawl. A visitor’s path through it says a lot. A visit to /specialty/oncology/ followed by a provider search for ?q=breast+lump is an ordinary page view to an analytics tool. It also says what the visitor is worried about.

So we hold the site to two rules. Nothing optional loads until the visitor says yes. And the services that run for everyone, error monitoring above all, never receive the search term or the condition page. The code comments cite HHS’s guidance on online tracking technologies, which treats searches on a provider directory as sensitive.

In May 2026 Revere asked us to go through every outside service the site talks to. The work went out as eleven pull requests between May 13 and June 22, 2026. Later changes have been built to the same rules.

Four categories, and three of them start off

Consent lives in a React context (ConsentContext) and is saved in the browser’s localStorage under privacy_consent_v1. There are four categories:

  • Necessary, always on. The site itself and Sentry error monitoring.
  • Functional. Google Maps and the Places “search by city” box.
  • Analytics. Google Analytics and Vercel Speed Insights.
  • Marketing, labeled “Advertising & Sharing” for visitors. Google Tag Manager, which carries the advertising tags for Meta, Google Ads, The Trade Desk and TVSquared.

Only Necessary starts on. TrackerScripts renders each script only when its category is true, so before a visitor decides, no optional script is in the page at all. GTM sits under marketing instead of analytics because the container holds ad pixels, and loading it on an analytics-only yes would mean the banner said one thing and the page did another. GA4 itself runs as a Google tag inside that container, so in practice it follows marketing consent too.

The banner’s Accept and Decline buttons are the same color, size and weight, with “Manage preferences” beside them. The preferences panel names the vendors in each category and has a “Do Not Sell or Share My Personal Information” button that turns analytics and marketing off in one click. A “Your Privacy Choices” link with California’s opt-out icon stays in the footer on every page. If the browser sends a Global Privacy Control signal, the site saves a decline and never shows the banner. The preferences panel says the signal was detected and honored.

Each saved choice carries updatedAt and a vendorListVersion. A choice older than 365 days is treated as missing, so the banner asks again. A choice saved under an older vendor list gets the same treatment, because a yes given before a vendor was added didn’t cover that vendor. The rule at the top of the file is that any change adding, removing or swapping a third-party vendor bumps the number. It’s at 2 now. The bump came when maps moved into their own functional category.

A made-up page view passes one visitor's consent choices, so the map loads, the advertising tags don't, and error monitoring gets the path with the specialty removed.

What error monitoring and our own events are allowed to see

The events we write ourselves carry as little as we can manage. Algolia runs the site search with its Insights click events off, so which result a visitor clicked doesn’t go out as an event. When we added scheduling click tracking in October 2026, each event (click_schedule_online, click_schedule_procedure, click_to_call, click_to_text) carried the department name and nothing more. No phone number or message text goes with it, and it fires only with marketing consent.

Sentry is harder, because it’s in the necessary category and runs for every visitor. An error report is only useful with context, and the default context is the URL, the route name and a trail of what the visitor clicked. So every event goes through sentryScrub.ts in three hooks, beforeSend, beforeSendTransaction and beforeBreadcrumb, in the browser, server and edge configs alike.

  • A path under a health-related prefix keeps the prefix and loses the rest, so /specialty/oncology/ becomes /specialty/[REDACTED]/. The prefixes are /search, /doctors, /specialty, /locations, /departments, /services and /live-better, the health blog.
  • Query keys that can carry a search or a condition (q, query, search, condition, symptom) and the Next.js route slug params are replaced with [REDACTED].
  • Prefetch URLs such as /_next/data/<build>/specialty/cardiology.json are unwrapped first, so they hit the same rules.
  • Server transactions named like GET /doctors/... have the method split off before the path is scrubbed. Passed through whole, the string gets mangled and the prefix never matches.
  • Click and input breadcrumbs are dropped outright. URLs inside the remaining breadcrumbs are scrubbed.
  • Events from the form endpoint, /api/sendEmail, lose the request body, cookies, headers and query string, plus their console breadcrumbs, since an email provider’s error message can echo what was typed.
  • If a URL won’t parse, a regex fallback applies the same rules. It never returns the original string.

Around that, sendDefaultPii is off, so no IP address or cookies are attached. Session replay isn’t installed. Sentry’s React component annotation is off too, because a component name like SpecialtyTemplate in a breadcrumb tells you what kind of page the visitor was on. Browser events go out through a same-origin /monitoring route.

The cost is real. An error on a specialty page reports as /specialty/[REDACTED]/, so the report alone doesn’t say which specialty. Finding it means reproducing the error.

A made-up Sentry error event being scrubbed in the browser, with each sensitive value replaced and the click breadcrumb dropped before anything is sent.

Maps wait for a yes, and videos use YouTube's no-cookie domain

Google Maps is the one outside service visitors use directly. MapFacade wraps the map component and renders the real map only with functional consent. Without it, a visitor gets MapFallback, a static image bundled with the site, a note that loading the map shares their IP address with Google, and an “Enable interactive map” button that opens the preferences panel. Specialty pages each have a static image of the clinics offering that specialty, 46 of them. The clinic list with addresses, phone numbers and directions is on the page either way, so declining costs a visitor the interactive map and nothing else. The Places autocomplete only mounts once the map has.

The site also doesn’t preconnect to maps.googleapis.com or maps.gstatic.com from its document head. A preconnect opens a connection on every page whether it has a map or not, and that connection gives Google the visitor’s IP address. The map SDK opens its own connections when it’s allowed to load, so the cost is one extra handshake on the locations page.

YouTube embeds go through toYouTubeNoCookieEmbed, which turns any YouTube URL format (watch, embed, youtu.be, shorts, live or a bare 11-character ID) into a youtube-nocookie.com/embed/ address. It runs inside the player component, so a video an editor adds next month gets converted without anyone having to remember. The host match is anchored, so a look-alike domain isn’t rewritten into a real embed.

A site-wide Referrer-Policy: strict-origin-when-cross-origin header and rel="noreferrer" on external links mean the scheduling services a visitor clicks through to never see which page they came from.

Declining clears what's already there

A visitor who accepted last month and declines today already has cookies like _ga, _fbp and _gcl_au on their device. Blocking future loads alone leaves them there. clearTrackingCookies.ts keeps two lists of anchored patterns, one for analytics and one for marketing, so _ga matches and a cookie that merely starts with _ga doesn’t. On decline it expires every match on the path and on each domain variant (the host, .reverehealth.com and the bare domain). Turning one category off in preferences clears only that category’s cookies.

A loaded script can’t be unloaded, and a running analytics tag will set its cookie again. So when a category goes from on to off, the page sets Google’s documented ga-disable-<id> flag and reloads, starting over without those scripts. A first decision never reloads, since nothing optional was loaded yet. A choice made in another tab arrives through the browser’s storage event and gets the same treatment.

How it's checked, and what isn't done

Honestly, this repository has no automated tests for the consent code. Each pull request carries a test plan we run by hand on a Vercel preview. Decline, then check DevTools for requests to Google and Meta and for leftover cookies. Accept, then confirm each category loads its own tags and no others. Set updatedAt in localStorage to a year ago and confirm the banner comes back. Turning those into browser tests is the obvious next step, and we haven’t done it.

The vendor-list rule depends on whoever adds a vendor remembering it. A pull request that forgets to bump the number won’t fail anything. And there’s still no server-side record of each decision. Consent lives in the visitor’s browser, so Revere can’t produce a log of who agreed to what and when.

We also don’t call this HIPAA compliance. Revere’s privacy policy, vendor agreements and legal review belong to Revere and its counsel. Our part is making the site do what the banner says.

Related case study

Revere Health

Questions about this project

HQ, a software design and development agency in Ogden, Utah, rebuilt how Revere Health's Next.js website loads analytics, advertising tags, error monitoring, maps and video in May and June 2026. Optional scripts load only after a visitor opts in by category, search terms and condition page paths are scrubbed out of Sentry error reports, declining clears tracking cookies already on the device, and the site honors the Global Privacy Control signal.

Revere Health's site, built by HQ, has four categories. Necessary is always on and covers the site and Sentry error monitoring. Functional covers Google Maps and Places autocomplete, Analytics covers Google Analytics and Vercel Speed Insights, and Marketing covers Google Tag Manager and the advertising tags inside it. Only Necessary starts on. A saved choice expires after 365 days, and it's also discarded when the site's vendor list version changes, so visitors are asked again whenever a vendor is added or removed.

Every Sentry event, performance transaction and breadcrumb passes through a scrubbing function before it's sent. Paths under health-related routes such as /specialty, /doctors and /search keep only the route prefix, query keys such as q, condition and symptom are replaced with [REDACTED], click and input breadcrumbs are dropped, and events from the site's form endpoint lose their request body, cookies and headers. If a URL can't be parsed, a fallback applies the same rules and never returns the original. HQ also turned off Sentry's default PII, session replay and React component annotation.

The site expires tracking cookies already on the device, such as _ga, _fbp and _gcl_au, across the site's domain variants, using anchored patterns so unrelated cookies aren't touched. If any optional category was previously on, the page sets Google Analytics' opt-out flag and reloads so the already loaded scripts stop running. The choice is saved and applied in other open tabs, and the site asks again after 365 days.

The interactive Google Map loads only when a visitor allows the functional consent category. Until then the site shows a static map image bundled with the site, with a note that loading the map shares the visitor's IP address with Google and a button to turn it on. The full clinic list with addresses, phone numbers and directions is always on the page, and HQ removed sitewide preconnect links to Google's map servers so pages without a map don't contact Google at all.

HQ doesn't describe its work on Revere Health's site as HIPAA compliance. The work makes the site's runtime behavior match its consent banner and keeps search terms and condition page paths out of error reports. Compliance also depends on vendor agreements, the privacy policy and legal review, which belong to the health system and its counsel. At the time of writing the site kept consent decisions only in the visitor's browser, with no server-side record.

Let's build together

Whether you're looking to design, build, update, or refresh an existing app or website, we can help. Even if we're not a good fit, we will help point you to somebody who might be.

Contact us