Skip to main content
This recipe takes RudderStack events that RudderStack’s ClickHouse destination loads into your cluster, and makes them ready for funnels, retention and paths in Kubit. At the end, every 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 KUBIT user, from the Kubit team
  • The names of 3 events in your signup or activation flow. This recipe uses Signup Started, Account Created and Project 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

1

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.
2

Give Kubit read access

Follow Connect ClickHouse. It creates the 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:
RudderStack creates a new table the first time it sees a new event name. A database grant covers those tables too.
3

Create one view for all events

RudderStack writes track, page and screen calls to separate tables. This view puts them in one table with the same columns:
Remove the pages or screens part if that table doesn’t exist. A web-only source has no screens table.
4

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.
5

Model the events in Kubit

In the Modeler:
  1. Add KUBIT_DB.rudder_events as a fact table, with timestamp as the event time. RudderStack corrects timestamp for client clock skew, so it’s closer to when the user acted than received_at.
  2. Add KUBIT_DB.rudder_identity as a dimension table, joined on rudder_events.anonymous_id = rudder_identity.anonymous_id as a left join. Events from users who never signed in stay in the model.
  3. 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.
6

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:
If step 2 is close to 0 while step 1 is large, the User subject is splitting people at signup. Users start anonymous and get a user_id at Account Created, so the steps only connect through the identity map.

Weekly retention

Build a Retention report: starting event Account 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 the ReplacingMergeTree 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.
If 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 stores DateTime 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 calculates timestamp 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:
If the count is high, use received_at as the event time in the model.

Booleans and nested properties

RudderStack stores booleans as UInt8 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