Every regulated DTC site I audit has at least one script the current team cannot explain, firing on a page where it should not be. The audit that catches this is a forty-five minute exercise with Chrome DevTools, the GTM admin, and a spreadsheet. The layout of the spreadsheet is what decides whether the audit changes anything. This is the workflow I run, the schema I write to, and the playbook for what to do when a vendor refuses to sign.
By the end you will have a network capture for three representative pages, a complete tag inventory from your GTM container, a vendor-by-vendor inventory, a classification of each vendor against BAA status and PHI access, and a prioritized action list. You will also have a decision tree for the inevitable case where a vendor will not sign at all.
Prerequisites
You need three things. A modern Chromium browser with DevTools (Edge, Brave, or Chrome itself). A spreadsheet, Notion board, or Airtable base for the inventory. Admin or read access to your GTM container and to any server-side container you run.
You also need a mental map of which pages on your site can carry protected health information or signal a user's health condition. The HHS Office for Civil Rights December 2022 bulletin and its March 2024 update made unauthenticated condition-relevant pages part of the scope. A symptom checker, a condition-specific landing page, or a treatment explainer all count, even though no member is logged in. If you have not drawn the PHI boundary yet, do that first; the regulated Next.js boundaries map walks through where the boundary lives across the App Router primitives.
Step 1: capture the network waterfall on three representative pages
Open the page. Open DevTools. Click the Network tab. Check "Disable cache" and "Preserve log". Hard reload (Cmd+Shift+R or Ctrl+Shift+R). Let the page sit for thirty seconds with no interaction. Then export a HAR file by right-clicking inside the network pane and choosing "Save all as HAR with content".
Pick three pages from three different page classes. The homepage. A condition or product detail page that signals a health-relevant intent. An authenticated member-area page if you have one. DTC sites usually run different tag stacks per class: marketing pages load campaign pixels, product pages may load A/B test scripts, the member area sometimes loads error trackers or session replay tools that the marketing pages do not.
Once you have the HAR for each page, group the requests by domain and write the unique third-party domains into a working list. This list is your audit's source of truth, not the GTM container. GTM is one of several places a script can come from. A theme could hardcode a <script> tag. A platform plugin could inject one. A previous developer could have inlined a snippet in a layout. The browser does not care where the request originated; it just makes the request.
# Extract unique third-party domains from a HAR file with jq
jq -r '.log.entries[].request.url' page.har \
| awk -F/ '{print $3}' \
| sort -u
Anything in that output that is not your own domain or a CDN you control is a candidate for the inventory.

