Skip to main content

Conversion Analytics & the Google Analytics bridge

Turing already measures the funnel server-side (outcome, experiment, persona). This is the other half: the browser-owned view, emitted at the lifecycle moments only the client can see — like a tab close — and delivered straight into your own GA4 next to acquisition source, campaign and audiences.

Turing's Chat Analytics and Experiments record conversion, abandonment and A/B outcomes server-side. But marketing teams run GA4 in the browser, where conversion lives next to media spend and audiences. Block Z adds a client-side, marketing-owned measurement layer that answers three concrete questions in your GA, not ours:

  1. Lead conversion in chat & semantic navigation.
  2. Drop-off / abandonment — and where the visitor gave up.
  3. Which persona A/B variation was preferred.

The architecture: a canonical bus + pluggable sinks

Google Analytics is not hard-coded into the SDK — that would break the zero-dependency, framework-agnostic, EDS-friendly contract. Instead:

  • a canonical, vendor-neutral event bus (createTuringAnalytics) that the chat / search controllers feed, plus
  • pluggable sinks — GA4 is the first adapter, not a dependency.

The bus is no-op when no analytics option is passed, so every existing embed is byte-for-byte unchanged.

Quick start (vanilla SDK)

import {
createTuringClient, createTuringAnalytics, googleAnalyticsSink,
createSearchController, createChatController,
} from "@viglet/turing-sdk";

const analytics = createTuringAnalytics({
sinks: [googleAnalyticsSink()], // auto-detects window.gtag / dataLayer
context: { site: "my-site" },
});

const client = createTuringClient({ baseURL: "/api" });

// One bus, shared by both surfaces → a single search → chat → lead funnel.
createSearchController(client, { site: "my-site" }, undefined, { analytics });
createChatController(client, { site: "my-site", analytics, goalSlots: ["email", "phone"] });

React

import { useTuringAnalytics, useTuringChat, useTuringSearch } from "@viglet/turing-react-sdk";

function Storefront() {
const { analytics } = useTuringAnalytics(); // GA4 sink auto-attached
const chat = useTuringChat({ site: "store", analytics, goalSlots: ["email"] });
const search = useTuringSearch(undefined, { analytics });
// search.trackResultClick(documentId, position) → server CTR + GA event
// ...
}

useTuringAnalytics() seeds the site from your <TuringProvider> automatically and returns a memoized bus (stable across renders).

The canonical event taxonomy

Vendor-neutral names — sinks map them verbatim, so a funnel built on turing_* is portable across GA4, Segment, Matomo, …

EventFired whenKey params
turing_chat_startconversation begins (first user message)mode
turing_chat_message_sentevery user turnturn, length
turing_chat_stepa flow node is reached (client mirror of the server funnel)node_id, flow_name, step
turing_chat_lead_captureda goal slot is written or a native form is submittedreason, goal_slot
turing_chat_abandoneda started, unconverted conversation ends (tab hide / unload / idle)reason, turns, last_step, elapsed_ms
turing_ab_variant_assignedthe A/B arm resolvesstamps experiment_key / variant_label / persona_id
turing_searcha query runsquery, results
turing_search_no_resultsa query returns 0 hits (content-gap signal)query
turing_search_result_clicka result is clickedquery, document_id, position
turing_search_refineda query is reformulatedfrom, to

Once turing_ab_variant_assigned fires, experiment_key / variant_label / persona_id are stamped on every subsequent event — so a GA4 comparison or audience answers "which persona variation converted" natively.

Abandonment: the signal the server can't see

The server only infers abandonment from a session TTL. The browser actually sees the visitor leave. A started conversation (≥1 user message) with no conversion emits a single turing_chat_abandoned on tab hide / page unload / idle timeout — carrying the last step reached and the turn count. On unload the GA4 sink flags the event transport_type: 'beacon' so navigator.sendBeacon flushes it before teardown.

createChatController(client, {
site: "my-site", analytics,
abandonment: { idleMs: 60_000, onHidden: true, onUnload: true }, // or `false` to disable
});

Cross-surface stitching (search → chat → lead)

Both the search and chat controllers key off the same TUR_SESSION cookie, and the bus stamps it as the session id. A chat lead therefore attributes back to the originating search query — GA4 shows one journey, not two disjoint funnels.

Sinks

  • googleAnalyticsSink({ measurementId?, transport? }) — maps each event to GA4 gtag('event', name, params) and/or a GTM dataLayer.push({ event, ... }). Identity fields are snake_cased to GA convention; the reserved session_id is namespaced to turing_session_id.
  • onEventSink(fn) — generic escape hatch for Segment / Matomo / Plausible or any custom pipeline.
  • debugSink() — console tracing during integration.

Adobe EDS zero-config

googleAnalyticsSink() auto-detects an existing window.gtag / window.dataLayer lazily at emit time. On a GTM-tagged or Adobe Edge Delivery Services site that already loads analytics, the bridge lights up with no measurement id and no extra config. See the turing-analytics EDS example block.

Non-goal

Turing does not ship its own conversion dashboards here — GA4 (and any sink) owns visualization; the SDK only emits clean, canonical events.


PageDescription
Chat AnalyticsThe server-side funnel (outcome, sentiment, tools)
ExperimentsA/B outcomes recorded server-side
React SDKWhere the client-side events are emitted
Token Usage · Cost GovernanceThe cost side of the funnel