AskSpot chat attribution in Google Analytics + script reference

This is the advanced companion to the main guide on tracking the AskSpot chat in Google Analytics. It covers the four levels of sales attribution, plus the full reference scripts – a production dataLayer bridge and a React hook – and a KPI glossary. Set up the basics first in the main GA4 guide; come here when you want depth.

Key takeaways

  • Three ways to stamp conversation_id on the purchase event: a GA4 user property, sessionStorage, or a cookie.
  • Attribution has four levels – from a zero-effort session segment to a hard A/B test.
  • The full production dataLayer bridge and a React hook are here, ready to paste.
  • Correlation isn’t causation – only the A/B test (level 4) proves the chat’s revenue impact.

Attribution: does the chat sell?

This is the question the whole setup is for. There are four levels – from the simplest to the most solid.

Level 1: session segment (zero extra work)

You have it straight after exploration B in the main guide. Compare the conversion of chat vs non-chat sessions.

  • Plus: works immediately, no code changes.
  • Minus: strong selection bias.

Don’t say “chat lifts conversion by X%” – say “sessions with a conversation convert X times more often”.

Level 2: conversation_id on the purchase event

You want to know which conversation ended in which order. This requires passing conversation_id to the purchase event, which you usually don’t control directly. Three ways:

Method A – GA4 User Property (simplest). In the bridge, at conversation start, set a user property. In GTM, under GA4 Event tag → User Properties, add:

User propertyValue
last_conversation_id{{DLV - conversationId}}

Register a User-scoped custom dimension for last_conversation_id in GA4. From then on every event from that user – including purchase – carries the dimension.

  • Plus: one change in GTM.
  • Minus: keeps only the last conversation; not cross-device.

Method B – a GTM variable reading sessionStorage. In the bridge (in the push function or the session_update listener) save the current ID:

try { sessionStorage.setItem("askspot_current_conversation", id); } catch (e) {}

In GTM create a Custom JavaScript variable:

function () {
  try {
    return sessionStorage.getItem("askspot_current_conversation") || "(not set)";
  } catch (e) {
    return "(not set)";
  }
}

and add it as a conversation_id parameter to your existing purchase tag.

  • Plus: works independently of user properties, survives navigation.
  • Minus: sessionStorage doesn’t cross domains; on a checkout in an isolated sandbox it may be unavailable.

Method C – a cookie on the parent domain (most robust). Instead of sessionStorage, save the ID in a cookie on the parent domain: document.cookie = "askspot_cid=" + encodeURIComponent(id) + "; path=/; domain=.yourdomain.com; max-age=1800; SameSite=Lax";

A cookie with domain=.yourdomain.com is visible on all subdomains – including a checkout that lives on a subdomain of the same parent domain. In GTM you read it with a First-Party Cookie variable.

  • Plus: works cross-subdomain.
  • Minus: it’s an analytics cookie – it must be subject to analytics_storage consent; write it only after consent and cover it in your privacy policy.

Level 3: conversions on the AskSpot side

Independently of your GA4, AskSpot can receive purchase events and tie them to conversations on our side. Then the AskSpot panel shows revenue attributed to conversations, with no GTM config. Two variants:

  • Automatic variant. Our script listens for purchase events in your environment (e.g. checkout_completed in Shopify Customer Events or purchase in the dataLayer), reads the conversationId from the widget’s memory, and sends the conversion – value + conversation ID – straight to our API. You don’t build anything.
  • Manual variant. You send conversions to our API yourself, from your backend. It’s the most solid option, because the data goes server-side (resistant to adblockers and to a lack of analytics-cookie consent, provided you have a legal basis). Ask us for the conversions API documentation.

Recommendation: do both. GA4 for behavioural analysis and comparison with the rest of your traffic; the AskSpot API as an independent, server-side source of truth for chat revenue. A gap between them is normal and itself informative (it shows how much data is lost on the frontend).

Level 4: A/B test (the only hard proof)

Everything above measures correlation. If you need a number you can show the board as “chat delivers X”, you need an experiment:

  • Split traffic: 90% with the widget, 10% without (control group),
  • assign stably per user (cookie), not per session,
  • a minimum of 2–4 weeks and a sufficient sample,
  • compare revenue per user (not session conversion) between the groups.

You can run the split in GTM (a random variable + cookie) or ask us for help on the integration-config side. Note: turning the chat off for 10% of customers has a support cost – it’s a business decision, not just an analytics one.

Appendix A – the full production script

