Back to blog

Form submission tracking and analytics in Laravel — without third-party tools

Manuk Minasyan · · 7 min read

What's the completion rate on your contact form?

If you don't know, you're in good company. Most Laravel developers ship forms without any form submission tracking and never measure whether those forms actually work. The form exists, submissions come in, everyone assumes it's fine. (If you're still building forms from scratch, adding analytics is even further down the priority list.)

But "submissions come in" doesn't tell you much. How many people loaded the form and left without filling it out? How many started typing and gave up halfway? Is the form too long? Is a specific field causing friction? You have no idea.

You can fix this without Google Analytics or any third-party tool.

What to track

Form analytics boil down to four events:

View — someone loaded the page with the form on it. This is your baseline. If 1,000 people view the form and 50 submit it, you have a 5% submission rate. That number tells you whether the problem is the form or the traffic.

Start — someone interacted with the first field. They clicked or tapped into the form and began filling it out. The gap between views and starts tells you whether people are interested but not engaging. If views are high and starts are low, the form might be below the fold, or the page copy isn't convincing enough.

Submit — someone completed and submitted the form. You're probably already tracking this (because you store submissions). But without views and starts, you can't calculate meaningful rates.

Completion rate — submissions divided by starts, as a percentage. This is the number that actually matters. Industry benchmarks vary, but a contact form under 50% completion rate probably has a problem. A multi-step application form at 30% might be normal.

There's a fifth metric worth tracking: average completion time. If your contact form takes people 8 minutes to fill out, something is wrong. If a 10-field application form takes 90 seconds, people are probably rushing through it and your data quality suffers.

Building it yourself

If you want to roll your own tracking, here's the approach.

The events table

Schema::create('form_events', function (Blueprint $table) {
    $table->id();
    $table->foreignId('form_id')->constrained()->cascadeOnDelete();
    $table->string('event_type'); // VIEW, START, SUBMIT
    $table->string('session_hash', 64); // anonymized visitor ID
    $table->json('metadata')->nullable();
    $table->timestamps();
});

Anonymizing visitors

You need to distinguish between visitors without storing personal data. Hash a combination of the session ID and date:

use Illuminate\Support\Facades\Hash;

function generateSessionHash(): string { $raw = request()->session()->getId() . now()->toDateString(); return hash('sha256', $raw); }

This gives you a unique-per-day identifier that can't be reversed back to a real session. The date component means the same person visiting tomorrow gets a different hash, so you're not building a tracking profile.

Tracking views

Fire a VIEW event when the form page loads. Deduplicate by session hash so refreshes don't inflate the count.

public function trackView(Form $form): void
{
    $hash = generateSessionHash();
FormEvent::firstOrCreate([
    'form_id' => $form->id,
    'event_type' => 'VIEW',
    'session_hash' => $hash,
]);

}

Tracking starts

Fire a START event on the first field interaction. You'll need a bit of JavaScript for this:

// On your form page
let formStarted = false;

document.querySelectorAll('form input, form textarea, form select') .forEach(field => { field.addEventListener('focus', () => { if (formStarted) return; formStarted = true;

        fetch(`/forms/${formId}/track`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
            },
            body: JSON.stringify({ event: 'START' }),
        });
    });
});

Tracking submissions

Fire a SUBMIT event when the form is successfully processed. This one's easy — add it to your existing submission handler:

public function submit(): void
{
    $data = $this->form->getState();
// Store submission...

FormEvent::create([
    'form_id' => $this->formModel->id,
    'event_type' => 'SUBMIT',
    'session_hash' => generateSessionHash(),
    'metadata' => [
        'started_at' => session("form_{$this->formModel->id}_started_at"),
        'submitted_at' => now()->toISOString(),
    ],
]);

}

Calculating metrics

public function getMetrics(Form $form, Carbon $from, Carbon $to): array
{
    $events = FormEvent::where('form_id', $form->id)
        ->whereBetween('created_at', [$from, $to]);
$views = (clone $events)->where('event_type', 'VIEW')->count();
$starts = (clone $events)->where('event_type', 'START')->count();
$submissions = (clone $events)->where('event_type', 'SUBMIT')->count();

return [
    'views' => $views,
    'starts' => $starts,
    'submissions' => $submissions,
    'view_to_start_rate' => $views > 0
        ? round(($starts / $views) * 100, 1)
        : 0,
    'completion_rate' => $starts > 0
        ? round(($submissions / $starts) * 100, 1)
        : 0,
];

}

Building a dashboard

You could build a Filament widget or page to display these metrics. A few stat cards, maybe a line chart showing submissions over time. That's another half-day of work on top of the tracking implementation.

Total effort

To build proper form analytics from scratch, you're looking at:

Realistically, that's a full day. Maybe more if you want charts and date range filtering.

The easier path

FilaForms includes all of this out of the box.

Every form automatically tracks views, starts, and submissions using an event-based system with SHA-256 session fingerprinting. No configuration needed — it starts tracking the moment you publish a form.

In the admin panel, each form shows:

No JavaScript to write, no API endpoints to build, no dashboard to design. It's there when you open the form in the admin.

The tracking is privacy-compliant by design. Session fingerprints use SHA-256, and no personally identifiable information is stored in the events table. Submission data lives in the submissions table; analytics data stays fully anonymized.

What the data tells you

Once you have form analytics, you start noticing things.

Low view-to-start rate (under 30%) — people see the form but don't engage. Maybe it's below the fold, or the page copy isn't convincing, or the form looks too long at first glance. Try moving it higher on the page or reducing visible fields.

Low completion rate (under 40% for a contact form) — people start filling it out but give up. A specific field might be confusing, or you have too many required fields. On multi-step forms, check which step people abandon.

High completion time (over 5 minutes for a simple form) — something is causing friction. An unclear field label, slow file upload, or confusing validation errors.

Completion rate drops after a change — you added a field or changed the layout and completions went down. Now you have data to revert instead of guessing.

None of these insights are possible if you only count submissions. The funnel (views → starts → submissions) is what makes the data useful.

Stop guessing about your forms

Most of us ship features with error tracking and run campaigns with conversion metrics, but ship forms with zero visibility. That's a gap worth closing.

Whether you build tracking yourself or use a tool that includes it, measure your forms. You'll find problems you didn't know existed.

Resources:

Related posts