> ## Documentation Index
> Fetch the complete documentation index at: https://docs2.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Managed Warehouse

> Get started with GrowthBook's fully managed data warehouse. One-click setup, built-in event tracking, and auto-created metrics.

GrowthBook Cloud offers a fully managed data warehouse and event tracking pipeline. Provision it in one click, send events from your app, and GrowthBook handles the rest. No infrastructure setup required.

<Info>
  **Not sure if Managed Warehouse is right for you?**

  See [Choose Your Data Path](/app/choose-data-path) for a side-by-side comparison with connecting your own warehouse.
</Info>

## What you get

When you provision the Managed Warehouse, GrowthBook automatically sets up your data source with everything pre-configured:

### Tables

We create 3 tables in a ClickHouse database managed by GrowthBook:

| Table                 | What it stores                                          | Indexed by                   |
| --------------------- | ------------------------------------------------------- | ---------------------------- |
| **events**            | All custom events (page views, purchases, clicks, etc.) | `event_name`, `timestamp`    |
| **experiment\_views** | Experiment exposure events                              | `experiment_id`, `timestamp` |
| **feature\_usage**    | Feature flag evaluation events                          | `feature`, `timestamp`       |

### Fact Table

An **Events** fact table is pre-built on top of the `events` table. This is the base you use to define metrics. It includes 16+ standard columns (timestamp, event name, device ID, geo and browser data, etc.) plus an `attributes` JSON column with everything your events send, and is ready to use immediately.

### Starter metrics

Three metrics are auto-created to get you started:

| Metric                  | Type  | What it measures               |
| ----------------------- | ----- | ------------------------------ |
| **Page Views per User** | Mean  | Average number of page views   |
| **Sessions per User**   | Mean  | Average number of sessions     |
| **Pages per Session**   | Ratio | Page views divided by sessions |

You can edit these or create your own. Read more about [metrics and fact tables](/app/metrics).

### Default identifiers and dimensions

Identifiers are the columns used to split traffic in experiments, and dimensions let you slice and dice results. These are set up out of the box:

* **Identifiers:** `user_id`, `device_id`
* **Dimensions:** `geo_country`, `ua_browser`, `ua_os`, `ua_device_type`, `utm_source`, `utm_medium`, `utm_campaign`

You can add more identifiers through your organization's attribute settings. See [Identifiers and custom attributes](#identifiers-and-custom-attributes) for details.

## Getting started

### 1. Provision the Managed Warehouse

Go to **Metrics and Data** → **Data Sources** and click **Create** on the Managed Warehouse option. GrowthBook provisions your ClickHouse database, tables, fact table, and starter metrics in seconds.

### 2. Install the SDK with tracking

Add the GrowthBook SDK to your application with the tracking plugin enabled. Here are the two most common setups:

