How to track the AskSpot chat in Google Analytics

The AskSpot widget emits public events (chat opened, new message, session change, and so on) that you can listen to with your own JS. What you do with them is up to you: pass them to the dataLayer (GTM), send them straight to GA4, or to your own internal analytics. This guide walks the core path – widget events, the dataLayer bridge, GTM and GA4 setup. Sales attribution, GA4 explorations and the full reference scripts live in the advanced companion, and Shopify has its own guide.

Key takeaways

  • The setup has four layers: AskSpot provides layer 0 (widget) and 1 (events); you build layers 2–4 (dataLayer bridge, GTM, GA4).
  • By default, load the widget through GTM; for large stores a hybrid is better (widget from the front, bridge from GTM).
  • conversationId is the key that ties everything together – it is created on the first message, not when the bubble opens.
  • A ready-made dataLayer bridge (Custom HTML tag in GTM) gets measurement running in minutes; the full production script is in the reference companion.
  • GA4 will always show fewer conversations than the AskSpot panel (consent, adblockers) – that is normal, not a bug.

Why do this – what you actually measure

The AskSpot chat widget has its own analytics panel – conversation counts, question categories, resolutions. But that is the chat’s own view. Google Analytics gives you the shopper-session view: where they came from, what they browsed before the chat, whether they bought after it, how much they spent. Connect the two and you answer questions neither can answer alone:

Business questionWhat you need
What % of traffic touches the chat at all?askspot_widget_open / all sessions
Do people who chat convert better?segment “sessions with askspot_conversation_started” vs the rest
How much revenue runs through the chat path?purchase in the chat segment
Which pages need the chat most?page_location on askspot_conversation_started
Does the chat save abandoned carts?path askspot_*begin_checkoutpurchase
Which specific conversations ended in a purchase?conversationId passed to purchase
How deep are the conversations?new_user_action counter per conversation

Architecture: four layers

The whole setup is four layers, one on top of the other:

LayerWhat it isWho builds it
0 – widget loadingThe AskSpot embed script (frontend / GTM / Custom Pixel) creates window.AskWidget[widgetId].AskSpot
1 – widget event APIwidget.addEventListener("new_user_action", cb) and widget.sessionInfo.conversationId.AskSpot
2 – the bridge to the dataLayerYour script maps widget events to your names: window.dataLayer.push({ event: "askspot_...", ... }).You
3 – Google Tag ManagerA Custom Event trigger + Data Layer Variables + a GA4 tag.You
4 – Google Analytics 4Events + custom dimensions + key events + explorations.You

The key principle: AskSpot provides only layers 0 and 1. Layers 2–4 are on your side – and that is deliberate. We do not impose event names, a payload format, or a convention. Match them to what you already have in GA4.

If you would rather we build layer 2 (the bridge) inside our custom script, that is possible – let’s arrange it individually. By default we assume you do it yourself, so you keep control of the naming.

Layer 0 – how you load the widget

The embed script looks like this (you get the integration ID from us – it is unique per widget):

<script
  crossorigin="anonymous"
  async
  src="https://chat.askspot.io/api/v1/integration/{YOUR_INTEGRATION_ID}/embed-script"
></script>

There are three sensible places to put it. The choice has real analytics consequences.

Comparing the methods

A. Directly in the frontendB. Through GTM (Custom HTML)C. Shopify Custom Pixel
Adblocker resistance✓ highest⚠ lowest – adblockers block gtm.js entirely⚠ depends on the pixel
Consent handling✗ you do it yourself✓ built in (Consent Mode / Cookiebot in GTM)✓ native Shopify consent API
Reach across the pathwhere your code runs✓ everywhere the container runsonly where the pixel runs
Works on Shopify checkout✗ no⚠ only with Checkout Extensibility / Shopify Plus✓ yes (but in a sandbox)
Widget renders✓ yes✓ yesno (sandbox = no access to the page DOM)
Time to ship a changefront deploy✓ container publishpixel publish
Duplication risk⚠ if you also leave the frontend one⚠ if you also leave the frontend one

Recommendation

Default: GTM (option B). Why:

  1. Consent is already solved – the GTM container usually knows Cookiebot / your CMP and respects Consent Mode. You don’t duplicate consent logic in three places.
  2. GTM works across the whole path, so you have one source of truth.
  3. Changes without a front deploy.

