Skip to content

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

graph TB subgraph Manager["FormDetectionManager"] direction TB Init["startObserving()"] Scan["Scan page for &lt;form&gt; elements"] Match{"Forms found?"} Apply["For each form: first canHandle() wins"] NoForm["Activate formless strategies"] MO["MutationObserver<br/><i>watches for dynamically added forms</i>"] Dedup["Deduplication<br/><i>WeakSet + 2s window</i>"] end Init --> Scan --> Match Match -->|Yes| Apply Match -->|No| NoForm Init --> MO Init --> Always subgraph FormStrategies["Form-Based Strategies (priority order)"] direction TB S1["① SuperformStrategy<br/><code>superform: true</code>"] S2["② HubSpotFormStrategy<br/><code>hubspot: true</code>"] S3["③ CTAPageFormStrategy<br/><code>cta_page_form: true</code>"] end subgraph FormlessStrategies["Formless Strategies (no &lt;form&gt; element)"] direction TB S5["⑤ NetworkObserverStrategy<br/><code>network_observer: true</code>"] S6["⑥ DomObserverStrategy<br/><code>dom_observer: false</code>"] end subgraph Always["Always-Active Strategies"] direction TB S4["④ ThankYouPageStrategy<br/><code>thankyou_page: true</code>"] S7["⑦ PostMessageStrategy<br/><code>post_message: true</code>"] end Apply --> FormStrategies NoForm --> FormlessStrategies S1 & S2 & S3 & S4 & S5 & S6 & S7 --> Dedup Dedup --> Event["rw_client_form_submitted"] classDef enabled fill:#d4edda,stroke:#28a745,color:#000 classDef disabled fill:#f8d7da,stroke:#dc3545,color:#000 classDef manager fill:#e2e3e5,stroke:#6c757d,color:#000 classDef event fill:#cce5ff,stroke:#004085,color:#000 class S1,S2,S3,S4,S5,S7 enabled class S6 disabled class Manager,Init,Scan,Match,Apply,NoForm,MO,Dedup manager class Event event

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:

  1. Listens for custom sf:complete event on the form
  2. 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:

  1. Watches the form's parent wrapper with MutationObserver
  2. Detects when HubSpot removes the form and replaces it with a success/error message
  3. 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:

  1. Listens for native submit events
  2. Checks HTML5 form validation before tracking
  3. If the form navigates away (different origin/path): tracks immediately as success
  4. If staying on page: tracks as unknown, then re-evaluates after 2 seconds using result detection heuristics
  5. 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:

  1. Checks the URL on initial page load
  2. Wraps history.pushState() and history.replaceState() to detect SPA navigation
  3. Listens for popstate events (back/forward)
  4. 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:

  1. Attaches click listener to the submit button
  2. On click, starts a PerformanceObserver to intercept network requests (fetch/XHR)
  3. Filters out known tracking pixels (Google Analytics, Facebook, LinkedIn, Segment, Sentry, PostHog, etc.)
  4. Requires minimum 100ms delay post-click (avoids pre-existing background requests)
  5. 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:

  1. Attaches click listener to submit button
  2. Polls every 500ms (up to 5 seconds) for DOM changes
  3. Checks if email input disappeared from DOM or became hidden
  4. 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.

Lifecycle

sequenceDiagram participant Page as Web Page participant Mgr as FormDetectionManager participant Strat as Strategies participant Event as rw_client_form_submitted Page->>Mgr: startObserving(context) Mgr->>Mgr: Scan for <form> elements alt Forms found on page loop Each <form> Mgr->>Strat: canHandle(form)? Strat-->>Mgr: First match wins Mgr->>Strat: startTracking(form) end else No forms found Mgr->>Strat: Activate NetworkObserver Mgr->>Strat: Activate DomObserver (if enabled) end Mgr->>Strat: Always: ThankYouPage.checkCurrentUrl() Mgr->>Strat: Always: PostMessage.startListening() Mgr->>Mgr: Start MutationObserver (dynamic forms) Note over Strat: User submits form... Strat->>Mgr: shouldTrackSubmission(form)? Mgr->>Mgr: Dedup check (WeakSet + 2s window) Mgr-->>Event: Fire rw_client_form_submitted Page->>Mgr: stopObserving() Mgr->>Strat: cleanup() all strategies Mgr->>Mgr: Disconnect MutationObserver