Drey / NDS verification

What the JavaScript actually does

The Drey article describes AutoMonitor in three sentences: it has 540 lines, it generates a session ID on line 7, it posts to analytics.infra.ndstudio.gov/metrics on line 8, and it "rewires the part of the browser that handles how a page talks to the outside world." All four claims are exactly true. What the article doesn't say is just how much the script captures, and how few of the standard guardrails a normal analytics tool would offer.

The full script source — sha256 cb4da9fc156568b09cdf9f01d4f343e204cbd4116db068e65099c74ba3ebb78e, served live from cdn.infra.ndstudio.gov/internal-analytics/script.js — reads as follows.


The architecture in one image

// Top-level structure of AutoMonitor:

class AutoMonitor {
  constructor() {                   // generates session-id, sets endpoint, starts
  init() / setupInstrumentation() { // wires up all the trackers

    instrumentImages()              // listens to every image load / error / cache hit
    instrumentFetch()               // REPLACES window.fetch with a wrapper
    instrumentXHR()                 // REPLACES XMLHttpRequest.prototype.open + .send
    instrumentButtons()             // click listener on every <button>, <a>, [role=button]โ€ฆ
    instrumentConsoleErrors()       // REPLACES console.error + console.warn
                                    // + window error + unhandledrejection listeners
    trackPageMetrics()              // Core Web Vitals (LCP, FID, CLS) + nav timing
    setupUnloadHandler()            // flush metrics on tab close + every 30s
  }

  track(name, duration, metadata)   // queues a metric onto this.metrics[]
  flush()                           // sends batch via navigator.sendBeacon
}

The piece that matters: three monkey-patches. AutoMonitor doesn't add a listener alongside window.fetch, XMLHttpRequest.send, and console.error. It replaces them with its own wrapper functions that call the originals after logging. From the moment the script loads, every network call the page makes โ€” to first-party APIs, to third parties, to anywhere โ€” and every console message and every JavaScript error is observed by this code before anything else.

This is not the usual way analytics libraries work. Most analytics tools add event listeners or hook into framework lifecycle methods. Monkey-patching low-level browser primitives is what tools like Sentry or DataDog do for application performance monitoring โ€” and even they make it opt-in. Here it is unconditional, on a federal government website, on a script that loads with no documentation, no privacy disclosure, and no cookieless or do-not-track respect.

Receipts: claim 25 โ€” line-by-line script verification ยท live source

What it records (every event type, with the fields it sends)

Each of these fires automatically โ€” no opt-in, no user-visible UI, no awareness on the visitor's part.

EventWhenFields sent
image_loadEvery <img> that finishes loadingfilename, width, height, load duration in ms
image_errorEvery image that fails to loadfilename, error type, duration
image_cacheEvery image served from the browser cachefilename, dimensions, estimated "render time" based on pixel count
api_call / api_errorEvery fetch() the page makesURL pathname, HTTP method, status code, ok flag, duration
xhr_call / xhr_errorEvery XMLHttpRequest the page makesURL pathname, method, status code, duration
button_clickEvery click on a button, anchor, role=button, [data-track], or input[type=submit/button]tag, type, visible text (up to 100 chars), id, full className, href, current page URL, click x/y coordinates, form id, form action
js_errorEvery uncaught JavaScript errormessage, source filename, line, column, full stack trace, page URL, user-agent string
promise_rejectionEvery unhandled async errorreason, stack, page URL
console_error / console_warnEvery console.error() / console.warn() the page emitsfull message (up to 500 chars), stack trace at call site, page URL
lcp / fid / clsCore Web Vitals as they firemetric value, page URL, referrer
page_load / dom_ready / first_byte / dom_parseOnce per navigation, 100ms after loadtiming values, page URL, referrer

Every payload above also carries: session (a UUID v4 generated once per page load, persisting across all events of that visit), timestamp (millisecond unix epoch), url (current page pathname), referrer (the page you came from).


How it sends the data

The transport is the interesting part. From line 434:

flush() {
  if (!this.metrics.length) return;
  const payload = this.metrics.splice(0);

  if (navigator.sendBeacon) {
    const success = navigator.sendBeacon(this.endpoint, JSON.stringify(payload));
    // ...
  } else {
    fetch(this.endpoint, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify(payload),
      keepalive: true
    })
  }
}

