From 4ebdea6beda7c26cd3a863ce20709edac97fe2ee Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Wed, 25 Sep 2024 13:11:35 +0100 Subject: [PATCH] feat(bi): Added foundations of insight variables (#25146) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- frontend/src/lib/api.ts | 15 ++ frontend/src/lib/constants.tsx | 1 + .../conditionalFormattingLogic.ts | 3 +- .../Components/Variables/NewVariableModal.tsx | 130 ++++++++++++++++++ .../Components/Variables/Variables.tsx | 81 +++++++++++ .../Components/Variables/addVariableLogic.ts | 94 +++++++++++++ .../Components/Variables/variablesLogic.ts | 89 ++++++++++++ .../DataVisualization/DataVisualization.tsx | 4 + .../queries/nodes/DataVisualization/types.ts | 28 ++++ frontend/src/queries/schema.json | 18 +++ frontend/src/queries/schema.ts | 7 + .../saved_queries/dataWarehouseViewsLogic.tsx | 14 +- latest_migrations.manifest | 2 +- posthog/api/__init__.py | 8 ++ posthog/api/insight_variable.py | 27 ++++ posthog/migrations/0480_insightvariable.py | 47 +++++++ posthog/models/__init__.py | 5 + posthog/models/insight_variable.py | 18 +++ posthog/schema.py | 11 ++ 19 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 frontend/src/queries/nodes/DataVisualization/Components/Variables/NewVariableModal.tsx create mode 100644 frontend/src/queries/nodes/DataVisualization/Components/Variables/Variables.tsx create mode 100644 frontend/src/queries/nodes/DataVisualization/Components/Variables/addVariableLogic.ts create mode 100644 frontend/src/queries/nodes/DataVisualization/Components/Variables/variablesLogic.ts create mode 100644 posthog/api/insight_variable.py create mode 100644 posthog/migrations/0480_insightvariable.py create mode 100644 posthog/models/insight_variable.py diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 89744fc3b0821..02698acc010da 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -9,6 +9,7 @@ import { stringifiedFingerprint } from 'scenes/error-tracking/utils' import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' import { getCurrentExporterData } from '~/exporter/exporterViewLogic' +import { Variable } from '~/queries/nodes/DataVisualization/types' import { AlertType, AlertTypeWrite, @@ -824,6 +825,11 @@ class ApiRequest { return this.externalDataSchemas(teamId).addPathComponent(schemaId) } + // Insight Variables + public insightVariables(teamId?: TeamType['id']): ApiRequest { + return this.projectsDetail(teamId).addPathComponent('insight_variables') + } + // ActivityLog public activity_log(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('activity_log') @@ -2205,6 +2211,15 @@ const api = { }, }, + insightVariables: { + async list(options?: ApiMethodOptions | undefined): Promise> { + return await new ApiRequest().insightVariables().get(options) + }, + async create(data: Partial): Promise { + return await new ApiRequest().insightVariables().create({ data }) + }, + }, + subscriptions: { async get(subscriptionId: SubscriptionType['id']): Promise { return await new ApiRequest().subscription(subscriptionId).get() diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 908a7e631b91c..2e5f942bb3cb1 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -216,6 +216,7 @@ export const FEATURE_FLAGS = { WEB_ANALYTICS_CONVERSION_GOALS: 'web-analytics-conversion-goals', // owner: @robbie-c WEB_ANALYTICS_LAST_CLICK: 'web-analytics-last-click', // owner: @robbie-c HEDGEHOG_SKIN_SPIDERHOG: 'hedgehog-skin-spiderhog', // owner: @benjackwhite + INSIGHT_VARIABLES: 'insight_variables', // owner: @Gilbert09 #team-data-warehouse WEB_EXPERIMENTS: 'web-experiments', // owner: @team-feature-success BIGQUERY_DWH: 'bigquery-dwh', // owner: @Gilbert09 #team-data-warehouse REPLAY_DEFAULT_SORT_ORDER_EXPERIMENT: 'replay-order-by-experiment', // owner: #team-replay diff --git a/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/conditionalFormattingLogic.ts b/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/conditionalFormattingLogic.ts index bad56f2a24b7a..670d7509a327d 100644 --- a/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/conditionalFormattingLogic.ts +++ b/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/conditionalFormattingLogic.ts @@ -19,8 +19,7 @@ export const conditionalFormattingLogic = kea([ path(['queries', 'nodes', 'DataVisualization', 'Components', 'conditionalFormattingLogic']), props({ rule: { id: '' }, key: '' } as ConditionalFormattingLogicProps), connect({ - values: [dataVisualizationLogic, ['query']], - actions: [dataVisualizationLogic, ['setQuery', 'updateConditionalFormattingRule']], + actions: [dataVisualizationLogic, ['updateConditionalFormattingRule']], }), actions({ selectColumn: (columnName: string) => ({ columnName }), diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Variables/NewVariableModal.tsx b/frontend/src/queries/nodes/DataVisualization/Components/Variables/NewVariableModal.tsx new file mode 100644 index 0000000000000..b7386fd745d5a --- /dev/null +++ b/frontend/src/queries/nodes/DataVisualization/Components/Variables/NewVariableModal.tsx @@ -0,0 +1,130 @@ +import { + LemonButton, + LemonInput, + LemonInputSelect, + LemonModal, + LemonSegmentedButton, + LemonSelect, +} from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { LemonField } from 'lib/lemon-ui/LemonField' + +import { Variable } from '../../types' +import { addVariableLogic } from './addVariableLogic' + +const renderVariableSpecificFields = ( + variable: Variable, + updateVariable: (variable: Variable) => void +): JSX.Element => { + if (variable.type === 'String') { + return ( + + updateVariable({ ...variable, default_value: value })} + /> + + ) + } + + if (variable.type === 'Number') { + return ( + + updateVariable({ ...variable, default_value: value ?? 0 })} + /> + + ) + } + + if (variable.type === 'Boolean') { + return ( + + updateVariable({ ...variable, default_value: value === 'true' })} + options={[ + { + value: 'true', + label: 'true', + }, + { + value: 'false', + label: 'false', + }, + ]} + /> + + ) + } + + if (variable.type === 'List') { + return ( + <> + + updateVariable({ ...variable, values: value })} + placeholder="Options..." + mode="multiple" + allowCustomValues={true} + options={[]} + /> + + + ({ label: n, value: n }))} + onChange={(value) => updateVariable({ ...variable, default_value: value ?? '' })} + allowClear + dropdownMaxContentWidth + /> + + + ) + } + + throw new Error(`Unsupported variable type: ${(variable as Variable).type}`) +} + +export const NewVariableModal = (): JSX.Element => { + const { closeModal, updateVariable, save } = useActions(addVariableLogic) + const { isModalOpen, variable } = useValues(addVariableLogic) + + return ( + + + Close + + save()}> + Save + + + } + > +
+ + updateVariable({ ...variable, name: value })} + /> + + {renderVariableSpecificFields(variable, updateVariable)} +
+
+ ) +} diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Variables/Variables.tsx b/frontend/src/queries/nodes/DataVisualization/Components/Variables/Variables.tsx new file mode 100644 index 0000000000000..53aad1eaeedcb --- /dev/null +++ b/frontend/src/queries/nodes/DataVisualization/Components/Variables/Variables.tsx @@ -0,0 +1,81 @@ +import { IconPlus } from '@posthog/icons' +import { LemonButton, LemonMenu } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' + +import { dataVisualizationLogic } from '../../dataVisualizationLogic' +import { addVariableLogic } from './addVariableLogic' +import { NewVariableModal } from './NewVariableModal' +import { variablesLogic } from './variablesLogic' + +export const Variables = (): JSX.Element => { + const { dataVisualizationProps, showEditingUI } = useValues(dataVisualizationLogic) + + const { featureFlags } = useValues(featureFlagLogic) + const { openModal } = useActions(addVariableLogic) + + const builtVariablesLogic = variablesLogic({ key: dataVisualizationProps.key }) + const { variables, variablesLoading, variablesForInsight } = useValues(builtVariablesLogic) + const { addVariable } = useActions(builtVariablesLogic) + + if (!featureFlags[FEATURE_FLAGS.INSIGHT_VARIABLES]) { + return <> + } + + return ( + <> +
+ {showEditingUI && ( + openModal('String'), + }, + { + label: 'Number', + onClick: () => openModal('Number'), + }, + { + label: 'Boolean', + onClick: () => openModal('Boolean'), + }, + { + label: 'List', + onClick: () => openModal('List'), + }, + ], + }, + { + label: 'Existing variable', + items: variablesLoading + ? [ + { + label: 'Loading...', + onClick: () => {}, + }, + ] + : variables.map((n) => ({ + label: n.name, + onClick: () => addVariable(n.id), + })), + }, + ]} + > + }> + Add variable + + + )} + {variablesForInsight.map((n) => ( +
{n.name}
+ ))} +
+ + + ) +} diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Variables/addVariableLogic.ts b/frontend/src/queries/nodes/DataVisualization/Components/Variables/addVariableLogic.ts new file mode 100644 index 0000000000000..2a146f069d092 --- /dev/null +++ b/frontend/src/queries/nodes/DataVisualization/Components/Variables/addVariableLogic.ts @@ -0,0 +1,94 @@ +import { actions, kea, path, reducers } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import { BooleanVariable, ListVariable, NumberVariable, StringVariable, Variable, VariableType } from '../../types' +import type { addVariableLogicType } from './addVariableLogicType' + +const DEFAULT_VARIABLE: StringVariable = { + id: '', + type: 'String', + name: '', + default_value: '', +} + +export const addVariableLogic = kea([ + path(['queries', 'nodes', 'DataVisualization', 'Components', 'Variables', 'variableLogic']), + actions({ + openModal: (variableType: VariableType) => ({ variableType }), + closeModal: true, + updateVariable: (variable: Variable) => ({ variable }), + }), + reducers({ + variableType: [ + 'string' as VariableType, + { + openModal: (_, { variableType }) => variableType, + }, + ], + isModalOpen: [ + false as boolean, + { + openModal: () => true, + closeModal: () => false, + }, + ], + variable: [ + DEFAULT_VARIABLE as Variable, + { + openModal: (_, { variableType }) => { + if (variableType === 'String') { + return { + id: '', + type: 'String', + name: '', + default_value: '', + } as StringVariable + } + + if (variableType === 'Number') { + return { + id: '', + type: 'Number', + name: '', + default_value: 0, + } as NumberVariable + } + + if (variableType === 'Boolean') { + return { + id: '', + type: 'Boolean', + name: '', + default_value: false, + } as BooleanVariable + } + + if (variableType === 'List') { + return { + id: '', + type: 'List', + name: '', + values: [], + default_value: '', + } as ListVariable + } + + throw new Error(`Unsupported variable type ${variableType}`) + }, + updateVariable: (state, { variable }) => ({ ...state, ...variable }), + closeModal: () => DEFAULT_VARIABLE, + }, + ], + }), + loaders(({ values }) => ({ + savedVariable: [ + null as null | Variable, + { + save: async () => { + return await api.insightVariables.create(values.variable) + }, + }, + ], + })), +]) diff --git a/frontend/src/queries/nodes/DataVisualization/Components/Variables/variablesLogic.ts b/frontend/src/queries/nodes/DataVisualization/Components/Variables/variablesLogic.ts new file mode 100644 index 0000000000000..62d968dd3d1dc --- /dev/null +++ b/frontend/src/queries/nodes/DataVisualization/Components/Variables/variablesLogic.ts @@ -0,0 +1,89 @@ +import { actions, afterMount, connect, kea, key, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { subscriptions } from 'kea-subscriptions' +import api from 'lib/api' + +import { HogQLVariable } from '~/queries/schema' + +import { dataVisualizationLogic } from '../../dataVisualizationLogic' +import { Variable } from '../../types' +import type { variablesLogicType } from './variablesLogicType' + +export interface VariablesLogicProps { + key: string +} + +export const variablesLogic = kea([ + path(['queries', 'nodes', 'DataVisualization', 'Components', 'Variables', 'variablesLogic']), + props({ key: '' } as VariablesLogicProps), + key((props) => props.key), + connect({ + actions: [dataVisualizationLogic, ['setQuery']], + values: [dataVisualizationLogic, ['query']], + }), + actions({ + addVariable: (variableId: string) => ({ variableId }), + }), + reducers({ + internalSelectedVariables: [ + [] as string[], + { + addVariable: (state, { variableId }) => { + return [...state, variableId] + }, + }, + ], + }), + loaders({ + variables: [ + [] as Variable[], + { + getVariables: async () => { + const insights = await api.insightVariables.list() + + return insights.results + }, + }, + ], + }), + selectors({ + variablesForInsight: [ + (s) => [s.variables, s.internalSelectedVariables], + (variables, internalSelectedVariables): Variable[] => { + if (!variables.length || !internalSelectedVariables.length) { + return [] + } + + return internalSelectedVariables + .map((variableId) => variables.find((n) => n.id === variableId)) + .filter((n): n is Variable => Boolean(n)) + }, + ], + }), + subscriptions(({ actions, values }) => ({ + variablesForInsight: (variables: Variable[]) => { + actions.setQuery({ + ...values.query, + source: { + ...values.query.source, + variables: variables.reduce((acc, cur) => { + if (cur.id) { + acc[cur.id] = { + variableId: cur.id, + } + } + + return acc + }, {} as Record), + }, + }) + }, + })), + afterMount(({ actions, values }) => { + Object.keys(values.query.source.variables ?? {}).forEach((variableId) => { + actions.addVariable(variableId) + }) + + actions.getVariables() + }), +]) diff --git a/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx b/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx index 8bbce53ef67ed..bf98b2523f158 100644 --- a/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx +++ b/frontend/src/queries/nodes/DataVisualization/DataVisualization.tsx @@ -28,6 +28,7 @@ import { LineGraph } from './Components/Charts/LineGraph' import { SideBar } from './Components/SideBar' import { Table } from './Components/Table' import { TableDisplay } from './Components/TableDisplay' +import { Variables } from './Components/Variables/Variables' import { dataVisualizationLogic, DataVisualizationLogicProps } from './dataVisualizationLogic' import { displayLogic } from './displayLogic' @@ -194,6 +195,9 @@ function InternalDataTableVisualization(props: DataTableVisualizationProps): JSX )} + + +
{showEditingUI && isChartSettingsPanelOpen && (
diff --git a/frontend/src/queries/nodes/DataVisualization/types.ts b/frontend/src/queries/nodes/DataVisualization/types.ts index d985f63715152..e1835c7d2394b 100644 --- a/frontend/src/queries/nodes/DataVisualization/types.ts +++ b/frontend/src/queries/nodes/DataVisualization/types.ts @@ -62,3 +62,31 @@ export const FORMATTING_TEMPLATES: FormattingTemplate[] = [ hideInput: true, }, ] + +export type VariableType = 'String' | 'Number' | 'Boolean' | 'List' + +interface VariableBase { + id: string + name: string + type: VariableType +} + +export interface StringVariable extends VariableBase { + type: 'String' + default_value: string +} +export interface NumberVariable extends VariableBase { + type: 'Number' + default_value: number +} +export interface BooleanVariable extends VariableBase { + type: 'Boolean' + default_value: boolean +} +export interface ListVariable extends VariableBase { + type: 'List' + values: string[] + default_value: string +} + +export type Variable = StringVariable | NumberVariable | BooleanVariable | ListVariable diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 88e966d00a973..c7268e4b00a17 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -5856,6 +5856,13 @@ "values": { "description": "Constant values that can be referenced with the {placeholder} syntax in the query", "type": "object" + }, + "variables": { + "additionalProperties": { + "$ref": "#/definitions/HogQLVariable" + }, + "description": "Variables to be subsituted into the query", + "type": "object" } }, "required": ["kind", "query"], @@ -5992,6 +5999,17 @@ "required": ["results"], "type": "object" }, + "HogQLVariable": { + "additionalProperties": false, + "properties": { + "value": {}, + "variableId": { + "type": "string" + } + }, + "required": ["variableId"], + "type": "object" + }, "HogQuery": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index eb3b58a4eabfd..4e0256813107c 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -265,10 +265,17 @@ export interface HogQLFilters { filterTestAccounts?: boolean } +export interface HogQLVariable { + variableId: string + value?: any +} + export interface HogQLQuery extends DataNode { kind: NodeKind.HogQLQuery query: string filters?: HogQLFilters + /** Variables to be subsituted into the query */ + variables?: Record /** Constant values that can be referenced with the {placeholder} syntax in the query */ values?: Record /** @deprecated use modifiers.debug instead */ diff --git a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx index 58a3825ecf9e0..418e9339dc304 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/dataWarehouseViewsLogic.tsx @@ -75,10 +75,12 @@ export const dataWarehouseViewsLogic = kea([ dataWarehouseSavedQueryMapById: [ (s) => [s.dataWarehouseSavedQueries], (dataWarehouseSavedQueries) => { - return dataWarehouseSavedQueries.reduce((acc, cur) => { - acc[cur.id] = cur - return acc - }, {} as Record) + return ( + dataWarehouseSavedQueries?.reduce((acc, cur) => { + acc[cur.id] = cur + return acc + }, {} as Record) ?? {} + ) }, ], }), @@ -86,7 +88,9 @@ export const dataWarehouseViewsLogic = kea([ afterMount: () => { actions.loadDataWarehouseSavedQueries() if (!cache.pollingInterval) { - cache.pollingInterval = setInterval(actions.loadDataWarehouseSavedQueries, 5000) + cache.pollingInterval = setInterval(() => { + actions.loadDataWarehouseSavedQueries() + }, 5000) } }, beforeUnmount: () => { diff --git a/latest_migrations.manifest b/latest_migrations.manifest index ac7570008fd03..dfe09cd82ef10 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0479_alter_sensitive_config_2 +posthog: 0480_insightvariable sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index 97f79f8545e03..396b6c92947e8 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -36,6 +36,7 @@ hog_function_template, hog, ingestion_warnings, + insight_variable, instance_settings, instance_status, integration, @@ -512,6 +513,13 @@ def register_grandfathered_environment_nested_viewset( ["team_id"], ) +projects_router.register( + r"insight_variables", + insight_variable.InsightVariableViewSet, + "insight_variables", + ["team_id"], +) + register_grandfathered_environment_nested_viewset( r"alerts", alert.AlertViewSet, diff --git a/posthog/api/insight_variable.py b/posthog/api/insight_variable.py new file mode 100644 index 0000000000000..b3c44962f68b0 --- /dev/null +++ b/posthog/api/insight_variable.py @@ -0,0 +1,27 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import serializers, viewsets + +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.models.insight_variable import InsightVariable + + +class InsightVariableSerializer(serializers.ModelSerializer): + class Meta: + model = InsightVariable + + fields = ["id", "name", "type", "default_value", "created_by", "created_at"] + + read_only_fields = ["id", "created_by", "created_at"] + + def create(self, validated_data): + validated_data["team_id"] = self.context["team_id"] + validated_data["created_by"] = self.context["request"].user + + return InsightVariable.objects.create(**validated_data) + + +class InsightVariableViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): + scope_object = "INTERNAL" + queryset = InsightVariable.objects.all() + serializer_class = InsightVariableSerializer + filter_backends = [DjangoFilterBackend] diff --git a/posthog/migrations/0480_insightvariable.py b/posthog/migrations/0480_insightvariable.py new file mode 100644 index 0000000000000..9a4e6a695f5e8 --- /dev/null +++ b/posthog/migrations/0480_insightvariable.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.15 on 2024-09-19 16:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import posthog.models.utils + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0479_alter_sensitive_config_2"), + ] + + operations = [ + migrations.CreateModel( + name="InsightVariable", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True, null=True)), + ( + "id", + models.UUIDField( + default=posthog.models.utils.UUIDT, editable=False, primary_key=True, serialize=False + ), + ), + ("name", models.CharField(max_length=400)), + ( + "type", + models.CharField( + choices=[("String", "String"), ("Number", "Number"), ("Boolean", "Boolean"), ("List", "List")], + max_length=128, + ), + ), + ("default_value", models.JSONField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="posthog.team")), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/posthog/models/__init__.py b/posthog/models/__init__.py index ad1c8cd224311..e3a4942335044 100644 --- a/posthog/models/__init__.py +++ b/posthog/models/__init__.py @@ -45,6 +45,7 @@ from .hog_functions import HogFunction from .insight import Insight, InsightViewed from .insight_caching_state import InsightCachingState +from .insight_variable import InsightVariable from .instance_setting import InstanceSetting from .integration import Integration from .messaging import MessagingRecord @@ -111,6 +112,7 @@ "HogFunction", "Insight", "InsightCachingState", + "InsightVariable", "InsightViewed", "InstanceSetting", "Integration", @@ -126,6 +128,7 @@ "PersonDistinctId", "PersonalAPIKey", "PersonOverride", + "PersonOverrideMapping", "Plugin", "PluginAttachment", "PluginConfig", @@ -152,6 +155,8 @@ "UserManager", "DataWarehouseTable", "ScheduledChange", + "WebExperiment", + "Comment", # Deprecated models here for backwards compatibility "Prompt", "PromptSequence", diff --git a/posthog/models/insight_variable.py b/posthog/models/insight_variable.py new file mode 100644 index 0000000000000..5e1e69708ddf9 --- /dev/null +++ b/posthog/models/insight_variable.py @@ -0,0 +1,18 @@ +from django.db import models + +from posthog.models.utils import CreatedMetaFields, UUIDModel, UpdatedMetaFields, sane_repr + + +class InsightVariable(UUIDModel, CreatedMetaFields, UpdatedMetaFields): + class Type(models.TextChoices): + STRING = "String", "String" + NUMBER = "Number", "Number" + BOOLEAN = "Boolean", "Boolean" + LIST = "List", "List" + + team = models.ForeignKey("Team", on_delete=models.CASCADE) + name = models.CharField(max_length=400) + type = models.CharField(max_length=128, choices=Type.choices) + default_value = models.JSONField(null=True, blank=True) + + __repr__ = sane_repr("id") diff --git a/posthog/schema.py b/posthog/schema.py index 52af2c2f9f3fe..a0ed9d57ffcee 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -727,6 +727,14 @@ class HogQLQueryModifiers(BaseModel): sessionTableVersion: Optional[SessionTableVersion] = None +class HogQLVariable(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + value: Optional[Any] = None + variableId: str + + class HogQueryResponse(BaseModel): model_config = ConfigDict( extra="forbid", @@ -4422,6 +4430,9 @@ class HogQLQuery(BaseModel): values: Optional[dict[str, Any]] = Field( default=None, description="Constant values that can be referenced with the {placeholder} syntax in the query" ) + variables: Optional[dict[str, HogQLVariable]] = Field( + default=None, description="Variables to be subsituted into the query" + ) class InsightActorsQueryOptionsResponse(BaseModel):