Form Analytics That Actually Help You Ship: How FlexForm Tracks Submissions, Sources, and Peak Hours
GET /forms/:id/submissions/analytics — six aggregations, one round trip.Most form builders ship analytics that tell you the total. FlexForm ships analytics designed to be acted on. Every submission carries the country it came from, the campaign that drove it, the embed it loaded inside, the active seconds the respondent actually spent, and the page they finished on. The aggregation runs in Postgres against an index, so the analytics page loads in under 200ms whether the form has 100 submissions or a million.
This post walks through what FlexForm tracks, where each field comes from, how the analytics endpoint stays constant-size at scale, and what the data lets a team actually do — pause Friday ad spend, route mobile traffic differently, retire a country-specific form variant, ship the next change.
Captured automatically
- UTM source, medium, campaign, term, content
- Browser, device type, operating system
- Country + region + city from IP (MaxMind GeoLite2)
- Embed parent URL when rendered inside another site
- Active time-to-complete (Page Visibility API)
- Per-page timestamps
Surfaced as
- Daily and hourly submission counts (timezone-aware)
- Top sources, devices, browsers, countries
- Average and median completion time
- Peak hour bucket (0–23)
- Embed vs direct submission split
- Filters by date range, status, and timezone
Where each metric comes from
FlexForm captures metadata at three points: when the session is created, when the respondent navigates between pages, and at submission. The capture path is deliberately split so that even abandoned drafts carry enough metadata to be useful for funnel analysis later.
| Field | Captured at | Source |
|---|---|---|
| ip_address | Session create + submit | X-Forwarded-For / X-Real-IP / RemoteAddr |
| browser, deviceType, os | Session start | Frontend collectSessionMetadata() |
| utm.source / medium / campaign | Session start | URL query params at form open |
| referrer, pageUrl | Session start | document.referrer + window.location.href |
| embed.parentUrl, embedType | Session start | Frontend getEmbedInfo() — parent window |
| geo.country / region / city | On CreateFromSession() | MaxMind GeoLite2-City lookup from IP |
| timeToComplete | Submission | Page Visibility API active-seconds (wall-clock fallback) |
| pageTimestamps | Each page navigation | Frontend page-save events |
Why server-side aggregation matters
A common mistake in form-builder analytics is fetching every submission row to the browser and computing aggregations client-side. This works at 1,000 rows. It dies at 100,000.
| Approach | 10k submissions | 100k submissions | 1M submissions |
|---|---|---|---|
| Fetch raw rows to browser | ~8MB / 2s | ~80MB / 20s | OOM |
| Server-side GROUP BY | ~2KB / 40ms | ~2KB / 60ms | ~2KB / 120ms |
FlexForm runs six parallel GROUP BY queries against the (form_id, submitted_at) composite index: total + timing, by day, by hour, by UTM source, by device, by browser, and by country. The response payload is the same shape every time. A workspace with three forms and a million combined submissions still loads its analytics page in well under a second.
The analytics endpoint
A single endpoint serves the entire analytics dashboard. It accepts a date range, a status filter, and an IANA timezone, and returns everything the dashboard renders in one round trip.
GET /forms/:id/submissions/analytics
?tz=Asia/Kolkata
&from=2026-04-01
&to=2026-05-15
&status=submitted{
"total": 1248,
"withTiming": 1190,
"avgTimeSec": 187.4,
"medianTimeSec": 142.0,
"peakHour": 14,
"embedCount": 312,
"timezone": "Asia/Kolkata",
"byDay": [ { "date": "2026-05-01", "count": 42 } ],
"byHour": [ { "hour": 14, "count": 187 } ],
"bySource": [ { "label": "google", "count": 540 } ],
"byDevice": [ { "label": "desktop", "count": 820 } ],
"byBrowser": [ { "label": "Chrome", "count": 710 } ],
"byCountry": [ { "label": "India", "count": 480 } ]
}What this lets a team actually do
The point of the dashboard is not the chart. It is the next action. Five concrete moves the data unlocks on a typical lead-gen form:
- Pause Friday paid traffic — if
byDayshows Friday submissions are 40% below Monday at the same spend, route budget elsewhere. - Reskin the mobile layout — if
byDeviceshows 87% mobile but the form was designed at desktop width, the layout work is overdue. - Cut the country-specific form variant — if
byCountryshows 92% of conversions from three countries, the long-tail variants are operational debt. - Restructure the embed strategy — if
embedCountis 312 out of 1,248, a quarter of submissions come from partner sites; their embed placement is the highest-leverage thing to tune. Web embed reference. - Front-load the form for the peak hour — when
peakHouris 14, schedule sales follow-up coverage for 2–4pm local time and route those submissions to the on-call rep.
What this is not (yet): a page drop-off funnel or a form views counter. The first depends on aggregating current_page_id from form_sessions — the data is captured, the aggregation is on the roadmap. Views are approximated by session count for now. Both are tracked as priority gaps in the analytics roadmap.
How FlexForm compares
Most form builders track submissions over time and average response time. The gap shows up in everything else.
| Metric | FlexForm | Typeform | Jotform | Google Forms |
|---|---|---|---|---|
| UTM / source tracking | Yes | No | Limited | No |
| Peak hour analysis | Yes | No | No | No |
| Browser breakdown | Yes | No | No | No |
| Embed parent URL tracking | Yes | No | No | No |
| Country / geo | Yes | No | Yes | No |
| Server-side aggregation | Yes | Unknown | Unknown | No |
Frequently asked questions
What does FlexForm track on every form submission?
▾
Every submission carries IP-derived country and city, browser, device type, operating system, screen resolution, referrer, full UTM parameters (source, medium, campaign, term, content), the embed parent URL when the form is loaded inside another site, active time-to-complete from the Page Visibility API, and per-page timestamps. All of it lands in the submission's metadata JSON column and feeds the analytics endpoint without extra configuration.
How does FlexForm compute peak hour for a form?
▾
FlexForm runs a GROUP BY on the submitted_at timestamp, converted to the workspace timezone passed as a tz parameter. The query returns submission counts for hour 0 through hour 23, and the peak hour is the bucket with the highest count. Because the aggregation runs in Postgres, response size stays constant whether the form has 100 or 100,000 submissions.
Why does FlexForm aggregate analytics server-side?
▾
Fetching raw rows to the browser breaks past about 10,000 submissions. At 100,000 submissions, a client-side analytics call returns roughly 80MB and takes 20 seconds. FlexForm runs six parallel GROUP BY queries against the (form_id, submitted_at) index. Response payload stays at roughly 2KB regardless of submission volume. The analytics page loads in under 200ms for forms with a million submissions.
Can I filter form analytics by date range and timezone?
▾
Yes. The analytics endpoint accepts from and to date parameters in RFC3339 or YYYY-MM-DD, a tz parameter for any IANA timezone (Asia/Kolkata, America/New_York, Europe/Berlin), and a status filter for submitted, synced, failed, or all. The day-by-day and hour-by-hour aggregations honor the timezone, so a 9am peak in Mumbai shows correctly when a US viewer changes their workspace timezone.
Does FlexForm track form views or just submissions?
▾
FlexForm currently tracks form sessions, which start when a respondent opens the form. Session counts approximate views for forms that load fast. A dedicated views counter and completion-rate metric are on the roadmap, alongside a page drop-off funnel that uses the already-captured current_page_id and pageTimestamps data.
See your form analytics in 60 seconds
Free Starter plan includes the full analytics endpoint — UTM, device, country, peak hour, embed source.
Start Free →