The conscious cost: adblockers that block googletagmanager.com will block the chat too. On a typical B2C store that is a few to low-double-digit percent of traffic. If the chat is a critical part of customer service, not just an “add-on”, consider a hybrid.

Hybrid (recommended for large stores): load the widget directly in the frontend (resistance), and the dataLayer bridge from GTM (consent + flexibility). The bridge only listens anyway – if GTM is blocked, the chat still works, you lose only the measurement.

Don’t duplicate

If you’re moving from the frontend to GTM – remove the script from the frontend. Two embed scripts loaded in parallel means:

  • two entries in window.AskWidget → the bridge may bind to the wrong one,
  • double listeners → events counted twice in GA4,
  • potentially two widgets in the DOM.

If you must have both temporarily, guard with document.getElementById("askspot-script") before adding a second one (see the reference companion) and be sure to use the window.askSpotDataLayerEventsBound flag so the bridge binds only once.

Hiding the widget on specific pages

On the cart, checkout and thank-you page the widget usually gets in the way. Don’t solve it by “not loading the script” (you’ll lose measurement) – ask AskSpot for a display rule on our side. Then the script runs, analytics fires, and the bubble doesn’t show.

If you want to hide the widget on specific URLs, send us a list of patterns – we set it up in the integration config. No changes to your code.

Layer 1 – the widget event API

Once the script loads, the widget exposes a global API:

window.AskWidget = {
  "<widgetId>": {
    sessionInfo: {
      chatId: "...",
      conversationId: "...",      // null before the first message
      sessionExpireTime: 1234567890,
      isMinimized: false
    },
    addEventListener: (eventName, callback) => void,
    openChat: () => void,
    closeChat: () => void,
    minimizeChat: () => void,
    changeLabel: (label) => void
  }
}

How to bind

const widgetId = Object.keys(window.AskWidget)[0];
const widget = window.AskWidget[widgetId];

widget.addEventListener("new_chat_message", (event) => {
  const conversationId = widget.sessionInfo?.conversationId;
  console.log("AI replied in conversation", conversationId, event);
});

Four things to keep in mind

  1. Event names are strings, not constants. To addEventListener you pass a string value ("showNotification"), not the key name from an internal enum (add_notification). Stick to the exact names in the “Widget event catalog” table.
  2. The widget loads asynchronously. Your script (especially from GTM) will almost certainly start before window.AskWidget exists. You need a retry with an interval – not a single setTimeout. In the examples we use setInterval(500ms) capped at 60 tries (30 seconds). That’s safe, because events only fire after user interaction anyway.
  3. The payload is sometimes a string. On some paths (iframe forwarding) the payload arrives as a JSON string, not an object. Unpack it defensively before you use it (pattern below).
  4. Read conversationId from widget.sessionInfo at event time – don’t cache it when you attach the listener, because it doesn’t exist yet.
const data = typeof event?.data === "string"
  ? JSON.parse(event.data)
  : event?.data;

Widget event catalog

Public events (use these)

EventWhen it firesPayloadAnalytics use
chat_loadedChat bundle finished loading after openingPerformance diagnostics
chat_openedChat was openedInterest – top of the chat funnel
chat_closedChat closed, conversation/UI state cleaned upEnd of interaction
minimize_chatChat minimized (not closed)“Set aside” vs “done”
new_user_actionUser sent a message / took an action{ actionName }Conversation start & depth
new_chat_messageAI replied (new assistant response){ awaitingUserResponse }Turn count, responsiveness
session_updateSession change{ chatId, conversationId, sessionExpireTime, isMinimized }Source of conversationId
agent_mode_enabledAgent (copilot) mode activated{ source } / { widgetId }Advanced-feature use
agent_mode_disabledAgent mode turned off{ source, reason? }as above
showNotificationProactive notification / nudge shown{ content, actions, durationMs }Proactive exposure – CTR denominator
hideNotificationNotification hiddenas above
custom_openHook: if you have a listener, a bubble click fires this instead of the default openCustom open logic

Six usually suffice: chat_opened, new_user_action, new_chat_message, minimize_chat, chat_closed, session_update.

