atrixANALYTICS

Migrating from PostHog, Segment or Statsig

Drop-in ingest endpoints, a posthog-js shim, and importers for history, flags and experiments.

There are three ways in, and you can combine them:

What it doesCode changes
Drop-in endpointsIngest accepts PostHog and Segment payloads. Point your existing SDKs at Atrix.A host and a key
@atrix.dev/posthog-compatThe posthog-js API on top of the native Atrix web SDK.One import line
atrix importBrings historical events and persons, plus flags and experiments.None

The safest cut-over is a dual-write period: keep your current tool, add Atrix next to it, and compare daily event counts for a week before switching the old one off.

Drop-in endpoints

Everything goes to your region's ingest host, https://eu.i.analytics.atrix.dev for EU projects. The key is always your Atrix public key (atx_pk_…), in the place the vendor SDK puts its own key.

Vendor pathAccepts
/capture, /i/v0/e, /e, /batchPostHog events: a single event, { batch: [...] }, or a bare array (posthog-js). gzip, zstd, base64 and data= form bodies.
/decide (?v=1 to 4), /flags?v=2PostHog flag evaluation, answered in PostHog's response shape
/v1/track, /v1/identify, /v1/page, /v1/screen, /v1/group, /v1/alias, /v1/batch, /v1/import (and /v1/t, /i, /p, /s, /g, /a, /b)Segment's HTTP API. The write key comes from HTTP Basic auth or writeKey in the body.

/e and /batch are also the native Atrix endpoints: a body in which every event carries a lib object is read as native, anything else as PostHog.

PostHog SDKs

Set the host to your region's ingest host and the key to your Atrix public key. For example, with posthog-js:

posthog.init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX", { api_host: "https://eu.i.analytics.atrix.dev" });

and with posthog-node, posthog-python or any other PostHog SDK, the same two values (host / api_host and the project key).

What is mapped:

  • distinct_id, $set and $set_once (top level or inside properties), $groups → groups (up to 5), $session_id, $window_id, and $device_id / $anon_distinct_id → the anonymous id used for merges.
  • $identify, $create_alias, $groupidentify, $set and $pageview keep their meaning. Other unknown $-prefixed events are kept under a prefix, for example posthog:$survey_shown.
  • Event uuids are kept when they are already UUIDv7; otherwise a deterministic UUIDv7 is derived, so retries still deduplicate.
  • $ip is stripped, like every other IP address.
  • Dropped silently: PostHog session-recording snapshots ($snapshot, $snapshot_items), heatmaps and performance events. Use Atrix session replay instead.
  • /decide answers with your Atrix flags and payloads; session recording, surveys, heatmaps and site apps are reported as off.
  • Merges follow Atrix identity rules: an alias that would join two identified users is refused.

Events arriving this way are tagged lib.name: "posthog-compat"; the original library name stays in $lib.

Segment

Set your Segment source's HTTP API host (or a Segment destination / analytics-node host option) to your region's ingest host, and use your Atrix public key as the write key:

curl -X POST https://eu.i.analytics.atrix.dev/v1/track \
  -u 'atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX:' \
  -H 'Content-Type: application/json' \
  -d '{ "userId": "user_42", "event": "Order Completed", "properties": { "revenue": 19.99 } }'
Segment callBecomes
trackthe same event name (unknown $ names get a segment: prefix)
identify$identify with traits as person properties ($set when there is no userId)
page$pageview with URL, path, referrer, title, name and category
screen$screen with $screen_name
group$groupidentify for group type company
alias$create_alias, subject to Atrix identity rules

The distinct id is userId, falling back to anonymousId. context fields map to OS, app version, locale and library; context.ip is dropped. Events are tagged lib.name: "segment-compat".

@atrix.dev/posthog-compat

A drop-in replacement for the posthog-js module. Events go to Atrix through the native web SDK (not the compat endpoints), so identity, consent, flags and replay are Atrix's own.

// before: import posthog from "posthog-js";
import posthog from "@atrix.dev/posthog-compat";

posthog.init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX", {
  capture_pageview: true,
  persistence: "localStorage",
  loaded: (ph) => ph.register({ app: "web" }),
});

posthog.identify("user_42", { plan: "pro" }, { first_seen: "2026-09-25" });
posthog.capture("signed_up", { source: "landing" });
posthog.group("company", "acme", { name: "Acme" });

const unsubscribe = posthog.onFeatureFlags((flags, variants) => {
  if (posthog.isFeatureEnabled("new-checkout")) console.log(flags, variants);
});
const variant: boolean | string | undefined = posthog.getFeatureFlag("pricing");

await posthog.flush();
unsubscribe();

