Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/docs/experimentation/connect-a-warehouse.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,65 @@ The current status is shown on the warehouse card in **Environment Settings > Wa
- **Connected**: events have been received; you are ready to run experiments.
- **Errored**: something went wrong; [contact support](/support/).

## Bring your own ClickHouse

Instead of the managed Flagsmith warehouse, you can store experiment events in your own **ClickHouse** instance.
Comment thread
Zaimwa9 marked this conversation as resolved.

1. Configure your instance by running the following against it, using an administrative user (the statements require
`CREATE USER`, `GRANT`, `CREATE DATABASE` and `CREATE TABLE` privileges). Replace `<USER>` and `<CHANGE_ME_PASSWORD>`
with your own values. This is a one-off setup step; the same SQL is available to copy from the Flagsmith dashboard.

```sql
-- Create a dedicated user for Flagsmith
CREATE USER IF NOT EXISTS <USER>
IDENTIFIED WITH sha256_password BY '<CHANGE_ME_PASSWORD>';

-- Allow it to write and read experiment events
GRANT SELECT, INSERT ON flagsmith_exp.events TO <USER>;

-- Allow it to check the events table exists
GRANT SHOW TABLES, SHOW COLUMNS ON flagsmith_exp.* TO <USER>;

-- Create the database and events table
CREATE DATABASE IF NOT EXISTS flagsmith_exp;

CREATE TABLE IF NOT EXISTS flagsmith_exp.events
(
environment_key LowCardinality(String),
event LowCardinality(String),
feature_name LowCardinality(String),
timestamp DateTime64(3),
collected_at DateTime64(3),
identifier String,
value String CODEC(ZSTD(3)),
traits String CODEC(ZSTD(3)),
metadata String CODEC(ZSTD(3)),
sdk_language LowCardinality(String),
sdk_version LowCardinality(String),

INDEX idx_identity identifier TYPE bloom_filter GRANULARITY 4,

CONSTRAINT environment_key_not_empty CHECK environment_key != '',
CONSTRAINT event_not_empty CHECK event != '',
CONSTRAINT timestamp_sane CHECK timestamp > toDateTime64('2020-01-01 00:00:00', 3)
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (environment_key, event, feature_name, timestamp, identifier);
```

2. Go to **Environment Settings > Warehouse**, select **ClickHouse** and enter your connection details: host, port (9440
by default, the native protocol port), database (`flagsmith_exp` by default), and the username and password of the
user you created in step 1.
3. Click **Test connection**. Once the test succeeds, save the connection.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4. Click **Send your first event** on the connection card to verify events are arriving in your warehouse; the button
disappears once the first event is received.

Connections use the ClickHouse native protocol, with TLS enabled by default.

Experiment results and exposure queries are currently computed from the managed Flagsmith warehouse; serving results
directly from your own ClickHouse instance is not supported yet.

## Coming soon

Bring your own warehouse: connections for **Snowflake**, **BigQuery** and **Databricks**, so experiment results are
Expand Down
4 changes: 2 additions & 2 deletions frontend/web/components/CalloutBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type CalloutBarTheme = 'light' | 'dark'