Internal events (do NOT bind to these)

These exist on the widget’s internal event bus but are not part of the public contract. They may disappear or change meaning without warning:

open_chat, close_chat, add_button, remove_button, add_bar, remove_bar, iframe_style, update_label, minimize_notification, end_conversation, floating_button_animation_finished, setup, resize, run_agent, stop_agent, agent_steps_state

To control the widget, use the public methods: openChat(), closeChat(), minimizeChat(), changeLabel(...) – don’t emit internal commands.

Business events from your conversation config

Beyond the above, the widget can emit its own events defined in your integration config (conversationContext.eventAction) – e.g. “user clicked a recommended product”, “redirect to a category”. These are the most interesting events from a sales perspective, but they are specific to your configuration.

Ask your AskSpot contact which eventActions are active on your widget. It’s worth asking before you finalise your measurement scope.

conversationId – the key that ties everything together

This is the most important identifier in the whole setup. It is also the key you use to join your data with the conversation analytics in the AskSpot panel.

Lifecycle

User stepEventconversationId
Lands on the pagewidget loadsnull
Clicks the bubblechat_openednull
Sends the FIRST messagenew_user_actionsession_update"abc-123" ✓ created here
Conversation continues (more messages, navigation)new_user_action / new_chat_message"abc-123" (stable)
Ends it and starts a new onenew_user_action"def-456" (NEW)

Practical consequences

  • chat_opened and chat_closed before the first message have no conversationId. That’s not a bug – the conversation doesn’t exist yet. Don’t patch it by substituting chatId; mixing identifier spaces is worse than an empty value. Stitch pre-first-message events by your own session identifier (GA4 client_id / session_id).
  • conversationId rotates. One user in one session can have several. If you count “unique chat users”, count by client_id, not conversationId. If you count “conversations” – by conversationId.
  • conversationId survives page navigation (it lives in the widget’s session memory), but your bridge’s sessionStorage may not – which is why the conversation_started dedup must keep the list of seen IDs in sessionStorage, not in a JS variable. Without that, every page reload mid-conversation reports another “conversation start” and inflates the metric.

Store it as a User Property (optional)

If you want conversationId available on every later event in the session (including purchase, which you don’t control), set it as a GA4 user property at conversation start. Details in the attribution companion.

Layer 2 – the dataLayer bridge (ready-made script)

This is your code. The script below is an example – match the askspot_* names and the parameter set to your own naming convention in GA4.

Minimal version (start in 5 minutes)

Paste it as a Custom HTML tag in GTM, fired on All Pages (or Initialization – All Pages).

<script>
(function () {
  window.dataLayer = window.dataLayer || [];

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

    function push(name) {
      window.dataLayer.push({
        event: name,
        conversationId: widget.sessionInfo && widget.sessionInfo.conversationId
      });
    }

    // dedup: the same conversation spans many page views - count the start once
    function firstInConversation(id) {
      if (!id) return false;
      var key = "askspot_started_seen";
      var list;
      try { list = JSON.parse(sessionStorage.getItem(key)) || []; } catch (e) { list = []; }
      if (list.indexOf(id) !== -1) return false;
      list.push(id);
      try { sessionStorage.setItem(key, JSON.stringify(list)); } catch (e) {}
      return true;
    }

    widget.addEventListener("chat_opened",   function () { push("askspot_widget_open"); });
    widget.addEventListener("minimize_chat", function () { push("askspot_widget_minimize"); });
    widget.addEventListener("chat_closed",   function () { push("askspot_widget_close"); });

    widget.addEventListener("new_user_action", function () {
      var id = widget.sessionInfo && widget.sessionInfo.conversationId;
      if (firstInConversation(id)) push("askspot_conversation_started");
    });

    return true;
  }

  // AskWidget loads async - retry until available (max 30 s)
  if (!init()) {
    var n = 0, t = setInterval(function () {
      if (init() || ++n > 60) clearInterval(t);
    }, 500);
  }
})();
</script>

Result: askspot_widget_open, askspot_conversation_started, askspot_widget_minimize, askspot_widget_close reach the dataLayer – each with a conversationId.

What the full version adds

