Explore — Google Analytics 4
Give Miso the reader id GA4 already has, and send a click on a related question to GA4 as an event.
Explore lists related questions under an article, and a reader who picks one lands on your answers page. Below we join GA4 and Miso on one reader id, then report the pick as a GA4 event, so the path from article to answer shows in one GA4 session.
You need gtag.js and the SDK on the page. See Explore — 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 the click to GA4
A click on a related question emits select on the Explore workflow, with the
question the reader chose:
function ga4ExploreEvents(client) {
client.ui.explore.on("select", ({ question }) => {
gtag("event", "miso_explore_click", { question: question.text.slice(0, 100) });
});
}
The click then goes wherever your link function points, usually the answers
page. There the same question arrives as a miso_question with
question_source: _organic, if that page reports to GA4 too. See
Ask — Google Analytics 4.
GA4 keeps parameter values to 100 characters. That is why question is cut at
100. To see it in reports, register it as a custom dimension under Admin ›
Custom definitions.
Put it together
The id handoff runs first, and start() sends the first request:
const misocmd = window.misocmd || (window.misocmd = []);
misocmd.push(async () => {
const client = new MisoClient("YOUR_PUBLISHABLE_API_KEY");
const explore = client.ui.explore;
const ga4 = await ga4Ids(client, "G-XXXXXXXXXX");
if (ga4) ga4ExploreEvents(client);
explore.useApi({ product_id: "art_001" });
explore.useLink((q) => `/ask.html?q=${encodeURIComponent(q)}`);
explore.start(); // the first request goes out here
});
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_explore_click |
Next
- Ask — Google Analytics 4 — the answers page these clicks lead to.