<Info>
  **Using eu-west-1?**

  Events default to the `us-east-1` ingestor. If you selected **eu-west-1** as your Data Region, add the `ingestorHost` (or `data-event-ingestor-host` for the script tag) override — see [Sending events to the right region](#sending-events-to-the-right-region) below — otherwise your events won't reach your warehouse.
</Info>

**HTML Script Tag**: add one line to your page:

```html theme={null}
<script async
  data-client-key="YOUR_CLIENT_KEY"
  data-tracking="growthbook"
  src="https://cdn.jsdelivr.net/npm/@growthbook/growthbook/dist/bundles/auto.min.js"
></script>
```

**JavaScript / React**: install the SDK and enable the tracking plugin:

```js theme={null}
import { GrowthBook } from "@growthbook/growthbook";
import {
  autoAttributesPlugin,
  growthbookTrackingPlugin
} from "@growthbook/growthbook/plugins";

const gb = new GrowthBook({
    clientKey: "YOUR_CLIENT_KEY",
    plugins: [
        autoAttributesPlugin(),
        growthbookTrackingPlugin()
    ]
});
```

The tracking plugin automatically sends feature usage and experiment view events. You can also log custom events:

```js theme={null}
gb.logEvent("Purchase", { amount: 49.99 });
```

See [all supported SDKs](#with-sdks) for more options including Node.js, Python, Java, PHP, Go, and Flutter.

### 3. Verify events are flowing

Open the **SQL Explorer** in GrowthBook and run a quick query to confirm events are arriving:

```sql theme={null}
SELECT event_name, COUNT(*) AS count
FROM events
WHERE timestamp >= now() - INTERVAL 1 HOUR
GROUP BY event_name
ORDER BY count DESC
```

You should see your events appear within seconds of sending them.

### 4. Create your first metric

Your auto-created metrics (Page Views per User, Sessions per User, Pages per Session) are ready to use immediately. To create a custom metric:

1. Go to **Metrics and Data** → **Fact Tables**.
2. Open the **Events** fact table.
3. Click **Add Metric** and configure it (for example, a Proportion metric filtered to `event_name = 'Purchase'` to track purchase conversion rate).

Read more about [metric types and configuration](/app/metrics).

### 5. Run your first experiment

With events flowing and metrics defined, you're ready to experiment:

1. Create a feature flag in **Features**.
2. Add an **Experiment** rule to it.
3. Start the experiment and watch results flow in on the **Results** tab.

For a detailed walkthrough, see our [guide to running Feature Flag Experiments](/feature-flag-experiments).

***

## Benefits

* **Fully managed**: No infrastructure management, scaling, or maintenance.
* **Seamless integration**: One-click setup + built-in tracking in our SDKs.
* **Instant data**: Events are enriched and available within seconds.
* **Raw SQL access**: Use the SQL Explorer to run custom queries against your data.

## How it works

We use ClickHouse, a database optimized for real-time analytics, to store your event data. The process is:

1. You send analytics events to our scalable ingestion API.
2. We enrich and store them in ClickHouse within seconds.
3. You can query the data with SQL, define metrics, and analyze experiment results with our stats engine.

## Sending events

There are 2 ways to send events to GrowthBook Cloud's Managed Warehouse:

1. With our [SDKs](#with-sdks) (limited language support).
2. With our [Ingestion API](#ingestion-api).

### With SDKs

The following SDKs have a built-in plugin to automatically send events.

* [HTML Script Tag](#html-script-tag)
* [Client-Side JavaScript / React](#client-side-javascript--react)
* [Node.js](#nodejs)
* Python
* Java
* PHP
* Golang
* Flutter
* Swift (Coming Soon)
* Kotlin (Coming Soon)

For everything else, use the [Ingestion API](#ingestion-api).

When the plugin is added, these SDKs will automatically send feature usage and experiment view events to GrowthBook. They also expose a helper method to log additional custom events with optional properties.

All of the attributes in the SDK are sent along with events as context. Make sure you do not include any sensitive information in your attributes (or if you do, anonymize it first).

#### HTML Script Tag

Add `data-tracking="growthbook"` to your script tag to enable.

```html theme={null}
<script async
  data-client-key="YOUR_CLIENT_KEY"
  data-tracking="growthbook"
  src="https://cdn.jsdelivr.net/npm/@growthbook/growthbook/dist/bundles/auto.min.js"
></script>
```

To track additional events, use the `window.gbEvents` global variable. You can push events to this array, and they will be tracked.

```html theme={null}
<script>
  // Ensure the global variable exists
  window.gbEvents = window.gbEvents || [];

  // Simple (no properties)
  window.gbEvents.push("Page View");

  function handleSignUpClick() {
    // With custom properties
    window.gbEvents.push({
      eventName: "Button Click",
      properties: {
        button: "Sign Up"
      }
    });
  }
</script>
<button onclick="handleSignUpClick()">Sign Up</button>
```

#### Client-Side JavaScript / React

Use the `growthbookTrackingPlugin` to enable tracking. We recommend also using the `autoAttributesPlugin` to include many common attributes in your events (browser, session\_id, etc.).

```js theme={null}
import { GrowthBook } from "@growthbook/growthbook";
import {
  autoAttributesPlugin,
  growthbookTrackingPlugin
} from "@growthbook/growthbook/plugins";

const gb = new GrowthBook({
    clientKey: "YOUR_CLIENT_KEY",
    plugins: [
        autoAttributesPlugin(),
        growthbookTrackingPlugin()
    ]
});
```

Use the `logEvent` method to track additional custom events:

```js theme={null}
// Simple (no properties)
gb.logEvent("Page View");

// With custom properties
gb.logEvent("Button Click", {
  button: "Sign Up",
});
```

#### Node.js

Use the `growthbookTrackingPlugin` to enable tracking:

```js theme={null}
import { GrowthBookClient } from "@growthbook/growthbook";
import { growthbookTrackingPlugin } from "@growthbook/growthbook/plugins";

const gb = new GrowthBookClient({
  clientKey: process.env.GROWTHBOOK_CLIENT_KEY,
  plugins: [growthbookTrackingPlugin()],
});
```

Use the `logEvent` method to track additional custom events:

```js theme={null}
gb.logEvent("Sign Up", {
  accountPlan: "pro"
}, userContext);
```

User-scoped instances also have a `logEvent` method that doesn't require the user context:

```js theme={null}
req.growthbook.logEvent("Sign Up", {
  accountPlan: "pro"
});
```

#### Sending events to the right region

The tracking plugin sends events to `https://us-east-1.gb-ingest.com` by default. If you selected **eu-west-1** as your Data Region when creating your Managed Warehouse, override the ingestor host so events reach the right ClickHouse cluster instead of being dropped:

* **HTML Script Tag**: add `data-event-ingestor-host="https://eu-west-1.gb-ingest.com"` to the script tag.
* **JavaScript / React / Node.js**: pass `ingestorHost` to `growthbookTrackingPlugin()`:

```js theme={null}
growthbookTrackingPlugin({
  ingestorHost: "https://eu-west-1.gb-ingest.com",
})
```

You can find your datasource's configured region on its settings page.

### Ingestion API

You can also send events directly to our ingestion API. The host depends on the **Data Region** you chose when creating your Managed Warehouse datasource:

* `us-east-1` → `https://us-east-1.gb-ingest.com`
* `eu-west-1` → `https://eu-west-1.gb-ingest.com`

Sending events to the wrong region's host means they won't reach the ClickHouse cluster your data warehouse actually lives in — always match the host to the region shown on your datasource's settings page.

Pass an array of event objects, each with the following properties:

* **event\_name**: The name of the event (e.g., "Purchase", "Button Click")
* **properties**: Optional key-value pairs with properties of the event itself
* **attributes**: Optional key-value pairs with attributes of the user or context at the time of the event
* **timestamp**: Optional ISO timestamp of when the event occurred. Only used when batching events with a `sentAt` field (see [Event timestamps](#event-timestamps) below).

```bash theme={null}
curl -X POST "https://YOUR_WAREHOUSE_REGION.gb-ingest.com/track?client_key=YOUR_CLIENT_KEY" \
-H "Content-Type: application/json" \
-d '[{
  "event_name": "Purchase",
  "properties": {
    "amount": 100
  },
  "attributes": {
    "user_id": "12345"
  }
}]'
```

Make sure you do not include any sensitive information in your events (or if you do, anonymize it first).

#### Identifiers

To tie events to users for experiment analysis, include identifier keys in `attributes`:

* **`user_id`**: a logged-in user ID. Promoted to the top-level `user_id` column at ingest.
* **`device_id`**: an anonymous device or browser ID. Promoted to the top-level `device_id` column at ingest.
* **`anonymous_id`** or **`id`**: also resolve to the `device_id` identifier, but at analysis time instead of ingest — they stay inside the `attributes` JSON.

Prefer the explicit `user_id` and `device_id` keys, and use each key for one kind of ID consistently. For example, if you send a logged-in user ID as `id` today and later start sending a real device ID as `device_id`, the `device_id` identifier will mix the two ID spaces and experiment analysis will silently drop units. See [Identifiers](#identifiers-and-custom-attributes) for how these keys map to identifiers.

#### Event timestamps

By default, events are timestamped with the time they are received by the ingestion API. If you batch events before sending, you can preserve each event's original timing by adding a per-event `timestamp` along with a top-level `sentAt` field:

```bash theme={null}
curl -X POST "https://YOUR_WAREHOUSE_REGION.gb-ingest.com/track?client_key=YOUR_CLIENT_KEY" \
-H "Content-Type: application/json" \
-d '{
  "sentAt": "2025-06-01T12:00:05.000Z",
  "events": [{
    "event_name": "Purchase",
    "timestamp": "2025-06-01T12:00:00.000Z",
    "properties": {
      "amount": 100
    },
    "attributes": {
      "user_id": "12345"
    }
  }]
}'
```

* **sentAt**: ISO timestamp of when the request was sent, from the same clock used for the event timestamps
* **timestamp**: ISO timestamp of when the event occurred. Must be earlier than `sentAt`, otherwise it is ignored.

We don't trust client timestamps directly. Instead, we subtract the difference between `sentAt` and each event's `timestamp` from our own server time when the request is received. This preserves the relative timing of your events while correcting for client clock skew.

#### Experiment view events

In order to use GrowthBook's experiment analysis features, you must send an event every time a user views an experiment. It must match the following format:

* **event\_name**: Must be `"Experiment Viewed"`
* **properties**: Must include the following key/value pairs:
  * `experimentId`: The ID of the experiment being viewed
  * `variationId`: The ID of the variation that was shown to the user

In addition, you must include `attributes` with the user attributes that were used to evaluate the experiment, plus any attributes you want to use as dimensions for slicing and dicing. At minimum this must include the attribute the experiment hashes on (its **Assign Variation by Attribute**), since that's the [identifier](#identifiers) used to join exposures to your metrics.

#### Feature usage events

To take advantage of GrowthBook's feature usage analytics, you must send an event every time a feature is evaluated with a specific format.

* **event\_name**: Must be `"Feature Evaluated"`
* **properties**: Must include the following key/value pairs:
  * `feature`: The name of the feature being evaluated
  * `value`: The feature's value that was returned from the evaluation
  * `source`: (optional) The source of the feature value (e.g., "defaultValue", "experiment", "force")
  * `ruleId`: (optional) The ID of the specific rule that was used to evaluate the feature (or `$default` if the default value was used)
  * `variationId`: (optional) If the value came from an experiment, the ID of the variation that was returned

#### Attributes

Attributes are key-value pairs that provide context about the user or environment at the time of the event. They can be used to slice and dice your data in analysis.

It's recommended to include the same attributes you use in your GrowthBook SDK.

Some attributes are automatically enriched by the ingestion API:

* `ip`: a geoip lookup is done and the following attributes are added. If an ip attribute is not provided, we use the IP address of the request.
  * `geo_country`
  * `geo_city`
  * `geo_lat`
  * `geo_lon`
* `ua`: the user agent is parsed and the following attributes are added. If a user agent attribute is not provided, we use the user agent of the request.
  * `ua_browser` (e.g. Safari)
  * `ua_os` (e.g. macOS)
  * `ua_device_type` (e.g. mobile)
* `url`: the URL is parsed and the following attributes are added:
  * `url_path` (e.g. /products/123)
  * `url_host` (e.g. [www.example.com](http://www.example.com))
  * `url_query` (e.g. ?utm\_source=google)
  * `url_fragment` (e.g. #section1)

It's also recommended to include attributes about which SDK is being used. This helps with debugging.

* `sdk_language` (e.g. python)
* `sdk_version` (e.g. 1.2.3)

#### Limits

When calling the ingestion API directly, be aware of the following default limits:

* Maximum of 100 events per request.
* Maximum of 1 request per second.

If you need to send more than this, reach out and we can increase your limits.

## Identifiers and custom attributes

All attributes sent with your events are stored in a native ClickHouse JSON column called `attributes` on the `events` table (event properties live in a `properties` JSON column). Every attribute is automatically queryable — no setup required.

Identifiers — the attributes used to split traffic in experiments — come in two flavors:

### Built-in identifiers

Every table has `user_id` and `device_id` columns. The columns always exist, but they only carry values when your events include one of the following attribute keys:

| Identifier  | Populated from attribute keys (first non-empty wins) | Intended for                      |
| ----------- | ---------------------------------------------------- | --------------------------------- |
| `user_id`   | `user_id`                                            | Logged-in user IDs                |
| `device_id` | `device_id`, `anonymous_id`, `id`                    | Anonymous, device, or session IDs |

The SDK tracking plugin applies this mapping automatically from your SDK attributes, and the [Ingestion API](#identifiers) does the same for the `user_id` and `device_id` keys. Events that carried their ID as `anonymous_id` or `id` are resolved into `device_id` when queries run, so experiment analysis works with any of these keys — including for past events.

When you assign an experiment by hash attribute, GrowthBook picks the matching identifier automatically: experiments assigned on the `id` or `anonymous_id` attribute use the `device_id` identifier for analysis.

### Custom identifiers

Any other scalar attribute marked as an **Identifier** under **SDK Connections** → **Attributes** (for example `account_id`) is automatically exposed as a top-level column on your fact tables and available for experiment assignment.

<Note>
  Identifier resolution reads from the `attributes` JSON column, so marking an attribute as an identifier applies retroactively to all past events that included it.
</Note>

## SQL Explorer

You can use the SQL Explorer to run ad-hoc queries against your events. This is useful for exploring your data, debugging issues, or creating custom reports.

<Note>
  The SQL Explorer only allows read-only SELECT queries. Write operations (INSERT, UPDATE, etc.) are prevented from being executed by the platform.
</Note>

You will see 3 tables in the SQL Explorer:

* **feature\_usage**: contains all feature usage events
* **experiment\_views**: contains all experiment view events
* **events**: contains all other events

### SQL best practices

#### Use indexes

For best performance, take advantage of the indexed columns in each table:

* **events** table:
  * `timestamp`: the time the event occurred
  * `event_name`: the name of the event (e.g., "Purchase", "Button Click")
* **feature\_usage** table:
  * `timestamp`: the time the event occurred
  * `feature`: the name of the feature being evaluated
* **experiment\_views** table:
  * `timestamp`: the time the event occurred
  * `experimentId`: the ID of the experiment being viewed

#### Querying attributes and properties

The `attributes` and `properties` columns are native ClickHouse JSON columns, so you can access fields inside them with dot syntax. Cast the value to get a concrete type:

```sql theme={null}
SELECT
  attributes.account_plan::Nullable(String) AS account_plan,
  toFloat64OrNull(properties.amount::Nullable(String)) AS amount
FROM events
WHERE timestamp >= '2025-06-01 00:00:00'
  AND event_name = 'Purchase'
```

Attributes marked as [Identifiers](#identifiers-and-custom-attributes) are already exposed as top-level columns, so you can reference them directly without the JSON path.

#### Identifier columns in custom SQL

The physical `user_id` and `device_id` columns only contain values that were promoted at ingest — by the SDK tracking plugin, or by the `user_id`/`device_id` attribute keys on the Ingestion API. Events that carried their ID as `anonymous_id` or `id` keep it inside the `attributes` JSON instead. GrowthBook-generated queries resolve this automatically, but hand-written SQL reads the raw columns.

If your custom SQL joins or groups on identifiers — especially in a [custom fact table](/app/metrics) used for experiment analysis — use the same resolution expressions GrowthBook uses:

```sql theme={null}
coalesce(
  nullIf(user_id, ''),
  nullIf(attributes.user_id::Nullable(String), '')
) AS user_id,
coalesce(
  nullIf(device_id, ''),
  nullIf(attributes.device_id::Nullable(String), ''),
  nullIf(attributes.anonymous_id::Nullable(String), ''),
  nullIf(attributes.id::Nullable(String), '')
) AS device_id
```

Keep the standard column names (`user_id`, `device_id`): experiment analysis joins your fact table to GrowthBook's exposure queries by identifier, so a fact table that extracts an ID from the JSON under a different name won't match any exposures.

#### Large queries

There is a limit of 1000 rows in the SQL Explorer. Returning raw events over a large time period will quickly exceed this limit.

To work around this, use `GROUP BY` in your queries to aggregate results.

One common aggregation is by time intervals. You can use the `toStartOf...` functions in ClickHouse for this. For example, to get daily event counts:

```sql theme={null}
SELECT
  toStartOfDay(timestamp) AS day,
  COUNT(*) AS count
FROM events
WHERE timestamp >= '2025-06-01 00:00:00'
GROUP BY day
ORDER BY day ASC
```

Whether you have thousands or millions of events, this query will always return a manageable number of rows (one per day).

There are also functions like `toStartOfHour`, `toStartOfMonth`, etc. that you can use to group by different time intervals.

## FAQ

### Can I use the Managed Warehouse alongside my own warehouse?

Yes. You can add a separate data source for your own warehouse at any time and use both simultaneously. Note that each experiment pulls data from a single data source, so you can't mix metrics from both warehouses within one experiment.

### How do I add custom identifiers like `account_id`?

`user_id` and `device_id` are built in. To add another identifier, go to **SDK Connections** → **Attributes** and mark the attribute as an **Identifier**. Include it in the `attributes` of your events and it will be exposed as a top-level column on your fact tables.

### Why are the `user_id`/`device_id` columns empty when I query directly?

Those columns are only filled at ingest when events include the `user_id` or `device_id` attribute keys (the SDK tracking plugin does this for you). If your events carried their ID as `anonymous_id` or `id`, it lives in the `attributes` JSON — experiment analysis resolves it into `device_id` automatically, but your own SQL needs to do the same. See [Identifier columns in custom SQL](#identifier-columns-in-custom-sql).

### What happens if I exceed my event limit?

You'll see a notification in GrowthBook when you're approaching your limit. On free plans, events stop being tracked for the remainder of the month and reset the following month. On paid plans, overage charges apply.

### Can I export my data?

You can query and export results through the SQL Explorer.

### Can I use the Managed Warehouse when self-hosting?

The Managed Warehouse is only available on GrowthBook Cloud. Self-hosted instances should [connect their own data warehouse](/warehouses).
