Class: Sentiero::Analytics::FormAnalyzer

Inherits:
Analyzer
  • Object
show all
Defined in:
lib/sentiero/analytics/form_analyzer.rb

Overview

Cross-session form analysis built from interaction patterns: rrweb input events (incremental, type 3 / source 5) carry the touched node's id and timing but never values, so this works with maskAllInputs enabled.

Attribution is per page: each session's windows are merged, ordered by time, and split into page segments on Meta-href boundaries (the shared Analyzer#each_page_segment mechanism). A (session, page) unit "starts" a form when the segment contains input events, and is "completed" only when a __form_submit custom event — emitted by the recorder's capture-phase document submit listener (config.track_forms) — lands in the same segment at or after the first input. A session counts as completed when EVERY page it interacted on was submitted, so a genuine submit elsewhere (a later todo add) can no longer mask an abandonment (the signup form it walked away from).

CAPTURE-VERSION NOTE: submits are counted ONLY from __form_submit events. Windows recorded before that capture existed (or with track_forms off) carry none and intentionally report ZERO submits — falling back to counting bare Meta/navigation events would resurrect the "navigating away counts as submitting" defect (product review P1.4/D4: 100% shown where the funnel proved 50%).

Fields are keyed per (page URL, node id) so the same rrweb node id on two different pages (ids reset every full-page load) no longer conflates two unrelated fields. Per field it reports touch rate (fraction of interacting sessions), aggregate time-to-fill, and re-fill frequency, plus a drop-off table (the last field touched in each abandoned page segment).

Compute-on-read: a full scan of Store#each_session_events up to the store's limits.analytics_max_scan_sessions — no fact-extraction tables.

Per-segment math (input recognition, field accumulation, drop-off, submit detection, and session-level started/completed semantics) lives in FormCollector so PageReportAnalyzer can share it without duplication.

Constant Summary collapse

FULL_SNAPSHOT =

rrweb EventType.FullSnapshot and NodeType.Element — used to read field identity (name/id/type) from the DOM snapshot for human field labels.

2
ELEMENT_NODE =
2
FORM_CONTROL_TAGS =
%w[input select textarea].freeze

Constants included from Events

Events::CUSTOM, Events::INCREMENTAL, Events::META, Events::SOURCE_INPUT, Events::SOURCE_MOUSE_INTERACTION, Events::SOURCE_SCROLL

Instance Attribute Summary

Attributes inherited from Analyzer

#store

Instance Method Summary collapse

Methods inherited from Analyzer

#initialize

Methods included from EntryAttribution

#earlier?, #first_meta_href, #same_origin?, #update_entry_candidate

Methods included from Bounded

#bounded_fetch, #bounded_increment

Methods included from Stats

#mean, #offset_ms, #percentile, #top_counts

Constructor Details

This class inherits a constructor from Sentiero::Analytics::Analyzer

Instance Method Details

#analyze(limit: nil, since: nil, until_time: nil) ⇒ Object

Aggregates form interactions across sessions. Returns per-field stats, the drop-off table, form-level completion rate, the raw submit count, and whether the scan was capped. since/until_time (epoch seconds) bound the scan at the store level.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/sentiero/analytics/form_analyzer.rb', line 54

def analyze(limit: nil, since: nil, until_time: nil)
  scan_cap = limit || store.limits.analytics_max_scan_sessions
  collector = FormCollector.new  # unbounded: no per-URL field cap here

  sessions = merge_windows(scan_cap, since, until_time)
  sessions.each { |session_id, session| analyze_session(collector, session_id, session) }

  started = collector.started_count
  {
    sessions_with_form_interaction: started,
    sessions_completed: collector.completed_count,
    completion_rate: ratio(collector.completed_count, started),
    total_submits: collector.total_submits,
    fields: collector.summarize_fields(started, include_labels: true),
    drop_off_fields: collector.summarize_drop_off(include_labels: true),
    was_truncated: sessions.size >= scan_cap
  }
end