navigator.sendBeacon is the W3C API designed specifically for telemetry that has to survive page unload. It is fire-and-forget: there is no response, no way for the page to know whether the beacon arrived, no way for the user to cancel it. Browsers prioritize beacon delivery even during tab close, navigation, or browser exit. keepalive: true on the fallback fetch does the same thing.

This is the correct API for the use case, and it is also the API that gives the receiver maximum chance to receive the data regardless of what the user does. Pressing the back button does not stop a beacon. Closing the tab does not stop a beacon. The data leaves the browser.

Batch flushes happen on three triggers:

Receipts: AutoMonitor source lines 434โ€“459 (sha256 cb4da9fc1565โ€ฆ)

What's "privacy-preserving" โ€” and what isn't

The script makes two visible nods to privacy:

  1. cleanUrl() on line 461 strips ?query=string and #fragment from URLs before they are recorded. So a search query passed via querystring would not be captured here. The pathname is kept in full.
  2. Image filenames are truncated to just the trailing path component: img.src.split('/').pop(). So /images/private/document.jpg becomes just document.jpg.

That's the entire privacy story. Everything else is sent verbatim:

There is no IP-address stripping. (sendBeacon and fetch both carry the visitor's IP to the server by definition โ€” and the server is the one who would strip it, if anyone did.) There is no consent banner, no opt-out, no respect for the browser's Do Not Track header, and no documentation of any of this anywhere on ndstudio.gov.


Cross-site analytics audit

The Drey article identifies PostHog on TrumpRx and AutoMonitor on ndstudio.gov. We probed every NDS-built or NDS-fronted site we could reach and grepped for tracker markers in the served HTML.

SiteAutoMonitorPostHogGoogle AnalyticsNotes
ndstudio.govโœ“ loadedโ€”โ€”Studio's own homepage
trumprx.govโ€”โœ“ loadedโ€”Federal drug-pricing site
realfood.govโ€”โ€”โ€”MAHA nutrition site (Next.js โ€” trackers may live in chunked JS bundles, not the served HTML)
nasaforce.govโ€”โœ“ loadedโ€”NEW finding โ€” article didn't flag this
americabydesign.govโ€”โ€”โ€”Same caveat as realfood โ€” trackers may be in chunks
genesis.energy.govโœ“ loadedโ€”โœ“ loadedNEW finding โ€” DOE/Oracle AI initiative also runs Google Analytics on top of AutoMonitor
retire.opm.govโ€”โ€”โ€”OPM digital retirements

What this means:

Receipts: AutoMonitor + PostHog scans across 7 sites; live URLs preserved as snapshots

The receiving endpoint

We probed https://analytics.infra.ndstudio.gov/metrics directly from outside an embedded JavaScript context, with the appropriate Origin: https://ndstudio.gov header and a well-formed JSON payload. Four HTTP methods, four responses:

GET     /metrics  โ†’  no response (000)
POST    /metrics  โ†’  no response (000)
OPTIONS /metrics  โ†’  no response (000)
HEAD    /metrics  โ†’  no response (000)

"No response" here means the connection drops without the server sending HTTP status, headers, or body. A normally-configured endpoint that wanted to reject our probe would return 401, 403, 405, or 404. This one returns nothing at all.

That behavior is consistent with two things: aggressive IP-based or origin filtering at the network edge, or a WAF rule that drops requests not matching a session-cookie pattern. Either way, the endpoint is configured to take data only from the embedded script's context โ€” which is the configuration you'd choose if you wanted the only thing entering your collector to be the data your own JavaScript chose to send.

It's also the configuration that prevents outside researchers from understanding what the endpoint logs, what it retains, or how it's processed.


How this compares to commercial analytics tools

AutoMonitorPostHog (default)Google Analytics 4Plausible / Fathom (privacy-first)
Page-view trackingyesyesyesyes
Click tracking with element datayes (full)yesyesno
Click coordinates (x, y)yesyes (replay)nono
Full session replay (keystrokes, mouse)noyes (default on)nono
JS error capture with stack tracesyesyesnono
Monkey-patches fetch / XHRyes (unconditional)no (opt-in)nono
Monkey-patches consoleyes (unconditional)nonono
Consent banner / opt-out UInoneyesyesyes
Respects DNT headernoyes (configurable)yes (configurable)yes
Source publicly auditablescript is downloadable; the receiving infrastructure is notopen sourcenoopen source
Published privacy policy for the collectornoneyesyesyes

AutoMonitor sits in a peculiar spot. It is technically less invasive than PostHog session replay (no keystrokes, no mousemove, no input values). It is also less constrained than any commercial product on this list. There is no consent UI on any page that runs it. There is no published privacy policy or SORN for it. There is no public documentation of what the receiver does with the payloads. And the script monkey-patches three browser primitives unconditionally โ€” a level of instrumentation normally reserved for either application performance monitoring tools (which are usually opt-in and documented) or for tools whose explicit purpose is to capture user behavior comprehensively.


Who can ship code here โ€” and what's documented about them

The AutoMonitor script and the rest of the JavaScript shipped from cdn.infra.ndstudio.gov are produced by the National Design Studio's engineering staff. Most of that staff moved over from the Department of Government Efficiency. None of them require Senate confirmation, none appear on White House salary reports, and the Executive Office of the President has no inspector general.

One of those engineers has a documented history that bears on what a reader should weigh when reading the technical findings above. Edward Coristine is a former DOGE engineer (now full-time federal employee, GS-15) who moved from DOGE through SSA to NDS. Per the Borges SSA whistleblower disclosure footnote 3 โ€” which cites Krebs on Security (2025-02-28), Bloomberg (2025-02-07), and a series of Wired articles โ€” Coristine:

None of this means Coristine wrote the AutoMonitor source we analyzed in the preceding sections โ€” we have no commit-level attribution. What it means is that the personnel pipeline that ships code to ndstudio.gov, which has no inspector general and no Senate confirmation, includes someone whose pre-government history is publicly documented in these terms. The Borges disclosure also separately documents that Coristine resigned from DOGE in June 2025 and reappeared at SSA "days later," and that on 2025-08-11 he was one of three DOGE-affiliated names the SSA Chief Data Officer asked for information about an unsecured cloud copy of 300+ million Americans' Social Security data โ€” and received no response.

Receipts: claim 66 ยท claim 80 ยท people roster โ€” Edward Coristine

What we'd test next

  1. Render the NDS sites in a headless browser and capture the full set of network calls โ€” that resolves the "Next.js may load trackers in chunks" gap for realfood / americabydesign / retire.opm.
  2. Read the PostHog initialization on TrumpRx โ€” specifically whether session replay is enabled (default) or disabled. The presence of posthog.init({ disable_session_recording: false }) would settle the most invasive question outright.
  3. Diff the AutoMonitor script week-over-week โ€” if new event types or fields appear, that's the most direct signal of scope creep.
  4. Test the analytics endpoint with a synthetic browser session โ€” if we can capture a real sendBeacon payload from inside Chrome DevTools, we can compare it to what the script claims it sends. (No tampering โ€” just observation.)

All scans, source code, and live URL probes referenced here were fetched and SHA-256 hashed before being cited; the hash appears next to the receipt at each callout. The full AutoMonitor source is sha256 cb4da9fc156568b09cdf9f01d4f343e204cbd4116db068e65099c74ba3ebb78e. Live URLs are linked at each callout.