Skip to content

Commit

Permalink
Merge branch 'master' into use-eventsource-for-queries
Browse files Browse the repository at this point in the history
  • Loading branch information
Twixes committed Jan 28, 2025
2 parents 9610bba + 63b775d commit fa0ac07
Show file tree
Hide file tree
Showing 119 changed files with 4,978 additions and 2,277 deletions.
2 changes: 0 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,6 @@
"WORKER_CONCURRENCY": "2",
"OBJECT_STORAGE_ENABLED": "True",
"HOG_HOOK_URL": "http://localhost:3300/hoghook",
"CDP_ASYNC_FUNCTIONS_RUSTY_HOOK_TEAMS": "",
"CDP_CYCLOTRON_ENABLED_TEAMS": "*",
"PLUGIN_SERVER_MODE": "all-v2"
},
"presentation": {
Expand Down
96 changes: 16 additions & 80 deletions dags/person_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
from clickhouse_driver import Client

from posthog import settings
from posthog.clickhouse.cluster import ClickhouseCluster, get_cluster
from posthog.clickhouse.cluster import (
ClickhouseCluster,
Mutation,
MutationRunner,
get_cluster,
)
from posthog.models.event.sql import EVENTS_DATA_TABLE
from posthog.models.person.sql import PERSON_DISTINCT_ID_OVERRIDES_TABLE

Expand Down Expand Up @@ -91,28 +96,6 @@ def sync(self, client: Client) -> None:
assert queue_size == 0


@dataclass
class Mutation:
table: str
mutation_id: str

def is_done(self, client: Client) -> bool:
[[is_done]] = client.execute(
f"""
SELECT is_done
FROM system.mutations
WHERE database = %(database)s AND table = %(table)s AND mutation_id = %(mutation_id)s
ORDER BY create_time DESC
""",
{"database": settings.CLICKHOUSE_DATABASE, "table": self.table, "mutation_id": self.mutation_id},
)
return is_done

def wait(self, client: Client) -> None:
while not self.is_done(client):
time.sleep(15.0)


@dataclass
class PersonOverridesSnapshotDictionary:
source: PersonOverridesSnapshotTable
Expand Down Expand Up @@ -194,76 +177,29 @@ def load(self, client: Client):
[[checksum]] = results
return checksum

def __find_existing_mutation(self, client: Client, table: str, command_kind: str) -> Mutation | None:
results = client.execute(
f"""
SELECT mutation_id
FROM system.mutations
WHERE
database = %(database)s
AND table = %(table)s
AND startsWith(command, %(command_kind)s)
AND command like concat('%%', %(name)s, '%%')
AND NOT is_killed -- ok to restart a killed mutation
ORDER BY create_time DESC
""",
{
"database": settings.CLICKHOUSE_DATABASE,
"table": table,
"command_kind": command_kind,
"name": self.qualified_name,
},
)
if not results:
return None
else:
assert len(results) == 1
[[mutation_id]] = results
return Mutation(table, mutation_id)

def enqueue_person_id_update_mutation(self, client: Client) -> Mutation:
table = EVENTS_DATA_TABLE()

# if this mutation already exists, don't start it again
# NOTE: this is theoretically subject to replication lag and accuracy of this result is not a guarantee
if mutation := self.__find_existing_mutation(client, table, "UPDATE"):
return mutation

client.execute(
@property
def person_id_update_mutation_runner(self) -> MutationRunner:
return MutationRunner(
EVENTS_DATA_TABLE(),
f"""
ALTER TABLE {settings.CLICKHOUSE_DATABASE}.{table}
UPDATE person_id = dictGet(%(name)s, 'person_id', (team_id, distinct_id))
WHERE dictHas(%(name)s, (team_id, distinct_id))
""",
{"name": self.qualified_name},
)

mutation = self.__find_existing_mutation(client, table, "UPDATE")
assert mutation is not None
return mutation

def enqueue_overrides_delete_mutation(self, client: Client) -> Mutation:
table = PERSON_DISTINCT_ID_OVERRIDES_TABLE

# if this mutation already exists, don't start it again
# NOTE: this is theoretically subject to replication lag and accuracy of this result is not a guarantee
if mutation := self.__find_existing_mutation(client, table, "DELETE"):
return mutation

client.execute(
@property
def overrides_delete_mutation_runner(self) -> MutationRunner:
return MutationRunner(
PERSON_DISTINCT_ID_OVERRIDES_TABLE,
f"""
ALTER TABLE {settings.CLICKHOUSE_DATABASE}.{table}
DELETE WHERE
isNotNull(dictGetOrNull(%(name)s, 'version', (team_id, distinct_id)) as snapshot_version)
AND snapshot_version >= version
""",
{"name": self.qualified_name},
)

mutation = self.__find_existing_mutation(client, table, "DELETE")
assert mutation is not None
return mutation


# Snapshot Table Management

Expand Down Expand Up @@ -375,7 +311,7 @@ def start_person_id_update_mutations(
shard_mutations = {
host.shard_num: mutation
for host, mutation in (
cluster.map_one_host_per_shard(dictionary.enqueue_person_id_update_mutation).result().items()
cluster.map_one_host_per_shard(dictionary.person_id_update_mutation_runner.enqueue).result().items()
)
}
return (dictionary, shard_mutations)
Expand All @@ -401,7 +337,7 @@ def start_overrides_delete_mutations(
dictionary: PersonOverridesSnapshotDictionary,
) -> tuple[PersonOverridesSnapshotDictionary, Mutation]:
"""Start the mutation to remove overrides contained within the snapshot from the overrides table."""
mutation = cluster.any_host(dictionary.enqueue_overrides_delete_mutation).result()
mutation = cluster.any_host(dictionary.overrides_delete_mutation_runner.enqueue).result()
return (dictionary, mutation)


Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/layout/navigation-3000/navigationLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export const navigation3000Logic = kea<navigation3000LogicType>([
identifier: 'LLMObservability',
label: 'LLM observability',
icon: <IconAI />,
to: urls.llmObservability('dashboard'),
to: urls.llmObservabilityDashboard(),
tag: 'beta' as const,
}
: null,
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/lib/components/Cards/InsightCard/QueryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ErrorBoundary } from '~/layout/ErrorBoundary'
import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { Query } from '~/queries/Query/Query'
import { Node } from '~/queries/schema'
import { QueryContext } from '~/queries/types'

import { InsightCardProps } from './InsightCard'
import { InsightDetails } from './InsightDetails'
Expand All @@ -19,11 +20,12 @@ export interface QueryCardProps extends Pick<InsightCardProps, 'highlighted' | '
query: Node
title: string
description?: string
context?: QueryContext
}

/** This is like InsightCard, except for presentation of queries that aren't saved insights. */
export const QueryCard = React.forwardRef<HTMLDivElement, QueryCardProps>(function QueryCard(
{ query, title, description, highlighted, ribbonColor, className, ...divProps },
{ query, title, description, context, highlighted, ribbonColor, className, ...divProps },
ref
): JSX.Element {
const { theme } = useValues(themeLogic)
Expand Down Expand Up @@ -65,7 +67,7 @@ export const QueryCard = React.forwardRef<HTMLDivElement, QueryCardProps>(functi
showEditingControls
/>
<div className="InsightCard__viz">
<Query query={query} readOnly embedded />
<Query query={query} readOnly embedded context={context} />
</div>
</ErrorBoundary>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @fileoverview A component that displays an interactive survey within a session recording. It handles survey display, user responses, and submission
*/
import { LemonButton, LemonCheckbox, LemonTextArea } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'

import { SurveyQuestion, SurveyQuestionType } from '~/types'

import { internalMultipleChoiceSurveyLogic } from './internalMultipleChoiceSurveyLogic'

interface InternalSurveyProps {
surveyId: string
}

export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): JSX.Element {
const logic = internalMultipleChoiceSurveyLogic({ surveyId })
const { survey, surveyResponse, showThankYouMessage, thankYouMessage, openChoice } = useValues(logic)
const { handleChoiceChange, handleSurveyResponse, setOpenChoice } = useActions(logic)

if (!survey) {
return <></>
}

return (
<div className="Popover Popover--padded Popover--appear-done Popover--enter-done my-4">
<div className="Popover__box p-4">
{survey.questions.map((question: SurveyQuestion) => (
<div key={question.question} className="text-sm">
{showThankYouMessage && thankYouMessage}
{!showThankYouMessage && (
<>
<strong>{question.question}</strong>
{question.type === SurveyQuestionType.MultipleChoice && (
<ul className="list-inside list-none mt-2">
{question.choices.map((choice, index) => {
// Add an open choice text area if the last choice is an open choice
if (index === question.choices.length - 1 && question.hasOpenChoice) {
return (
<div className="mt-2" key={choice}>
{choice}
<LemonTextArea
placeholder="Please share any additional comments or feedback"
onChange={setOpenChoice}
value={openChoice ?? ''}
className="my-2"
/>
</div>
)
}
return (
<li key={choice}>
<LemonCheckbox
onChange={(checked) => handleChoiceChange(choice, checked)}
label={choice}
className="font-normal"
/>
</li>
)
})}
</ul>
)}
<LemonButton
type="primary"
disabledReason={
surveyResponse.length === 0 && openChoice === null
? 'Please select at least one option'
: false
}
onClick={handleSurveyResponse}
>
{question.buttonText ?? 'Submit'}
</LemonButton>
</>
)}
</div>
))}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { actions, afterMount, kea, key, listeners, path, props, reducers } from 'kea'
import posthog from 'posthog-js'

import { Survey } from '~/types'

import type { internalMultipleChoiceSurveyLogicType } from './internalMultipleChoiceSurveyLogicType'

export interface InternalSurveyLogicProps {
surveyId: string
}

export const internalMultipleChoiceSurveyLogic = kea<internalMultipleChoiceSurveyLogicType>([
path(['lib', 'components', 'InternalSurvey', 'internalMultipleChoiceSurveyLogicType']),
props({} as InternalSurveyLogicProps),
key((props) => props.surveyId),
actions({
getSurveys: () => ({}),
setSurvey: (survey: Survey) => ({ survey }),
handleSurveys: (surveys: Survey[]) => ({ surveys }),
handleSurveyResponse: () => ({}),
handleChoiceChange: (choice: string, isAdded: boolean) => ({ choice, isAdded }),
setShowThankYouMessage: (showThankYouMessage: boolean) => ({ showThankYouMessage }),
setThankYouMessage: (thankYouMessage: string) => ({ thankYouMessage }),
setOpenChoice: (openChoice: string) => ({ openChoice }),
}),
reducers({
survey: [
null as Survey | null,
{
setSurvey: (_, { survey }) => survey,
},
],
thankYouMessage: [
'Thank you for your feedback!',
{
setThankYouMessage: (_, { thankYouMessage }) => thankYouMessage,
},
],
showThankYouMessage: [
false as boolean,
{
setShowThankYouMessage: (_, { showThankYouMessage }) => showThankYouMessage,
},
],
openChoice: [
null as string | null,
{
setOpenChoice: (_, { openChoice }) => openChoice,
},
],
surveyResponse: [
[] as string[],
{
handleChoiceChange: (state, { choice, isAdded }) =>
isAdded ? [...state, choice] : state.filter((c: string) => c !== choice),
},
],
}),
listeners(({ actions, values, props }) => ({
/** When surveyId is set, get the list of surveys for the user */
setSurveyId: () => {},
/** Callback for the surveys response. Filter it to the surveyId and set the survey */
handleSurveys: ({ surveys }) => {
const survey = surveys.find((s: Survey) => s.id === props.surveyId)
if (survey) {
posthog.capture('survey shown', {
$survey_id: props.surveyId,
})
actions.setSurvey(survey)

if (survey.appearance?.thankYouMessageHeader) {
actions.setThankYouMessage(survey.appearance?.thankYouMessageHeader)
}
}
},
/** When the survey response is sent, capture the response and show the thank you message */
handleSurveyResponse: () => {
const payload = {
$survey_id: props.surveyId,
$survey_response: values.surveyResponse,
}
if (values.openChoice) {
payload.$survey_response.push(values.openChoice)
}
posthog.capture('survey sent', payload)

actions.setShowThankYouMessage(true)
setTimeout(() => actions.setSurvey(null as unknown as Survey), 5000)
},
})),
afterMount(({ actions }) => {
/** When the logic is mounted, set the surveyId from the props */
posthog.getSurveys((surveys) => actions.handleSurveys(surveys as unknown as Survey[]))
}),
])
2 changes: 2 additions & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export const FEATURE_FLAGS = {
WEB_REVENUE_TRACKING: 'web-revenue-tracking', // owner: @robbie-c #team-web-analytics
LLM_OBSERVABILITY: 'llm-observability', // owner: #team-ai-product-manager
ONBOARDING_SESSION_REPLAY_SEPERATE_STEP: 'onboarding-session-replay-separate-step', // owner: @joshsny #team-growth
EXPERIMENT_INTERVAL_TIMESERIES: 'experiments-interval-timeseries', // owner: @jurajmajerik #team-experiments
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

Expand Down Expand Up @@ -316,6 +317,7 @@ export const SESSION_REPLAY_MINIMUM_DURATION_OPTIONS: LemonSelectOptions<number
]

export const UNSUBSCRIBE_SURVEY_ID = '018b6e13-590c-0000-decb-c727a2b3f462'
export const SESSION_RECORDING_OPT_OUT_SURVEY_ID = '0194a763-9a13-0000-8088-32b52acf7156'

export const TAILWIND_BREAKPOINTS = {
sm: 526,
Expand Down
Loading

0 comments on commit fa0ac07

Please sign in to comment.