Ask — Google Analytics 4

Give Miso the reader id GA4 already has, and send what happens in the question box to GA4 as events.

Your GA4 property knows who is on the page. Miso's question box knows what they asked, what they read, and whether the answer helped. Below we join the two: one reader id on both sides, then five GA4 events fired from the Ask workflow's own hooks.

You need gtag.js and the SDK on the page. See Ask — Quick Start for the SDK install.


One reader id on both sides

GA4 knows every browser by a client_id, and a signed-in reader by a user_id on top. Miso has the same pair: anonymous_id for the browser and user_id for the reader. Give Miso the GA4 values for both, and a report in either system can be joined to the other, before and after sign-in.

IdGA4Miso
The browser, alwaysclient_id, minted by gtag.jsclient.context.anonymous_id, set to that client_id
The reader, when signed inuser_id, set by youclient.context.user_id, the same value

The SDK reads the ids from client.context at the moment it sends a request, so the handoff runs before the first request. The function below does the handoff. It goes in a script of its own, or at the top of the SDK script:

// If gtag.js has not run yet, define the queue it expects. The calls below then
// wait in dataLayer until it loads. Harmless when the gtag snippet already ran.
window.dataLayer = window.dataLayer || [];
window.gtag = window.gtag || function () { dataLayer.push(arguments); };

// Give Miso the GA4 ids. Resolves true when GA4 answered, false when it did not.
async function ga4Ids(client, measurementId) {
  if (window.READER_ID) {
    client.context.user_id = READER_ID;
    client.context.user_type = "subscriber"; // if you meter, see /operate/metering
  }

  const clientId = await new Promise((resolve) => {
    gtag("get", measurementId, "client_id", resolve);
    setTimeout(() => resolve(null), 2000);
  });
  if (!clientId) return false;

  client.context.anonymous_id = clientId;
  if (window.READER_ID) gtag("config", measurementId, { user_id: READER_ID });
  return true;
}

Three names in there are yours:

  • G-XXXXXXXXXX, the argument you pass as measurementId, is the Measurement ID of your GA4 web data stream. It is under Admin › Data streams › your stream in GA4, and in your existing gtag snippet after gtag/js?id=.
  • READER_ID stands for wherever your page keeps the signed-in reader's id: a global your template writes, a cookie, or your auth library.
  • "subscriber" is the reader's tier, when you meter.

The return value is the all-or-none switch. When gtag.js is blocked, or loads after the two seconds, GA4 never answers with a client_id. The function then returns false and the page skips every GA4 event, so a session is either fully joined or absent from GA4. Miso still receives user_id and its own anonymous_id, and the answer box starts.

With Google Tag Manager there is no gtag.js on the page. Read the browser id from the _ga cookie instead, whose value is GA1.1. followed by the client_id, and send the events through dataLayer and a GTM trigger:

const match = document.cookie.match(/(?:^|; )_ga=GA\d\.\d\.([^;]+)/);
if (match) client.context.anonymous_id = match[1];

Send question events to GA4

The Ask workflow emits an event at each step of a question. In the following function, we turn five of them into GA4 events:

function ga4AskEvents(client) {
  const asks = client.ui.asks;

  asks.on("request", ({ payload }) => {
    gtag("event", "miso_question", {
      question: payload.question.slice(0, 100),
      question_source: (payload.metadata || {}).question_source,
      parent_question_id: payload.parent_question_id,
    });
  });

  asks.on("ready", ({ workflow }) => {
    gtag("event", "miso_answer", { question_id: workflow.questionId });
  });

  asks.on("error", ({ workflow }) => {
    gtag("event", "miso_answer_error", { question_id: workflow.questionId });
  });

  asks.on("feedback", ({ workflow, value }) => {
    gtag("event", "miso_feedback", { question_id: workflow.questionId, value });
  });

  asks.on("tracker", ({ workflow, type, role, event_target, items }) => {
    if (type !== "click" || role !== "sources" || event_target !== "citation-link") return;
    gtag("event", "miso_citation_click", {
      question_id: workflow.questionId,
      product_id: items[0],
    });
  });
}
Ask hookGA4 eventFires when
requestmiso_questionA question goes to Miso. question_source says how: _organic for a typed question, _suggested_questions for a click on a related question. A follow-up carries parent_question_id.
readymiso_answerThe answer has arrived and starts to show. done fires later, once the last word is on screen.
errormiso_answer_errorThe question failed.
feedbackmiso_feedbackThe reader clicks Helpful or Not helpful. value is helpful or unhelpful.
tracker, a click on sources from a citation-linkmiso_citation_clickThe reader opens a cited article. product_id is the article.

Every hook hands you the workflow that owns the event. Its questionId is the question the event belongs to, which for a follow-up differs from client.ui.ask.questionId, the root of the thread.

A related question the reader clicks is a new question, so it arrives as its own miso_question and miso_answer pair.

GA4 keeps event names to 40 characters, parameter names to 40, and parameter values to 100. That is why question is cut at 100. To see a parameter in reports, register it as a custom dimension under Admin › Custom definitions.


Put it together

The id handoff and the styles load side by side, and the question box starts when both are in:

const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
  const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");

  const [ga4] = await Promise.all([ga4Ids(client, "G-XXXXXXXXXX"), client.ui.ready]);
  if (ga4) ga4AskEvents(client);

  const root = document.querySelector("#miso-ask-combo");
  root.innerHTML = MisoClient.ui.defaults.ask.templates.root();
  client.ui.ask.autoQuery(); // the first request goes out here
});

What stays with Miso

The SDK already reports to Miso on its own: which sources were shown and seen, which were clicked, and the reader's feedback. Dojo builds its answer metrics from those, so there is no need to send the same to GA4 twice. The events above give GA4 what it is good at: the question box as a step in the reader's session, next to page views, sign-ups and subscriptions.


Check it

  1. On a test page, add debug_mode: true to the config call in your gtag snippet, so the page reports to DebugView:

    gtag("config", "G-XXXXXXXXXX", { debug_mode: true });
    

    To test a live page without editing it, install the Google Analytics Debugger extension and turn it on. It puts the browser in debug mode for every page.

  2. Load the page, ask a question, click a citation, click Helpful.

  3. In GA4, open Admin › DebugView and pick your device in the top-left list. Each event appears in the timeline with its parameters.


Summary

StepWhat you did
One reader idGA4's client_id is Miso's anonymous_id, and your user_id is on both when signed in.
All or noneNo client_id from GA4 means no GA4 events, and the answer box starts anyway.
Eventsmiso_question, miso_answer, miso_answer_error, miso_feedback, miso_citation_click

Next