TL;DR, Quick Answer
7 min readCustom events let you measure real blog readership with article progress and capped active time, then track server-side registrations accurately -- going far beyond basic pageview metrics.
Server side analytics tracking is useful when the event happens on your server or when client-side tracking is unreliable. Two common examples are blog readership and registrations. A browser can estimate article progress and active visible time; the server can tell you whether an account was actually created.
The best setup combines both without collecting unnecessary personal data.
Measuring real blog reading
A pageview is a weak content metric. Readers bounce immediately, leave the tab open, or scroll to the bottom without reading. A better "read" event combines:
- Minimum active time based on word count.
- Scroll depth within the article container.
- Visibility state so background tabs do not count.
- A one-time event so refreshes do not duplicate reads.
For example, a 1,200-word article might require at least 5 minutes of active time at roughly 220 to 250 words per minute plus 80 percent article scroll. Do not treat that number as universal. Technical docs, legal content, and comparison posts read at different speeds.
- Counts an immediate bounce
- Counts a tab left open in the background
- Counts a fast scroll to the bottom
- Needs minimum active time based on word count
- Needs 80 percent scroll depth in the article container
- Pauses when the tab is not visible
- Fires once per pageview
Client-side event example
const article = document.querySelector('[data-article]');
const words = Number(article?.dataset.words || 0);
const estimatedSeconds = Math.round((words / 230) * 60);
const minSeconds = Math.min(420, Math.max(30, estimatedSeconds * 0.6));
const maxCreditSeconds = Math.min(900, Math.max(90, estimatedSeconds * 1.5));
let activeSeconds = 0;
let articleVisible = false;
let reachedDepth = false;
let sent = false;
const depthMarker = document.createElement('span');
depthMarker.setAttribute('aria-hidden', 'true');
depthMarker.style.cssText = 'position:absolute;top:75%;left:0;width:1px;height:1px;';
if (article) {
article.style.position ||= 'relative';
article.appendChild(depthMarker);
}
function maybeSendRead() {
if (sent || !article || !reachedDepth || activeSeconds < minSeconds) return;
sent = true;
navigator.sendBeacon(
'/analytics/article-read',
JSON.stringify({
slug: article.dataset.slug,
wordCount: words,
activeSeconds,
})
);
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.target === article) articleVisible = entry.isIntersecting;
if (entry.target === depthMarker && entry.isIntersecting) reachedDepth = true;
}
maybeSendRead();
},
{ threshold: 0 }
);
if (article) {
observer.observe(article);
observer.observe(depthMarker);
}
setInterval(() => {
if (sent || !articleVisible || document.visibilityState !== 'visible') return;
activeSeconds = Math.min(activeSeconds + 1, maxCreditSeconds);
maybeSendRead();
}, 1000);This avoids counting a fast scroll to the bottom as a read. It also caps active-time credit, pauses when the page is hidden, and uses an article-level depth marker instead of measuring scroll depth against the whole document.

Server-side registration tracking
Registrations should be tracked after the server confirms success, not when the user clicks submit. Client-side form submits overcount because validation can fail, duplicate requests can happen, and bots can trigger forms.
A server-side event should include only safe fields:
- Event name:
registration_completed. - Timestamp.
- Anonymous visitor or session reference if available and lawful.
- Plan or signup path.
- UTM source captured at landing.
- Account type or workspace size bucket.
Avoid email, name, IP address, raw user ID, password status, or free-text fields in analytics. If you need to connect events to accounts internally, store that mapping in your product database, not in a third-party analytics event.
Deduplication
Use an idempotency key for server events. A registration event should fire once per account creation, even if the user refreshes the success page or a webhook retries.
await analytics.track({
event: 'registration_completed',
idempotencyKey: `registration:${account.id}`,
properties: {
plan: account.plan,
source: attribution.source,
campaign: attribution.campaign,
},
});Privacy checklist
- Do not track reading on sensitive pages unless necessary.
- Keep article events aggregate where possible.
- Strip query parameters before storing paths.
- Do not send personal data in event properties.
- Document retention for raw events.
- Respect consent requirements for client-side storage or identifiers.
Server-side tracking is not automatically privacy-friendly. It is better when it records verified business events with minimized payloads. Use the browser for signals only the browser can know, and use the server for facts only the server can confirm.
Data quality checks
Compare article-read events against pageviews. If every pageview becomes a read, the threshold is too low or the event is firing on load. If almost no reads appear for long articles with strong engagement, the scroll calculation is wrong: it uses the whole document instead of the article container.
For registrations, reconcile analytics events with the application database. A daily count of registration_completed should match account creation within expected retry and deletion behavior. If it does not, fix the server event before using it for marketing attribution.
Consent and transparency
If the tracking uses only aggregate events and no device storage, the privacy burden is lower. If you set identifiers, link reading events to accounts, or use the data for personalization, update consent and privacy notices accordingly. Server-side does not mean invisible; users still deserve an honest explanation.
Reliability Details That Matter
When sending read events from the browser, prefer delivery methods designed for page unloads. The Beacon API lets the browser send a small asynchronous request without blocking navigation, which is useful for analytics events that fire near the end of a visit (MDN Beacon API). Keep payloads small and non-sensitive because beacons are not a place for large debug objects.
Flowsery
Start Your 14-Day Free Trial
Real-time dashboard
Goal tracking
Cookie-free tracking
For registration events, design the server-side event like a financial transaction:
- Fire it only after the account or registration record is committed.
- Include an idempotency key based on the internal record.
- Retry safely if the analytics endpoint is temporarily unavailable.
- Store a short audit log of delivery status.
- Reconcile counts against the product database.
If attribution matters, capture UTMs at landing time and store them in a first-party attribution record with a clear retention period. When registration succeeds, attach safe campaign fields such as source, medium, campaign, and landing page. Do not attach the visitor's full browsing history.
For reading-time analytics, be careful with tabs left open. Count active visible time, pause when the page is hidden, and consider a maximum credit window so a lunch-break tab does not become a "40 minute read." For very short posts, use a minimum threshold; for long posts, avoid requiring 100% scroll because readers may get value before the final author bio or related-post section.
The best signal is not "time spent" alone. It is time plus meaningful progress plus a follow-up action such as newsletter signup, product-page visit, or registration.

