Fix the Bloomreach Engagement SDK Double-Load Warning in GTM

Teal cover. Headline — Fix the SDK Double-Load in GTM. Right side — a browser console warning that the SDK loaded twice under Consent Mode above a one-line JavaScript guard that prevents the second load. Subheadline — Consent Mode fires your template twice.

A GTM Custom Template behaves differently from a Custom HTML tag when Consent Mode queues a release. Here's why your exception trigger looks green in Tag Assistant but the SDK still loads twice, and the single-line guard that ends the warning for good.

The warning

Open DevTools → Console on a site that loads Bloomreach Engagement through the GTM Gallery Custom Template, accept the cookie banner on a fresh visit, and you may see this:

Exponea [timestamp] The SDK is already available on 
the window, maybe the SDK file is downloaded twice

Exponea [timestamp] The SDK is already available on 
the window, maybe the SDK file is downloaded twice

Exponea [timestamp] The SDK is already available on 
the window, maybe the SDK file is downloaded twice

It fires on the first page load of every fresh-consent session. Event tracking still works. Session tracking still works. The warning is a diagnostic signal from the SDK saying: something initialized me, and then something initialized me again.

We tracked this through three diagnostic sessions before we understood what was producing it. The answer is a combination of two GTM quirks that interact in a way most guides don't cover.

Why the obvious fixes don't work

The setup that produces the warning is a conventional, consent-compliant one:

  • Bloomreach Engagement Tag (Custom Template from the Gallery)

  • Two firing triggers: DOM Ready and cookie_consent_update

  • Consent Settings → Additional Consent Checks require analytics_storage

The intent is straightforward. Returning visitors with stored consent get the tag on DOM Ready. New visitors who click Accept get the tag on cookie_consent_update. The consent check gates both paths.

The first instinct when you see the double-load warning is to add an exception trigger: a Custom JavaScript variable that checks whether window.exponea.track is already a function, and a Blocking Trigger that fires on DOM Ready or cookie_consent_update when the variable returns true. The exact pattern that works for Hotjar on the same site.

It does not work for the Bloomreach Engagement Custom Template. Tag Assistant shows the exception filter evaluating correctly, all green checks, and the tag still fires. This is the first quirk.

Quirk 1: GTM Custom Templates don't honor exceptions the same way Custom HTML does

A GTM Gallery Custom Template is sandboxed. It runs in a restricted permission model that does not behave identically to a Custom HTML tag, including in how it interacts with Blocking Triggers. In our own debugging, an exception trigger with a passing filter blocked a Custom HTML tag (Hotjar) but did not stop the Custom Template tag (Bloomreach Engagement) from firing under the same container, same workspace, same trigger configuration.

We have not found a specific line in GTM's documentation that explains this. The observation stands: exception triggers that work reliably on Custom HTML tags are not a reliable defense against a Custom Template tag initializing twice. If your article or StackOverflow answer says "add an exception trigger," verify it on your own container before relying on it.

Quirk 2: Consent Mode releases queued tags without re-evaluating firing filters

The second quirk is harder to spot and, once seen, harder to unsee.

With Consent Mode v2 and Additional Consent Checks requiring analytics_storage, the DOM Ready firing of the Bloomreach Engagement Tag is held by the consent framework. The tag is matched to its trigger, but not executed. It sits in a pending state.

When the user clicks Accept on the cookie banner, Framer pushes cookie_consent_update to the dataLayer, consent flips to granted, and GTM releases the pending DOM Ready tag. This is intended behavior. The surprise is what GTM doesn't do on release: it does not re-evaluate the firing trigger's filter conditions.

If your firing trigger had a filter like JS - exponea not yet loaded equals true, that filter was evaluated before the tag was queued, when no SDK was on the page. By the time the tag gets released, the cookie_consent_update firing has already loaded the SDK. The filter would now evaluate false. But GTM doesn't check. The queued DOM Ready tag runs. The SDK loads a second time.

