Importing data
Bring the history of Umami, Simple Analytics, Matomo, GoatCounter, Google Analytics 4 or PostHog into a site, what each export must contain, and how fields are mapped.
Import adds the past visits, pageviews and custom events from another analytics tool to one of your sites, so the dashboard does not start empty. Imported rows carry the name of their source, are kept apart from what the tracker recorded, and can be removed again without touching tracked data.
- Importing the same file twice adds nothing the second time, and a run that stopped halfway can be started again with the same file.
- Rows older than the site’s retention limit are skipped, because the next purge would delete them.
- Imported and tracked visitors are never merged, even when they cover the same days.
- Fields a source does not record stay empty and show as unknown, never as zero. Visits without a known length are left out of the average visit duration.
Import in the dashboard
Open Settings, then Data, and find Import data. Choose the source and the export files. The dashboard reads them in your browser and shows a preview before anything is stored: how many visits and events it found, the first and last visit, how many rows it skipped, a note when the site already has visits in those dates, and the first few visits as they will be stored.
Import then sends the visits in small chunks, with a progress bar, so a large file does not hit the request limit of a Worker. Cancel stops after the chunk that is running; choose the same files again to finish, and what is already stored is not added twice. The result lists what was imported, what was already there, what was older than the retention limit, and the first skipped rows with their reason.
Everything an import added is listed under Imported data, with its date range and counts. Remove deletes that import’s visits and events after you confirm the numbers, and leaves everything the tracker recorded.
Nothing is stored when a file is not the one the source exports: the dashboard says which file, or which column, is the problem.
Import from the command line
analytik import --from umami --file events.csv --file sessions.csv --file event-data.csv --site SITE_ID --db "$DATABASE_URL"
analytik import --from simple-analytics --file simple-analytics.csv --site SITE_ID --db sqlite:analytik.db
analytik import --from matomo --file matomo-visits.json --site SITE_ID --db sqlite:analytik.db
analytik import --from goatcounter --file goatcounter-export.csv --site SITE_ID --db sqlite:analytik.db
analytik import --from ga4 --file ga4-events.jsonl --site SITE_ID --db sqlite:analytik.db
analytik import --from posthog --file posthog-events.json --site SITE_ID --db sqlite:analytik.db
--db takes the same values as analytik migrate: postgres://..., sqlite:<path> or a .db file. The command applies pending migrations first, reads and maps every file, and only then writes, so a wrong file stores nothing. It prints how many visits and events were imported, how many were already there, how many were older than the site’s retention limit, and the first skipped rows with their reason.
Cloudflare D1 cannot be reached from the command line. Use the import in Settings, which sends the file in small chunks.
Supported sources
| Source | What it gives | Status |
|---|---|---|
| Umami | Visits, pageviews and custom events with their details | Supported |
| Simple Analytics | Raw pageviews and events | Supported |
| Matomo | Visits with their page views, events and goals | Supported |
| GoatCounter | Raw hits and events with their session | Supported |
| Google Analytics 4 | Raw events, through the BigQuery export | Supported |
| PostHog | Events with their properties | Supported |
| Plausible | Daily totals only, no visits | Not possible |
Plausible’s dashboard export is a ZIP of aggregated report CSVs (visitors and pageviews per page, source and country, at most 300 rows each), and its Stats API is aggregated too. Raw events exist only in the Enterprise plan’s daily Parquet export to S3. Individual visits cannot be rebuilt from either. Google Analytics 4’s own reports and API are aggregated; its raw events exist only in the BigQuery export, which is why that is the file Analytik reads. Universal Analytics has no raw export at all.
Umami
Umami keeps its data in three tables. Export each one as CSV, with the column names as they are in the database:
psql "$DATABASE_URL" -c "\copy (SELECT * FROM website_event WHERE website_id = 'WEBSITE_ID') TO 'events.csv' CSV HEADER"
psql "$DATABASE_URL" -c "\copy (SELECT * FROM session WHERE website_id = 'WEBSITE_ID') TO 'sessions.csv' CSV HEADER"
psql "$DATABASE_URL" -c "\copy (SELECT d.* FROM event_data d WHERE d.website_id = 'WEBSITE_ID') TO 'event-data.csv' CSV HEADER"
Only the events file is required. The sessions file adds browser, operating system, device and place, and the event data file adds the properties of custom events. A JSON file with the same fields works too, with either snake_case or camelCase names.
| Umami | Analytik |
|---|---|
visit_id |
the visit |
session_id |
the visitor |
created_at |
event time; the first one starts the visit |
event_type 1 (pageview) |
a pageview on url_path |
event_type 2 and event_name |
a goal with that name and its event_data properties |
referrer_domain, referrer_path, referrer_query |
the referrer of the visit’s first event, then source and channel as for tracked visits |
utm_source, utm_medium, utm_campaign, utm_term, utm_content |
the campaign fields of the visit |
browser, os, device, country, region, city |
the same fields, written the way the tracker writes them |
hostname |
the visit’s hostname |
Umami has no separate visit length, so a visit is as long as the time between its first and last event, which is also how Umami itself counts it. A visit with one event has no known length. Exports from before Umami recorded visit_id are split into visits by session, with a new visit after 30 minutes without events.
Link and pixel events (event_type 3 and 4) and rows without a time, a session or a page are skipped, and the result lists them with their row number.
Simple Analytics
Export the raw data points from Simple Analytics, pages and events together (type=all), and name the fields you want, because leaving fields out returns an older set:
curl -H "Api-Key: $SA_API_KEY" \
"https://simpleanalytics.com/api/export/datapoints?version=6&format=csv&hostname=example.com&start=2025-01-01&end=2026-03-01&type=all&fields=added_iso,hostname,path,datapoint,is_robot,is_unique,document_referrer,referrer_hostname,referrer_path,utm_source,utm_medium,utm_campaign,utm_content,utm_term,duration_seconds,user_agent,device_type,country_code,browser_name,browser_version,os_name,os_version,screen_width,screen_height,lang_language,lang_region" \
-o simple-analytics.csv
Only added_iso and path are required. Every other field improves the result, and the browser, system, device and country fields matter most, because they decide which rows belong to one visit.
Simple Analytics keeps no visitor or visit id. Its session_id identifies a single page load, so Analytik cannot use it. A visit is rebuilt from rows that share browser, system, device, country, screen and language, with no gap longer than 30 minutes, and that combination is also the visitor. Two people on identical setups in the same country can end up as one visitor, the same trade-off as a cookieless tracker.
| Simple Analytics | Analytik |
|---|---|
added_iso |
event time; the first one starts the visit |
datapoint = pageview |
a pageview on path |
any other datapoint |
a goal with that name |
document_referrer, or referrer_hostname and referrer_path |
the referrer of the visit’s first row |
utm_* |
the campaign fields of the visit |
duration_seconds |
summed into the visit length, otherwise first to last row |
browser_name, os_name, device_type, country_code |
the same fields, written the way the tracker writes them |
Rows with is_robot set, and rows without a time or a path, are skipped and listed with their row number.
If you import a second export that overlaps the first, no row is stored twice. Exporting the same date range again is the safest way to continue an import that stopped.
Matomo
Matomo has no export of raw visits in its interface, but its Reporting API returns them as JSON. Ask for the visits of one site and one date range with Live.getLastVisitsDetails, in pages so a large site does not time out:
curl "https://matomo.example.com/index.php?module=API&method=Live.getLastVisitsDetails&idSite=1&period=range&date=2025-01-01,2026-03-01&format=json&filter_limit=-1&token_auth=$MATOMO_TOKEN" \
-o matomo-visits.json
Use a token with view access to the site. A large range can be split into several files and imported one after the other; no visit is stored twice. The file is a list of visits; each one has an actionDetails list with its page views, events and goals.
| Matomo | Analytik |
|---|---|
idVisit |
the visit |
visitorId |
the visitor |
visitDuration |
the visit length; 0 is left unknown |
actionDetails of type action |
a pageview on the path of its url |
actionDetails of type event |
a goal named eventAction, with category, name and value as properties |
actionDetails of type goal |
a goal named goalName |
referrerUrl, referrerName (not for direct visits) |
the referrer of the visit |
campaignSource, campaignMedium, campaignName, campaignKeyword, campaignContent |
the campaign fields of the visit |
browserName, operatingSystem, deviceType, countryCode, region, city |
the same fields, written the way the tracker writes them |
Downloads, outlinks and site searches are skipped and listed with the visit’s position in the file, as are visits without any page view, event or goal.
GoatCounter
In GoatCounter open Settings, then Export, and download the CSV. It holds every hit, with events marked in the Event column. You can pick the file in the dashboard or pass it to analytik import.
GoatCounter keeps a Session per person but no visit id, so a visit ends after 30 minutes without hits, and the session is also the visitor. It records no hostname, visit length or city.
| GoatCounter | Analytik |
|---|---|
Date |
event time; the first one starts the visit |
Path |
a pageview on that path |
Event true |
a goal named after the path |
Referrer and Referrer scheme |
the referrer; a campaign (c) becomes the campaign source |
Browser, System (versions dropped) |
browser and operating system |
Screen size |
device: under 768 px mobile, under 1024 px tablet |
Location |
country and region |
Hits from robots (Bot above 0) and rows without a date or a path are skipped and listed with their row number.
Google Analytics 4
GA4 gives raw events only through its BigQuery export, so the property must have been linked to BigQuery; the export holds data from the day it was linked, not before. Export the events_* tables as JSON (newline-delimited) from BigQuery, for example with bq extract --destination_format=NEWLINE_DELIMITED_JSON to Cloud Storage, and download the files. Pick several files together or one at a time.
A visit is a user_pseudo_id with a ga_session_id, and the user is the visitor. Fields the export nests are read from where GA4 puts them.
| GA4 | Analytik |
|---|---|
event_timestamp (microseconds) |
event time |
page_view |
a pageview on the path of page_location |
| any other event | a goal with that name |
page_referrer, source, medium, campaign (event params) |
the referrer and campaign of the visit; collected_traffic_source is the fallback |
engagement_time_msec (event param) |
summed into the visit length |
device.category, device.operating_system, device.web_info.browser |
device, system and browser |
geo.country, geo.region, geo.city |
the place |
session_start, first_visit, user_engagement and scroll are left out because a page view already says it. Events without a page_location are skipped and listed with their row number. The export’s traffic_source describes the user’s first visit, not the session, so it is not used.
PostHog
Export events with their properties as JSON: the events API (/api/projects/PROJECT_ID/events/, saved page by page), or a batch export to S3, BigQuery or another destination. Properties may be an object or a JSON string; both work, and so does the API’s { "results": [...] } wrapper.
A visit is a $session_id; events without one are split by distinct_id after 30 minutes of silence, and the distinct id is the visitor.
| PostHog | Analytik |
|---|---|
timestamp |
event time; the first one starts the visit |
$pageview |
a pageview on $pathname or $current_url |
| an event you named yourself | a goal with that name |
$referrer, utm_* |
the referrer and campaign of the visit |
$browser, $os, $device_type, $geoip_*, $host |
the same fields, written the way the tracker writes them |
PostHog’s own $ events (autocapture, page leave, identify, feature flags) are left out. Events with no page are skipped and listed.