Supported: init, capture (with $set, $set_once, timestamp, uuid), identify, setPersonProperties and people.set / people.set_once, reset, register, register_once, register_for_session, unregister, group, get_distinct_id, get_session_id, isFeatureEnabled, getFeatureFlag, getFeatureFlagPayload, onFeatureFlags, reloadFeatureFlags, the opt-in/opt-out family, startSessionRecording / stopSessionRecording, and flush. The native client is available as posthog.atrix.

init options read: api_host, autocapture, capture_pageview, persistence, cookie_domain, disable_persistence, disable_session_recording, opt_out_capturing_by_default, bootstrap (feature flags and payloads), property_denylist, debug and loaded.

Safe no-ops: alias (identify instead), surveys, the toolbar, heatmaps, site apps and debug(). Nothing throws.

atrix import

The atrix CLI imports history and configuration. Events keep their original ids and timestamps, so running an import twice does not double count. Everything is batched, gzipped and retried.

Common options:

OptionFor
--key / ATRIX_PUBLIC_KEYeventsThe target project's public key; events land in its environment.
--capture-url / ATRIX_CAPTURE_URLeventsDefaults to the key's region.
--dry-run, --outallValidate and write NDJSON (or print the plan) instead of sending.
--project, --api-url / ATRIX_API_URLflags, experimentsThe project UUID and control plane, with ATRIX_TOKEN holding a token with flags:write and experiments:write.
--enable-envflagsTurn flags that were live at the source on in this environment. Otherwise they are created off.

Pass --api-url https://app.analytics.atrix.dev (or set ATRIX_API_URL) for flags and experiments: the CLI's built-in default is not the served host yet.

From PostHog

From export files:

export ATRIX_PUBLIC_KEY=atx_pk_eu_production_…
atrix import posthog --events events-2026-08.jsonl.gz --events events-2026-09.jsonl.gz \
  --persons persons.csv --group-types 0=company,1=workspace

Or straight from the PostHog API, including flags and experiments:

export POSTHOG_PERSONAL_API_KEY=phx_…
export ATRIX_TOKEN=atx_pat_…
atrix import posthog --posthog-host https://eu.posthog.com --posthog-project 12345 \
  --fetch events,persons,flags,experiments \
  --project <atrix-project-uuid> --api-url https://app.analytics.atrix.dev --dry-run

Drop --dry-run once the output looks right. Persons become person properties on their first distinct id, with their other ids linked. Flags are created off and experiments as drafts; review them before launch. Bucketing differs from PostHog's, so users are re-bucketed once when a flag goes live.

Input files can be JSON Lines, a JSON array, a PostHog API page ({ results: [...] }), a single object, or CSV with a header row, optionally gzipped.

From Statsig

export ATRIX_TOKEN=atx_pat_…
atrix import statsig --gates gates.json --dynamic-configs dynamic_configs.json --experiments experiments.json \
  --project <atrix-project-uuid> --api-url https://app.analytics.atrix.dev --dry-run

The files are Statsig Console API exports. Gates become boolean flags (rules become release conditions, pass percentages become rollouts), dynamic configs become multivariate flags whose variants carry the config values, and experiments become a multivariate flag plus a draft experiment with the first group as control. Rule conditions with no Atrix equivalent are skipped with a warning.

From Segment or June

atrix import segment --file archive-2026-08.jsonl.gz --file archive-2026-09.jsonl.gz --dry-run --out preview.ndjson
atrix import segment --file archive-2026-08.jsonl.gz --file archive-2026-09.jsonl.gz

Archived Segment spec messages (and June exports) are mapped exactly as the live /v1/* endpoint maps them.

API mapping for hand-written wrappers

If you would rather move to the native SDK, the calls map one to one:

PostHogAtrix web SDK
posthog.capture(event, props)atrix.track(event, props)
posthog.identify(id, set, setOnce)atrix.identify(id, set, setOnce)
posthog.reset()atrix.reset()
posthog.group(type, key, props)atrix.group(type, key, props)
posthog.register(props)atrix.register(props)
posthog.isFeatureEnabled(key)atrix.isEnabled(key)
posthog.getFeatureFlag(key)atrix.getVariant(key)
posthog.getFeatureFlagPayload(key)atrix.getPayload(key)
posthog.onFeatureFlags(cb)atrix.onFlags(cb)
posthog.opt_out_capturing()atrix.optOut()
import * as atrix from "@atrix.dev/analytics-web";

type Props = Record<string, string | number | boolean | null | undefined>;

atrix.init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX", { superProperties: { environment: "production" } });

export const track = (event: string, props?: Props) => atrix.track(event, props);
export const identifyPlayer = (p: { id: string; city?: string | null; level?: number | null }) =>
  atrix.identify(p.id, { city: p.city, level: p.level });
export const resetAnalytics = () => atrix.reset();

On this page