Context
No visibility into form funnel today. Need metrics per form:
- Completion rate — submitted / engaged sessions
- Drop-off rate — per-field abandonment
- Median time to submit
- View count vs engaged (first interaction)
Proposed approach
Data model
New doctype FP Submission Session (one row per visit):
| field |
purpose |
form (Link) |
parent form |
session_id (Data, unique) |
uuid from sessionStorage |
user (Link, nullable) |
logged-in user |
guest_hash (Data) |
hashed IP+UA for guest dedupe |
started_at (Datetime) |
first interaction |
last_activity_at (Datetime) |
rolling |
submitted_at (Datetime, nullable) |
finalize |
submission (Dynamic Link, nullable) |
resulting doc |
last_field_reached (Data) |
last focused field |
engaged (Check) |
first focus fired |
abandoned (Check) |
cron-marked after stale |
user_agent, referrer (Data) |
context |
Optional follow-up doctype FP Submission Event (append-only) — session, event_type (focus/blur/submit), field, timestamp. Enables fine-grained drop-off later. Out of scope for first cut.
Lifecycle
- Page open — frontend generates
session_id, stores in sessionStorage. No backend call yet.
- First field focus — POST
/api/method/forms_pro.api.metrics.start_session → creates row with engaged=true.
- Field blur — throttled beacon updates
last_field_reached, last_activity_at. Batch every 5s or on blur.
- Submit — existing
submit_form_response sets submitted_at, links submission.
- Tab close / navigation —
visibilitychange → navigator.sendBeacon() final flush.
- Stale sweeper — daily cron marks sessions with
last_activity_at > 30min and no submitted_at as abandoned=true.
API
New module forms_pro/api/metrics.py:
start_session(form, session_id) — allow_guest=True, idempotent
update_session(session_id, last_field, ...) — accepts beacon blob
get_form_metrics(form) — aggregate query for dashboard
Frontend
- New composable
useSubmissionMetrics(formId) in frontend/src/composables/
- Wire into
SubmissionPage.vue and PublicEdit.vue (skip edit mode for metrics)
visibilitychange listener fires sendBeacon with current state
- Display metrics on
ManageForm.vue overview
Metric queries
- Completion rate =
count(submitted_at) / count(engaged=true)
- Median time =
percentile_cont(0.5) within group (order by submitted_at - started_at)
- Drop-off per field =
count(last_field_reached=X AND submitted_at IS NULL) / count(reached X)
Edge cases
- Guests — opaque
session_id, no user link
- Refresh mid-fill —
sessionStorage persists session_id; resume row
- Edit mode — skip logging entirely
- Multi-tab — separate session_ids, accepted
- Bots —
engaged=false filters out view-only noise
- Beacon drops (~5%) — cron sweeper backstops
- Write amplification — throttle blur events, no per-keystroke writes
- PII — log field names only, never values
Out of scope
- Per-field event log (
FP Submission Event) — follow-up
- External analytics integration (PostHog)
- A/B testing form variants
- Geo / device breakdown
Context
No visibility into form funnel today. Need metrics per form:
Proposed approach
Data model
New doctype
FP Submission Session(one row per visit):form(Link)session_id(Data, unique)sessionStorageuser(Link, nullable)guest_hash(Data)started_at(Datetime)last_activity_at(Datetime)submitted_at(Datetime, nullable)submission(Dynamic Link, nullable)last_field_reached(Data)engaged(Check)abandoned(Check)user_agent,referrer(Data)Optional follow-up doctype
FP Submission Event(append-only) —session,event_type(focus/blur/submit),field,timestamp. Enables fine-grained drop-off later. Out of scope for first cut.Lifecycle
session_id, stores insessionStorage. No backend call yet./api/method/forms_pro.api.metrics.start_session→ creates row withengaged=true.last_field_reached,last_activity_at. Batch every 5s or on blur.submit_form_responsesetssubmitted_at, linkssubmission.visibilitychange→navigator.sendBeacon()final flush.last_activity_at > 30minand nosubmitted_atasabandoned=true.API
New module
forms_pro/api/metrics.py:start_session(form, session_id)—allow_guest=True, idempotentupdate_session(session_id, last_field, ...)— accepts beacon blobget_form_metrics(form)— aggregate query for dashboardFrontend
useSubmissionMetrics(formId)infrontend/src/composables/SubmissionPage.vueandPublicEdit.vue(skip edit mode for metrics)visibilitychangelistener firessendBeaconwith current stateManageForm.vueoverviewMetric queries
count(submitted_at) / count(engaged=true)percentile_cont(0.5) within group (order by submitted_at - started_at)count(last_field_reached=X AND submitted_at IS NULL) / count(reached X)Edge cases
session_id, no user linksessionStoragepersists session_id; resume rowengaged=falsefilters out view-only noiseOut of scope
FP Submission Event) — follow-up