Article · Network & API Mocking for Reliable Tests

Intercepting Third-Party Analytics Scripts in Cypress

Analytics tags, consent banners, session recorders, chat widgets and A/B testing scripts have three properties that make them poison for a test suite: they load from someone else's infrastructure, they mutate the DOM on their own schedule, and they are entirely irrelevant to what you are asserting. Blocking them is one of the highest-yield stability changes available. This guide extends Cypress Network Interception Patterns with a policy for third-party traffic that keeps the suite fast without losing the coverage that actually matters.

12 sections URL: /network-api-mocking-for-reliable-tests/cypress-network-interception-patterns/intercepting-third-party-analytics-scripts-in-cypress/
What a third-party tag adds to every test External latency, unpredictable DOM mutation, consent overlays and console noise all enter the test through scripts unrelated to the assertions. page under testyour assertions external latencyvendor CDN, not yours DOM mutationoverlays, injected nodes consent bannercovers the click target console errorsfail strict error checks none of these relate to the behaviour under test, and all of them can fail it
Third-party tags contribute four independent sources of flakiness and zero assertions.

Root cause #

A third-party script is code you did not write, delivered by infrastructure you do not control, executing in the same document as the feature under test. Its latency is the vendor’s; its availability is the vendor’s; its behaviour can change on any given day when the vendor deploys. That alone makes it a non-deterministic input into every test that loads it.

The DOM effects are what turn that into concrete failures. Consent management platforms render a full-viewport overlay that intercepts clicks — a test that clicks a button “sometimes” fails because the banner appeared a few hundred milliseconds later than usual. Session recorders wrap event handlers and can swallow or delay them. Chat widgets inject a floating element that covers the corner of the page where a submit button happens to sit. Each is the moving-target problem from a source the application code does not even reference.

There is a second-order cost that shows up in reporting rather than in failures. Third-party scripts produce console errors and failed requests of their own, so any check that fails a test on an uncaught error or a broken asset — a useful check — becomes noisy and gets disabled. Blocking the tags restores the value of that check, because everything it then reports belongs to your application.

Step-by-step fix #

1. Block the tags at the network layer #

cy.intercept can force a request to fail or return an empty body before the script ever executes. A short allow-nothing list applied globally is usually the single biggest stability win available to a Cypress suite.

// cypress/support/e2e.js
// Trade-off: blocking by pattern is coarse and needs updating as vendors are
// added; the alternative is every spec inheriting four sources of flakiness.
const THIRD_PARTY = [
  '**/*.googletagmanager.com/**',
  '**/*.google-analytics.com/**',
  '**/*.hotjar.com/**',
  '**/*.intercom.io/**',
  '**/consent.cookiebot.com/**',
];

beforeEach(() => {
  for (const pattern of THIRD_PARTY) {
    cy.intercept(pattern, { statusCode: 204, body: '' });
  }
});

Returning an empty 204 is usually gentler than aborting: some loaders retry on a network error and log noisily, whereas an empty successful response simply produces a script that does nothing.

2. Stub the global the tag would have created #

Blocking the script means the global it defines never exists, and application code that calls it will throw. Provide a no-op stand-in before the app boots, and you get stability without changing production code.

// Trade-off: a stub keeps the app's call sites working and means the real
// vendor integration is never exercised — cover that separately, if at all.
cy.visit('/checkout', {
  onBeforeLoad(win) {
    win.dataLayer = [];                       // tag manager queue
    win.gtag = cy.stub().as('gtag');          // assertable analytics calls
    win.analytics = { track: cy.stub().as('track'), page: cy.stub() };
  },
});

Stubbing rather than merely blocking has a bonus: the analytics contract becomes testable. Asserting that checkout fires a purchase event with the right value is a real requirement in most products, and it is far easier against a stub than against a vendor’s network endpoint.

Block, stub, then assert Blocking removes the network dependency, stubbing keeps call sites working, and assertions turn analytics into tested behaviour. block the requestno vendor dependency stub the globalcall sites keep working assert the eventsanalytics becomes tested the third step converts a source of flakiness into a source of coverage
Blocking alone leaves the call sites broken; stubbing is what makes blocking safe, and assertions are the payoff.

A consent overlay is the single most disruptive third-party element because it deliberately blocks interaction. Seed the consent state before the app boots rather than clicking through the banner in every test.

// Trade-off: seeding consent skips the banner entirely, so keep one spec that
// exercises the real consent flow — it is a legal requirement, not decoration.
cy.visit('/', {
  onBeforeLoad(win) {
    win.localStorage.setItem('cookie-consent', JSON.stringify({
      analytics: true, marketing: false, version: 2,
    }));
  },
});

The timing rule is the same one that governs all pre-boot state: the value must be written before the application’s scripts read it, which is what onBeforeLoad guarantees and a post-visit write does not.

4. Assert the analytics contract deliberately #