Step 2: read the GTM container, every tag and trigger
In the GTM admin, open Workspace > Tags and enumerate every tag. Note the tag type, the trigger that fires it, and the variables it depends on. Do the same for Triggers and Variables. Export the container as JSON via Admin > Export Container; that file is your permanent record of the starting state and what you will diff against on the next audit.
If you run a server-side container, repeat the process there. The server container has its own clients, tags, and triggers, and a vendor that has a BAA on the web side may not have one on whatever the server container forwards data to.
Two patterns to flag during the read. Tags scoped to "All Pages" that should be condition-scoped: a retargeting pixel set to fire on every page view will fire on condition pages too, which is exactly what the OCR bulletin treats as a disclosure. The fix is a trigger condition that excludes condition-relevant URL paths. And tags whose triggers reference deprecated events or unresolved variables: they look inert in the admin but can still fire under specific conditions, and only the waterfall capture from Step 1 tells you whether they actually do.
Now compare the GTM tag list to the network waterfall list. Anything in the waterfall but not in GTM is a script your theme or platform is loading independently. Those are the hardest to remove because they require a code change, not a tag change. Note them on the inventory with their entry point.
Step 3: build the vendor inventory schema
The inventory is what makes this audit a living document instead of a one-time deliverable. The schema needs eight columns.
| vendor | category | what it loads | where (page paths) | data sent | BAA status | alternative | owner |
|--------------|--------------------|----------------------|--------------------|------------------------|--------------------|-----------------------|-------------|
| Vendor A | Web analytics | gtag.js + sendBeacon | All pages | URL, IP, user agent | Available, unsigned| Server-side w/ BAA | Marketing |
| Vendor B | Session replay | recorder.js | All pages | DOM snapshots | Not available | Self-hosted alt | Engineering |
| Vendor C | Transactional email| api call from server | Backend | Member email, name | BAA signed | - | Engineering |
| Vendor D | Chat widget | widget.js + ws | Marketing pages | Chat transcript | Not available | Vendor with BAA path | Support |
The status column is an enum: BAA signed, Available unsigned, Not available. Three values, no fuzziness. "We have an MSA" is not a BAA. "They sent us a draft" is not a BAA. Either there is a fully executed business associate agreement on file, or there is not.
The alternative column is the most underrated part of the schema. When a vendor refuses to sign, the inventory needs to record what you would do instead, so the next person who reviews it does not have to redo the analysis. A blank alternative column means the audit is incomplete.
The owner column matters because regulated DTC orgs lose track of vendors precisely when ownership transfers. The previous marketing lead added the chat widget, then left, and now nobody is responsible for renewing or reviewing it. Forcing an owner per vendor makes the loss-of-ownership case visible the next time someone scans the sheet.
The schema is portable across Notion, Airtable, Google Sheets, or a lightweight Postgres table. I have run this audit with the inventory living in all four. The format does not matter; the discipline of filling it in does.

Step 4: classify each vendor against BAA and PHI access
Each row in the inventory falls into one of four classes once the audit is complete.
BAA signed, data-minimal. A transactional email vendor that only ever receives a confirmation email and the member's first name. A CDN with a signed BAA that proxies cached static assets and only sees IP. The safe rows.
BAA signed, data-rich. An error tracker that captures stack traces and request bodies. A session-replay vendor that captures DOM snapshots. The BAA covers you legally; the data volume is still a blast radius if the vendor is breached. These rows need PHI scrubbing at the source: strip request bodies, mask member identifiers, and configure the SDK to skip authenticated routes if you can.
No BAA, no PHI access. A static-asset CDN that only serves your own JS bundles and never sees a member identifier. An open-source analytics tool you self-host. Fine as long as the "no PHI access" claim holds. The audit checks the claim, not just records it.
No BAA, has PHI access. The fourth class is where the work is. Meta Pixel firing on a condition page. GA4 collecting URL paths that include a member ID. Hotjar session replay running on the appointment booking page. Every row in this class needs an action in Step 5.
Two patterns repeat in the fourth class on every regulated DTC audit I run. A pixel that was added during a campaign launch and never had its trigger conditions reviewed afterward. A free-tier vendor that was selected because the team did not realize the BAA tier would be necessary. The pattern is not that someone made a deliberate choice to send PHI to a non-BAA vendor; it is that nobody noticed the boundary moved when the business added a feature.
For the mechanics of evaluating a specific vendor's BAA, the BAA vendor audit checklist covers the questions to ask before signing. The list of vendors that do not offer a BAA at all (Meta consumer Pixel, free-tier GA4, LinkedIn Insight Tag) is depressingly stable; if a vendor is on that list in the BAA column, the row goes straight to Step 5.

