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?#
- Open your live site, then your browser's developer tools, Network tab, and filter for
collect. - Reload. You should see
script.jsload, then aPOSTto/api/collect. - Read the answer:
| What you see | It means | Fix |
|---|---|---|
No script.js request | The tag is missing, or something blocked it. | View source and search for script.js. Then see ad blockers and CSP. |
script.js loads, no collect | The script chose not to send. | You are on localhost, the browser is excluded, it is an automated browser, or data-site is missing. |
200 | Pulse stored it. | Look in Realtime. If you still see nothing, check the site and date range. |
202 | Pulse 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 site | The 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 site | The 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 site | Wrong site id. | Copy the snippet again from Settings, Tracking. |
429 | Rate 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.comorpulse.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:
<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:
<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.
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
#/pathstyle hash routes (sent as/app#/path: the page's own path, then the route; the hash's query string is dropped). Plain#anchorjumps 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.titlebefore 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.