Some problems announce themselves loudly. Others hide in the gap between "it works on my machine" and "it works for everyone." This one lived firmly in that gap.

The symptom seemed simple: some users couldn't load our HubSpot-powered forms. The cause was reasonable enough; browser privacy settings and tracking blockers were stepping in and refusing to let anything resembling a tracker through. But the moment we tried to measure the problem, we hit a wall that turned a seemingly quick fix into something far more interesting.

The tool we'd normally reach for to catch these failures, real user monitoring, was being blocked by the exact same protections that were breaking the forms. Our New Relic RUM looked like a tracker because, well, it is one. We were effectively trying to install a smoke detector in a room where smoke was setting off a device that disabled smoke detectors.

This article walks through how we got around that paradox: proxying New Relic through our own first-party domain so it stops looking like third-party tracking, handling the complications that come with subdomains, keeping our telemetry data clean, and finally building reliable detection for HubSpot failures; even the silent ones where nothing loads at all. Along the way we'll cover the CDN and dispatcher wiring, testing in a rapid development environment, deploying through Cloud Manager, and the burn-in period that turns a clever workaround into a trustworthy metric.

An important note provided by New Relic:

When you use the proxy method, it is important to ensure that you have the right to do so based on any contractual, regulatory, or other legal obligations you may have to your end users and/or site visitors.

Making New Relic First-Party

Instead of loading New Relic directly from its standard endpoints, the solution was to proxy everything through our own domain. This shifts the requests from third-party → first-party, which avoids most tracking protections.

Two endpoints were introduced:

Hardcoding Proxy Values

The simplest way to configure first-party proxying in New Relic is to set the proxy paths directly in NREUM.init as static strings. You point assets at the URL that serves the New Relic agent script chunks, and beacon at the URL that receives telemetry data; both routed through your own domain rather than New Relic's CDN.

proxy: {
  assets: 'www.arborydigital.com/nr/agent',
  beacon: 'www.arborydigital.com/nr/data'
}

This is easy to reason about. The paths are visible at a glance, require no runtime logic, and are set before the agent initializes; so there is no timing concern about whether the configuration lands in time. As long as your server has those two routes configured and forwarding correctly to New Relic, it works. For sites that live on a single domain with no subdomain traffic, this is often all you need.

Proxying Across Subdomains

When your site uses subdomains, such as blog.arborydigital.com, a single hardcoded proxy path is not enough. Each subdomain operates from its own origin, and browser security rules mean that a request from blog.arborydigital.com to a proxy path hosted on www.arborydigital.com is technically a cross-origin request. While this may work, it defeats part of the point of first-party proxying, which is to send telemetry through the same origin the browser already trusts.

To solve this, the proxy configuration can be set dynamically at runtime. Rather than pointing to a fixed domain, the script checks window.location.hostname to determine which origin the page is currently running on. If that hostname matches your root domain or any of its subdomains, the proxy endpoints are constructed using window.location.origin — meaning blog.arborydigital.com will route its New Relic traffic through blog.arborydigital.com/nr-assets/ and blog.arborydigital.com/nr-beacon, while www.arborydigital.com uses its own equivalent paths.

This approach requires that each subdomain has the corresponding proxy routes configured at the server or CDN level. The New Relic agent configuration, including the beacon and errorBeacon fields in NREUM.info, also needs to be updated to reflect the current hostname so that the agent knows where to report data.

ajax.deny_list

The New Relic browser agent is configured to route all telemetry through a first-party proxy blog.arborydigital.com/nr/data instead of New Relic's own servers bam.nr-data.net. This avoids blockers that target known NR domains.

A side effect of this: the NR agent monitors all outgoing XHR/fetch requests on the page to capture them as AJAX events; including its own beacon calls going to blog.arborydigital.com/nr/data. Without the deny list, NR would record its own telemetry POSTs as AJAX events, which would then be sent as more telemetry, polluting AJAX data with internal NR traffic.