Step 5: act on the gaps in priority order
First, remove anything no current team member can justify. The audit will surface tags from campaigns that ended, agencies that left, and tools that were evaluated and forgotten. Removing these is a same-day fix in GTM and a one-line revert in the theme. The action with the highest payoff, because the data flow stops immediately and there is no replacement to negotiate.
Second, for kept-but-unsigned vendors that have a BAA path, request the BAA. Most vendors with a BAA tier require an enterprise plan or a healthcare add-on. Get the legal contact on the vendor's side, send your standard BAA template, and put the conversation on a deadline. While you wait, scope the vendor's tag to non-PHI pages via GTM trigger conditions so the disclosure stops in the meantime.
Third, for vendors with no BAA path, decide between scoping to non-PHI pages forever, replacing the vendor, or dropping the data source entirely. The decision is in the next section.
Fourth, document the rest in the inventory with the alternative noted, and put a quarterly review on the calendar. Add a script-change trigger to the review: any time the marketing team adds a new tag, the inventory is updated and re-classified before the tag goes live.
The browser-level complement to this discipline is a Content Security Policy. Once the inventory is clean, configure CSP script-src and connect-src directives that explicitly allow only the domains in the inventory. CSP report-only is the right starting place; collect violations for two weeks before flipping to enforce. The CSP catches a future tag added by someone who skipped the inventory.
What to do when a vendor refuses to sign
The most common situation in a regulated DTC audit. The marketing team relies on Meta Pixel for retargeting. Meta does not offer a BAA for the consumer Pixel. The audit just classified the Pixel as "no BAA, has PHI access" because it fires on a condition page. The marketing lead is not going to accept "remove it" as the only answer.
There are four real options.
Option one: scope the script to pages with no PHI or condition signal. In GTM, this is a trigger condition that excludes condition-relevant URL paths from the firing rule. The Pixel still runs on the homepage and the generic landing pages; it does not run on the condition pages or the member area. Marketing keeps most of the retargeting value. The compliance posture holds. This is the right answer for most cases.
Option two: switch to a server-side approximation. Run a server-side GTM container that receives events from your site, strips identifiers and any condition-signal fields, and forwards a sanitized event to the vendor. The Pixel still fires, but the payload no longer includes anything the OCR would treat as PHI. This works for measurement but not for retargeting; the cookies the vendor needs for retargeting are set by the browser, not the server. The server-side CAPI walkthrough covers the field-stripping pattern for the Meta Conversions API specifically.
Option three: replace the vendor with one that does sign. The replacements are usually a step down in feature parity but offer the BAA. Self-hosted PostHog or Plausible for product analytics. A covered analytics platform built for healthcare. A different chat vendor with a BAA tier. The cost is real; the audit's job is to put it on the table so the business can decide.
Option four: drop the data source. The radical option, and sometimes the right one. If the data the vendor produces is not used in any decision the business makes, the vendor is overhead, not value. The audit often surfaces a vendor that has been firing for two years and was last logged into eighteen months ago.
The decision criterion is the marketing impact versus the compliance posture, with the compliance posture as the floor not the ceiling. A vendor that drives ten percent of revenue and refuses to sign is a different conversation than a vendor that drives nothing measurable. Both go into the inventory with their alternative noted; only one gets a heated discussion in the next leadership meeting.
“Compliance is the floor, not the ceiling. The marketing impact is what gets traded against alternatives.
”
Common mistakes the audit catches
Auditing only the homepage. The homepage is usually the cleanest page on the site because it is the most visible. The condition pages and the member area carry the actual risk and they are where new tags get added without scrutiny.
Trusting GTM as the source of truth. The network waterfall in Step 1 will surface scripts GTM does not know about. Theme-hardcoded scripts, platform-injected scripts, and old snippets in custom layouts all bypass GTM. The waterfall is non-negotiable.
Forgetting subdomains. Most regulated DTC operators run a marketing site on one subdomain and a member portal on another. Each subdomain often has its own tag stack. The audit is incomplete until every subdomain in scope has been captured.
The consent banner gets missed often. The banner is itself a third-party script, and many consent banners ship their own analytics about how users interact with the banner. The banner's privacy policy may say "we help you comply"; the network behavior may include unsigned analytics. The waterfall sees both.
A subtler mistake: failing to capture what the script does once consent is granted. Many cookie banners change cookie writes but do not change the scripts loaded. The Pixel still loads; it just does not write its primary cookie. From the OCR's perspective, the disclosure happens when the script makes the network request, not when the cookie is written. Consent does not undo the disclosure unless the script is gated on consent before it loads.
What to try next
Automate the network waterfall. A short Playwright script that loads each representative page, captures the HAR, and diffs the unique third-party domains against the inventory will tell you when something new appears. Run it on a weekly cron and post the diff to Slack; the audit becomes self-monitoring.
Set up CSP in report-only mode. Violation reports go to a collector you run, without blocking anything. After two weeks of reports you have a real list of every domain the browser is contacting, and you can flip the policy to enforce mode with confidence. The audit-logging companion covers the server-side ingestion pattern for CSP violation reports if you want to keep the data structured.
Pair the script audit with a privacy audit. The script inventory tells you what is loaded; the privacy gaps tell you what the loaded scripts are doing legally. The GDPR and CCPA gaps post covers the consent and disclosure gaps that are common alongside script gaps. The two audits run on the same page set, with adjacent inventories.
For a productized version of this exact workflow, the productized DTC stack audit runs the whole audit and delivers the inventory and recommendations as a written report. The case study at the server-side tracking rebuild is what the after-state looks like once the audit's actions have been implemented end to end.

