If you’re running lead generation or e-commerce funnels on WordPress, the quality of your analytics is only as good as the quality of your event data. Most sites bolt on a JavaScript tracking snippet, call it done, and then wonder why their conversion numbers don’t match what Sales or Stripe is reporting. PostHog gives you the tools to fix that – but only if you understand the difference between client-side and server-side capture, and why the two need to work together instead of one replacing the other.
This post walks through how to set up a WordPress + PostHog funnel that actually holds up: what to capture in the browser, what to capture on the server, why your web-to-lead form submissions specifically belong on the server side, and how to stitch both halves into one clean user journey.
Client-Side Capture: What It’s Good For
Client-side tracking (the posthog-js snippet, loaded via a plugin like WP PostHog Integration, a custom script in your theme, or Google Tag Manager) is where you get:
- Autocapture – clicks, form field interactions, page views, and rage-clicks with zero manual instrumentation
- Session replay – watching real visitors move through your funnel, which is invaluable for finding friction points in a checkout or lead form
- Feature flags and A/B tests rendered before the page paints
- Rich browser context – UTM parameters, referrer, device, screen size, scroll depth
This is the layer you want for understanding behavior: where people hesitate, where they abandon, what they interact with before converting. It’s fast to set up and gives you full-funnel visibility with minimal engineering effort.
The catch: client-side events are only as reliable as the browser they run in. Ad blockers, privacy extensions, Safari’s Intelligent Tracking Prevention, and users who simply navigate away before the request fires will all silently drop events. For behavioral analytics, that noise is tolerable. For anything tied to revenue or lead attribution, it isn’t.
Server-Side Capture: What It’s Good For
Server-side tracking uses the posthog-node SDK (or a direct call to PostHog’s Capture API) from your WordPress backend – a custom plugin, a form handler, or a webhook receiver. Server-side events are:
- Not spoofable or blockable – there’s no browser in the loop to drop the request
- Authoritative for business-critical actions – payments, subscription changes, and lead form submissions that trigger downstream processes (CRM entry, email sequences, sales notifications)
- Enrichable with backend data – deal value, lead score, CRM fields, plan tier – data that never touches the browser at all
The tradeoff is that server-side capture on its own tells you that something happened, not the story of how the visitor got there. You lose the session replay, the click trail, the “how did they arrive at this decision” context that client-side gives you.
Which is exactly why you need both.
Why Web-to-Lead Capture Should Be Handled Server-Side
Lead form submissions are the one place I’d push back hardest against a client-side-only setup. Here’s why:
1. Ad blockers and privacy tools disproportionately affect exactly the events you care about most. A form submission fired only via a client-side posthog.capture() call can be blocked before it ever leaves the browser – meaning your most valuable conversion event is the one most likely to go missing from your data.
2. Timing is unreliable. If a visitor submits a form and the page immediately redirects or the tab closes, a client-side event can get cut off mid-flight. A server-side capture, fired the moment your form handler processes the submission, doesn’t have that race condition.
3. It should be the single source of truth for revenue and pipeline reporting. If your form handler is what’s actually creating the lead record (in your CRM, in a spreadsheet, wherever), that’s also the moment that should log the PostHog event – with a distinct_id and properties pulled straight from the validated submission, not from whatever the browser happened to report.
4. It lets you attach data the browser never sees. Lead score, form validation results, spam-filter status, which sales rep gets assigned – all backend-only information you can attach directly to the event at capture time.
The practical pattern: your WordPress form handler (Gravity Forms, WPForms, or a custom wp_ajax hook) fires the posthog-node capture call server-side, right where it already handles the submission and any CRM push. The client-side layer still tracks that the form was viewed, started, and abandoned – but the final “lead captured” event is server-side and authoritative
Coupling Client-Side and Server-Side Tracking
The part that trips most people up isn’t setting up each layer individually – it’s making sure they describe the same person in PostHog instead of creating two disconnected identities.
Use one distinct_id across both layers. When a visitor lands anonymously, posthog-js assigns them an anonymous distinct_id and stores it in a cookie. When your server needs to fire an event for that same visitor (say, on form submission), pull that same distinct_id from the PostHog cookie (ph_<project_key>_posthog) server-side and pass it into your posthog-node capture call. This is what stitches the pre-submission browsing session to the post-submission lead record into a single person timeline.
Call identify() once you know who they are. The moment a lead submits real contact info, call posthog.identify() – client-side, server-side, or both – with a stable ID (email, CRM record ID, or user ID). This merges the anonymous browsing history with the now-known person, so you can see the full journey: which landing page, which blog post, which ad, all the way through to the lead record.
Don’t double-fire the same event from both layers. Pick one layer as the source of truth per event type. Page views, clicks, and scroll depth: client-side. Form submission, purchase, subscription change: server-side. If both layers fire “form_submitted,” you’ll double-count conversions and muddy your funnel metrics.
Pass through UTM and session context. If your server-side event needs marketing attribution data (UTM source, campaign, referrer), capture that client-side at page load, store it in a hidden form field or session value, and forward it to the server-side event’s properties when the form is submitted. That way your revenue-attributing event still carries full marketing context.
A Quick Note on GDPR and CCPA
Both client-side and server-side tracking involve processing personal data, so your privacy disclosures need to cover both. Under GDPR, if you have EEA visitors, you generally need opt-in consent before non-essential tracking scripts run, and that consent choice should also govern whether server-side events tied to that visitor fire. Under CCPA, prior consent isn’t required, but if the data you collect is sold or shared, you need a clear opt-out mechanism such as a “Do Not Sell or Share My Personal Information” link. In both cases, your privacy policy should plainly disclose what’s captured (client and server), why, and how visitors can opt out or request deletion – and a consent-management tool (Cookiebot, OneTrust, etc.) wired to suppress both layers, not just the client-side snippet, is the safest way to enforce it. This isn’t legal advice – check with counsel for your specific situation.
Putting It Together
A funnel built this way looks like:
- Visitor lands on a page → client-side autocapture logs the page view and UTM context
- Visitor scrolls, clicks, starts the form → client-side captures engagement and session replay
- Visitor submits the form → your WordPress form handler validates it, creates the lead, and fires a server-side
posthog-nodeevent with the samedistinct_id, enriched with backend-only lead data identify()merges the anonymous session into the now-known lead- You get a single, reliable timeline: acquisition channel → on-site behavior → verified conversion – without ad blockers eating your most important number
That combination is what makes PostHog worth the setup effort over a plugin that just drops a tracking pixel and calls it analytics.