# Troubleshooting

Clicktag records nothing when the hostname is unregistered or unverified (and `www` is a separate hostname), or when an ad blocker, Do Not Track or your CSP stops the script. **Setup check** in site settings tests your tag and CSP for you.

## Diagnostic checklist

If pageviews or events do not appear in your dashboard, check these common causes:

### 1. Verify hostname registration

The collector ignores unregistered hostnames and still returns HTTP 200. Register the hostname before testing. Collector instances can cache a missing hostname for up to 15 seconds.

Verify that the hostname configured in your script matches the hostname in [/app](https://clicktag.app/app):

- Hostnames are case-insensitive, but `www.example.com` and `example.com` are treated as distinct domains. Registration trims spaces and reduces a full URL to its hostname, and the collector normalizes `data-hostname` the same way; a hostname with a port, an IP address or `localhost` never matches a site.
- To combine apex and `www` traffic, set the same explicitly registered `data-hostname` on both versions of your site. Without that setting, the script uses `location.host`.
- Also confirm the site is **verified**: traffic for unverified sites is discarded. Verification normally happens automatically when the first visit arrives after installing the tag, if the served homepage HTML contains the tag with `data-verify`. In Site settings > Installation, **Run setup check** shows whether that HTML has a Clicktag tag and whether your CSP header allows it, and **Check verification** under **Verify another way** reports why the tag, DNS and file checks failed.

### 2. Verify script source

For new installations, load `https://clicktag.io/latest.js`. `https://clicktag.app/latest.js` and the existing Totallytics script hosts remain supported. A Simple Analytics script, from their CDN or a proxy of it, sends to Simple Analytics instead. A self-hosted copy of `latest.js` still reaches Clicktag, but automatic verification and the setup check only recognize tags loaded from Clicktag hosts.

### 3. Duplicate script guards

The default tracker sets `window.tt_tt_loaded = true` when executed, so a second copy of the Clicktag tag on the same page halts immediately; keep only one. A retained Simple Analytics tag does NOT block Clicktag (it guards on its own `window.sa_loaded`); both track independently. Clicktag events use `tt_event`; SA events use `sa_event`. Migrate preload queues as well as event calls, and do not assign both trackers the same custom namespace.

### 4. Ad blockers, DNT, and automated browsers

- Do Not Track: The script checks `navigator.doNotTrack`. By default, `"1"` stops normal collection. The collector also drops hits sent with a `DNT: 1` header, even when the script's check is turned off.
- Browser extensions: Content blockers may intercept `latest.js` or `simple.gif`.
- Headless automation: The script marks `navigator.webdriver = true` traffic as automated; matching user-agents are also classified as bots, and so are browsers on major cloud networks that report a `UTC` or unknown timezone. Bot rows do not appear in normal visitor and pageview totals.

Always test using a real, unblocked browser profile.

### 5. Content Security Policy (CSP)

Check your browser console for CSP violation notices. Ensure the tag's script host is in `script-src` (and `script-src-elem` when defined), and that `https://clicktag.io` is in `img-src` and `connect-src`; legacy Totallytics tags still need `https://totallytics.com` for their beacons. A policy without those directives falls back to `default-src`. Site settings > Installation > **Run setup check** reports which directive blocks what, with the exact fix, for your `Content-Security-Policy` response header; it does not read `<meta>` or report-only policies.

### 6. Processing delays

An HTTP 200 response from the collector signals successful receipt, not instantaneous persistence. Aggregated dashboard metrics update asynchronously. Check `GET /api/sites/<hostname>/breakdown?dim=pages` across an active date range rather than expecting zero-latency counters.

## HTTP status codes

### 400 Bad Request

- **Collector (`/simple.gif`, `/noscript.gif`, `/events`, `/append`)**: Returns plain text errors: `hostname is required`, `event name is required` or `unsupported type: <type>`. `/events` and `/append` also return `invalid JSON` when the body is not a JSON object, and `POST required` for other methods. `/r` answers a non-GET request with `GET required`, as do `/latest.js`, the pixels and `/healthz` for methods other than GET and HEAD.
- **API (`/api/*`)**: Check the response's `error` field. Invalid ranges, invalid hostnames on registration, and unknown breakdown dimensions return `400`; unknown query keys are generally ignored. Send valid JSON objects for writes: an unreadable site PATCH body returns `400 invalid settings`; `display_name` must be a string and `is_public` must be a boolean. Statistics ranges are at most 400 days and use Unix seconds, not milliseconds. Import requests instead use date-only strings and their own [range rules](/docs/imports). Use one of the 10 [supported dimensions](/docs/stats). A breakdown `limit` clamps to 1-100 and fractions round down; zero or non-numeric input uses the default 10.

### 401 Unauthorized

An owner-only or account endpoint lacks a valid Firebase ID token or management key in the `Authorization: Bearer <TOKEN>` header. A missing or expired ID token returns `sign in required`; an unknown or revoked management key returns `invalid or revoked management key`, on public reads as well. `/api/live/*` accepts only Firebase ID tokens. Refresh your token in [/docs/authentication](/docs/authentication). For `/api/ingest`, use a site API key (`tt_...`) or a management key with the `ingest` scope instead; its error distinguishes a missing or malformed site key from an invalid or revoked one.

### 403 Forbidden

You own the site, but this operation requires domain verification. Follow [Verify ownership](/docs/sites#verify-ownership). API-only sites can create ingestion keys and read request metrics before verification, but creating, changing or testing alerts still needs proof of ownership. Management keys also get `403`: `scope <scope> required` when the key lacks a scope, and `management keys cannot call this endpoint` on routes that need you signed in; see [Errors and limits](/docs/authentication#errors-and-limits).

### 404 Not Found

The requested site or import job does not exist or is not accessible to this account. Private metadata and web-statistics reads also return `404 unknown site` for a missing or invalid Firebase ID token (an invalid management key returns `401`), so check the owner token as well as the exact hostname. A signed-in non-owner gets the same response as an unknown site.

### 409 Conflict

Returned when attempting to add a hostname via `POST /api/sites` that another account already claimed (`This site is already registered to another account`); registering a hostname you already own returns `200` with your existing site. Check your site list via `GET /api/sites` before attempting registration. `POST /api/sites/<hostname>/verify` returns `409` (`Site registration changed. Reload and try again.`) when the site was deleted or registered again during the check. Imports also return `409` for an already-active import on that site, an invalid cancel/retry transition, or a retry after the job's API key was deleted; see [Imports](/docs/imports). Email reports, management keys, Search Console and Cloudflare crawler endpoints have their own `409` cases.

### 413 Payload Too Large

`POST /events` and `POST /append` take one JSON object of at most 64 KiB, and `POST /api/ingest` a batch of at most 4 MiB. Larger bodies return `payload too large`. Send one hit per collector request, and split API batches, each with a new `batch_id`.

### 422 Unprocessable Entity

`POST /api/sites/<hostname>/verify` returns `422` with `verification not found yet` when the tag, DNS and file checks all fail. The response's `tag`, `dns` and `file` objects each carry a `detail` saying what was found; fix one of them and try again.

### 429 Too Many Requests

A user may create at most 20 new import jobs in a rolling 24-hour window (`too many imports today, try again tomorrow`). Wait before creating more; do not submit duplicate jobs to retry existing work. Changes made with a management key (any method but `GET`) draw from a per-key budget of about 60 requests, refilled at about one per second: after `rate limited, retry in <n>s`, wait for the `Retry-After` header. Per-site caps also return `429`: `too many goals for this site`, `too many alerts for this site` and `too many recipients for this site`, as do alert, report and digest tests repeated within 30 seconds (`a test was just sent, try again in a moment`) and too many open live-view tickets (`too many live tickets`). Verifying the same hostname again within about 10 seconds, or refreshing its setup check that soon, returns `rate limited, retry in <n>s` with `Retry-After`.

### 500 Internal Server Error

An unexpected server-side error occurred. Retry safe read requests with a short, bounded backoff. Do not blindly repeat event POSTs or site mutations after an ambiguous failure; inspect the current state first.

### 502 Bad Gateway and 503 Service Unavailable

A queue or upstream service was unavailable. Starting or retrying an import returns `502` with `import queue unavailable, please try again`; saving a Cloudflare token or starting crawler tracking returns `502` with `Could not reach Cloudflare. Please try again.` when Cloudflare fails; Search Console and Cloudflare crawler syncs return `503` when their queue is unavailable; `/api/ingest` returns `503` with `ingest temporarily unavailable, retry the same batch`. Retry with a short, bounded backoff.

## Operational behavior

- **Rate limiting**: Statistics and collection have no documented request quota or rate-limit headers. Historical imports and management-key changes have the limits above. This is not an unlimited-throughput guarantee. Bound concurrency and handle upstream/network errors.
- **Health check**: The `/healthz` endpoint confirms worker process responsiveness. It does not check database connectivity or collector readiness.
- **Overview vs breakdown**: Both use exact requested timestamp boundaries. Overview uses hourly buckets for ranges up to four days and daily buckets for longer ranges in `tz`. Visitors count distinct daily visitor hashes. See the [Stats Reference](/docs/stats) for definitions.
- **Site capacity**: Each account can register a maximum of 50 sites.
- **Deleted sites**: Deletion removes the site registration, not its historical rows. Verified site lookups can remain cached for up to two minutes; missing or unverified registrations for up to 15 seconds. See [Sites](/docs/sites).
- **Discrepancies with Simple Analytics**: Compare identical dates and timezones. Simple Analytics counts unique entries as visitors while Clicktag counts distinct daily visitors, so Clicktag visitors usually run lower outside imported days. Time on page uses eligible page-duration medians in both, but collection, bot filtering, and country mapping can still differ. See the [Migration Guide](/docs/migrate-simple-analytics).

## Import completed with zero rows

Check the source hostname, including `www`, the selected UTC dates, and whether Simple Analytics returns data for that range. Inspect `chunks_done`, `chunks_total`, and `rows_skipped`; a completed status alone is not proof that expected history arrived.

Keep your Simple Analytics account and exports until actual report counts are verified. Record the job ID, requested range, and source hostname if the result is unexpected. Do not start repeated overlapping jobs as a diagnostic step: each counts toward the limit of 20 jobs per rolling 24 hours, and records already imported are skipped rather than duplicated.
