All case studies

Senior living

Tour requests hidden in a Shadow DOM chatbot

A senior living community's tour bookings happened inside a chatbot no listener could see. I tracked them by watching the widget's own saved state.

// symptom
The contact form conversion worked, but appointment requests never registered as conversions.
// cause
Appointments were booked through a chatbot rendered in Shadow DOM, with no public events, so click listeners couldn't see it.
// fix
A hook on localStorage writes that watches the widget's session state and fires one lead event per new lead ID.
// result
Appointment conversions tracking across 4 ad platforms. The pattern became a reusable template for sister properties.
  1. 01 contact click → conversion ✓
  2. 02 appointment submit → no event ✗
  3. 03 widget root: #shadow-root no API ⚠
  4. ── patch: hook localStorage.setItem ──
  5. 04 lead_created fired once ✓
  6. 05 Google · Meta · UET · TTD ✓
  • Tealium iQ
  • Chatbot widget
  • Google Ads
  • Meta
  • Microsoft UET
  • The Trade Desk

The problem

After a broken tag install was repaired, a senior living community’s contact form conversion started working. Appointment requests still didn’t. Those are the leads that matter most.

What I found

The site’s click listener only matched the contact button. Appointments weren’t booked through a page form at all. They went through a scheduling chatbot.

The chatbot renders inside Shadow DOM. Its buttons and fields sit behind a boundary that normal page listeners can’t reach. It also exposes no public events to hook into.

But it does keep its session state in the browser’s localStorage, and that state changes when a lead is created.

The fix

Instead of fighting the Shadow DOM, I went around it. A small extension wraps localStorage.setItem, watches the widget’s session keys, and fires a lead_created event once per new lead ID. That event drives the conversion tags for all four ad platforms.

Why not the browser’s storage event? It only fires in other tabs, never the one making the change. Wrapping setItem catches it in the same tab.

How I proved it

A debug mode logged the event on a completed tour request, and each ad platform’s conversion fired from it.

After

The pattern became a reusable template. It’s since been applied to sister properties using the same widget, including a separate once-per-session “chat opened” engagement conversion.

Seeing something like this?

Tell me what's not counting