Factcat

An open-source alternative to Amplitude and Mixpanel that runs in your own data warehouse. Factcat generates SQL and runs it in your BigQuery or Snowflake — no SDK, no ingestion, nothing hosted.

pip install factcat

BigQuery

Map one wide events table in your project. Factcat generates SQL and runs it there. It does not ingest rows.

Credentials

gcloud auth application-default login unless you set a key file under Advanced. Billing project runs the job; the field is pre-filled from ADC, then GOOGLE_CLOUD_PROJECT, then gcloud config get-value project. Project that holds the table is only if the dataset is in another GCP project. Dataset, table, entity id, and timestamp are not in that cache — pick them here. Location is taken from the dataset.

Need BigQuery Job User on the billing project and Data Viewer on the dataset.

Dataset and table are lists after the billing project is set. They stay visible and greyed until then, same as Snowflake's catalog chain. Each list loads as soon as the previous field is set the first time (tables when the dataset is set, columns when the table is set), then is cached. Returning to Setup does not re-query. Refresh on a field reloads that list.

Table shape

One table. One row per event. Properties are real columns. Event types that do not use a column leave it null (sparse). Not a JSON properties bag, and not one table per event type.

event_name     STRING      -- 'purchase', 'page_view', …
occurred_at    TIMESTAMP   -- UTC instant (see Time below)
account_id     INT64       -- null when the event has no account
country        STRING      -- null when unknown
revenue        NUMERIC     -- null on non-purchase events

purchase fills revenue; page_view leaves it null. Extra grains (account, subscription, booking) are extra id columns, null when that event is not about that grain.

Flatten JSON / STRUCT in dbt before mapping. Other layouts (JSON bags, one table per event) are a later closed list, not “any schema”.

Entity id

Whichever grain this report is about — not hard-coded to user_id.

The User / Customer label is display only. It does not pick the column.

Timestamp

Must be an instant, not a calendar DATE and not a wall-clock TIME.

Type Meaning Set “Timestamp stored as”
TIMESTAMP UTC instant (BigQuery) UTC instant
DATETIME Civil date-time, no zone Reporting timezone
INT64 / INTEGER Unix epoch Seconds, milliseconds, or microseconds since 1970-01-01 UTC

STRING timestamps are not accepted. DATE is a day, not an event time. FLOAT epochs are not accepted.

Reporting timezone is whose midnight is a “day”, and whose Monday is a “week”. CURRENT_DATE and day buckets follow that zone. Week start (Monday/Sunday) is applied after the instant is converted to that calendar.

If the column is DATETIME, it is already civil time in the reporting zone. Do not store local time in a TIMESTAMP and label it UTC.

Event name

Optional STRING column. Mapping persists as you pick fields; it does not query. Look back for event names on this page (90 days by default; 0 is all time) is the window for the Events picker. That job does not use the scan cap. Events Refresh event names re-runs it; the chevron next to it raises the window. The filter isolates the timestamp column so a table partitioned on it can prune. Optional: allow Factcat to create and maintain tables in a project and dataset for better performance. First fetch creates fc_event_names if it is missing (materialized view, or a table if the source cannot back a view). Later Refresh reads the view, or rebuilds the table snapshot. The object stores a fingerprint of the mapped table and event-name column. Lookback does not apply (and is hidden here) while that dest is set. Catalog jobs do not use the scan cap. Events then filters event_name = '…'.