Widget Interaction Tracking¶
Overview¶
Rose stores interaction data in the browser that client websites can read to determine if a user filling a form has previously interacted with Rose. This enables attribution of form submissions to Rose conversations.
Storage¶
Rose stores data in two locations:
Cookies¶
| Cookie Name | Description |
|---|---|
rose_last_active_session |
Session ID of the last conversation where user sent a message |
rose_last_active_session_date |
Unix timestamp (milliseconds) when session was last active |
rose_client_id |
Stable visitor ID (PostHog distinct_id) |
Cookie Properties:
- Expiry: 1 year
- Secure: HTTPS only
- SameSite: Lax
- Cross-subdomain: Cookies are set on the root domain (e.g.,
.example.com) so they work across subdomains
localStorage¶
Detailed analytics stored under a single rose key:
localStorage.rose = {
version: 1,
widget: {
analytics: {
counters: {
widgetImpressions: number,
messagesSent: number,
ctaClicks: number,
formsSubmitted: number,
loginClicks: number
},
lastSessionId: string | null,
lastActiveSessionId: string | null,
lastActiveSessionDate: number | null,
clientId: string | null
}
}
}
Fields:
| Field | Type | Description |
|---|---|---|
counters.widgetImpressions |
number | Number of times widget was displayed |
counters.messagesSent |
number | Number of messages user sent to Rose |
counters.ctaClicks |
number | Number of CTA button clicks |
counters.formsSubmitted |
number | Forms submitted after Rose interaction |
counters.loginClicks |
number | Login button clicks after Rose interaction |
lastSessionId |
string | null | Most recent session ID (any interaction) |
lastActiveSessionId |
string | null | Session ID where user last sent a message |
lastActiveSessionDate |
number | null | Unix timestamp (ms) of last message sent |
clientId |
string | null | Stable visitor ID (PostHog distinct_id) |
sessionStorage¶
Current chat state stored per browser tab:
Key: inboundx_chat_state_{siteName}
Contains: conversationPairs, isExpanded, sessionId, siteName
Reading Rose Data¶
From Cookies¶
function getRoseCookies() {
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [name, value] = cookie.trim().split('=');
acc[name] = decodeURIComponent(value);
return acc;
}, {});
return {
sessionId: cookies['rose_last_active_session'] || null,
sessionDate: cookies['rose_last_active_session_date']
? parseInt(cookies['rose_last_active_session_date'], 10)
: null,
clientId: cookies['rose_client_id'] || null
};
}
From localStorage¶
function getRoseLocalStorage() {
const roseData = localStorage.getItem('rose');
if (!roseData) return null;
const data = JSON.parse(roseData);
return data?.widget?.analytics || null;
}
Key Identifiers¶
| Identifier | Description | Use Case |
|---|---|---|
sessionId |
Session ID where user last sent a message | Link form submission to conversation |
clientId |
Stable visitor ID (PostHog distinct_id) | Track user across sessions |
sessionDate |
Timestamp of last activity | Measure time between chat and form |
Form Detection Strategies¶
Rose uses a modular, strategy-based system to detect form submissions across different form technologies. The FormDetectionManager orchestrates 7 independent strategies, each targeting a specific form type or submission pattern.
Architecture Overview¶
Strategy Priority & Selection¶
The manager applies the first strategy whose canHandle() returns true. This means priority order matters — a Superform is never handled by HubSpot or CTA strategies.
| # | Strategy | Default | Activation Condition |
|---|---|---|---|
| 1 | Superform | true |
Form has sf attribute or [sf-step] children |
| 2 | HubSpot | true |
Form has class hsfc-Form/hs-form, data-hsfc-id, or action containing hsforms.com |
| 3 | CTA Page Form | true |
Standard <form> on a page matching form_tracking_pages URL patterns |
| 4 | Thank You Page | true |
Current URL matches a thankyou_page_patterns entry (no form needed) |
| 5 | Network Observer | true |
No <form> found + ≥2 orphan inputs + email input + submit button |
| 6 | DOM Observer | false |
Same as Network Observer (disabled by default — more brittle) |
| 7 | PostMessage | true |
Listens globally for window.postMessage from known iframe providers |
Strategy Details¶
① SuperformStrategy — Webflow Superform multi-step forms¶
How it detects: Looks for the sf attribute on the form, parent container, or [sf-step] children.
How it tracks submission:
- Listens for custom
sf:completeevent on the form - Watches for target step (default
sf-step="step9") to become visible via MutationObserver
Indicators: superform_custom_event_sf_complete or superform_target_step_reached:step9
② HubSpotFormStrategy — HubSpot embedded React forms¶
How it detects: Checks for HubSpot-specific classes (hsfc-Form, hs-form), data-hsfc-id attribute, or action URL containing hsforms.com.
How it tracks submission: HubSpot forms handle submission entirely in React and don't fire native submit events. The strategy:
- Watches the form's parent wrapper with MutationObserver
- Detects when HubSpot removes the form and replaces it with a success/error message
- Falls back to submit button click + 3-second visibility check
Indicators: hubspot_success_message_added, hubspot_form_replaced, hubspot_success_text_detected
③ CTAPageFormStrategy — Standard HTML <form> submissions¶
How it detects: Enabled on pages matching configured form_tracking_pages URL patterns.
How it tracks submission:
- Listens for native
submitevents - Checks HTML5 form validation before tracking
- If the form navigates away (different origin/path): tracks immediately as
success - If staying on page: tracks as
unknown, then re-evaluates after 2 seconds using result detection heuristics - Fallback: listens for submit button clicks (for Stimulus/Turbo/AJAX forms)
Result detection heuristics (used by the manager):
| Signal | Result |
|---|---|
| Form removed from DOM | success |
Form hidden (display:none, visibility:hidden) |
success |
Success element found (.success, .confirmation, [role="status"]) |
success |
Error element found (.error, .alert, [role="alert"]) |
failed |
| All form inputs empty (reset) | success |
| HTML5 validation errors | failed |
| None of the above | unknown |
④ ThankYouPageStrategy — URL-based conversion tracking¶
How it detects: Checks if the current URL matches patterns from the thankyou_page_patterns config.
How it tracks: This strategy doesn't interact with forms at all — it's purely URL-based. It:
- Checks the URL on initial page load
- Wraps
history.pushState()andhistory.replaceState()to detect SPA navigation - Listens for
popstateevents (back/forward) - Tracks each unique URL only once
Indicators: thankyou_page_match:<pattern>
⑤ NetworkObserverStrategy — SPA forms without <form> element¶
How it detects: Activated only when no <form> elements exist on the page. Requires:
- ≥2 orphan input fields (not inside a
<form>) - At least one email input (strongest signal)
- A submit-like button (keywords: submit, send, sign up, register, get started, envoyer, soumettre, etc.)
How it tracks submission:
- Attaches click listener to the submit button
- On click, starts a
PerformanceObserverto intercept network requests (fetch/XHR) - Filters out known tracking pixels (Google Analytics, Facebook, LinkedIn, Segment, Sentry, PostHog, etc.)
- Requires minimum 100ms delay post-click (avoids pre-existing background requests)
- 10-second timeout for auto-cleanup
Indicators: network_observer_detected, initiator_fetch or initiator_xmlhttprequest
⑥ DomObserverStrategy — SPA multi-step forms (disabled by default)¶
How it detects: Same conditions as NetworkObserverStrategy (orphan inputs + email + submit button).
How it tracks submission:
- Attaches click listener to submit button
- Polls every 500ms (up to 5 seconds) for DOM changes
- Checks if email input disappeared from DOM or became hidden
- Checks for success text ("thank you", "merci", "success", "confirmation")
Why disabled by default: More brittle than NetworkObserver — assumes email input removal or success text appearance, which doesn't apply to all SPA forms. Enable per-domain when you know the form behaves this way.
Indicators: dom_observer_detected, email_input_disappeared, success_text_detected
⑦ PostMessageStrategy — Cross-origin iframe forms¶
How it detects: Listens globally for window.postMessage events matching known iframe provider signatures.
Supported providers:
| Provider | Detection Signal | Submission Signal |
|---|---|---|
| HubSpot | event.data.type === 'hsFormCallback' |
eventName === 'onFormSubmitted' |
| Cal.com | action.startsWith('booking') or action === 'linkReady' |
action === 'bookingSuccessfulV2' or 'rescheduleBookingSuccessfulV2' |
| Typeform | type === 'form-submit' or 'form-ready' |
type === 'form-submit' |
Indicators: provider_hubspot, provider_calcom, provider_typeform + provider-specific details
Configuration¶
Strategies are toggled per-domain in the analytics config under form_detection_strategies:
{
"form_detection_strategies": {
"superform": true,
"hubspot": true,
"cta_page_form": true,
"thankyou_page": true,
"network_observer": true,
"dom_observer": false,
"post_message": true
}
}
All strategies default to enabled except dom_observer. Set any strategy to false to disable it for a specific domain.