The extended version: conversation depth, proactive notifications, agent mode, double-binding protection, and an optional cross-subdomain attribution cookie.

<script>
(function () {
  "use strict";

  // ── CONFIG ────────────────────────────────────────────────────
  var PREFIX       = "askspot_";
  var COOKIE_NAME  = "askspot_cid";
  var COOKIE_DOMAIN = "";        // e.g. ".yourdomain.com" - empty = off
  var COOKIE_TTL   = 1800;       // seconds
  var MAX_RETRIES  = 60;         // 60 × 500 ms = 30 s
  // ──────────────────────────────────────────────────────────────

  window.dataLayer = window.dataLayer || [];
  if (window.askSpotDataLayerEventsBound) return;   // already bound

  var messageCounters = {};      // conversationId -> user message counter

  function safeGet(key) {
    try { return sessionStorage.getItem(key); } catch (e) { return null; }
  }
  function safeSet(key, val) {
    try { sessionStorage.setItem(key, val); } catch (e) {}
  }

  function setConversationCookie(id) {
    if (!COOKIE_DOMAIN || !id) return;
    try {
      document.cookie = COOKIE_NAME + "=" + encodeURIComponent(id)
        + "; path=/; domain=" + COOKIE_DOMAIN
        + "; max-age=" + COOKIE_TTL + "; SameSite=Lax";
    } catch (e) {}
  }

  // dedup conversation start - survives navigation between pages
  function firstInConversation(id) {
    if (!id) return false;
    var key = "askspot_started_seen";
    var list;
    try { list = JSON.parse(safeGet(key)) || []; } catch (e) { list = []; }
    if (list.indexOf(id) !== -1) return false;
    list.push(id);
    safeSet(key, JSON.stringify(list));
    return true;
  }

  function nextMessageIndex(id) {
    if (!id) return 1;
    var key = "askspot_msgcount_" + id;
    var n = parseInt(safeGet(key) || "0", 10) + 1;
    safeSet(key, String(n));
    messageCounters[id] = n;
    return n;
  }

  function unwrap(event) {
    if (!event) return {};
    var d = event.data !== undefined ? event.data : event;
    if (typeof d === "string") {
      try { return JSON.parse(d); } catch (e) { return { raw: d }; }
    }
    return d && typeof d === "object" ? d : {};
  }

  function init() {
    var ids = window.AskWidget ? Object.keys(window.AskWidget) : [];
    if (!ids.length) return false;

    var widget = window.AskWidget[ids[0]];
    if (!widget || typeof widget.addEventListener !== "function") return false;

    function cid() {
      return (widget.sessionInfo && widget.sessionInfo.conversationId) || "";
    }

    function push(name, extra) {
      var payload = {
        event: PREFIX + name,
        conversationId: cid(),
        pageLocation: location.href
      };
      if (extra) {
        for (var k in extra) {
          if (Object.prototype.hasOwnProperty.call(extra, k)) payload[k] = extra[k];
        }
      }
      window.dataLayer.push(payload);
    }

    // ── widget state ──────────────────────────────────────────
    widget.addEventListener("chat_opened",   function () { push("widget_open"); });
    widget.addEventListener("minimize_chat", function () { push("widget_minimize"); });
    widget.addEventListener("chat_closed",   function () { push("widget_close"); });

    // ── user message ──────────────────────────────────────────
    widget.addEventListener("new_user_action", function (event) {
      var data = unwrap(event);
      var id   = cid();

      if (firstInConversation(id)) {
        setConversationCookie(id);
        push("conversation_started", { actionName: data.actionName || "" });
      }

      push("user_message", {
        actionName:   data.actionName || "",
        messageIndex: nextMessageIndex(id)
      });
    });

    // ── AI reply ──────────────────────────────────────────────
    widget.addEventListener("new_chat_message", function (event) {
      var data = unwrap(event);
      push("ai_message", {
        awaitingUserResponse: data.awaitingUserResponse === true,
        messageIndex: messageCounters[cid()] || 0
      });
    });

    // ── session: refresh cookie when conversationId appears/changes
    widget.addEventListener("session_update", function (event) {
      var data = unwrap(event);
      if (data.conversationId) setConversationCookie(data.conversationId);
    });

    // ── proactive notifications (denominator for nudge CTR) ────
    widget.addEventListener("showNotification", function (event) {
      var data = unwrap(event);
      push("notification_shown", {
        notificationContent: typeof data.content === "string"
          ? data.content.slice(0, 100)
          : ""
      });
    });

    // ── agent mode ────────────────────────────────────────────
    widget.addEventListener("agent_mode_enabled",  function () { push("agent_mode", { agentModeState: "enabled" }); });
    widget.addEventListener("agent_mode_disabled", function () { push("agent_mode", { agentModeState: "disabled" }); });

    window.askSpotDataLayerEventsBound = true;
    return true;
  }

  if (!init()) {
    var n = 0;
    var t = setInterval(function () {
      if (init() || ++n >= MAX_RETRIES) clearInterval(t);
    }, 500);
  }
})();
</script>