You can verify this in GTM Preview. At the cookie_consent_update event, the Bloomreach Engagement Tag shows two firing triggers as red X (filters failed), and a firing status of Succeeded. The red X tells you the filters say don't fire. The Succeeded tells you GTM fired it anyway, because at the moment it was queued the filters said go, and consent release doesn't re-evaluate.

Why the combination produces the warning

Put the two quirks together. The Custom Template ignores your exception trigger, and the released queued tag ignores your firing-trigger filter. On the first fresh-consent page load, the SDK loads via cookie_consent_update, then loads again via the queued DOM Ready release, and the SDK self-reports the duplicate:

The SDK is already available on the window, maybe the SDK file is downloaded twice
The SDK is already available on the window, maybe the SDK file is downloaded twice
The SDK is already available on the window, maybe the SDK file is downloaded twice

The warning is accurate. The SDK was downloaded twice. Your filter was correct. Your exception trigger was configured properly. The GTM interaction between Custom Template sandboxing and Consent Mode queue release is what made both defenses ineffective.

The fix: inline guard inside a Custom HTML tag

The pattern that works moves the guard from GTM's firing logic into the tag body itself. A Custom HTML tag with a page-level flag set on the first line cannot be tricked by queue release, because the flag is checked at tag execution time every time, not at trigger-evaluation time.

Create a new Custom HTML tag in GTM, name it something like Bloomreach Engagement Tag (HTML), and paste:

<style>.xnpe_async_hide{opacity:0 !important}</style>
<script>
(function () {
  if (window.__brEngagementInitialized) return;
  window.__brEngagementInitialized = true;

  !function(e,n,t,i,r,o){/* ... Bloomreach SDK loader snippet ... */}(
    document,"exponea","script","webxpClient",window,{
      target: "https://api-XXX.exponea.com",
      token: "YOUR-PROJECT-TOKEN",
      experimental: { non_personalized_weblayers: true },
      new_experiments: { mode: "async" },
      track: {
        visits: true,
        google_analytics: false
      }
    }
  );
  exponea.start();
})();
<

<style>.xnpe_async_hide{opacity:0 !important}</style>
<script>
(function () {
  if (window.__brEngagementInitialized) return;
  window.__brEngagementInitialized = true;

  !function(e,n,t,i,r,o){/* ... Bloomreach SDK loader snippet ... */}(
    document,"exponea","script","webxpClient",window,{
      target: "https://api-XXX.exponea.com",
      token: "YOUR-PROJECT-TOKEN",
      experimental: { non_personalized_weblayers: true },
      new_experiments: { mode: "async" },
      track: {
        visits: true,
        google_analytics: false
      }
    }
  );
  exponea.start();
})();
<

<style>.xnpe_async_hide{opacity:0 !important}</style>
<script>
(function () {
  if (window.__brEngagementInitialized) return;
  window.__brEngagementInitialized = true;

  !function(e,n,t,i,r,o){/* ... Bloomreach SDK loader snippet ... */}(
    document,"exponea","script","webxpClient",window,{
      target: "https://api-XXX.exponea.com",
      token: "YOUR-PROJECT-TOKEN",
      experimental: { non_personalized_weblayers: true },
      new_experiments: { mode: "async" },
      track: {
        visits: true,
        google_analytics: false
      }
    }
  );
  exponea.start();
})();
<

Use the full SDK loader snippet from your Bloomreach project's Web SDK installation page, not the abbreviated form above. Bloomreach generates the snippet per tenant, copy it verbatim from the webapp and paste the whole thing between the braces. Keep the IIFE wrapper and the two guard lines at the top.

What the two guard lines do. On the first fire of the tag, window.__brEngagementInitialized is undefined, the guard condition is false, execution proceeds, the flag is set to true, the SDK loads and initializes. On every subsequent fire (the queued DOM Ready release, a cookie_consent_update from consent renewal, a client-side reload that re-triggers page-view events) the flag is already true, the early return fires, no SDK download, no second exponea.start() call, no warning.