The minimal version doesn’t measure conversation depth or proactive nudges. The full production script (in the reference companion) adds:

  • askspot_user_message with a messageIndex counter – lets you measure average conversation length,
  • askspot_ai_message with awaitingUserResponse,
  • askspot_notification_shown – the denominator for proactive-nudge CTR,
  • askspot_agent_mode – agent-mode usage,
  • double-binding protection (window.askSpotDataLayerEventsBound),
  • pageLocation on every event (useful when GA4 attributes an event to a different page than the one it actually fired on).

Naming convention – do it once, properly

Before you paste the script, decide the names. Changing them a month later is a hole in your historical data. Recommended pattern: askspot_<object>_<action>, snake_case, always the askspot_ prefix (so one regex in GTM catches everything).

Widget eventSuggested dataLayer name
chat_openedaskspot_widget_open
minimize_chataskspot_widget_minimize
chat_closedaskspot_widget_close
first new_user_action in a conversationaskspot_conversation_started
every new_user_actionaskspot_user_message
new_chat_messageaskspot_ai_message
showNotificationaskspot_notification_shown
agent_mode_enabledaskspot_agent_mode

Mind the GA4 limit: free GA4 caps unique event names at 500 per property. Eight names is nothing, but don’t generate names dynamically (e.g. askspot_msg_1, askspot_msg_2) – use parameters.

Layer 3 – Google Tag Manager setup

Variables (Data Layer Variables)

Variables → New → Data Layer Variable. Create one for each parameter you want to use:

GTM variable nameData Layer name
DLV – conversationIdconversationId
DLV – messageIndexmessageIndex
DLV – actionNameactionName
DLV – awaitingUserResponseawaitingUserResponse
DLV – pageLocationpageLocation

Set the Default Value to (not set) where a parameter is sometimes empty (e.g. conversationId on askspot_widget_open). Otherwise GA4 gets undefined and the parameter drops out of the report instead of showing “not set”.

Trigger – one regex for everything

Triggers → New → Custom Event:

  • Event name: ^askspot_w+
  • ✓ tick Use regex matching
  • Fire on: All Custom Events

Name it CE – AskSpot (all). One trigger handles every current and future event with the askspot_ prefix. If you want to fire selected events separately (e.g. only askspot_conversation_started as a conversion), add extra triggers with an exact name match – but keep the catch-all tag too.

GA4 Event tag

Tags → New → Google Analytics: GA4 event.

FieldValue
Configuration tag / Measurement IDyour existing GA4 Configuration tag
Event name{{Event}}
TriggerCE – AskSpot (all)

Event parameters:

Parameter nameValue
conversation_id{{DLV - conversationId}}
message_index{{DLV - messageIndex}}
action_name{{DLV - actionName}}
page_location_custom{{DLV - pageLocation}}

Why {{Event}} as the name? Because one tag handles all AskSpot events. The alternative – a separate tag per event – is clearer in an audit, but multiplies the work. At 8 events the catch-all tag wins.

Consent settings on the tag

On the Advanced → Consent Settings tab, set for the bridge tag (Custom HTML) and the GA4 tag: Require additional consent for tag to fire: analytics_storage. GTM then holds the tag until consent is granted, with no logic in your code. Details in the “Consent” section.

Load order

If you load both the widget and the bridge from GTM, set:

  • Tag AskSpot – embed script (Custom HTML) → trigger Initialization – All Pages
  • Tag AskSpot – dataLayer bridge (Custom HTML) → trigger All Pages; under Advanced → Tag Sequencing tick “Fire a tag before this tag fires”: AskSpot – embed script

In practice the retry in the bridge handles it anyway, but explicit ordering shortens the time to the first event.

Layer 4 – Google Analytics 4

Events alone aren’t enough in GA4. Without the steps below, the parameters won’t appear in reports.

Register custom dimensions

Admin → Custom definitions → Create custom dimension:

Display nameScopeEvent parameter
AskSpot Conversation IDEventconversation_id
AskSpot Message IndexEventmessage_index
AskSpot Action NameEventaction_name

Dimensions work only from the moment you create them. GA4 doesn’t backfill. Register them the same day you publish the GTM container – otherwise you lose the first days of data. Limit: 50 Event-scoped dimensions in standard GA4. Check how many you have free before adding three.

Mark key events

