track, page and screen call counts as an Event, and each person counts as one User whether they were signed in or not. It takes about an hour.
It combines three pages and adds the RudderStack-specific parts: Connect ClickHouse, Connect RudderStack and Product analytics in your warehouse.
Before you start
- RudderStack’s ClickHouse destination is enabled and has completed at least one sync
- The ClickHouse database set on that destination (the Database field in RudderStack). This recipe calls it
<rudderstack_db>. - An admin ClickHouse user that can create users, roles, databases and views
- The password for the
KUBITuser, from the Kubit team - The names of 3 events in your signup or activation flow. This recipe uses
Signup Started,Account CreatedandProject Created.
Prompt for your coding agent
Paste this into Claude Code, Cursor or Codex. It drafts the SQL and leaves running it to you:Steps
Check the RudderStack tables
Run this as your admin user to confirm the destination has loaded data:You should see
tracks, identifies, and pages or screens, plus one table per track event name.Give Kubit read access
Follow Connect ClickHouse. It creates the RudderStack creates a new table the first time it sees a new event name. A database grant covers those tables too.
KUBIT user, the KUBIT_R and KUBIT_W roles and the KUBIT_DB helper database, allows Kubit’s IP ranges and adds the connection.In its Grant permissions step, grant read access on the whole RudderStack database instead of table by table:Create one view for all events
RudderStack writes Remove the
track, page and screen calls to separate tables. This view puts them in one table with the same columns:pages or screens part if that table doesn’t exist. A web-only source has no screens table.Create the identity map
This view maps each anonymous ID to the most recent user ID it was identified as:Use
identifies, not users. The ClickHouse destination skips the users table by default (the Skip User Table setting), and users has no anonymous_id column.Model the events in Kubit
In the Modeler:
- Add
KUBIT_DB.rudder_eventsas a fact table, withtimestampas the event time. RudderStack correctstimestampfor client clock skew, so it’s closer to when the user acted thanreceived_at. - Add
KUBIT_DB.rudder_identityas a dimension table, joined onrudder_events.anonymous_id = rudder_identity.anonymous_idas a left join. Events from users who never signed in stay in the model. - Create the subjects below.
The User column picks the signed-in ID first, then the ID the device later signed in as, then the anonymous ID. Events from before and after signup then count as one person.
Add event properties
For each event you analyze, join its table on
id and enable the property columns you need as fields, for example project_created.template.Enable context_* columns you filter or group by often, such as context_os_name, context_app_version and context_page_path. They exist on every RudderStack table.Verify
Check the model with three reports. Each has a SQL query to run in ClickHouse for the same numbers. Compare a full day that ended at least 1 hour ago, so RudderStack’s sync has finished. Totals should match within 1%.Daily active users
Build a Query report counting unique Users, all events, daily, for the last 14 days. Or ask your assistant:“Show daily active users for the last 14 days”
Signup funnel
Build a Funnel with 3 steps,Signup Started, Account Created and Project Created, with a 1-day conversion window over the last 30 days. Or ask:
“Build a funnel from Signup Started to Account Created to Project Created in the last 30 days, with a 1 day conversion window”Step 1 should be close to:
user_id at Account Created, so the steps only connect through the identity map.
Weekly retention
Build a Retention report: starting eventAccount Created, returning on any event, weekly, 8 weeks. Or ask:
“Weekly retention for 8 weeks for users who did Account Created, returning on any event”
If it fails
Errors while creating the views
Counts are higher than expected
Duplicate rows are the most common reason. RudderStack creates ClickHouse tables with theReplacingMergeTree engine, ordered by (received_at, id). When RudderStack retries a load, the same event can be stored twice until ClickHouse merges the table parts in the background, which can take minutes or hours.
row_count is larger than unique_events, the table has unmerged duplicates. Event counts in Kubit can include them. Unique user counts don’t change.
Counts are lower than expected
RudderStack lowercases event names and converts them to snake case for table names. Search for
account_created, not Account Created. The original name is in event_text.
Daily numbers are shifted
ClickHouse storesDateTime values as UTC instants and shows them in the server’s time zone unless the column or query sets one. Check it with SELECT timezone(), and compare one day in UTC with toDate(timestamp, 'UTC') = yesterday().
Events land in the future
RudderStack calculatestimestamp as received_at - (sent_at - original_timestamp) to correct for client clock skew. A device with a badly wrong clock can still produce future dates:
received_at as the event time in the model.
Booleans and nested properties
RudderStack stores booleans asUInt8 in ClickHouse, so filter on = 1, not = true. Nested properties are flattened with the parent key as prefix: { plan: { tier: "pro" } } becomes plan_tier. If JSON Columns is set on the destination, the listed paths go into one native JSON column instead, which needs ClickHouse 25.3 or later.
Next steps
- Reports overview: every report type and when to use it.
- Dashboards: put the three check reports on one dashboard.