To fix this and avoid your data rates looking like something out of Akira, we can block these beacon calls by using the built in wildcard. This would end up looking like blog.arborydigital.com/nr/data/* . When this is implemented it tells New Relic to not create an ajax entry when browser data is successfully captured. All of the information inside the POST is preserved and there will no longer be a redundant ajax entry.

Using a Query Parameter as a Feature Flag

Gating the proxy behind ?proxy=yes lets you test the behavior in production without affecting any other visitors. Anyone without that parameter in their URL bypasses the block entirely and, in this case, New Relic would not fire. You open a tab with the parameter appended, confirm in DevTools that requests are routing through your own domain, and once everything checks out, you remove the condition from the script. From there, the hostname check takes over and the proxy activates for all traffic automatically.

Implementing the New Relic Agent

You can find the JavaScript snippet by:

  1. Log into New Relic One (one.newrelic.com)
  2. Navigate to BrowserAdd dataBrowser monitoring
  3. Select the application (App ID) or create a new one
  4. Choose the Copy/Paste deployment method with the SPA loader
  5. Copy the full generated <script> snippet

When you add the RUM script to your repository you'll want to make sure it lives inside of it's own newrelic.js file so we can call it inside of head.html

Once this is done you'll need to add the specific proxy settings into this string. In the NREUM.init block of the generated snippet, add the proxy config:

proxy: {
  assets: 'www.arborydigital.com/nr/agent',
  beacon: 'www.arborydigital.com/nr/data'
}

Wiring the Proxy (EDS / CDN Layer)

The proxy itself lives at the CDN layer. The goal is simple: forward incoming /nr/* requests to the real New Relic endpoints.

The origin selector must exclude /nr/agent/* and /nr/data/* so these requests are not routed to the Edge Delivery Services origin. Instead, they fall through to the default AEM publish origin where the Apache ProxyPass rules execute. Inside of your cdn.yaml that would look like this:

originSelectors:
  rules:
    - name: arbory-da
      when:
        allOf:
          - reqProperty: path
            like: /*
          # ...existing exclusions...
          - reqProperty: path
            doesNotMatch: /nr/agent/*
          - reqProperty: path
            doesNotMatch: /nr/data/*
      action:
        type: selectOrigin
        originName: arbory-da

You'll also need to edit your VirtualHost (.vhost) file as well:

# New Relic Browser Agent Proxy
SSLProxyEngine on

# Proxy New Relic agent JS chunks (lazy-loaded scripts)
<Location "/nr/agent/">
    ProxyPass "https://js-agent.newrelic.com/"
    ProxyPassReverse "https://js-agent.newrelic.com/"
    RewriteEngine Off
</Location>

# Proxy New Relic beacon/telemetry data
<Location "/nr/data/">
    ProxyPass "https://bam.nr-data.net/"
    ProxyPassReverse "https://bam.nr-data.net/"
    RewriteEngine Off
</Location>

Key details:

Detecting HubSpot Failures (Even When Nothing Loads)

Once New Relic is unblocked, the next problem is that HubSpot failures don’t always throw obvious errors; especially when the script never loads.

To handle this, two detection paths were added:

1. Script Load Failure

Detect when the HubSpot script fails to load entirely:

script.onerror = () => {
  reportError('HubSpot script failed to load');
};

2. Runtime Failure

Catch errors after the script loads:

window.addEventListener('error', (e) => {
  if (e.filename && e.filename.includes('hsforms')) {
    reportError('HubSpot runtime error');
  }
});

Both feed into the same reporting function:

function reportError(errorMsg) {
  console.error(errorMsg);
  if (window.newrelic) {
    window.newrelic.noticeError(new Error(errorMsg));
  }
}

Verifying It Actually Works

Once everything is wired up, validation is pretty straightforward but still important. Testing should focus on browsers with strict tracking protection/extensions enabled, where you should see HubSpot forms fail to load, errors appear in the console, and those same errors show up in New Relic. In DevTools, confirm the agent loads from /nr/agent/ and that beacons are sent to /nr/data/, with no requests going to third-party domains like bam.nr-data.net. In New Relic, verify that errors are appearing in the dashboard, events match the triggered failures, and sessions are being recorded. If sessions are still missing, the proxy likely isn’t configured correctly

Testing with a rapid development environment (RDE)

If you're one of our habitual readers you might remember an article I wrote about using a rapid development environment (RDE) to test CDN config changes. When testing this new RUM I used exactly that!

When following best practices, development will occur on a branch. To make sure your changes are being made to a branch instead of main you'll need to make additional changes to cdn.yaml :

    origins:
     # ...existing code...
      - name: arbory-da
        domain: (BRANCH_NAME)--(rest of domain).aem.live
        forwardHost: false

There are two main commands you'll need once you are ready to push your changes:

aio aem:rde:install -t env-config ./config : this will push the contents of your /config and the exclusions we made earlier.

aio aem:rde:install -t dispatcher-config ./dispatcher/src : this will push the contents of your /src—namely the changes in your dispatcher configuration .vhost .

Since the RDE functions on a different domain than your other environments you'll need to specify a new url to proxy through. Otherwise it will be seen as a third-party domain:

proxy: { 
   assets: 'your-rde-domain.aem.live/nr/agent', 
   beacon: 'your-rde-domain.aem.live/nr/data' 
}

Cloud Manager Pipeline Deployment (Dev + Prod)

Deploying this through Adobe Cloud Manager requires a bit more coordination than a typical code change. The proxy behavior depends on multiple layers (CDN, dispatcher, and browser), and all of them need to be aligned within the same deployment.

How the Deployment Works

A pipeline execution will:

For this implementation, the dispatcher and CDN changes are tightly coupled. The proxy will not function correctly unless both are present and consistent.

Required Changes (Must Deploy Together)

The following updates need to be included in the same pipeline run:

If any of these are missing or out of sync, requests will either be misrouted, blocked, or fall back to third-party endpoints.

Burn In Period

Now that the proxy has been configured and deployed, you'll need to have a burn-in period to ensure everything is working as expected. Since failures depend on real user conditions, like privacy settings, extensions, and network behavior, it will take some time to get a complete picture.

Once you've taken some time and verified that data is being properly ingested, the next step is to review that data; validate error rates against expected traffic, ensure coverage across environments, and refine any gaps (missed events, inconsistent domains, or edge-case blockers). From there, you can begin treating this as a reliable metric, trend over time, and use to quantify real user impact rather than anecdotal reports.

Final Thoughts

What started as "just add some monitoring" turned into a tour through browser privacy models, first-party proxying, CDN routing, and the subtle art of measuring something that, by design, resists being measured.

The core insight is worth holding onto: when the thing you're trying to observe and the thing you're observing it with are both treated as unwelcome by the browser, you can't monitor your way out with off-the-shelf tools. You have to change how the browser perceives your telemetry. Proxying New Relic through our first-party domain did exactly that—shifting requests from third-party to first-party, sidestepping the blockers, and giving us a genuine window into failures that were previously invisible.

But the proxy was only half the battle. Detecting HubSpot failures, especially the silent ones where the script never loads, required deliberate handling of both load errors and runtime errors, all funneled into a single reporting path. And none of it counts for anything until the data proves itself during the burn-in period, where real user conditions replace assumptions with evidence.

The payoff is the ability to trade anecdotes for numbers. Instead of "some users say the form won't load," we now have a measurable, trendable signal that quantifies real user impact. Sometimes the most valuable fix isn't the one that solves the problem outright; it's the one that finally lets you see it clearly. And from there, you can actually start to improve it.

Troubleshooting

Issue
Cause
Fix
CORS error with doubled <https://https//...>
<https://> included in proxy values
Use bare hostname only: arborydigital.com/nr/agent
No data in New Relic
Script not loading or proxy misconfigured
Check browser DevTools Network tab for NR requests; verify proxy reverse rules
Session Replay not recording
Sampling rate is 10%
Increase sampling_rate in NREUM.init.session_replay or test with error to trigger 100% capture
newrelic object undefined
Script load order wrong
Ensure newrelic.js is the first <script> in head.html

About The Author

Noah Mattison

Technical Delivery Manager at Arbory Digital

Noah is a Computer Science graduate from UNC Wilmington and a 2x certified AEM Developer. Originally on a pre-med track with a passion for cancer detection and AI, he brings a problem-solving mindset to his work at Arbory. He helps plan project execution, delegates work across teams, maintains strong communication, and supports internal operations. He believes in Arbory’s community and vision, and outside of work, he’s environmentally focused and enjoys staying active outdoors, including camping, surfing, rock climbing.

Contact Noah on LinkedIn

Like what you heard? Have questions about what’s right for you? We’d love to talk! Contact Us

category
AEM Technical Help
tags
AEM
number of rows
1