type CalloutBarProps = {
theme?: CalloutBarTheme
icon: ReactNode
icon?: ReactNode
prefix: ReactNode
label: ReactNode
expanded?: boolean
Expand All @@ -14,7 +14,7 @@ type CalloutBarProps = {

const CalloutBar: FC<CalloutBarProps> = ({
expanded,
icon,
icon = <>{'<>'}</>,
label,
onClick,
prefix,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Input from 'components/base/forms/Input'
import Switch from 'components/Switch'
import ErrorMessage from 'components/ErrorMessage'
import FieldError from 'components/base/forms/FieldError'
import WarehouseSetupSqlHelp from './WarehouseSetupSqlHelp'
import WarningMessage from 'components/WarningMessage'
import { ClickHouseConfig } from 'common/types/responses'
import { useTestWarehouseConnectionConfigMutation } from 'common/services/useWarehouseConnection'
Expand All @@ -21,6 +22,7 @@ import {
getButtonLabel,
getTestFailureWarning,
getWarehouseErrorMessage,
isMissingEventsTableDetail,
} from './warehouseFormUtils'
import './ConfigForm.scss'

Expand Down Expand Up @@ -50,11 +52,14 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
const [database, setDatabase] = useState(defaults.database)
const [username, setUsername] = useState(defaults.username)
const [password, setPassword] = useState('')
const [secure, setSecure] = useState(defaults.secure)
const [secure] = useState(defaults.secure)
Comment thread
Zaimwa9 marked this conversation as resolved.
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState(false)
const [testState, setTestState] = useState<TestState>('idle')
const [testDetail, setTestDetail] = useState<string | null>(null)
// Sticky: the setup SQL stays offered once a test reports the table missing,
// even while the user edits fields (which resets the live test state).
const [showSetupSql, setShowSetupSql] = useState(false)
const testRevision = useRef(0)

const [testConnectionConfig, { isLoading: isTesting }] =
Expand Down Expand Up @@ -104,6 +109,7 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
} else {
setTestState('errored')
setTestDetail(result.status_detail)
setShowSetupSql(isMissingEventsTableDetail(result.status_detail))
}
} catch {
if (revision !== testRevision.current) return
Expand Down Expand Up @@ -147,7 +153,7 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
</div>

<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Name</label>
<label className='wh-config-form__label'>Name this warehouse</label>
<Input
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
Expand Down Expand Up @@ -179,13 +185,13 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
/>
</div>
<div className='wh-config-form__field'>
<label className='wh-config-form__label'>Database</label>
<label className='wh-config-form__label'>Database Name</label>
<Input
value={database}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setField(setDatabase)(e.target.value)
}
placeholder='flagsmith'
placeholder='flagsmith_exp'
/>
</div>
</div>
Expand All @@ -197,7 +203,7 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setField(setUsername)(e.target.value)
}
placeholder='default'
placeholder='The user from the setup script'
/>
</div>

Expand All @@ -223,13 +229,20 @@ const ClickHouseConfigForm: FC<ClickHouseConfigFormProps> = ({

<div className='wh-config-form__field'>
<div className='d-flex flex-row align-items-center gap-2'>
<Switch checked={secure} onChange={setField(setSecure)} />
<Switch checked={secure} disabled />
<label className='wh-config-form__label mb-0'>
Secure connection (TLS)
</label>
Comment thread
Zaimwa9 marked this conversation as resolved.
</div>
</div>

{isEdit && (
<WarehouseSetupSqlHelp
database={database.trim() || CLICKHOUSE_DEFAULTS.database}
showInitially={showSetupSql}
/>
)}

{error && <ErrorMessage error={getWarehouseErrorMessage(isEdit)} />}

<div className='wh-config-form__actions'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Tooltip from 'components/Tooltip'
import Icon from 'components/icons/Icon'
import Button from 'components/base/forms/Button'
import WarehouseEventCodeHelp from './WarehouseEventCodeHelp'
import WarehouseSetupSqlHelp from './WarehouseSetupSqlHelp'
import { CLICKHOUSE_DEFAULTS } from './clickhouseConfig'
import WarehouseStats from './WarehouseStats'

type WarehouseConnectionCardProps = {
Expand Down Expand Up @@ -57,6 +59,12 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
const isFlagsmith = connection.warehouse_type === 'flagsmith'
const isPending = connection.status === 'pending_connection'
const isConnected = connection.status === 'connected'
// ClickHouse connections are born connected, so the first-event nudge stays
// until events actually arrive in the customer's warehouse.
const showSendFirstEvent = isFlagsmith
? !isPending && !isConnected
: connection.warehouse_type === 'clickhouse' &&
!connection.total_events_received

const handleDelete = () => {
openConfirm({
Expand Down Expand Up @@ -143,8 +151,20 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
</div>
)}
<WarehouseEventCodeHelp />
{connection.warehouse_type === 'clickhouse' && (
<div className='mt-3'>
<WarehouseSetupSqlHelp
database={
(connection.config &&
'database' in connection.config &&
connection.config.database) ||
CLICKHOUSE_DEFAULTS.database
}
/>
</div>
)}
<div className='d-flex justify-content-end mt-3'>
{onTestConnection && (
{onTestConnection && !isConnected && (
<Button
id='warehouse-connection-test'
theme='outline'
Expand All @@ -155,7 +175,7 @@ const WarehouseConnectionCard: FC<WarehouseConnectionCardProps> = ({
{isSendingTestEvent ? 'Testing...' : 'Test connection'}
</Button>
)}
{isFlagsmith && !isPending && !isConnected && (
{showSendFirstEvent && (
<Button
id='warehouse-send-first-event'
theme='primary'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,57 @@
border-radius: var(--radius-lg);
}

&__steps {
display: flex;
flex-direction: column;
}

&__step {
display: flex;
gap: 16px;

&:not(:last-child) &-content {
padding-bottom: 32px;
}
}

&__step-rail {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}

&__step-marker {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex-shrink: 0;
border: 1px solid var(--color-border-default);
border-radius: 50%;
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
background: var(--color-surface-subtle);
}

&__step-connector {
flex: 1;
width: 1px;
background: var(--color-border-default);
}

&__step-toggle {
display: flex;
gap: 8px;
}

&__step-content {
flex: 1;
min-width: 0;
}

&__flagsmith-description {
font-size: 16px;
font-weight: 700;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { FC, useState } from 'react'
import Icon from 'components/icons/Icon'
import Button from 'components/base/forms/Button'
import BareButton from 'components/base/forms/BareButton'
import { WarehouseType } from 'common/types/responses'
import { ConfigFormData } from './ConfigForm'
import SelectableCard from 'components/base/SelectableCard'
import ConfigForm from './ConfigForm'
import Utils from 'common/utils/utils'
import ClickHouseConfigForm from './ClickHouseConfigForm'
import WarehouseSqlSnippet from './WarehouseSqlSnippet'
import { getClickHouseOnboardingSql } from './clickhouseSetupSql'
Comment thread
Zaimwa9 marked this conversation as resolved.
import { ClickHouseFormData } from './clickhouseConfig'
import './WarehouseSetup.scss'

Expand All @@ -29,6 +32,7 @@ const WarehouseSetup: FC<WarehouseSetupProps> = ({
}) => {
const [selectedType, setSelectedType] =
useState<WarehouseTypeOption>('flagsmith')
const [sqlExpanded, setSqlExpanded] = useState(true)
const clickhouseEnabled = Utils.getFlagsmithHasFeature('clickhouse_warehouse')
const configurableTypes: WarehouseTypeOption[] = clickhouseEnabled
? ['flagsmith', 'clickhouse']
Expand Down Expand Up @@ -132,10 +136,55 @@ const WarehouseSetup: FC<WarehouseSetupProps> = ({
)}

{selectedType === 'clickhouse' && (
<ClickHouseConfigForm
environmentId={environmentId}
onSave={onCreateClickHouse}
/>
<div className='warehouse-setup__steps'>
<div className='warehouse-setup__step'>
<div className='warehouse-setup__step-rail'>
<div className='warehouse-setup__step-marker'>1</div>
<div className='warehouse-setup__step-connector' />
</div>
<div className='warehouse-setup__step-content'>
<BareButton
className='warehouse-setup__step-toggle mb-2'
aria-expanded={sqlExpanded}
onClick={() => setSqlExpanded(!sqlExpanded)}
>
<h6 className='mb-0'>
Configure your warehouse for experimentation
</h6>
<Icon
name={sqlExpanded ? 'chevron-up' : 'chevron-down'}
width={22}
fill='#656D7B'
Comment thread
Zaimwa9 marked this conversation as resolved.
/>
</BareButton>
{sqlExpanded && (
<>
<p className='text-muted mb-3'>
Run this once against your ClickHouse instance using an
administrative user. Replace {'<USER>'} and{' '}
{'<CHANGE_ME_PASSWORD>'} with your own values.
</p>
<WarehouseSqlSnippet sql={getClickHouseOnboardingSql()} />
</>
)}
</div>
</div>
<div className='warehouse-setup__step'>
<div className='warehouse-setup__step-rail'>
<div className='warehouse-setup__step-marker'>2</div>
</div>
<div className='warehouse-setup__step-content'>
<h6 className='mb-2'>Connect your warehouse</h6>
<p className='text-muted mb-3'>
Enter the connection details for the user you created in step 1.
</p>
<ClickHouseConfigForm
environmentId={environmentId}
onSave={onCreateClickHouse}
/>
</div>
</div>
</div>
)}

{!configurableTypes.includes(selectedType) && (
Expand Down
Loading
Loading