Events produced by this script:

dataLayer eventParameters
askspot_widget_openconversationId, pageLocation
askspot_widget_minimizeconversationId, pageLocation
askspot_widget_closeconversationId, pageLocation
askspot_conversation_started+ actionName
askspot_user_message+ actionName, messageIndex
askspot_ai_message+ awaitingUserResponse, messageIndex
askspot_notification_shown+ notificationContent (trimmed to 100 chars)
askspot_agent_mode+ agentModeState

A note on volume. askspot_user_message and askspot_ai_message fire on every conversation turn. At high traffic that’s a significant event volume in GA4 (cardinality limits, BigQuery cost). If you don’t need conversation-depth analysis, comment out those two listeners – the rest works independently.

Appendix B – React version (hook)

For React apps where you load the widget directly from code (the frontend method – see the main guide). The hook handles script loading, binding retry, and cleanup on unmount.

import { useEffect } from "react";

const ASKSPOT_SCRIPT_ID = "askspot-script";
const ASKSPOT_SCRIPT_SRC =
  "https://chat.askspot.io/api/v1/integration/{YOUR_INTEGRATION_ID}/embed-script";

type AskSpotEventPayload = Record<string, unknown> | undefined;

type AskSpotWidget = {
  sessionInfo?: { conversationId?: string };
  addEventListener: (
    eventName: string,
    callback: (event?: AskSpotEventPayload) => void
  ) => void;
};

declare global {
  interface Window {
    AskWidget?: Record<string, AskSpotWidget>;
    dataLayer?: Record<string, unknown>[];
    askSpotDataLayerEventsBound?: boolean;
  }
}

const seenConversationIds = new Set<string>();

function markConversationStarted(conversationId: string) {
  if (seenConversationIds.has(conversationId)) return false;
  seenConversationIds.add(conversationId);
  return true;
}

function pushAskSpotEvent(
  eventName: string,
  widget: AskSpotWidget,
  payload?: AskSpotEventPayload
) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: eventName,
    conversationId: widget.sessionInfo?.conversationId,
    askspotPayload: payload,
  });
}

function bindAskSpotDataLayerEvents() {
  if (window.askSpotDataLayerEventsBound) return true;

  const widget = Object.values(window.AskWidget || {})[0];
  if (!widget) return false;

  widget.addEventListener("new_user_action", (event) => {
    const conversationId = widget.sessionInfo?.conversationId;
    pushAskSpotEvent("askspot_new_user_action", widget, event);
    if (!conversationId || !markConversationStarted(conversationId)) return;
    pushAskSpotEvent("askspot_conversation_started", widget, event);
  });

  widget.addEventListener("new_chat_message", (event) => {
    pushAskSpotEvent("askspot_new_chat_message", widget, event);
  });

  window.askSpotDataLayerEventsBound = true;
  return true;
}

function scheduleAskSpotDataLayerBinding() {
  let attempts = 0;
  let intervalId = 0;

  const tryBind = () => {
    if (bindAskSpotDataLayerEvents() || ++attempts >= 60) {
      window.clearInterval(intervalId);
    }
  };

  tryBind();
  intervalId = window.setInterval(tryBind, 500);
  return () => window.clearInterval(intervalId);
}

export function useAskSpotScript(enabled: boolean) {
  useEffect(() => {
    if (!enabled) return;

    if (document.getElementById(ASKSPOT_SCRIPT_ID)) {
      return scheduleAskSpotDataLayerBinding();
    }

    const script = document.createElement("script");
    script.id = ASKSPOT_SCRIPT_ID;
    script.async = true;
    script.crossOrigin = "anonymous";
    script.src = ASKSPOT_SCRIPT_SRC;

    let cleanupBinding: (() => void) | undefined;

    const handleScriptLoad = () => {
      cleanupBinding = scheduleAskSpotDataLayerBinding();
    };

    script.addEventListener("load", handleScriptLoad);
    document.head.appendChild(script);

    return () => {
      cleanupBinding?.();
      script.removeEventListener("load", handleScriptLoad);
    };
  }, [enabled]);
}

