WP Manifestindependent plugin directory
manifest / analytics / sitepulse-analytics

SitePulse Analytics releases

Custom analytics tracker for Wordpress sites that can generate reports and send analytics data to API endpoints

by Chris Paschall · github.com/magellan-web-dev/sitepulse-analytics

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/magellan-web-dev/sitepulse-analytics/archive/refs/heads/main.zip

Readme

SitePulse Analytics

A self-hosted visitor analytics plugin for WordPress. SitePulse tracks page views, link and button clicks, form submissions, confirmed form conversions with campaign attribution, mouse hover activity, and scroll depth, then surfaces everything inside the WordPress dashboard — making it easy to identify popular pages, which campaigns and channels actually produce leads, and the areas of your content visitors engage with. On a configurable schedule, aggregated analytics (including individual attributed conversions) can also be delivered as JSON POST requests to one or more webhook endpoints.

Features

  • Page view tracking — every frontend page load is recorded with URL, title, and referrer.
  • Click tracking — links, buttons, submit inputs, and role="button" elements, with the element's label and destination URL.
  • Form submission tracking — native submit events are captured before any AJAX handler can swallow them, so Elementor and similar AJAX forms are counted too. Counted at submit time, so these are submission attempts, not confirmed successes.
  • Confirmed conversions — a separate form_success event fires only when the form plugin reports that the server accepted the submission: Elementor Pro's submit_success, Contact Form 7's wpcf7mailsent, WPForms' wpformsAjaxSubmitSuccess, and Gravity Forms' gform_confirmation_loaded, plus a spa:conversion DOM event for custom goals. These integrations detect AJAX submissions — the events above fire on the AJAX success response. A traditional (non-AJAX) submission that navigates to a new page has no success event the tracker can observe, so forms configured that way are counted as attempts but not as confirmed conversions; use the spa:conversion event (e.g. on a thank-you page) for those. Every conversion carries a unique conversion id (duplicates from retried deliveries are never double-counted — the REST endpoint rejects a form_success without a valid, bounded id) and a snapshot of the session's campaign attribution at conversion time, so each conversion record is self-contained — even when the tagged landing happened earlier in the session or the campaign data has since aged out.
  • Campaign attribution — all six utm parameters (utm_source, utm_medium, utm_campaign, utm_id, utm_term, utm_content) are captured from tagged landing URLs, and ad-click identifiers (gclid, gbraid, wbraid, fbclid, msclkid, ttclid, twclid, li_fat_id) are recognized: only the parameter name is stored (never the value), and when no utm tags are present the implied source/medium is filled in (e.g. gclidgoogle / cpc). Attribution is last-touch within the session, and the snapshot rides on every event in the session — pageviews and conversions, but also clicks, form attempts, hovers, and scroll milestones — so intermediate funnel steps can be segmented by campaign. Untagged acquisition persists too: the referrer a session entered through is stored client-side and sent as session_referrer, so an organic or referral visit keeps its channel across internal navigation instead of degrading to Direct at conversion time.
  • Channel grouping — every attributed event is classified into a marketing channel at ingestion (Paid Search, Paid Social, Organic Search, Organic Social, Email, Display, Affiliate, SMS, Referral, Direct, Other), with source aliases normalized (fb / facebook.comfacebook) so reports don't fragment. When an event's own referrer is internal or missing, classification falls back to the session's persisted entrance referrer (session_referrer). Referrer hosts match a search engine or social network only in the registrable-domain position (www.google.co.uk matches; a lookalike like google.example.test does not). Both the alias map and the channel rules are filterable.
  • Hover tracking — records when a visitor's pointer rests on an interactive element for a configurable dwell time (default 800 ms), once per element per page view. Add data-spa-hover to any element — images included — to opt it into hover tracking.
  • Scroll depth — 50/100% milestones, once each per page view.
  • Custom server-side events — record anything else with spa_track_event().
  • Dashboard analytics — summary cards, an accessible daily page-view chart (single-Tab-stop keyboard navigation, touch/mouse tooltips, visible axes, and a data-table fallback), a print-optimized Print / Save as PDF report view, top pages, top landing pages, top clicked elements, top forms, most-hovered elements, top referrers, campaign performance (sessions, conversions, session conversion rate), a keyword/creative drilldown (utm_term / utm_content), a channel breakdown, a device breakdown, and a recent-activity feed. Reports are grouped into an always-visible Overview plus collapsible Content, Engagement, Acquisition, Devices, and Recent Activity sections, filterable by 7/30/90-day periods (calendar days, UTC, with the exact date range shown).
  • Webhook delivery — aggregated analytics sent as JSON to any number of endpoints (HTTPS required; a filter allows HTTP for development) on an hourly, twice-daily, daily, or weekly schedule, with per-endpoint delivery windows, optional HMAC-signed requests (shared or per-endpoint signing secrets), optional history backfill for newly added endpoints, automatic retries on failure, a manual test-send button, and a delivery log. Conversion delivery is lossless: windows holding more than 100 individual conversions are split into consecutive deliveries instead of dropping the overflow.
  • Bounded storage — a daily cleanup cron deletes events older than the configured retention window (default 90 days, adjustable 7–365).
  • GitHub-powered updates — new releases published to the GitHub repository appear as standard update notifications on the Plugins screen.