Admin → Key events → New key event: askspot_conversation_started – this is your main chat micro-conversion. Do not mark askspot_user_message or askspot_ai_message as key events – they fire many times per session and distort the conversion rate.

Audiences – the foundation for attribution

Admin → Audiences → New audience → Create a custom audience:

  • Audience “Chat users”: condition – event askspot_conversation_started occurred at least once; membership 30 days.
  • Audience “Opened, didn’t write”: condition – askspot_widget_open ≥ 1 AND askspot_conversation_started = 0. Useful for judging whether the bubble promises something the chat doesn’t deliver.

Audiences start collecting from the moment you create them – same as dimensions. Create them right away.

Reports and explorations

A. Chat funnel (Funnel exploration)

StepCondition
1session_start
2askspot_widget_open
3askspot_conversation_started
4add_to_cart
5begin_checkout
6purchase

Set an open funnel (not closed) – a shopper may add to cart before chatting – and split by Device category and Session default channel group.

What it tells you: where people drop off. A big drop between step 2 and 3 = the bubble draws people in but the first message is too hard (consider starter prompts). A big drop between 3 and 4 = conversations don’t lead to the product.

B. Conversion: chat vs no chat

Explore → Free-form.

  • Rows: Session default channel group
  • Values: Sessions, Purchases, Revenue, Session conversion rate
  • Comparison segments: A = sessions containing askspot_conversation_started; B = sessions not containing it

This is not proof of causation. People who chat are, by definition, more engaged – they’d buy more often without the chat too. This table shows correlation, and must be presented that way. Hard proof needs an A/B test (see the attribution companion).

C. Where conversations start

Explore → Free-form.

  • Rows: Landing page or Page location
  • Filter: event name askspot_conversation_started
  • Values: Event count

The pages with the most conversation starts are where the content doesn’t answer the questions – a direct input for content and UX.

D. Conversation depth

  • Rows: AskSpot Message Index
  • Values: Event count

The distribution shows which message people drop off at. If 70% of conversations end on the first message, either the chat answers perfectly first time or it answers so badly the user gives up – cross it with the AskSpot panel (resolution) to settle which.

E. Path after a conversation (Path exploration)

Explore → Path exploration. Starting point = event askspot_conversation_started; steps +1, +2, +3 = event names. Shows what users actually do right after a conversation. If page_view on a category dominates, the chat is driving traffic; if session_end does, it isn’t.

Export to BigQuery (advanced)

GA4 samples and aggregates. If you want to compute the chat’s real revenue impact per conversation, connect BigQuery Export (free in standard GA4) and join on conversation_id with the conversation export from the AskSpot panel. It’s the only way to analyse without cardinality limits.

Attribution: does the chat sell? → once the reports are in place, the four levels of sales attribution – plus the full production scripts and a KPI glossary – are in the advanced companion.

The principle

The AskSpot chat has two natures and two consent regimes:

FunctionNatureConsent
The chat widget itself (customer service)usually functionally necessaryusually needs no analytics consent
Sending events to GA4 / the dataLayeranalytical✓ requires analytics_storage
The askspot_cid attribution cookieanalytical✓ requires analytics_storage

This is a practical classification, not legal advice. Settle the final classification of the chat in your CMP with your data-protection officer. If you need it, we have ready wording for the terms and privacy policy covering the chat integration – ask your contact.

Recommendation: leave it to GTM

The cleanest solution: don’t write consent logic in your code. Instead:

  1. Make sure your CMP (Cookiebot, Consentmanager, OneTrust…) runs in Google Consent Mode v2 and is deployed in GTM.
  2. On the AskSpot tags (bridge + GA4) set Advanced → Consent Settings → Require additional consent: analytics_storage.
  3. Done. GTM holds the tags until consent and fires them once it’s granted (thanks to Consent Initialization).

Benefits: one place to manage, consistency with the rest of your measurement, no drift risk when the CMP changes.

If you must check consent in code

Sometimes it’s necessary – e.g. in a Shopify Custom Pixel, where GTM can’t reach. A Cookiebot pattern in the Shopify sandbox:

const getCookiebotConsent = async () => {
  try {
    const cookieString = await browser.cookie.get('CookieConsent');
    if (!cookieString) return null;
    return {
      marketing:   cookieString.includes('marketing:true'),
      statistics:  cookieString.includes('statistics:true'),
      preferences: cookieString.includes('preferences:true')
    };
  } catch (e) {
    return null;
  }
};

async function loadIfAnalyticsConsentGranted() {
  const consent = await getCookiebotConsent();
  if (consent?.statistics) loadAskSpot();
}

Three traps with this approach:

  1. String parsing is brittle. includes('statistics:true') stops working if Cookiebot changes the cookie format. Treat it as temporary.
  2. No reaction to a consent change. The code above checks consent once. If the user accepts cookies after the code runs, nothing happens. Add a listener for your CMP’s consent-change event and retry.
  3. By default, no consent = no chat. If you classified the chat as functionally necessary, blocking it under statistics is excessive – then block only the bridge, not the widget itself.

Consent Mode v2 – what happens without consent

With Consent Mode v2 and a consent refusal, GA4 sends cookieless pings – without identifiers, but counted in conversion modelling. AskSpot events will not be sent if the tag requires analytics_storage. The result: your conversation numbers in GA4 will be lower than in the AskSpot panel. That’s expected and correct.

On Shopify? The storefront works exactly as above; the checkout runs in an isolated Custom Pixel sandbox with its own rules. See AskSpot analytics on Shopify.

Debugging and the QA checklist

Check layer 1 (widget) – browser console

// 1. Is the widget there at all?
Object.keys(window.AskWidget || {});
// expected: ["<some-id>"] - if [] the script didn't load

// 2. Attach a logger to everything
(function () {
  var w = window.AskWidget[Object.keys(window.AskWidget)[0]];
  ["chat_loaded","chat_opened","chat_closed","minimize_chat",
   "new_user_action","new_chat_message","session_update",
   "agent_mode_enabled","agent_mode_disabled","showNotification"]
   .forEach(function (name) {
     w.addEventListener(name, function (e) {
       console.log("[AskSpot]", name, e, "cid:", w.sessionInfo && w.sessionInfo.conversationId);
     });
   });
  console.log("logger attached");
})();

// 3. Check session state
window.AskWidget[Object.keys(window.AskWidget)[0]].sessionInfo;

Write a message to the chat and watch the console. You should see new_user_actionsession_update (with conversationId) → new_chat_message.

Check layer 2 (dataLayer)

// live preview of every push with the askspot_ prefix
(function () {
  var orig = window.dataLayer.push;
  window.dataLayer.push = function () {
    var a = arguments[0];
    if (a && typeof a.event === "string" && a.event.indexOf("askspot_") === 0) {
      console.log("[dataLayer]", a);
    }
    return orig.apply(window.dataLayer, arguments);
  };
  console.log("dataLayer tap active");
})();

// history (what already fired)
window.dataLayer.filter(function (x) {
  return x.event && String(x.event).indexOf("askspot_") === 0;
});

Check layers 3 and 4

  • GTM Preview (Tag Assistant): turn on preview, walk the path, check that askspot_* events appear in the left panel and the GA4 tag shows Fired. Click the tag → Values → check that conversation_id isn’t undefined.
  • GA4 DebugView: Admin → DebugView. Needs an active GTM Preview or the GA Debugger extension. Expand an event and check the parameters.
  • GA4 Realtime reports: Reports → Realtime → Event count by event name. Delay up to ~1 min.
  • Standard reports: data appears after 24–48 h. Don’t panic before then.

Pre-publish checklist

Layer 0 – loading

  • The AskSpot script loads exactly once (Network → filter embed-script, one entry).
  • Object.keys(window.AskWidget).length === 1.
  • After moving from the frontend to GTM: the old script is removed from the theme / code.
  • The widget doesn’t show on the cart / checkout / thank-you page (rule on the AskSpot side).

Layers 1–2 – events and bridge

  • askspot_widget_open fires on bubble open.
  • askspot_conversation_started fires on the first message.
  • askspot_conversation_started does not fire again after a page reload mid-conversation ← the most common bug.
  • conversationId is non-empty on askspot_conversation_started.
  • The bridge doesn’t attach listeners twice (window.askSpotDataLayerEventsBound === true).
  • Events fire on all page types (home, category, product, cart).