Usage with consent handling:

const analyticsConsent = useConsent("statistics");  // your CMP logic
useAskSpotScript(analyticsConsent);

Differences vs Appendix A:

Appendix A (vanilla/GTM)Appendix B (React)
Start dedupsessionStorage – survives a reloadin-memory Set – lost on reload
Message counteryesno
Attribution cookieyes (optional)no
Script loadingassumes the widget is already thereloads it itself
Event namesaskspot_user_message, askspot_ai_messageaskspot_new_user_action, askspot_new_chat_message

Unify the names. Appendix B uses names that mirror the widget events 1:1 (askspot_new_user_action), Appendix A uses descriptive names (askspot_user_message). Pick one convention before you start. The ^askspot_w+ regex in GTM catches both, but you’ll see two unrelated event sets in GA4 if you change your mind mid-way.

In an SPA the in-memory Set is enough (no full reloads). With SSR / MPA move the dedup to sessionStorage – otherwise you’ll inflate conversation_started.

Appendix C – KPI glossary

Metrics worth reporting, and how to compute each in GA4.

KPIDefinitionHow to compute
Chat reach% of sessions where the bubble was openedsessions with askspot_widget_open / all sessions
Engagement rate% of opens that ended in a first messageaskspot_conversation_started / askspot_widget_open
Conversation penetration% of sessions with a conversationsessions with askspot_conversation_started / all sessions
Average conversation depthhow many turns a typical conversation hasaverage of message_index per conversation_id
Proactive-nudge CTRnotification effectivenessaskspot_widget_open (after notification_shown) / askspot_notification_shown
Conversion with chatCR of sessions with a conversationpurchases in the “chat users” segment / sessions in that segment
Uplift (correlational)how many times better chat sessions convertCR with chat / CR without chat – don’t call it the chat’s impact
Chat-assisted revenuerevenue in sessions with a conversationpurchase revenue in the “chat users” segment
Attributed revenuerevenue tied to specific conversationspurchase with a non-empty conversation_id
Causal upliftthe chat’s real impactfrom an A/B test only

Recommended starter set (5 metrics): chat reach, conversation penetration, average depth, conversion with vs without chat, chat-assisted revenue. The rest once the first five are stable.

LinkedIn

Written by

AskSpot Team

AskSpot builds AI Chat and Inbox Agents for e-commerce: on-site product advice and 24/7 support across email, chat and marketplaces, in 200+ languages.

Case studies

Real results for e-commerce stores

See how online retailers use AskSpot to automate conversations, reduce support workload and grow sales.

All case studies

Morele.net

How Morele.net cut customer service costs by 90%

Morele.net cut support costs 90%, from 50 agents to 5. AskSpot resolves 65% of its 37,600 monthly chats with no human and answers Allegro questions too.

Read the story →

Militaria.pl

How Militaria resolves 72% of chats in 8 languages

Militaria.pl runs AskSpot across three domains in eight languages: 61% recommendation click-through, 26% add-to-cart after a chat, 72% resolved with no human.

Read the story →

4FIZJO

How 4FIZJO resolves 61% of customer chats with AI

AskSpot's AI Chat resolves 61% of 4FIZJO's 21,360 monthly chats with no human across 6 markets; its Inbox Agent auto-resolves 25% of email tickets.

Read the story →

Meblobranie

How chat advice puts €1.9M of garden furniture in baskets a month

Meblobranie's AskSpot agent answers the questions garden furniture is bought on – size, colour, anchoring, spare nets. Chat advice puts €1.9M into the cart a month, 20% of chats fill a basket, 70% resolved.

Read the story →

Szopex

How one AI agent runs across Szopex’s three retail brands

One AskSpot agent runs across Szopex's three storefronts – Sklep Biegacza, Warsaw Sneaker Store and SKStore – on three e-commerce platforms. 67% of chats resolved, 58% product CTR, 11.7% end in a purchase.

Read the story →

moderno.dk

How moderno.dk resolves 64% of furniture chats with no human

moderno.dk's AskSpot agent resolves 64% of furniture questions with no human – delivery, sizing, stock and order changes – and turns 12.3% of chats into carts.

Read the story →

16case studies

See every story

Every merchant we have measured – what shoppers asked, what the agent did, and the numbers it moved.

Browse them all →

Ready to make every conversation sell?

Book a demo and see AskSpot on your own catalog. Onboarding takes a few days.