Back to blog

Laravel Form Analytics: Completion, Step Drop-Off, and Privacy

Manuk Minasyan · · 6 min read
Laravel Form Analytics: Completion, Step Drop-Off, and Privacy

Useful form analytics answers three questions: did visitors see the form, where did engaged visitors stop, and did the server accept the submission? You can answer those inside Laravel without recording answers or sending visitor behavior to a third-party analytics platform.

The difficult part is not drawing a funnel. It is defining events precisely enough that refreshes, multiple tabs, failed validation, background tabs, and network loss do not turn the dashboard into fiction.

Start with a versioned event contract

Use stable event names and stable identifiers:

form.viewed
form.started
form.step_viewed
form.step_completed
form.validation_failed
form.submitted

Store form_id, form_version_id, a stable step or field code where relevant, an event-schema version, server receive time, and a scoped session token. Allowlist attributes. Do not capture answers, free text, filenames, email addresses, or raw validation input in analytics.

Dynamic values belong in attributes, not names. form.step_completed with step_code=employment is queryable; employment_step_completed fragments the contract.

Define every denominator

These ratios answer different questions:

Do not call all of them “completion rate.” Do not publish a universal good/bad threshold. Traffic source, device mix, form purpose, length, and audience intent can move a baseline dramatically. Compare the same form version over time and evaluate controlled changes.

Make the server authoritative

A click on Submit is not a completed submission. Record form.submitted only after server validation succeeds and the submission transaction commits.

Client events are useful for view, start, step, and timing context, but they are best effort. Browsers can be offline, block requests, restore pages from back/forward cache, or close before sending the final beacon.

For a small final timing event, navigator.sendBeacon() is appropriate during visibilitychange. The W3C Beacon specification is explicit that a successful call means the browser queued the data, not that your server received it. Analytics must never block the actual form submission.

Use race-safe deduplication

An application-level firstOrCreate() is not sufficient by itself: two concurrent requests can both observe that no event exists. Put the intended event cardinality in the database.

For example, if a view should occur once per form version and session window, create a unique index covering that tuple. Then use an atomic insert-or-ignore or catch the unique-key conflict. A start event may have a different uniqueness contract from a validation-failure event, so avoid one generic constraint that erases useful data.

The session token should be scoped and short-lived. A keyed HMAC over a rotating identifier can reduce raw identifier exposure, but it is usually pseudonymous, not automatically anonymous. Hashes can still allow singling out and linking. Include analytics in the privacy notice, retain it for a documented period, and assess applicable cookie or storage rules.

Measure step drop-off without collecting answers

A multi-step form needs two separate events:

The gap between viewed and completed points to friction. A categorized form.validation_failed event can identify a field code and rule category such as required, email, or max—never the rejected value.

Abandonment is inferred after a time window. The browser cannot know that a person will never return. Label it accordingly in the dashboard.

For implementation details of the wizard itself, see the multi-step Filament form guide.

Report active time, not one misleading average

Elapsed time between first input and submit can include lunch, a sleeping laptop, or a background tab. If timing matters:

  1. accumulate time only while the document is visible;
  2. pause after a defined inactivity window;
  3. report median and p75/p90, not only the mean;
  4. segment by form version and device class only when groups are large enough for privacy.

Performance and completion are related but distinct. A slow interaction can cause drop-off, while a confusing question can be fast and still fail. Use the Laravel form performance guide to diagnose timing separately.

A minimal storage design

A compact event table can contain:

id
form_id
form_version_id
event_name
event_version
session_token
step_code (nullable)
field_code (nullable)
error_category (nullable)
occurred_at_client (nullable, untrusted)
received_at_server

Index common funnel dimensions such as (form_id, form_version_id, event_name, received_at_server). Add separate unique indexes for events whose definition requires deduplication. Avoid a single JSON dump of arbitrary browser properties; it is harder to govern and easy to turn into accidental surveillance.

Test the data, not just the chart

A credible analytics feature needs failure cases:

Also test bots, a submit with no captured start, back/forward cache restoration, offline browsing, and a deployment that changes steps mid-session.

Privacy is a design constraint, not a badge

Self-hosting removes a third-party recipient, but it does not automatically make analytics anonymous or compliant. Document the purpose, fields, retention, access, and deletion behavior. Keep low-volume segments out of dashboards that could single out a person, and do not reuse the session token across unrelated contexts.

The GDPR form checklist covers the broader data lifecycle. Legal requirements vary by purpose and jurisdiction, so treat this implementation as engineering guidance rather than legal advice.

The payoff for precision is practical: when a metric moves, you know whether the form changed—not merely whether the event collector changed its mind about what “started” or “submitted” means.

Related posts