Installation

  1. Copy the sitepulse-analytics folder into wp-content/plugins/ (the folder name must remain sitepulse-analytics).
  2. Activate SitePulse Analytics on the Plugins screen. Activation creates the events table and schedules the cleanup and webhook cron events.
  3. Visit SitePulse in the admin menu to view analytics, and SitePulse → Settings to configure tracking and webhooks. SitePulse → About carries this documentation inside wp-admin.

How tracking works

A single dependency-free script (assets/js/tracker.js) is enqueued deferred in the footer of frontend pages. It batches events in memory and delivers them to a public REST endpoint:

POST /wp-json/sitepulse/v1/track
Content-Type: application/json

{"batch_id": "b3f2a9c1b8e0d", "events": [{"type": "pageview", "page_url": "...", "page_title": "...", ...}]}

Batches flush every 5 seconds, when 20 events accumulate, and on page exit via navigator.sendBeacon, so events are not lost when a visitor navigates away. Every batch is persisted before it is sent — to a bounded sessionStorage map keyed by batch id, with an in-memory map standing in when sessionStorage is unavailable — and removed only when the server acknowledges it (a 2xx, or a 4xx other than 429 that retrying cannot fix). A batch whose page is destroyed before the response arrives, or that fails with a network error, a 5xx, a 429 (rate limited), or a 503 (events could not be stored — the endpoint refuses to acknowledge what never reached the database), is therefore resent by a later flush — by the same page or by the next page in that tab. Each batch is removed only by its own acknowledgment, so one batch's success can never discard another's undelivered events. Fetch-delivered batches are at-least-once, and replays are idempotent: the batch_id travels in the request body and rows are stored under a unique (batch_id, event ordinal) database key, so a replayed batch whose response was lost is deduplicated server-side instead of inflating page-view, click, and conversion counts. Fresh page-exit batches handed to navigator.sendBeacon are the exception: an accepted hand-off only means the browser queued the request, so those are treated as delivered — best-effort — because keeping every one persisted would resend every exit batch on the next page. A batch that already survived a failed or unacknowledged send, however, stays persisted even through an accepted beacon hand-off — its loss is exactly what retrying exists to prevent, and the server-side batch dedup absorbs the resend if the beacon did land.

