Troubleshooting

Troubleshooting

Most problems are one of five things: the tag is not on the page, the page's domain does not match the site, a blocker or policy stops the request, you are on localhost, or the browser is excluded. Start with the checklist, then jump to the section that fits.

Start here: is anything arriving?#

  1. Open your live site, then your browser's developer tools, Network tab, and filter for collect.
  2. Reload. You should see script.js load, then a POST to /api/collect.
  3. Read the answer:
What you seeIt meansFix
No script.js requestThe tag is missing, or something blocked it.View source and search for script.js. Then see ad blockers and CSP.
script.js loads, no collectThe script chose not to send.You are on localhost, the browser is excluded, it is an automated browser, or data-site is missing.
200Pulse stored it.Look in Realtime. If you still see nothing, check the site and date range.
202Pulse dropped it as a bot.Check the User-Agent: crawlers, monitors, HTTP libraries and headless browsers are dropped. For apps and scripts, set a descriptive one.
400 hostname does not match siteThe page's domain is not the site's domain or a subdomain.Add the site under the domain you actually serve, or fix a typo in Settings, Sites.
400 origin does not match siteThe browser's Origin differs from your domain.Usually a proxy or an iframe on another domain. Serve the script from the page itself.
404 unknown siteWrong site id.Copy the snippet again from Settings, Tracking.
429Rate limited (120 a minute per IP, 6,000 a minute per site).Normal traffic never hits this. A proxy that hides visitor IPs can; see proxies.

Ad blockers#

Some browsers and extensions block analytics by URL. A blocked script shows in the Network tab as blocked or ERR_BLOCKED_BY_CLIENT, and works when you turn the blocker off. Pulse sets no cookies and collects no personal data, but a blocker cannot know that.

What usually helps#

  • Serve Pulse from your own domain. If you self-host, use a neutral subdomain of your site (stats.example.com or pulse.example.com) instead of a recognisable analytics hostname. See self-hosting.
  • Accept some loss. A small share of visitors always run blockers. Pulse counts the rest, and your sales still come from your payment provider, so revenue is exact either way.

Proxying through your own site#

You can serve the script and receive events through a path on your own domain. Point the script at the proxied collect URL with data-api:

html
<script defer data-site="YOUR_SITE_ID" data-api="/stats/collect" src="/stats/script.js"></script>
// next.config.ts
const nextConfig = {
  async rewrites() {
    return [
      { source: '/stats/script.js', destination: 'https://stats.shedi.app/script.js' },
      { source: '/stats/collect', destination: 'https://stats.shedi.app/api/collect' },
    ];
    // Rewrites cannot set X-Real-IP; Next forwards the visitor's address as X-Forwarded-For (see the note below).
  },
};
export default nextConfig;

Localhost and local files#

The script does nothing on localhost, 127.0.0.1, [::1] or file: pages, so development never pollutes your numbers. To test your setup, add data-allow-localhost to the tag:

html
<script defer data-allow-localhost data-site="YOUR_TEST_SITE_ID" src="https://stats.shedi.app/script.js"></script>
  • The server also checks the domain. Create a separate Pulse site with the domain localhost, which accepts any host, and use its id while developing. Your real site stays clean.
  • Remove data-allow-localhost (and use the real id) before you ship.

Content-Security-Policy on your site#

If your site sends a Content-Security-Policy header, it must allow the Pulse origin for scripts and for the request that sends events. Symptoms: a console error mentioning Content Security Policy and script.js or /api/collect.

text
Content-Security-Policy:
  script-src 'self' https://stats.shedi.app;
  connect-src 'self' https://stats.shedi.app;
  • If your policy uses nonces or strict-dynamic, put the nonce on the Pulse script tag as well.
  • Allow the exact origin from your snippet, with no trailing slash.

Single-page apps#

Pulse listens to the browser's history API (pushState, replaceState, popstate) and sends a pageview each time the full URL changes. Install the tag once in the shell page.

  • Missing page views after navigation: your router must use the history API, or #/path style hash routes (sent as /app#/path: the page's own path, then the route; the hash's query string is dropped). Plain #anchor jumps are deliberately not page views.
  • Too many page views: apps that rewrite the URL on every filter change or keystroke (with replaceState) create a page view each time. Stop updating the URL for transient state, or use custom events instead.
  • Double counting: the script must be loaded once. Loading it in a component that mounts on each route adds duplicate listeners.
  • Wrong titles: the title is read when the route changes. Set document.title before you push the new URL.

The numbers look wrong#

  • Too low: blockers (above), your own excluded browser, bots filtered out, or the time zone shifting what “today” means. Check Settings, Sites for the zone.
  • Too high: a second script instance, your own visits (see exclude your visits), or URL rewriting in a single-page app.
  • Visitors differ from another tool: see the definitions. Pulse counts hashed IP plus browser per month.
  • Sales missing or without a source: see revenue attribution.

Common issues#

Cloudflare Rocket Loader is on and nothing arrives
Rocket Loader can defer scripts in a way that hides the tag from itself. Add data-cfasync="false" to the Pulse script tag, or exclude it from Rocket Loader.
A cookie-consent tool blocks the script
Pulse sets no cookies, so you can load it without waiting for consent. Remove it from your consent tool's blocked list if your policy allows it.
Everything worked and then stopped
Check that nothing changed in your site's head (a theme update can drop custom code), that the site still exists in Settings, and that your domain matches.