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:
/nr/agent/→ serves the New Relic browser agent/nr/data/→ receives beacon (telemetry) requests
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:
- Log into New Relic One (one.newrelic.com)
- Navigate to Browser → Add data → Browser monitoring
- Select the application (App ID) or create a new one
- Choose the Copy/Paste deployment method with the SPA loader
- 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
- A note:
<script src="/scripts/newrelic.js"></script>needs to be the first script you initialize, immediately after anymetatags.
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:
SSLProxyEngine onrequired because the upstream targets are HTTPSRewriteEngine Offprevents AEM's default rewrite rules from interfering with the proxy paths
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:
- Build and validate the application
- Deploy dispatcher configuration (including
.vhost, filters, and proxy rules) - Deploy CDN configuration (
cdn.yaml, including origin selectors) - Invalidate cache and promote changes across the environment
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:
-
CDN (
cdn.yaml)- Exclude
/nr/agent/*and/nr/data/*from Edge Delivery routing
- Exclude
-
Dispatcher (
.vhost)ProxyPass/ProxyPassReverserules for New Relic endpointsSSLProxyEngine on- Disable rewrites for proxy paths
-
Dispatcher filters
- Explicit allow rule for
/nr/*
- Explicit allow rule for
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
<https://https//...><https://> included in proxy valuesarborydigital.com/nr/agentsampling_rate in NREUM.init.session_replay or test with error to trigger 100% capturenewrelic object undefinednewrelic.js is the first <script> in head.htmlAbout The Author
Like what you heard? Have questions about what’s right for you? We’d love to talk! Contact Us