Use a project-specific flag name. __brEngagementInitialized is clean enough if you only run one BR project on the page. If you have multiple tenants or naming collisions, scope it: __clientBrInitialized, __brProject1443cInitialized, whatever you use in your own naming conventions. Page globals are cheap, collisions are expensive.

GTM tag configuration

  1. Tag type: Custom HTML

  2. HTML: the snippet above with your real token and target

  3. Advanced Settings → Support document.write: checked. The Bloomreach SDK bundles the postscribe library, which overrides document.write and document.writeln to inject markup asynchronously. (Postscribe was archived by its maintainer in 2024, but Bloomreach ships a vendored copy — the SDK keeps working, it just means the upstream library is frozen at v2.0.8.) Without this GTM setting, the bundled writes throw and parts of the SDK initialization can fail silently.

  4. Consent Settings → Additional Consent Checks: require analytics_storage

  5. Triggering: both DOM Ready and cookie_consent_update (the shared, filter-less originals)

Pause the old Custom Template tag before you preview. Two SDK loaders racing on the same page collide regardless of the guard, because each is initializing its own copy of window.exponea in parallel.

Verification

After publishing, hard reload the site in a fresh incognito window and walk through both visitor paths.

Fresh visitor path. Open the site, accept the cookie banner, watch the console. You should see exactly one download of exponea.min.js in the Network tab (filter by exponea) and no double-load warning in the Console. Then check Bloomreach Engagement → Data & Assets → Events: session_start and page_visit should arrive within a few seconds, stamped with your project token.

Returning visitor path. Close the tab, open a second fresh incognito window (so stored consent persists), navigate to the site again, do not touch the banner. exponea.min.js downloads once, SDK initializes once, Events arrive.

If the console is clean for both paths, the fix is live.

Cleanup

After verification, delete the scaffolding that was part of the filter-based approach:

  • The old Bloomreach Engagement Tag (Custom Template from the Gallery)

  • Any exception trigger named something like Exception — Exponea Already Initialized

  • Any filter-variant triggers named DOM Ready — Exponea or cookie_consent_update — Exponea

  • Any "check whether SDK is initialized" Custom JavaScript variable used by those triggers

  • Any cleanup tag sequenced after the template that was meant to flag initialization

If you run Hotjar with an exception trigger, leave that alone. Hotjar is typically a Custom HTML tag, and Custom HTML tags do honor exception triggers correctly, so the existing defense stays sound.

Troubleshooting

Still seeing the double-load warning after the swap. The most likely cause is the old Custom Template tag is still active in parallel. Check Tags view in GTM and confirm it's paused or deleted. Republish the container.

exponea.start is not a function error in console. The Bloomreach loader snippet you copied is missing the last segment that installs start synchronously. Re-copy from Bloomreach's Web SDK page, and make sure you include the final anonymous-function call starting with function(e,n,t){var i;e[n]._initializeConfig(t)...} that adds e[n].start to the object.

Tag fires but SDK never initializes. Check that Support document.write is enabled on the Custom HTML tag and that your Additional Consent Check aligns with how your consent banner maps to analytics_storage. Open GTM Preview and confirm analytics_storage is granted at the moment the tag fires.

Warning returns after a week of clean operation. Someone republished the container with the old Custom Template re-enabled, or a second tag in the workspace is also loading the SDK. Audit Tags view for any Custom HTML or Custom Template that contains the string exponea.start or exponea.min.js.

The broader point

Platforms accumulate behaviors that interact in ways no single documentation page explains. A Custom Template that sandboxes differently from Custom HTML. A Consent Mode queue that doesn't re-evaluate filters on release. Neither is a bug. Each is defensible in isolation. Together they produce a symptom that resists the exact debugging pattern (exception triggers, firing filters) that the GTM interface pushes you toward.

The lesson: when a vendor-provided tag doesn't behave the way the GTM UI suggests it should, move the defense into code you control. An inline guard at the top of a Custom HTML tag costs two lines, cannot be bypassed by queue mechanics, and keeps the console clean across every visitor path.

Related manuals