# Migrate from Simple Analytics

Clicktag runs a vendored Simple Analytics v11 script, so supported `data-` settings carry over: swap the script host, rename the `sa_` globals and queues to `tt_`, and import your history from Simple Analytics separately.

## 1. Audit the existing install and choose a cutover

Search all app roots, HTML templates, generated-page builders, tag-manager entries, and proxy rules. Look for `simpleanalytics`, `sa_settings`, `sa_event`, `sa_pageview`, `noscript.gif`, `auto-events.js`, and any first-party `proxy.js`.

Record the hostname used in your Simple Analytics dashboard, script attributes, event names, and any manual pageview logic. Check separate marketing pages and cached HTML, not just the main application.

If historical continuity matters, plan the [historical data transfer](#historical-data) before switching. Record the actual cutover time in UTC and keep Simple Analytics export access until your archive is verified. Installing the new tag starts new collection; it does not copy old data.

## 2. Register the matching hostname

[Sign in](https://clicktag.app/login) and add the site in [your dashboard](https://clicktag.app/app), or follow the [agent setup guide](/docs/agents) to register it through `POST /api/sites` with a Clicktag access token.

Use a bare hostname, such as `example.com`. Clicktag lowercases hostnames but **does not strip `www`**. If both `www.example.com` and `example.com` should report under one site, explicitly set `data-hostname="example.com"`. If you want separate reports, register each hostname separately.

Match the hostname used for historical exports deliberately. Do not assume that a hostname accepted by Simple Analytics will be grouped the same way here. The script and optional pixel need the same intended destination. See [Site management](/docs/sites) and Simple Analytics' [hostname override documentation](https://docs.simpleanalytics.com/overwrite-domain-name).

Site ownership and dashboard visibility are configured separately in Clicktag. A Simple Analytics API key cannot create or manage Clicktag sites.

## 3. Swap the script and optional pixel

Replace the existing core tag, preserving supported attributes.

Before:

```html
<script async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
<noscript>
  <img
    src="https://queue.simpleanalyticscdn.com/noscript.gif"
    alt=""
    referrerpolicy="no-referrer-when-downgrade"
  />
</noscript>
```

After, with explicit attribution to `example.com`:

```html
<script
  async
  data-hostname="example.com"
  src="https://clicktag.io/latest.js"
></script>
<noscript>
  <img
    loading="lazy"
    src="https://clicktag.io/noscript.gif?hostname=example.com"
    alt=""
    referrerpolicy="no-referrer-when-downgrade"
    style="position:fixed;top:0;left:0"
  />
</noscript>
```

Keep the tag once per shared HTML document. The optional `noscript` pixel belongs in the server-rendered body and does not inherit the script's settings. Its page path normally comes from `Referer`; retain an appropriate referrer policy or provide values explicitly from your template. In JSX, use `referrerPolicy`. Keep `loading="lazy"` before `src`: it stops the pixel from loading when JavaScript renders it, which would count the pageview twice.

Update each independent entrypoint and the source of generated pages, then rebuild. Confirm the live HTML changed after deployment and any cache invalidation.

**Dual-tagging works with separate globals.** Clicktag defaults to `tt_tt_loaded` and `tt_event`, while Simple Analytics keeps `sa_loaded` and `sa_event`. Each tracker consumes its own preload queue regardless of load order. An action reaches only the API you call; call both explicitly when you want comparison events in both systems.

An old Simple Analytics `integrity` hash will not match Clicktag' modified script. Review that policy rather than carrying the hash over. Clicktag does not serve Simple Analytics' alternate `latest.dev.js`, light, or SRI script endpoints.

## 4. Preserve settings and event behavior

Clicktag serves a vendored Simple Analytics v11 script with a different collector destination. It supports these configuration patterns:

| Setting                                           | Migration check                                                          |
| ------------------------------------------------- | ------------------------------------------------------------------------ |
| `data-hostname`                                   | Use the exact registered destination.                                    |
| `data-ignore-pages`                               | Keep intended excluded paths and wildcard patterns.                      |
| `data-allow-params`                               | Retain the permitted query parameters, not arbitrary URL data.           |
| `data-ignore-metrics`                             | Preserve deliberately excluded metrics.                                  |
| `data-strict-utm`                                 | Preserve the choice of strict UTM parameter names.                       |
| `data-non-unique-hostnames`                       | Keep the configured referrer hostnames treated as non-unique.            |
| `data-path-overwriter`, `data-metadata-collector` | Keep the named global callbacks available before collection.             |
| `window.tt_settings`                              | Rename `sa_settings`; keep configuration before the async tag. |
| `data-namespace`                                  | Default is now `tt`; migrate custom globals together and avoid an SA namespace. |
| `data-event-global` (`data-sa-global` alias)     | Use an unused event global or its queue; `sa_event` is reserved for SA. |

This compatibility applies to the vendored script, not every feature or future release of Simple Analytics.

### SPA and manual pageviews

Automatic collection handles the initial load, `history.pushState`, and `popstate` when the tracked path changes. Hash routing needs `data-mode="hash"`. There is no direct `replaceState` listener; a query-only change does not create a new page path.

Do not add manual route tracking on top of automatic collection. If the existing integration uses `data-auto-collect="false"`, rename its initial and navigation calls to `window.tt_pageview()` and wait for the script to load. That function is only exposed in manual mode. Consecutive identical tracked paths are suppressed, even when called manually.

See [Installation](/docs/installation) and the upstream [custom pageview guide](https://docs.simpleanalytics.com/trigger-custom-page-views).

### Custom and automatic events

Custom calls keep their arguments but use the Clicktag global:

```javascript
if (typeof window.tt_event === "function") {
  window.tt_event("newsletter_signup", { source: "footer" });
}
```

This guard avoids an error but skips the event if the tag is not ready. Rename the preload queue to `tt_event` too, using the [event queue example](/docs/events#browser-event-tracking), or wait for load. An existing `sa_event` queue is never consumed by Clicktag. Keep browser globals out of server-side execution. Event names replace non-alphanumeric runs with underscores and trim outer underscores. A callback is not proof of storage; it can also run after a local validation failure without sending a request.

Simple Analytics' [automated-events helper](https://docs.simpleanalytics.com/automated-events) is a separate script. **Clicktag serves neither `/auto-events.js` nor `/auto.js`.** Do not change only the hostname of that helper. Its default SA globals do not route to Clicktag. Replace required outbound, download, or email-click tracking with explicit `tt_event` [event calls](/docs/events), or independently adapt and test the helper against your chosen TT namespace.

## 5. Update CSP and proxy destinations

Add the Clicktag origin to the existing policy:

```text
script-src 'self' https://clicktag.io;
connect-src 'self' https://clicktag.io;
img-src 'self' https://clicktag.io;
```

Merge these sources; do not replace the whole CSP. Update `script-src-elem` if it is defined, and preserve existing nonces and hashes. No new `'unsafe-inline'` allowance is required for the external tag.

`/latest.js` needs script permission. `/simple.gif` pageviews and events, append fallbacks, and `/noscript.gif` need image permission. Duration and scroll updates use `navigator.sendBeacon()` to **`/append`, covered by `connect-src`**. Remove old Simple Analytics origins only after confirming no retained component needs them.

### First-party proxies need a separate audit

A local-looking `/proxy.js` may still send everything to Simple Analytics. Check its upstream source and the actual collector requests. The [upstream proxy guide](https://docs.simpleanalytics.com/proxy) helps identify routes you may already have.

The simplest cutover uses the direct Clicktag tag and retires unused Simple Analytics proxy routes. Clicktag' tag has a fixed collector origin; **proxying `/latest.js` alone does not make collection first-party**. There is no Clicktag proxy-generator endpoint or supported runtime collector-base setting.

If you must keep first-party collection, treat it as a separate integration: the script's actual destination and proxy routes must agree. Preserve query strings for `/simple.gif` and `/noscript.gif`, and methods and bodies for `/append` or server-side `/events`. Do not cache collector responses. Recheck attribution and visitor counts after proxy changes rather than assuming header or IP behavior stayed identical.

## 6. Verify the live cutover

Build and deploy the updated site. On a real route in a normal browser, inspect `/latest.js` and the next `/simple.gif` request: check the destination, `type=pageview`, hostname, and path. Test one SPA navigation and an existing custom-event action. Check append traffic when leaving the page if duration and scroll matter to your setup.

Confirm the matching data in the dashboard or authenticated breakdown API. Use a narrow time window and compare the page count before and after a visit, or send one permitted, uniquely named verification event. The [agent verification workflow](/docs/agents#4-verify-stored-data-not-just-a-request) includes commands.

A `200` response is only an acknowledgement, not proof that a row was stored. Localhost, bot/headless tests, Do Not Track, blockers, and an unregistered hostname can make a network-only check misleading. Poll reads while waiting for asynchronous processing; do not repeatedly send the test event.

### Update reporting integrations separately

Clicktag does not mirror Simple Analytics' `/{hostname}.json` [Stats API](https://docs.simpleanalytics.com/api/stats). Replace those clients with the [Clicktag Stats API](/docs/stats): `/api/sites/{hostname}/overview` and `/api/sites/{hostname}/breakdown`, using Unix-second `from`/`to` parameters and an owner bearer token for private sites.

Do not forward Simple Analytics' `Api-Key`, `User-Id`, `fields`, or date-format `start`/`end` parameters unchanged. The `visitors` field counts distinct daily visitors, not SA's unique entries, so the two differ; imported days keep SA's count. `avg_duration_s` keeps its legacy name but returns the median page duration after summing increments and excluding pages below five seconds. Country uses browser timezone rather than IP; mapping and bot filtering can still differ. Compare the same date boundaries and timezone rather than expecting identical live delivery.

## Historical data

Site owners can import historical analytics directly from the **Integrations** tab of `/app/{hostname}/settings` under **Import data**. Starting an import requires a [verified site](/docs/sites#verify-ownership). The only supported import provider is SimpleAnalytics. CSV uploads and ongoing sync are not supported.

### Import workflow

1. Sign in as the site owner and open `/app/{hostname}/settings?tab=integrations`.
2. Under **Import data**, click **New import** and set **Source** to **SimpleAnalytics**.
3. Fill in **User ID**, **API key**, **Source hostname**, **From**, and **To**.
4. Click **Start import**.

Simple Analytics API credentials serve as source configuration and are distinct from Clicktag Bearer tokens (see [Authentication](/docs/authentication)). The source hostname can differ from your Clicktag site to accommodate domain migrations. Credentials are validated against Simple Analytics before the import queues. Once queued, the background task runs independently of browser sessions. The settings panel polls every 3 seconds while active, showing `queued`, `running`, `completed`, `failed`, or `canceled` statuses alongside chunk progress, `rows_imported`, and errors.

Usage is limited to 1 active import per website and 20 new jobs per user per rolling 24 hours. The source API key is omitted from job responses. Its stored value is cleared when the job completes; failed and canceled jobs retain it for retry.

### Cutover and date boundaries

- Date ranges use inclusive UTC days. Start dates must be on or after 2010-01-01, with at most 1,826 days between the two dates.
- Monthly chunks preserve the selected start and end dates; a mid-month start does not import earlier days.
- To cut over tracking cleanly, pick midnight UTC on your switch date and set the import end date to the preceding day. Importing the full cutover day after an intraday switch can overlap live tracking.

### Deduplication and data fidelity

Imports preserve SA source UUIDs and skip repeated source records across jobs. TT live events and SA exports have independent IDs, so importing over live coverage can still count both; set a deliberate historical cutoff. Inspect reports and keep source archives when replacing existing history.

- **Counts**: The counter covers completed chunks and includes pageviews, custom events, and engagement append records. It is not a pageview or visitor total, and partial writes from an interrupted chunk may not be included.
- **Fidelity**: Imported visitors come from exported unique-entry flags, not fabricated identities. Original person/session continuity and custom event metadata are not restored. Engagement uses the parent pageview's date and bot flag; browser/OS labels are normalized consistently.
- **Idempotency**: Completed chunks are recorded; interrupted chunks may be replayed, with stable source records skipped at insertion. `Cancel` stops later work without rolling back stored rows or aborting a chunk already writing. `Retry` resumes from the last cursor on failed or canceled runs; verify existing counts first.
- **API usage**: Do not backfill data using `POST /events`, which discards historical timestamps. Use the [Imports API](/docs/imports) for historical records.

### Verify the import

Verify imports by reviewing pageviews, summed unique-entry flags, specific paths, and events over matching UTC intervals. A completed job alone does not establish parity or delivery completeness.

To preserve full raw metadata, download standalone archives directly from Simple Analytics via their [website export interface](https://simpleanalytics.com/select-website/export) or [data points export API](https://docs.simpleanalytics.com/api/export-data-points).

Export pageviews and events separately. Select metadata fields explicitly if you need them in the archive, and inspect the CSV header and a sample before closing your Simple Analytics account. The source Stats API contains aggregated reports, not a replacement for raw exports.