Once the vendor globals are stubs, the events themselves are ordinary assertions — and they catch a category of regression that otherwise reaches production silently, because nobody notices a missing event until a report is empty at the end of the month.

// Trade-off: asserting event payloads couples tests to the tracking plan,
// which is appropriate — the tracking plan is a product requirement.
cy.findByRole('button', { name: 'Complete purchase' }).click();
cy.get('@gtag').should('have.been.calledWithMatch', 'event', 'purchase', {
  value: 42.5,
  currency: 'EUR',
});

5. Decide what happens to unknown third-party hosts #

A hard-coded blocklist ages: a new vendor is added and nobody updates the suite. Inverting it — allow your own origins, block everything else — keeps coverage of the policy itself, at the cost of occasional deliberate exceptions.

// Trade-off: an allowlist is stricter and will block a legitimate new
// integration until someone adds it, which is the feedback you want.
const OWN = /(^\/|localhost|\.example\.com)/;
beforeEach(() => {
  cy.intercept({ url: /^https?:\/\// }, (req) => {
    if (!OWN.test(new URL(req.url).host)) {
      req.reply({ statusCode: 204, body: '' });      // any third party
    }
  });
});

6. Keep one spec that runs with tags enabled #

Blocking everywhere means a broken tag configuration never fails a test. One tagged spec that loads the real scripts against a staging property preserves that coverage, and its occasional instability is contained.

Pitfalls #

  • Blocking the script but not stubbing its global. Application code calling gtag(...) throws. Mitigation: stub the globals in onBeforeLoad.
  • Aborting instead of returning an empty 204. Some loaders retry and log noisily on network errors. Mitigation: reply with an empty success.
  • Clicking through the consent banner in every test. Slow and timing-dependent. Mitigation: seed consent state before boot, and keep one spec for the real flow.
  • Setting consent after cy.visit(). The banner already rendered. Mitigation: write it in onBeforeLoad.
  • A blocklist nobody maintains. New vendors slip in and the flakiness returns. Mitigation: prefer an own-origin allowlist.
  • Blocking everything and never testing the tags. A broken tracking plan ships unnoticed. Mitigation: one tagged spec with real scripts.
  • Ignoring the widget that covers the button. Chat launchers sit exactly where submit buttons live. Mitigation: block them, and assert click targets are not obscured.
Blocklist versus own-origin allowlist A blocklist ages as vendors are added; an allowlist blocks new third parties by default and needs deliberate exceptions. blocklist explicit, readable, ages quietly a new vendor is unblocked by default own-origin allowlist new third parties blocked by default needs a deliberate exception to allow one
The allowlist fails safe: an unknown vendor is blocked until someone decides otherwise.

Reliability targets #

Metric Target Notes
Third-party requests in a test run 0 Excluding the one tagged spec
Failures caused by consent overlays 0 Consent seeded before boot
Analytics events asserted 1 per key conversion Purchase, sign-up, activation
Suite time saved by blocking 10–30% Vendor scripts are often the slowest requests
Console errors from third parties 0 Restores the value of strict error checks
Third-party isolation scorecard Targets for third-party requests, consent failures, asserted events and time saved. 0third-party requests 0consent failures 1event per conversion 10–30%time saved
Blocking third parties usually pays for itself in run time before any stability benefit is counted.

Frequently Asked Questions #

Q: Is blocking analytics hiding a real integration problem? A: It is deferring it deliberately, which is different. A functional test of checkout should fail when checkout breaks, not when a vendor’s CDN is slow. Keep the integration covered by asserting the events against stubs, plus one spec that loads the real tags — that combination catches a broken tracking plan without letting it destabilise everything else.

Q: The consent banner still appears even though I blocked the script. Why? A: Either the banner is rendered by your own application rather than the vendor, or the vendor script is served from a first-party proxy domain that the blocklist does not match. Check the request in the command log; a first-party proxy is common precisely because it evades ad blockers, and it evades your patterns for the same reason.

Q: Should I block third parties in Playwright too? A: Yes, and the mechanism is the same idea with a different API — a route that fulfils vendor URLs with an empty body, registered in a shared fixture. The policy should be identical across runners, or the two suites will have different stability characteristics for reasons unrelated to the application.

Q: Blocking a vendor script broke the page entirely. What now? A: The application depends on a global that script defines, and the dependency is unguarded. Stub the global in onBeforeLoad to restore the call sites, and treat the crash as a finding rather than an inconvenience: a page that white-screens when a third-party CDN is slow will do the same for real users on a bad connection. The stub keeps the suite green; the resilience fix belongs in the product.

Q: What about scripts that are genuinely part of the product, like a payment iframe? A: Those are dependencies, not decoration, and blocking them removes real coverage. Mock them at the boundary you control — a sandbox mode from the provider, or a stubbed iframe served from your own origin — rather than blocking them outright. The trade-off is the same one weighed throughout Network & API Mocking for Reliable Tests: fidelity where the behaviour matters, determinism everywhere else.