Google Analytics 4
Give Miso the reader id GA4 already has, and send what happens in the answer box to GA4 as events.
Your GA4 property knows who is on the page. Miso's answer 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 a handful of GA4 events fired from the SDK's own hooks.
You need gtag.js and the Answers 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 block below sits between creating the client and the call that sends the
first request: autoQuery() on the Ask page, start() on an Explore page.
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");
const hasGtag = typeof gtag === "function";
// 1. Ids first. The browser id, always:
await new Promise((resolve) => {
if (!hasGtag) return resolve(); // no gtag on the page: Miso uses its own id
gtag("get", "G-XXXXXXXXXX", "client_id", (id) => {
client.context.anonymous_id = id;
resolve();
});
setTimeout(resolve, 2000); // gtag.js blocked by the browser: carry on without it
});
// And the reader id on top, when signed in:
if (window.READER_ID) {
if (hasGtag) gtag("config", "G-XXXXXXXXXX", { user_id: READER_ID });
client.context.user_id = READER_ID;
client.context.user_type = "subscriber"; // if you meter, see /operate/metering
}
// 2. Then the answer box, as in the quick start.
await client.ui.ready;
document.querySelector("#miso-ask-combo").innerHTML =
MisoClient.ui.defaults.ask.templates.root();
client.ui.ask.autoQuery(); // the first request goes out here
});
On an article page with the Explore unit, the order is the same. The ids
block goes in, then the Explore setup, and start() sends the first request:
misocmd.push(async () => {
const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");
// 1. Ids first: the same block as above.
// 2. Then Explore, as in its quick start.
const explore = client.ui.explore;
explore.useApi({ product_id: "art_001" });
explore.useLink((q) => `/ask.html?q=${encodeURIComponent(q)}`);
explore.start(); // the first request goes out here
});
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.
Keep anonymous_id set for signed-in readers too. Every interaction Miso
records carries both ids, so a reader's clicks from before they signed in and
after line up under one client_id. Questions from a signed-in reader carry
user_id and user_type.
Without the ids the SDK mints its own anonymous_id and keeps it in the
browser. The override lives for the page load, so this block runs on every
page that has the answer box.
Two things can be missing, and the block survives both. A content blocker
stops gtag.js from loading, so the client_id callback never fires: the
timeout lets the answer box start after two seconds with Miso's own id. Or
there is no gtag function on the page at all, because the snippet is absent
or Google Tag Manager loads GA4 for you: hasGtag skips the GA4 calls and the
answer box starts at once.
With Tag Manager, read the browser id from the _ga cookie instead. Its
value is GA1.1. followed by the client_id:
const match = document.cookie.match(/(?:^|; )_ga=GA\d\.\d\.([^;]+)/);
if (match) client.context.anonymous_id = match[1];
Send answer events to GA4
The SDK emits an event at each step of a question. In the following block, we
turn five of them into GA4 events. It goes in the same callback as the ids,
before autoQuery():
const ga = (...args) => { if (typeof gtag === "function") gtag(...args); };
const asks = client.ui.asks;
asks.on("request", ({ payload, session }) => {
ga("event", "miso_question", {
question: payload.question.slice(0, 100),
question_source: session.meta.question_source,
});
});
asks.on("done", ({ workflow }) => {
ga("event", "miso_answer", { question_id: workflow.questionId });
});
asks.on("error", ({ workflow }) => {
ga("event", "miso_answer_error", { question_id: workflow.questionId });
});
asks.on("feedback", ({ workflow, value }) => {
ga("event", "miso_feedback", { question_id: workflow.questionId, value });
});
document.addEventListener("click", (event) => {
const link = event.target.closest("a.miso-citation-link");
if (!link) return;
ga("event", "miso_citation_click", {
question_id: client.ui.ask.questionId,
citation_index: Number(link.dataset.index),
link_url: link.href.split("#")[0],
});
}, true);
| SDK 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. |
done |
miso_answer |
The answer is fully on screen. |
error |
miso_answer_error |
The question failed. |
feedback |
miso_feedback |
The reader clicks Helpful or Not helpful. value is helpful or unhelpful. |
Click on a.miso-citation-link |
miso_citation_click |
The reader opens a cited article. citation_index is the number shown in the answer, and link_url is the article. |
ga is a one-line wrapper that drops the call when gtag is missing, so the
hooks are safe on a page without the snippet. With Tag Manager, push to
dataLayer there instead and fire the events from a GTM trigger.
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.
Explore: a click on a related question
The Explore unit lists related questions under an article. A click on one
emits select on the Explore workflow, with the question the reader chose:
const ga = (...args) => { if (typeof gtag === "function") gtag(...args); };
const explore = client.ui.explore;
explore.on("select", ({ question }) => {
ga("event", "miso_explore_click", { question: question.text.slice(0, 100) });
});
The click then goes wherever your link function points, usually the answers
page, where the question arrives as a miso_question with
question_source: _organic.
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 answer box as a step in the reader's session, next to page views, sign-ups and subscriptions.
Check it
-
Add
debug_mode: trueto the gtag config on a test page. Ask a question, click a citation, click Helpful. - In GA4, open Admin › DebugView and pick your device. Each event shows 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. |
| Six events | miso_question, miso_answer, miso_answer_error, miso_feedback, miso_citation_click, miso_explore_click. |
| Nothing twice | Impressions and clicks on sources stay with Miso and Dojo. |