Read And Registration QA
Treat article reads as a content-quality signal, not proof that one identifiable person consumed every word. Validate that:
- A background tab does not accumulate time.
- A quick scroll does not fire a read.
- Long articles cap active-time credit.
- The event fires once per pageview.
- Sensitive article categories use aggregate reporting only.
- Registration events match committed accounts, not button clicks.
Keep the browser signal and the server fact separate. The browser can estimate meaningful reading. The server can confirm registration. Combining them carefully is useful; pretending either one is perfect creates noisy analytics.
Frequently Asked Questions
Why does a pageview undercount and overcount at the same time?
A pageview fires the moment the page loads, so it counts a bounce, a tab left open, and a fast scroll to the bottom the same as a genuine read. Pairing it with minimum active time, scroll depth, and visibility state separates real reading from those cases. A one-time event on top stops refreshes from inflating the count further.
What words-per-minute range should I use for the active-time threshold?
The post uses roughly 220 to 250 words per minute as a starting point, which puts a 1,200-word article's minimum active time at about 5 minutes. Treat that as a baseline, not a universal number. Technical docs, legal content, and comparison posts read at different speeds, so recalibrate per content type.
Why measure scroll depth against the article container instead of the whole document?
Measuring against the whole document skews the number when headers, footers, or related-post sections change the page length. The client-side example anchors an 80 percent depth marker inside the article element itself with an IntersectionObserver, so the calculation tracks the article, not the surrounding layout. That is also the fix when almost no reads appear for long, well-engaged articles despite strong scroll behavior.
Should I use 100 percent scroll depth as the read threshold?
A full-scroll requirement can miss readers who already got the value they came for before an author bio or a related-posts block at the end. The post's example uses 80 percent scroll depth instead. For long-form content, avoiding the 100 percent bar keeps the metric closer to what "read" actually means.
How do I stop a background tab from padding the active-time count?
Check document.visibilityState alongside an IntersectionObserver watching whether the article itself is on screen. The example's one-second interval only increments activeSeconds when the article is visible and the tab's visibility state is "visible", so a tab left open in the background stops accumulating time immediately.
What is a sensible cap on active-time credit for long articles?
The post caps credited active time so a tab left open through a lunch break does not register as a 40-minute read. The example implementation clamps activeSeconds with Math.min against a maxCreditSeconds ceiling derived from the article's estimated reading time. Long articles still get a generous window, just not an unbounded one.
Why should registration events fire on server confirmation instead of on form submit?
Client-side form submits overcount because validation can fail, duplicate requests can happen, and bots can trigger forms without creating anything. A server-confirmed event only fires once the account actually exists, which is the fact analytics is supposed to represent. That is also why the post treats the event like a financial transaction, committed first and tracked second.
What fields are safe to include on a registration_completed event?
Stick to the event name, timestamp, an anonymous visitor or session reference where lawful, plan or signup path, UTM source captured at landing, and account type or workspace size bucket. Leave out email, name, IP address, raw user ID, password status, and free-text fields. If you need to connect events to accounts internally, keep that mapping in the product database, not in the analytics event.
Flowsery
Start Your 14-Day Free Trial
Real-time dashboard
Goal tracking
Cookie-free tracking
How does the idempotency key prevent duplicate registration events?
The key is built from the account's own ID, for example registration:${account.id}, so the analytics platform recognizes a repeat send as the same event rather than a new one. That protects against a user refreshing the success page or a webhook retrying after a timeout. The event still fires exactly once per account creation no matter how many times the trigger runs.
How do I check whether the registration tracking is actually accurate?
Reconcile the daily count of registration_completed events against account creation in the application database. The two numbers should match within expected retry and deletion behavior. If they diverge, fix the server event itself before using the numbers for marketing attribution.
Was This Article Helpful?
Let us know what you think!
See us more often in Google
One click marks Flowsery as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.
Before you go...
Flowsery
Revenue-first analytics for your website
Track every visitor, source, and conversion in real time. Simple, powerful, and cookie-free.
Real-time dashboard
Goal tracking
Cookie-free tracking
Related Articles


Key Insights - Html5 Video Analytics
Useful HTML5 video analytics needs six events, not a per-second stream. The schema for HTML5, YouTube and Vimeo, plus the funnels worth building on it.


A Practical Guide to Scroll Depth
Pageviews cannot tell you whether someone read or bounced. How scroll depth is measured, which segments matter, and where the CTA should really sit.


A Practical Guide to 404 Errors
A 404 error is a failed journey, not just a status code. Track them as events, rank broken URLs by lost traffic, then redirect only the ones that matter.