The endpoint defends itself in layers: it accepts only whitelisted, currently-enabled event types; requires tracked page URLs to be http(s) URLs on this site's host (foreign schemes, and ports the site doesn't actually run on, are rejected or normalized away); rejects requests whose Origin/Referer header names a foreign host; ignores known bots and empty user agents; drops batches carrying DNT: 1 / Sec-GPC: 1 headers when the privacy-signal option is on (server-side backstop for the tracker's own check); caps request body and batch sizes; requires scalar field values and sanitizes/truncates every one; and rate-limits by event count — 300 events per IP per minute plus a site-wide cap of 3,000/minute, both tunable via the spa_rate_limits filter — using atomic object-cache counters when a persistent object cache is available, and an atomic single-statement database counter otherwise (also the fail-closed fallback whenever a cache increment fails — a flaky cache never disables the limit), so the caps are a hard bound on stored volume even under heavy concurrency. The per-IP check runs first and a rejected sender never consumes the site-wide budget, so one flooding IP can't starve legitimate visitors out of the global allowance. Accepted events are written with a single multi-row INSERT per request. The IP is used only as a hashed, short-lived rate-limit key and is never stored with analytics data (behind a reverse proxy, map the real client IP with the spa_client_ip filter). If the site-wide cap is reached, a warning appears on the dashboard for 24 hours so dropped events don't go unnoticed. For very-high-traffic sites, raise the limits via the filter and consider edge/WAF protection in front of the endpoint.

Privacy posture

  • No cookies are set. The session identifier lives in localStorage and rotates after 30 minutes of inactivity, so it groups one visit (across tabs) without becoming a persistent visitor ID.
  • Tracked URLs are canonicalized to scheme + host + path — no query strings are ever stored, so search terms, tokens, and emails in a page URL's or referrer's query string never reach the database. The session's entrance referrer (session_referrer) is normalized the same way and is used only to classify the marketing channel at ingestion — it is never stored as its own field.
  • What is retained, stated precisely: campaign parameter values (utm_source, utm_medium, utm_campaign, utm_id, utm_term, utm_content) are stored after text sanitization (source/medium are lowercased and alias-normalized) — except values containing an @, which are dropped as likely email addresses — so never put personal information in UTM parameters. Ad-click identifiers (gclid, fbclid, …) are handled more strictly: only the parameter name is stored (as click_id_type); the identifier value is a cross-site advertising ID that may qualify as personal data, so the tracker never even sends it. Clicked mailto: and tel: destinations are stored whole (the address/number is the destination); if that is unacceptable for your site, disable click tracking or strip target_url via the spa_tracked_event filter.
  • Optional: honor Do Not Track / Global Privacy Control browser signals (off by default; toggle under Settings → Tracking). Enforced both in the tracker (it never starts) and at the REST endpoint (batches carrying DNT: 1 / Sec-GPC: 1 are discarded). Note DNT/GPC is an opt-out signal, not a consent mechanism — if your jurisdiction requires consent, gate the tracker with your consent tool.
  • What page-level data can still contain, stated plainly: URL paths are stored, and a path itself can embed personal data if your site puts it there (e.g. /reset/<token>/, /account/jane@example.com/, /order/1234/) — the same applies to page titles, clicked element labels, and form names, which are stored as visitors see them. If your URLs or titles carry personal data, exclude or rewrite those fields with the spa_tracked_event filter before storage.
  • The webhook signing secret is stored in the plugin's settings option (like other WordPress secrets such as SMTP credentials, it is readable by anyone with database access or manage_options) and is rendered back into the settings field for editing. Rotate it if you believe an admin session or database copy was exposed.
  • The Delivery Log retains each delivery's full request payload and the endpoint's response. Structured JSON responses have sensitive-looking keys redacted automatically; non-JSON response bodies are stored as received (truncated to 64 KB). Use the spa_delivery_log_row filter for site-specific redaction, or skip logging entirely.
  • No IP addresses or user agents are stored — only a coarse mobile/desktop device bucket.
  • Logged-in users are excluded from tracking by default (toggleable in settings).
  • Data is automatically deleted after the retention window.

Read the full README on GitHub →

Releases

TagPublished
v1.8.0 Jul 22, 2026
v1.7.0 Jul 17, 2026
v1.6.0 Jul 17, 2026
v1.5.0 Jul 16, 2026
v1.4.0 Jul 12, 2026
v1.3.0 Jul 12, 2026
v1.2.0 Jul 11, 2026
v1.1.1 Jul 11, 2026
v1.1.0 Jul 11, 2026

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.