FAQ
Do I need a BAA for analytics if my marketing pages have no logged-in users?
The OCR's March 2024 update extended the BAA requirement to unauthenticated pages that signal a user's health condition. A symptom checker, a condition-specific landing page, or a treatment explainer can be in scope even without a login. The test is whether the page combined with the data sent (URL path, IP, user agent) could identify a user with a specific health interest. If yes, the analytics vendor receiving that data needs a BAA.
Does GTM consent mode solve this on its own?
No. Consent mode changes how a script behaves once consent is collected; it does not prevent the script from firing in the first place if the trigger conditions allow it. From the OCR's perspective, a network request from a covered entity's site to a non-BAA vendor is a disclosure regardless of cookie state. Consent mode is useful for GDPR and CCPA compliance; it is not a HIPAA defense.
Server-side GTM with a server-container host, does that count as a BAA gap?
The server-container host is itself a vendor that needs a BAA if PHI flows through the container. Some hosts offer a healthcare tier with a BAA; others do not. Audit the host the same way you audit any vendor. Beyond the host, the downstream vendors the server container forwards events to are separate vendors and need their own BAAs. Server-side GTM does not collapse the supply chain; it just moves the entry point.
How often should I re-audit?
Quarterly is the floor, with a script-change trigger as the more rigorous discipline. Any time the marketing team adds a new tag, requests a new vendor evaluation, or changes a trigger, the inventory is reviewed before the change goes live. The Playwright cron job mentioned earlier surfaces drift between scheduled audits.
What is the fastest version of this if I have one day?
Capture the network waterfall on three pages (one hour). Read the GTM container and export it (one hour). Build a six-row inventory of the top six third-party domains by request volume (one hour). Classify each row against BAA status using the vendor's public privacy and security pages (two hours). Write three sentences per row of action plan (one hour). The remaining time is for the email to the marketing lead with the gap list and the proposed actions.
Sources and specifics
- The audit workflow uses Chrome DevTools Network panel, the GTM admin (web container plus any server container), and a portable inventory in Notion, Airtable, Google Sheets, or a lightweight Postgres table.
- The pattern has been run on regulated DTC operators in 2024 and 2025; the inventory schema is reused unchanged across operators in different verticals.
- The BAA threshold for tracking technologies is informed by the HHS Office for Civil Rights December 2022 bulletin and the March 2024 update, which extended scope to unauthenticated condition-relevant pages.
- The audit cadence recommendation is quarterly with a script-change trigger; weekly automated drift detection is the layer above.
- The four-class classification (BAA signed and minimal, BAA signed and data-rich, no BAA and no PHI access, no BAA and has PHI access) is the working frame; only the fourth class requires immediate action.
