W skrócie
- Trzy sposoby, by przypisać
conversation_iddo zdarzenia purchase: GA4 user property,sessionStoragealbo cookie. - Atrybucja ma cztery poziomy – od segmentu sesji bez żadnej pracy po twardy test A/B.
- Pełny produkcyjny most do dataLayer i hook Reacta są tutaj, gotowe do wklejenia.
- Korelacja to nie przyczynowość – dopiero test A/B (poziom 4) dowodzi wpływu czatu na przychód.
Atrybucja: czy czat sprzedaje?
To pytanie, dla którego robi się całe wdrożenie. Są cztery poziomy – od najprostszego do najsolidniejszego.
Poziom 1: segment sesji (zero dodatkowej pracy)
Masz to od razu po eksploracji B w głównym poradniku. Porównujesz konwersję sesji z czatem i bez.
- Plus: działa natychmiast, bez zmian w kodzie.
- Minus: silny bias selekcji.
Nie mów „czat podnosi konwersję o X%” – mów „sesje z rozmową konwertują X razy częściej”.
Poziom 2: conversation_id na zdarzeniu purchase
Chcesz wiedzieć, która rozmowa skończyła się którym zamówieniem. Wymaga przekazania conversation_id do zdarzenia zakupowego, którego zwykle nie kontrolujesz bezpośrednio. Trzy sposoby:
Sposób A – GA4 User Property (najprostszy). W moście, przy starcie rozmowy, ustaw user property. W GTM, w Tag GA4 Event → Właściwości użytkownika, dodaj:
| Właściwość użytkownika | Wartość |
|---|---|
last_conversation_id | {{DLV - conversationId}} |
Zarejestruj w GA4 wymiar niestandardowy o zakresie User dla last_conversation_id. Od tej pory każde zdarzenie tego użytkownika – w tym purchase – będzie miało ten wymiar.
- Plus: jedna zmiana w GTM.
- Minus: trzyma tylko ostatnią rozmowę; nie działa cross-device.
Sposób B – zmienna GTM czytająca sessionStorage. W moście (w funkcji push lub w listenerze session_update) zapisuj bieżące ID:
try { sessionStorage.setItem("askspot_current_conversation", id); } catch (e) {}
W GTM utwórz zmienną typu Custom JavaScript:
function () {
try {
return sessionStorage.getItem("askspot_current_conversation") || "(not set)";
} catch (e) {
return "(not set)";
}
}
I dołóż ją jako parametr conversation_id do swojego istniejącego tagu purchase.
- Plus: działa niezależnie od user properties, przeżywa nawigację.
- Minus:
sessionStoragenie przechodzi między domenami; na checkoucie w izolowanym sandboxie może być niedostępny.
Sposób C – cookie na domenie głównej (najbardziej odporny). Zamiast sessionStorage zapisz ID w cookie na domenie nadrzędnej: document.cookie = "askspot_cid=" + encodeURIComponent(id) + "; path=/; domain=.twojadomena.pl; max-age=1800; SameSite=Lax";
Cookie z domain=.twojadomena.pl jest widoczne na wszystkich subdomenach – w tym na checkoucie, jeśli działa on na subdomenie tej samej domeny głównej. W GTM odczytasz je zmienną typu First-Party Cookie.
- Plus: działa cross-subdomain.
- Minus: to cookie analityczne – musi podlegać zgodzie
analytics_storage; zapisuj je tylko po uzyskaniu zgody i uwzględnij w polityce prywatności.
Poziom 3: konwersje po stronie AskSpot
Niezależnie od Twojego GA4, AskSpot może odbierać zdarzenia zakupowe i wiązać je z rozmowami po swojej stronie. Wtedy w panelu AskSpot widzisz przychód przypisany do rozmów, bez konfiguracji w GTM. Dwa warianty:
- Wariant automatyczny. Nasz skrypt nasłuchuje zdarzeń zakupowych w Twoim środowisku (np.
checkout_completedw Shopify Customer Events albopurchasew dataLayer), odczytujeconversationIdz pamięci widgetu i wysyła konwersję – wartość + identyfikator rozmowy – bezpośrednio do naszego API. Nie musisz nic budować. - Wariant ręczny. Wysyłasz konwersje do naszego API sam, ze swojego backendu. To najsolidniejsza opcja, bo dane idą server-side (odporne na adblocki i brak zgód na cookies analityczne, o ile masz podstawę prawną). Poproś nas o dokumentację API konwersji.
Rekomendacja: zrób jedno i drugie. GA4 do analizy zachowań i porównań z resztą ruchu; API AskSpot jako niezależne, server-side źródło prawdy o przychodzie z rozmów. Rozjazd między nimi jest normalny i sam w sobie wiele mówi (pokazuje skalę utraty danych po stronie frontendu).
Poziom 4: test A/B (jedyny twardy dowód)
Wszystko powyżej mierzy korelację. Jeśli potrzebujesz liczby, którą można pokazać zarządowi jako „czat dowozi X zł”, potrzebujesz eksperymentu:
- Podziel ruch: 90% z widgetem, 10% bez (grupa kontrolna),
- przypisanie stabilne per użytkownik (cookie), nie per sesja,
- minimum 2–4 tygodnie i wystarczająca próba,
- porównaj przychód per użytkownik (nie konwersję sesji) między grupami.
Podział zrealizujesz w GTM (losowa zmienna + cookie) albo poproś nas o wsparcie po stronie konfiguracji integracji. Uwaga: wyłączanie czatu 10% klientów ma koszt obsługowy – to decyzja biznesowa, nie tylko analityczna.
Aneks A – pełny skrypt produkcyjny
Wersja rozszerzona: głębokość rozmowy, notyfikacje proaktywne, tryb agenta, ochrona przed podwójnym bindowaniem i opcjonalne cookie do atrybucji cross-subdomain.
<script>
(function () {
"use strict";
// ── KONFIGURACJA ──────────────────────────────────────────────
var PREFIX = "askspot_";
var COOKIE_NAME = "askspot_cid";
var COOKIE_DOMAIN = ""; // np. ".twojadomena.pl" - puste = wyłączone
var COOKIE_TTL = 1800; // sekundy
var MAX_RETRIES = 60; // 60 × 500 ms = 30 s
// ──────────────────────────────────────────────────────────────
window.dataLayer = window.dataLayer || [];
if (window.askSpotDataLayerEventsBound) return; // już podpięte
var messageCounters = {}; // conversationId -> licznik wiadomości usera
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 startu rozmowy - przeżywa nawigację między podstronami
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);
}
// ── stan widgetu ──────────────────────────────────────────
widget.addEventListener("chat_opened", function () { push("widget_open"); });
widget.addEventListener("minimize_chat", function () { push("widget_minimize"); });
widget.addEventListener("chat_closed", function () { push("widget_close"); });
// ── wiadomość użytkownika ─────────────────────────────────
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)
});
});
// ── odpowiedź AI ──────────────────────────────────────────
widget.addEventListener("new_chat_message", function (event) {
var data = unwrap(event);
push("ai_message", {
awaitingUserResponse: data.awaitingUserResponse === true,
messageIndex: messageCounters[cid()] || 0
});
});
// ── sesja: odśwież cookie, gdy pojawi się / zmieni conversationId
widget.addEventListener("session_update", function (event) {
var data = unwrap(event);
if (data.conversationId) setConversationCookie(data.conversationId);
});
// ── proaktywne notyfikacje (mianownik do CTR zaczepek) ────
widget.addEventListener("showNotification", function (event) {
var data = unwrap(event);
push("notification_shown", {
notificationContent: typeof data.content === "string"
? data.content.slice(0, 100)
: ""
});
});
// ── tryb agenta ───────────────────────────────────────────
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>
Zdarzenia produkowane przez ten skrypt:
| Zdarzenie w dataLayer | Parametry |
|---|---|
askspot_widget_open | conversationId, pageLocation |
askspot_widget_minimize | conversationId, pageLocation |
askspot_widget_close | conversationId, pageLocation |
askspot_conversation_started | + actionName |
askspot_user_message | + actionName, messageIndex |
askspot_ai_message | + awaitingUserResponse, messageIndex |
askspot_notification_shown | + notificationContent (obcięte do 100 zn.) |
askspot_agent_mode | + agentModeState |
Uwaga o wolumenie. askspot_user_message i askspot_ai_message lecą przy każdej turze rozmowy. Przy dużym ruchu to znaczący wolumen zdarzeń w GA4 (limity kardynalności, koszt BigQuery). Jeśli nie potrzebujesz analizy głębokości rozmów, zakomentuj te dwa listenery – reszta działa niezależnie.
Aneks B – wersja React (hook)
Dla aplikacji Reactowych, gdzie ładujesz widget bezpośrednio z kodu (wariant frontendowy – zob. główny poradnik). Hook obsługuje ładowanie skryptu, retry bindowania i sprzątanie przy odmontowaniu.
import { useEffect } from "react";
const ASKSPOT_SCRIPT_ID = "askspot-script";
const ASKSPOT_SCRIPT_SRC =
"https://chat.askspot.io/api/v1/integration/{TWOJE_ID_INTEGRACJI}/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]);
}
Użycie z obsługą zgody:
const analyticsConsent = useConsent("statistics"); // Twoja logika CMP
useAskSpotScript(analyticsConsent);
Różnice vs Aneks A:
| Aneks A (vanilla/GTM) | Aneks B (React) | |
|---|---|---|
| Deduplikacja startu | sessionStorage – przeżywa przeładowanie | Set w pamięci – ginie przy przeładowaniu |
| Licznik wiadomości | tak | nie |
| Cookie do atrybucji | tak (opcjonalnie) | nie |
| Ładowanie skryptu | zakłada, że widget już jest | ładuje sam |
| Nazwy zdarzeń | askspot_user_message, askspot_ai_message | askspot_new_user_action, askspot_new_chat_message |
Ujednolić nazwy. Aneks B używa nazw kopiujących zdarzenia widgetu 1:1 (askspot_new_user_action), Aneks A – nazw opisowych (askspot_user_message). Wybierz jedną konwencję przed startem. Regex ^askspot_w+ w GTM złapie obie, ale w GA4 zobaczysz dwa niepowiązane zestawy zdarzeń, jeśli w trakcie zmienisz zdanie.
W SPA Set w pamięci wystarcza (nie ma pełnych przeładowań). Przy SSR / MPA przenieś deduplikację na sessionStorage – inaczej zawyżysz conversation_started.
Aneks C – słownik KPI
Metryki, które warto raportować, i sposób ich liczenia w GA4.
| KPI | Definicja | Jak policzyć |
|---|---|---|
| Zasięg czatu | % sesji, w których dymek został otwarty | sesje z askspot_widget_open / wszystkie sesje |
| Współczynnik zagajenia | % otwarć, które skończyły się pierwszą wiadomością | askspot_conversation_started / askspot_widget_open |
| Penetracja rozmów | % sesji z rozmową | sesje z askspot_conversation_started / wszystkie sesje |
| Średnia głębokość rozmowy | ile tur ma typowa rozmowa | średnia z message_index per conversation_id |
| CTR zaczepek proaktywnych | skuteczność notyfikacji | askspot_widget_open (po notification_shown) / askspot_notification_shown |
| Konwersja z czatem | CR sesji z rozmową | zakupy w segmencie „użytkownicy czatu” / sesje w tym segmencie |
| Uplift (korelacyjny) | ile razy lepiej konwertują sesje z rozmową | CR z czatem / CR bez czatu – nie nazywaj tego wpływem czatu |
| Przychód wspomagany rozmową | przychód w sesjach z rozmową | purchase revenue w segmencie „użytkownicy czatu” |
| Przychód atrybuowany | przychód powiązany z konkretnymi rozmowami | purchase z niepustym conversation_id |
| Uplift przyczynowy | realny wpływ czatu | wyłącznie z testu A/B |
Zalecany zestaw na start (5 metryk): zasięg czatu, penetracja rozmów, średnia głębokość, konwersja z czatem vs bez, przychód wspomagany. Reszta, gdy pierwsze pięć jest stabilne.








