Customer.io's In-App Debugger Tells You Why a Web Message Isn't Showing—in One URL Parameter

On 9 September 1947, the operators of the Harvard Mark II pulled a moth out of a relay, taped it into the logbook, and wrote beside it: "First actual case of bug being found." The note is one of computing's most-reproduced artefacts, now held by the Smithsonian. It usually gets told as the origin of the word "bug"—it isn't. Thomas Edison was already writing about "bugs" in his equipment in an 1878 letter, nearly seventy years before. What the moth really marks is something else: for most of computing's history, debugging meant hunting for a thing you couldn't see.

A state read-out is the opposite of taping a moth in a book. Instead of guessing what the machine is doing, you ask it, and it tells you. That is what Customer.io shipped on 11 June 2026 for web in-app messages.

The day before, we published a five-minute decision tree for in-app messages that won't show. The next day, Customer.io shipped an in-app debugger that turns most of that tree into a live read-out. Add cio_debug_session=true to any page on your site and an overlay shows you the SDK's actual state. You see which workspace it's connected to, who it has identified, what route it thinks you're on, and every message active or waiting in the queue. This post is the operator's guide to it—what each section means, how to read it against the usual silent failures, and the one limitation that decides whether it helps you at all.

The tool, in one URL parameter

The debugger is built into the Customer.io JavaScript SDK—the same snippet that identifies your visitors and renders web in-app messages—so there's nothing extra to install. Add cio_debug_session=true to the URL of any page that already runs the SDK, then reload:

https://www.example.com/pricing?cio_debug_session=true

An overlay—the Customer.io In-App SDK Debugger—appears in the bottom-right corner. It shows the SDK's live state and updates on its own as that state changes: when the route changes, when a message displays or is dismissed, when the inbox updates. To close it, click the × in the corner or reload the page without the parameter.

One thing to settle before you read the rest. The debugger doesn't fix anything. It's a read-only view of what the SDK is doing right now, and Customer.io's docs are explicit that it "doesn't change how messages are delivered or displayed." It tells you where to look. That's the right mental model: a dashboard, not a repair tool.

Reading the four sections against the usual silent failures

The overlay is organised into four sections—Config, User, Route and Messages. Read top to bottom, they answer one question: is the SDK set up correctly, and if a message isn't showing, where's the break? Each section maps onto a cause from the decision tree, so you can stop reasoning about a step and just read it.

Config: are you even on the right workspace?

Config confirms two things—that the SDK loaded, and how it's receiving messages. Two fields carry the weight.

The Site ID is the credential the SDK is using to talk to Customer.io. The most common silent failure in a multi-workspace setup is sending your test from one workspace while the page runs the Site ID of another; staging versus production is the classic version. The message sends fine. It just never had a chance of arriving, because the page is listening on a different workspace. Check the Site ID here against the workspace you sent from, and if they don't match, that's your bug.

Underneath sits the Connection field, which shows how the SDK pulls messages. SSE is a real-time server-sent-events connection. A value like Polling 30s means it's checking on an interval instead, which is normal when you haven't sent messages recently; the SDK switches to SSE once messages start flowing. And if Config reads NOT INITIALIZED, the SDK didn't start at all. Stop there and check your installation, because nothing downstream will work until it does.

User: identified, anonymous, or nobody at all

The User section shows who the SDK has identified on this page, and it has three readings—only one of which is always a problem.

A truncated user token means a person is identified. That's the value you passed to cioanalytics.identify(), and it's what you want to see for any personalised message. Check that it resolves to the person you expect, because a stale or wrong identify call will quietly show the wrong token while everything looks healthy.

(anonymous) means the SDK is using an anonymousId, so the person isn't identified. This used to be a flat bug. It isn't anymore: Customer.io now supports anonymous in-app messages, and the docs call this state "normal and expected" when that's what you're sending. So (anonymous) is only a problem if the message you're testing needs an identified person.

(none) is the reading that's always wrong. It means there's no userId and no anonymousId at all, which usually points to a problem with how the SDK initialised. Treat (none) the way you'd treat NOT INITIALIZED: the foundation isn't there, so fix that first.

Route and Messages: page rules, the queue, and what "eligible" really means

The last two sections are where most "it just won't appear" bugs actually live.

