"Loading the script violates the Content Security Policy"
Loading the script 'https://sparlos.se/script.js' violates the
following Content Security Policy directive: "script-src 'self'".Recommended: add sparlos.se *.sparlos.se to your CSP.
A bare apex plus a wildcard subdomain future-proofs against us moving the tracker to a dedicated subdomain (for example cdn.sparlos.se or events.sparlos.se) without you having to touch your CSP again.
Option 1: Do you only have default-src?
One line is enough. default-src is the fallback for all fetch directives that are not explicitly set.
Content-Security-Policy:
default-src 'self' sparlos.se *.sparlos.se;Option 2: Do you have explicit script-src and connect-src?
Common on security-conscious sites (Vercel default, banks, healthcare). In that casedefault-src is overridden per directive and you have to add Spårlös to both.
Content-Security-Policy:
script-src 'self' sparlos.se *.sparlos.se;
connect-src 'self' sparlos.se *.sparlos.se;- script-src allows the
<script>tag to load. - connect-src allows the tracker to POST events to
/api/event. Without this the script loads but every pageview is blocked separately. - Do you use
script-src-elemorconnect-src-elemexplicitly? Add Spårlös there too, they overridescript-srcandconnect-srcin turn.
Where do you put the CSP header? In your web server config (nginx, Apache), in a middleware/route (Next.js, Express), or in a <meta http-equiv="Content-Security-Policy"> tag in the HTML head. It does not matter which method, the addressing is the same.
Do you prefer an explicit protocol (some security policies require it)? https://sparlos.se https://*.sparlos.se works identically on HTTPS sites. The bare-host version above inherits the protocol from the page, which is always HTTPS in production.
script.js once, serve from https://your-site.com/script.js; connect-src to sparlos.se must still be allowed).Adblockers block the tracker
net::ERR_BLOCKED_BY_CLIENT on script.js or /api/event.For WordPress sites: use our WordPress plugin with proxy mode on (the default). The script and the event endpoint are then served via your own domain, which adblockers cannot match without blocking your entire site.
For other frameworks: build your own reverse proxy:
# Next.js / Vercel: src/app/sparlos/script.js/route.ts
export async function GET() {
const upstream = await fetch('https://sparlos.se/script.js');
return new Response(await upstream.text(), {
headers: {
'content-type': 'application/javascript',
'cache-control': 'public, max-age=3600',
},
});
}Plus a parallel route for /sparlos/event that proxies the POST to https://sparlos.se/api/event with the visitor IP in the x-forwarded-for header (we need it for country lookup and discard it immediately after).
The script loads but no data shows on the dashboard
Go through the list in this order:
- Check the site_id. Open view-source and find
<script data-site="...">. The site ID must match the one shown on your site settings page in Spårlös. Common mistake: pasting in the wrong site code snippet. - Wait 30 seconds and reload. The dashboard does not update in real time, it polls every 5 seconds. The first pageview should appear in the Live view within 30 seconds.
- Have you enabled DNT or GPC? We respect Do Not Track and Global Privacy Control both on the client and the server side. If YOU have it enabled in your browser, your visits are not counted. Test in another browser or turn off DNT/GPC temporarily to verify.
- Are you using a bot-like User-Agent? Our bot filter drops GPTBot, ClaudeBot, headless Chrome, Playwright and 50+ others. If you test with
curlor an automation tool, the request is not counted. - Click "Test installation" on the snippet. We have a button on the site settings page that actually goes out and fetches your homepage to verify that the code snippet is there AND that we have seen pageview events from your site_id in the last hour.
- CSP or adblocker? See the CSP and Adblockers sections above.
- Localhost? See Testing on localhost below. We bail by default on
localhostand private IP addresses. - Path blocklist? If you have configured a path blocklist in the site settings and your test page matches a pattern (for example
/admin/*), the request is ignored silently.
Cloudflare WAF or rate limit blocks events
403 Forbidden / 429 Too Many Requests in the Network tab for /api/event.- Allow-list
sparlos.seas an origin in your WAF. - If you have a rate-limit rule on
/api/event(why would you? it is our endpoint, not yours), remove it. - Bot Fight Mode: add a bypass rule for the Spårlös tracker User-Agent
Sparlos-Uptime/1.0(if you also use the uptime monitor against your own site). - Cloudflare Workers: if you have a worker that modifies requests, check that it does not strip the
x-forwarded-fororcf-connecting-ipheaders (we use them for country lookup).
The tracker does not work on localhost
http://localhost:3000 or similar and no pageview comes through.data-include-localhost="true" on the script tag during development:<script defer
data-site="YOUR_SITE_ID"
data-include-localhost="true"
src="https://sparlos.se/script.js">
</script>Do not forget to remove the attribute before deploying to production. Otherwise you risk your team localhost test traffic counting against your monthly quota.
SPA with hash routing (/#/route) only counts the start page
/ regardless of which page the visitor is on./#/cart always have location.pathname === '/'. We have a safeguard that detects the #/ pattern and uses the hash as the pathname instead, but it only triggers on the hashchange event.For modern SPAs (React Router v6+ with BrowserRouter, Vue Router 4 with createWebHistory, Next.js, SvelteKit, Nuxt) we hook in automatically via pushState/replaceState/popstate. You do not need to do anything.
For HashRouter or other exotic patterns: call manually on every navigation:
window.sparlos = window.sparlos || {};
// On every navigation in your SPA:
window.sparlos.pageview && window.sparlos.pageview();For the cart-drawer pattern (a modal that changes the UX but not the URL), trigger a pageview anyway if you want to measure the interaction as a page. Or use goal tracking on a thank-you redirect URL.
WordPress: all pages show as /
/ regardless of which page the visitor is actually on.?p=123 URLs). In that case every page technically has the path / in the URL and our tracker has nothing else to go on.Spårlös shows fewer visitors than GA4
- We filter out more bots. GA4 counts traffic from GPTBot, headless Chrome, scrapers and similar as real visitors. We remove them at ingest. Typical difference: 5-15% of raw traffic.
- We do not link visitors across days. Our visitor hash rotates daily (privacy by design). If the same person visits your site on Monday and Tuesday, that is 2 unique visitors to us, 1 to GA4 (which stores a persistent visitor ID in a cookie).
- We respect DNT and GPC. GA4 counts traffic from users who have Do Not Track enabled. We do not. Small effect (~5%) on a general audience, larger on tech.
Tracking subdomains and multiple sites
blog.your-site.com and app.your-site.com as separate subdomains and want to track them either as ONE site or as SEPARATE ones.As one site: use the SAME data-site="..." in the code snippet on all subdomains. Paths land in the same dashboard, and sorting shows where users land regardless of subdomain.
As separate sites: create one site each in Spårlös (with a unique site_id per subdomain) and use the respective ID. On Starter and up the number of sites is unlimited so this costs nothing extra.
Cross-subdomain visitor tracking does NOT happen regardless of which approach you choose. Our visitor hash includes the site_id, so the same visitor moving between blog.your-site.com and app.your-site.com is counted as two different unique visitors. That is intentional (privacy by design).
Still not solved?
Email [email protected] with a description of the symptom, which browser and version, and if possible a screenshot of the Network tab. We reply within 24 working hours.
Back to the documentation