Frontend IntegrationSDKGTM Integration

GTM Integration

Load the Sophi Web SDK via Google Tag Manager with consent-aware script injection and app-side initialization.

Web Object (analytics / `window.sophi_object`)

For @usesophi/sophi-web-object (commerce tracking without the widget), use Web Object GTM integration.

Use this setup when the Sophi Web SDK (chat widget) is loaded by Google Tag Manager (GTM), not from npm runtime imports.

For most teams, the recommended GTM setup is load on all pages + consent-aware storage upgrade. See Cookie Consent Management.

Browser global

When loaded by script, the SDK is exposed as:

window.SophiWebSDK.SophiWidget;

Script URL (current)

Use this script URL in GTM-injected script element:

https://cdn.jsdelivr.net/npm/@usesophi/sophi-web-sdk@latest/dist/index.umd.js

This is the default model we recommend: load Sophi on every page, keep it memory-only before consent, then upgrade to persistent storage when the visitor accepts cookies.

1. Load the SDK on all pages

Use a GTM tag that injects the SDK on page load regardless of consent:

<script>
  (function () {
    if (window.SophiWebSDK && window.SophiWebSDK.SophiWidget) return;

    var src = "https://cdn.jsdelivr.net/npm/@usesophi/sophi-web-sdk@latest/dist/index.umd.js";
    var script = document.createElement("script");
    script.src = src;
    script.async = true;
    script.onload = function () {
      window.dispatchEvent(new CustomEvent("sophi:sdk_loaded"));
      window.dispatchEvent(new CustomEvent("sophi:init"));
    };
    document.head.appendChild(script);
  })();
</script>

2. Initialize memory-only

Initialize with storageConsent: false so no cookies are written yet:

const SophiWidgetClass = await waitForSophiSDK(5000);
const widget = new SophiWidgetClass();
await widget.init({
  apiKey: "YOUR_SOPHI_API_KEY",
  container: "#sophi-widget-container",
  userId,
  userName,
  environment: "production",
  storageConsent: false, // memory-only until consent
});

Add a small Custom HTML tag (or app-side listener) tied to your consent-updated event that upgrades the live widget in place:

<script>
  if (window.sophiWidget) {
    // Widget Launcher
    window.sophiWidget.setConsent(true);
  }
</script>

If you manage the SophiWidget instance yourself, call widget.setStorageConsent(true) from your app code instead.

Tag

  • Tag Type: Custom HTML
  • Purpose: Load SDK only after functional consent is granted (strict policy)

Triggers

Attach the same Custom HTML tag to both triggers:

  1. Consent on Granted
  2. Page onload

This covers:

  • consent already granted before page load
  • consent granted during current session

Custom HTML behavior

The GTM tag should:

  1. Read and parse cookie_consent cookie as JSON
  2. Check parsed.functional === true
  3. If true, inject script URL (above) once
  4. On script load, dispatch:
    • sophi:sdk_loaded
    • sophi:init
<script>
  (function () {
    function getCookie(name) {
      var match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
      return match ? decodeURIComponent(match[2]) : null;
    }

    function hasFunctionalConsent() {
      try {
        var raw = getCookie("cookie_consent");
        if (!raw) return false;
        var parsed = JSON.parse(raw);
        return parsed && parsed.functional === true;
      } catch (e) {
        return false;
      }
    }

    if (!hasFunctionalConsent()) return;
    if (window.SophiWebSDK && window.SophiWebSDK.SophiWidget) return;

    var src = "https://cdn.jsdelivr.net/npm/@usesophi/sophi-web-sdk@latest/dist/index.umd.js";
    var script = document.createElement("script");
    script.src = src;
    script.async = true;

    script.onload = function () {
      window.dispatchEvent(new CustomEvent("sophi:sdk_loaded"));
      window.dispatchEvent(new CustomEvent("sophi:init"));
    };

    document.head.appendChild(script);
  })();
</script>

App-side wait flow

Initialize the widget only when:

  • functional consent exists
  • waitForSophiSDK(5000) resolves
  • readiness is confirmed by either:
    • sophi:init window event
    • window.SophiWebSDK.SophiWidget presence
const SophiWidgetClass = await waitForSophiSDK(5000);
const widget = new SophiWidgetClass();
await widget.init({
  apiKey: "YOUR_SOPHI_API_KEY",
  container: "#sophi-widget-container",
  userId,
  userName,
  environment: "production",
});

Keep configuration and event listeners in application code.
GTM should only handle consent-gated script loading.

Troubleshooting

Script does not load from GTM

  • Confirm the Custom HTML tag fires on the expected consent triggers.
  • Confirm your consent condition resolves to granted state.
  • Open GTM Preview mode and verify the tag actually fires on page load and consent change.

window.SophiWebSDK is undefined

  • SDK script has not loaded yet, or load was blocked by consent/trigger logic.
  • Confirm jsDelivr URL is valid and reachable.
  • Delay widget init until your wait strategy succeeds (sophi:init event or waitForSophiSDK).

waitForSophiSDK timeout

  • Increase timeout if consent workflow is delayed.
  • Ensure consent update event is pushed when user accepts.
  • Ensure GTM trigger for consent-updated path exists (not only initial page load).

Init runs before container exists

  • Make sure #sophi-widget-container exists in DOM before calling widget.init.
  • In React/Angular, initialize after component mount/view init.
  • Attach the same SDK load tag to both:
    1. initial granted-consent trigger
    2. mid-session consent-granted trigger
  • Confirm the mid-session trigger is tied to your consent update event.

Duplicate init / repeated listeners

  • Keep a single widget instance reference and guard against re-init.
  • On teardown, call widget.destroy() and clear references.
  • Register listeners once per widget instance to avoid duplicate callbacks.

Optional helper example (waitForSophiSDK)

This is a reference example only to help teams implementing GTM-based loading. You do not have to copy this exact helper; any equivalent wait strategy is fine.

const SOPHI_INIT_EVENT = "sophi:init";
const POLL_INTERVAL_MS = 150;

export function waitForSophiSDK(timeoutMs: number): Promise<new () => any> {
  return new Promise((resolve, reject) => {
    if (typeof window === "undefined") {
      reject(new Error("waitForSophiSDK: window is undefined"));
      return;
    }

    const w = window as Window & { SophiWebSDK?: { SophiWidget: new () => any } };
    let timeoutId: ReturnType<typeof setTimeout> | null = null;
    let pollId: ReturnType<typeof setInterval> | null = null;

    function cleanup() {
      window.removeEventListener(SOPHI_INIT_EVENT, onInit);
      if (timeoutId) clearTimeout(timeoutId);
      if (pollId) clearInterval(pollId);
      timeoutId = null;
      pollId = null;
    }

    function tryResolve() {
      if (w.SophiWebSDK?.SophiWidget) {
        cleanup();
        resolve(w.SophiWebSDK.SophiWidget);
      }
    }

    function onInit() {
      tryResolve();
    }

    // Already available
    tryResolve();
    if (w.SophiWebSDK?.SophiWidget) return;

    // GTM might dispatch `sophi:init`
    window.addEventListener(SOPHI_INIT_EVENT, onInit);
    // Or set `window.SophiWebSDK` without the event
    pollId = setInterval(tryResolve, POLL_INTERVAL_MS);

    timeoutId = setTimeout(() => {
      cleanup();
      reject(new Error(`Sophi SDK did not load within ${timeoutMs}ms`));
    }, timeoutMs);
  });
}

On this page