Route shows the path the SDK thinks you're on—the value it matches your messages' page rules against. If it reads NONE, the SDK doesn't know what page you're on, so it can't match any page rule. In a single-page app this is the usual culprit. The SDK only sends a page call automatically on a full page load, so on client-side route changes you have to send analytics.page() yourself. Miss that, and Route stays stale while the user clicks around, and a perfectly good page rule never matches.

Messages lists every message the SDK currently knows about, with a count in the heading. Each one is tagged with its display type—modal, overlay, or inline—and a state. Active means it's eligible to show, or is showing, on this page. Queued means it's being held, and the debugger tells you why.

That "why" is the useful part. The debugger validates each message's conditions and flags the failures with a ✕. A ✕ on the Route Rule means the page rule doesn't match the current route—often the single-page-app page-call problem again. A ✕ on the Target means the CSS selector for an inline message or tooltip didn't match an element on the page, so there's nowhere to render it. A ✓ means the condition is satisfied.

Be precise about what "eligible" covers here, though. The debugger shows what reached the SDK and validates two display conditions: the route rule and the target element. It doesn't evaluate the upstream reasons a message might never arrive—segment membership, frequency rules, or whether the trigger you configured fired and entered the person into the campaign at all. Those decisions live in the workflow, not the SDK, which is exactly where the manual decision tree still earns its place. And it's worth knowing what an active, displayed message does and doesn't tell you: that state is precisely what Customer.io counts as "opened"—displayed, not read.

The limitation that matters: web only

Here's the catch, and it's a real one. The debugger only works for web in-app messages. It's built into the JavaScript SDK, full stop—there's no equivalent overlay in the iOS, Android, React Native, Flutter or Expo SDKs. If your in-app messages live in a mobile app, this tool does nothing for you.

Mobile isn't left with nothing, though. The documented path is to set your mobile SDK's log level to debug, which traces a message through its whole lifecycle—receipt, display, dismissal—in your device logs. It's more work than reading an overlay, and you need a build running at the right log level, but it's there. For everything the logs don't make obvious, the manual checklist still applies. It was written channel-agnostic, and every step except the web-console test works the same on a phone.

So the honest summary: web bugs now get a five-second answer, and mobile bugs still get the manual checklist.

A QA workflow for a mixed web-and-mobile team

If your team ships in-app messages on both web and mobile, the debugger changes your routine on one side and leaves the other as it was. A workflow that uses each tool for what it's good at:

For web, you now have three testing tools, and they do different jobs. Live Preview opens your site with a preview bar and lets you adjust position and display settings on the fly—use it while you're building, to get the look right. Send Test delivers a real one-time message to a chosen person—use it to confirm a finished message shows up where you expect. The debugger is the diagnostic layer for when Send Test doesn't appear: it tells you whether the SDK initialised, who it identified, what route it's on, and why a message is queued. Build with Live Preview, confirm with Send Test, and when a real message doesn't show, append cio_debug_session=true and read the four sections.

For mobile, the routine doesn't change. Set the SDK log level to debug, send your test, and trace the message through the device logs. The discipline worth adding sits upstream of all of it. When a bug report lands, your first question is "web or mobile?"—because the answer decides whether you get a five-second read-out or a ten-minute trace.

One more habit. Because the debugger runs off a URL parameter, anyone who knows the parameter can open it on your live site. It's safe—it only ever reflects that visitor's own session—but it isn't hidden. Keep cio_debug_session=true in your team's QA notes, not in a public help doc.

Frequently asked questions

How do I turn on the Customer.io in-app debugger?

Add cio_debug_session=true to the URL of any page on your site that already runs the JavaScript SDK, then reload the page. For example, https://www.example.com/pricing?cio_debug_session=true. The Customer.io In-App SDK Debugger overlay appears in the bottom-right corner and updates automatically as the SDK's state changes. There's nothing to install—the debugger is built into the SDK you already have on the page.

What does cio_debug_session=true actually show?

It shows the SDK's live state on the current page, organised into four sections: Config, User, Route and Messages. Config confirms the SDK initialised and which Site ID it's using, and User shows who the SDK has identified. Route shows the path it's matching page rules against, and Messages lists every message it knows about—active or queued—with the reason any message is being held. Together they tell you whether the SDK is set up correctly and, if a message isn't showing, where the problem is.