Layers 3–4 – GTM and GA4

  • All DLV variables have a Default Value ((not set)).
  • The ^askspot_w+ trigger regex catches all events.
  • The GA4 tag has analytics_storage set in Consent Settings; on a consent refusal the tags do not fire (test in incognito).
  • Custom dimensions and the key event are created before publishing the container.
  • After 48 h the GA4 numbers are the same order of magnitude as the AskSpot panel (a 10–40% shortfall is normal).

Shopify – additionally: the Custom Pixel deployed on a test shop first; verified whether the sandbox blocks access to conversation memory (whether conversationId is available on the checkout); if it blocks – a fallback to the askspot_cid cookie on the parent domain enabled; a test purchase links to a conversation (confirmed on the AskSpot side – message your contact with the date and time of the test).

Verification on the AskSpot side. We store conversations and conversions on our side. After your test we can confirm whether purchases link correctly to conversations – even if something isn’t visible on your end. Write to your contact with the date and approximate time of the test.

The most common implementation mistakes

  1. A duplicated script after moving to GTM. A script left in the frontend + a new one in GTM = doubled events. Symptom: twice as many conversations in GA4 as in the panel. Always remove the old one.
  2. conversation_started counted on every page view. No dedup by conversationId in sessionStorage. Symptom: “conversation starts” ≈ “user messages”. Fix with firstInConversation.
  3. Caching conversationId when attaching the listener. const cid = widget.sessionInfo.conversationId outside the callback returns null forever. Read it inside the callback.
  4. Custom dimensions created after the fact. GA4 doesn’t backfill. Two weeks of data without conversation_id is two weeks lost.
  5. No Default Value on the DLV variables. undefined means the parameter doesn’t reach GA4 at all.
  6. One try instead of a retry. setTimeout(init, 1000) works on a fast connection and fails on 3G. Always setInterval with a cap.
  7. Checking consent only once. The user accepts cookies after 3 s, the code checked at 1 s → no measurement for the whole “late acceptors” group. Listen for the consent change.
  8. Treating GA4 as the source of truth for billing. It will always be lower. The AskSpot panel is what you invoice from.
  9. Renaming events mid-flight. askspot_chat_startaskspot_conversation_started after a month = two unrelated data sets. Settle the convention before you start.
  10. Drawing causal conclusions from segments. “Chat sessions convert 3× better” doesn’t mean “chat triples conversion”. Without an A/B test, say correlation.

Full scripts & KPI glossary → the complete production dataLayer bridge, a React hook and a KPI glossary are in the reference companion.

What this guide doesn’t settle

A few things depend on your specific install and need checking with us or a test:

  1. Which business events (conversationContext.eventAction) are active on your widget – ask your AskSpot contact.
  2. Whether the Shopify Custom Pixel sandbox allows access to conversation memory – it needs a test on your install.
  3. The AskSpot conversions API documentation (the server-side variant from Level 3) – available on request.
  4. Rules for hiding the widget on specific URLs – we configure this on our side, send a list of patterns.
  5. How to classify the chat in your CMP (functionally necessary vs analytical) – your DPO’s decision; we have ready wording for the privacy policy.

Want to see exactly what you’re measuring? See the AI Chat Agent page – and if you also run the inbox, the AI Inbox Support Agent.

Why does GA4 show fewer conversations than the AskSpot panel?

Because some traffic declines analytics consent, and adblockers block GTM/GA4. Under Consent Mode v2, AskSpot events don’t fire without analytics_storage consent. A 10–40% shortfall is normal and correct – the AskSpot panel, not GA4, is what you invoice from.

When is the conversationId created?

On the user’s first message (new_user_action), not when the bubble opens. chat_opened before the first message has conversationId = null – that’s not a bug, the conversation doesn’t exist yet.

Does the chat really lift conversion?

The “chat vs non-chat sessions” segment shows correlation, not causation – people who chat are, by definition, more engaged. Hard proof of “chat delivers X” comes only from an A/B test with a stable control group.

Do I have to write code, or does AskSpot do it?

AskSpot provides layers 0 and 1 (widget + events). You build layers 2–4 (bridge, GTM, GA4) so you control the naming. If you’d rather we build the bridge inside our custom script, that’s possible – let’s arrange it individually.

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.