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.
| Id | GA4 | Miso |
|---|---|---|
| The browser, always | client_id, minted by gtag.js | client.context.anonymous_id, set to that client_id |
| The reader, when signed in | user_id, set by you | client.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 asmeasurementId, 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 aftergtag/js?id=.READER_IDstands 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 hook | GA4 event | Fires when |
|---|---|---|
request | miso_question | A 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. |
ready | miso_answer | The answer has arrived and starts to show. done fires later, once the last word is on screen. |
error | miso_answer_error | The question failed. |
feedback | miso_feedback | The reader clicks Helpful or Not helpful. value is helpful or unhelpful. |
tracker, a click on sources from a citation-link | miso_citation_click | The 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
-
On a test page, add
debug_mode: trueto 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.
-
Load the page, ask a question, click a citation, click Helpful.
-
In GA4, open Admin › DebugView and pick your device in the top-left list. Each event appears in the timeline with its parameters.
Summary
| Step | What you did |
|---|---|
| One reader id | GA4's client_id is Miso's anonymous_id, and your user_id is on both when signed in. |
| All or none | No client_id from GA4 means no GA4 events, and the answer box starts anyway. |
| Events | miso_question, miso_answer, miso_answer_error, miso_feedback, miso_citation_click |
Next
- Explore — Google Analytics 4 — the same for related questions on an article page.
- Events & Custom JS — every hook the SDK offers.