Does the in-app debugger work for mobile apps?

No. The debugger is built into the Customer.io JavaScript SDK and works for web in-app messages only. It isn't available in the iOS, Android, React Native, Flutter or Expo SDKs. To debug mobile in-app messages, Customer.io's documented approach is to set your mobile SDK's log level to debug and trace the message through your device logs, from receipt to dismissal.

Why does the debugger say my user is anonymous?

(anonymous) means the SDK is using an anonymousId token rather than an identified user, so no cioanalytics.identify() call has run for this person on this page. That's only a bug if the message you're testing needs an identified person; Customer.io supports anonymous in-app messages, and the docs treat the anonymous state as normal and expected when that's what you're sending. The reading to worry about is (none), which means there's no user or anonymous token at all and usually signals an initialisation problem.

What does "NOT INITIALIZED" mean in the Config section?

It means the SDK didn't start correctly on the page, so nothing else will work—no identification, no route matching, no messages. When you see it, the fix is upstream of the debugger: check your JavaScript snippet is installed and loading on the page. Until Config shows a Site ID and a connection, the other three sections won't tell you anything useful.

The Route section shows NONE. What does that mean?

It means the SDK doesn't know what page you're on, so it can't match any of your messages' page rules. On a normal full page load the SDK sends a page call automatically, but in a single-page app, client-side route changes don't trigger one. You have to call analytics.page() yourself on each route change. If Route reads NONE while the user navigates, that missing page call is almost always the cause.

Why is my message "queued" with a ✕ next to its Route Rule?

A ✕ on the Route Rule means the message's page rule doesn't match the route the SDK currently thinks you're on, so the message won't display on this page. Check two things: that the route shown in the Route section is the one you expect, and that your page rule actually matches it. In a single-page app, a stale route from a missing analytics.page() call is the common reason a rule that looks correct still fails to match.

Why is my inline or tooltip message queued with a ✕ on its Target?

The ✕ on the Target means the CSS selector for that inline or tooltip message didn't match any element on the page, so there's nowhere to anchor it. Confirm the element exists at the moment the message tries to render, and that the selector is correct. A typo, a renamed class, or an element that loads later than the message all produce the same ✕. Inline and tooltip messages depend on their target far more than modals and overlays, which render against the page itself.

Does the debugger explain why a message failed eligibility?

Partly. It shows whether each message is active or queued and flags two display conditions when they fail: a route-rule mismatch and a missing target element. What it doesn't show is the upstream eligibility logic—segment membership, frequency caps, or whether the campaign entered the person in the first place. Those decisions happen in the workflow before anything reaches the SDK, so for "why did this person never get the message at all", you're back to the workflow and the decision tree, not the overlay.

Can customers see the debug overlay on my live site?

Only if they know to add cio_debug_session=true to the URL themselves. The debugger is safe to run on a live site: it's a read-only view that reflects only the current visitor's own session and doesn't change how messages are delivered. But it isn't access-controlled, so treat the parameter as mildly sensitive—keep it in your internal QA notes rather than a public help article, and nobody will stumble onto it.

How is the debugger different from Live Preview and Send Test?

They solve three different problems. Live Preview opens your site with a preview bar so you can adjust a message's position and display settings while you build it; it's web-only and runs as a timed session. Send Test delivers a real, one-time message to a specific person to confirm it shows up where you expect, and it works on both web and mobile. The debugger is the diagnostic layer for when a real message doesn't appear—it reads out the SDK's state so you can see why. Build with Live Preview, confirm with Send Test, diagnose with the debugger.

How do I turn the debugger off?

Click the × in the top-right corner of the overlay, or reload the page without the cio_debug_session parameter in the URL. The debugger only runs while the parameter is present, so a clean page load with no parameter removes it entirely. Nothing persists between sessions—it's tied to that URL, not to your account or the workspace.

Sources

David Crowther
Book a free consultation →

On our call, you'll be speaking with David Crowther, founder of NerveCentral.

Our initial consultation is not a sales call—you'll talk, I'll listen and ask questions—then I'll come back to you within 48 hours with our best ideas on how to grow your business.