Skip to content

Commit

Permalink
Merge branch 'master' into re-expose-invites-and-dashboard-collaborators
Browse files Browse the repository at this point in the history
  • Loading branch information
Twixes authored Mar 13, 2024
2 parents f367e4e + a2c5efa commit a0e03ea
Show file tree
Hide file tree
Showing 14 changed files with 571 additions and 147 deletions.
8 changes: 4 additions & 4 deletions cypress/e2e/experiments.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ describe('Experiments', () => {

// Select goal type
cy.get('[data-attr="experiment-goal-type-select"]').click()
cy.contains('Trend').should('be.visible')
cy.contains('Conversion funnel').should('be.visible')
cy.get('.Popover__content').contains('Trend').should('be.visible')
cy.get('.Popover__content').contains('Conversion funnel').should('be.visible')

// Add secondary metric
const secondaryMetricName = `Secondary metric ${Math.floor(Math.random() * 10000000)}`
Expand All @@ -65,8 +65,8 @@ describe('Experiments', () => {
.type(secondaryMetricName)
.should('have.value', secondaryMetricName)
cy.get('[data-attr="metrics-selector"]').click()
cy.contains('Trends').should('be.visible')
cy.contains('Funnels').should('be.visible')
cy.get('.Popover__content').contains('Funnels').should('be.visible')
cy.get('.Popover__content').contains('Trends').should('be.visible')
cy.get('[data-attr="create-annotation-submit"]').click()
cy.contains(secondaryMetricName).should('exist')

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
179 changes: 152 additions & 27 deletions frontend/src/scenes/experiments/ExperimentNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@ import './Experiment.scss'

import { IconPlusSmall, IconTrash } from '@posthog/icons'
import { LemonDivider, LemonInput, LemonTextArea, Tooltip } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { BindLogic, useActions, useValues } from 'kea'
import { Form, Group } from 'kea-forms'
import { ExperimentVariantNumber } from 'lib/components/SeriesGlyph'
import { MAX_EXPERIMENT_VARIANTS } from 'lib/constants'
import { IconChevronRight } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonField } from 'lib/lemon-ui/LemonField'
import React, { useEffect } from 'react'
import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
import { capitalizeFirstLetter } from 'lib/utils'
import React from 'react'
import { insightDataLogic } from 'scenes/insights/insightDataLogic'
import { insightLogic } from 'scenes/insights/insightLogic'

import { Query } from '~/queries/Query/Query'
import { InsightType } from '~/types'

import { EXPERIMENT_INSIGHT_ID } from './constants'
import { experimentLogic } from './experimentLogic'
import { ExperimentInsightCreator } from './MetricSelector'

const Header = (): JSX.Element => {
const { currentFormStep } = useValues(experimentLogic)

const steps = ['Info', 'Goal', 'Code']
const steps = ['Info', 'Goal']

return (
<div className="flex justify-between mb-6">
Expand Down Expand Up @@ -165,41 +174,157 @@ const StepInfo = (): JSX.Element => {
}

const StepGoal = (): JSX.Element => {
return <div>Goal</div>
}
const { experiment, exposureAndSampleSize, experimentInsightType, groupTypes, aggregationLabel } =
useValues(experimentLogic)
const { setExperiment, setNewExperimentInsight, createExperiment } = useActions(experimentLogic)

// insightLogic
const logic = insightLogic({ dashboardItemId: EXPERIMENT_INSIGHT_ID })
const { insightProps } = useValues(logic)

// insightDataLogic
const { query } = useValues(insightDataLogic(insightProps))

const StepCode = (): JSX.Element => {
return <div>Code</div>
return (
<div className="flex flex-col h-screen">
<div className="flex-auto space-y-8">
<div>
<h3>Participant type</h3>
<div>
This sets default aggregation type for all metrics and feature flags. You can change this at any
time by updating the metric or feature flag.
</div>
<LemonDivider />
<LemonRadio
value={
experiment.parameters.aggregation_group_type_index != undefined
? experiment.parameters.aggregation_group_type_index
: -1
}
onChange={(rawGroupTypeIndex) => {
const groupTypeIndex = rawGroupTypeIndex !== -1 ? rawGroupTypeIndex : undefined

setExperiment({
parameters: {
...experiment.parameters,
aggregation_group_type_index: groupTypeIndex ?? undefined,
},
})
setNewExperimentInsight()
}}
options={[
{ value: -1, label: 'Persons' },
...Array.from(groupTypes.values()).map((groupType) => ({
value: groupType.group_type_index,
label: capitalizeFirstLetter(aggregationLabel(groupType.group_type_index).plural),
})),
]}
/>
</div>
<div>
<h3>Goal type</h3>
<LemonDivider />
<LemonRadio
className="space-y-2 -mt-2"
value={experimentInsightType}
onChange={(val) => {
val &&
setNewExperimentInsight({
insight: val,
properties: experiment?.filters?.properties,
})
}}
options={[
{
value: InsightType.FUNNELS,
label: (
<div className="translate-y-2">
<div>Conversion funnel</div>
<div className="text-xs text-muted">
Track how many people complete a sequence of actions and/or events
</div>
</div>
),
},
{
value: InsightType.TRENDS,
label: (
<div className="translate-y-2">
<div>Trend</div>
<div className="text-xs text-muted">
Track a cumulative total count of a specific event or action
</div>
</div>
),
},
]}
/>
</div>
<div>
<h3>Goal criteria</h3>
<div>
{experimentInsightType === InsightType.FUNNELS
? "Create the funnel where you'd like to see an increased conversion rate."
: 'Create a trend goal to track change in a single metric.'}
</div>
<LemonDivider />
<div className="p-4 border rounded mt-4 w-full lg:w-3/4 bg-white">
<ExperimentInsightCreator insightProps={insightProps} />
</div>
</div>
<div className="pb-4">
<h3>Goal preview</h3>
<div className="mt-4 w-full lg:w-3/4">
<BindLogic logic={insightLogic} props={insightProps}>
<Query query={query} context={{ insightProps }} readOnly />
</BindLogic>
</div>
</div>
</div>
<div className="sticky bottom-0 -mx-4 z-50 pt-1 border-t bg-bg-3000">
<LemonButton
className="px-4 pt-3 pb-4"
type="primary"
onClick={() => {
const { exposure, sampleSize } = exposureAndSampleSize
createExperiment(true, exposure, sampleSize)
}}
>
Create experiment
</LemonButton>
</div>
</div>
)
}

export function ExperimentNext(): JSX.Element {
const { currentFormStep, props } = useValues(experimentLogic)
const { setCurrentFormStep } = useActions(experimentLogic)

useEffect(() => {
setCurrentFormStep(0)
}, [])
const { experimentId, editingExistingExperiment, currentFormStep, props } = useValues(experimentLogic)

const stepComponents = {
0: <StepInfo />,
1: <StepGoal />,
2: <StepCode />,
}
const CurrentStepComponent = (currentFormStep && stepComponents[currentFormStep]) || <StepInfo />

return (
<div>
<Header />
<Form
id="experiment-step"
logic={experimentLogic}
formKey="experiment"
props={props}
enableFormOnSubmit
className="space-y-6 experiment-form"
>
{CurrentStepComponent}
</Form>
</div>
<>
{experimentId === 'new' || editingExistingExperiment ? (
<div>
<Header />
<Form
id="experiment-step"
logic={experimentLogic}
formKey="experiment"
props={props}
enableFormOnSubmit
className="space-y-6 experiment-form"
>
{CurrentStepComponent}
</Form>
</div>
) : (
<h2>{`Experiment ${experimentId} draft/results`}</h2>
)}
</>
)
}
25 changes: 13 additions & 12 deletions frontend/src/scenes/experiments/experimentLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,7 @@ export const experimentLogic = kea<experimentLogicType>([
setNewExperimentInsight: async ({ filters }) => {
let newInsightFilters
const aggregationGroupTypeIndex = values.experiment.parameters?.aggregation_group_type_index
if (filters?.insight === InsightType.FUNNELS) {
newInsightFilters = cleanFilters({
insight: InsightType.FUNNELS,
funnel_viz_type: FunnelVizType.Steps,
date_from: dayjs().subtract(DEFAULT_DURATION, 'day').format('YYYY-MM-DDTHH:mm'),
date_to: dayjs().endOf('d').format('YYYY-MM-DDTHH:mm'),
layout: FunnelLayout.horizontal,
aggregation_group_type_index: aggregationGroupTypeIndex,
...filters,
})
} else {
if (filters?.insight === InsightType.TRENDS) {
const groupAggregation =
aggregationGroupTypeIndex !== undefined
? { math: 'unique_group', math_group_type_index: aggregationGroupTypeIndex }
Expand All @@ -380,6 +370,17 @@ export const experimentLogic = kea<experimentLogicType>([
...eventAddition,
...filters,
})
} else {
newInsightFilters = cleanFilters({
insight: InsightType.FUNNELS,
funnel_viz_type: FunnelVizType.Steps,
date_from: dayjs().subtract(DEFAULT_DURATION, 'day').format('YYYY-MM-DDTHH:mm'),
date_to: dayjs().endOf('d').format('YYYY-MM-DDTHH:mm'),
layout: FunnelLayout.horizontal,
...(aggregationGroupTypeIndex !== undefined && {
aggregation_group_type_index: aggregationGroupTypeIndex,
}),
})
}

actions.updateQuerySource(filtersToQueryNode(newInsightFilters))
Expand Down Expand Up @@ -663,7 +664,7 @@ export const experimentLogic = kea<experimentLogicType>([
experimentInsightType: [
(s) => [s.experiment],
(experiment): InsightType => {
return experiment?.filters?.insight || InsightType.TRENDS
return experiment?.filters?.insight || InsightType.FUNNELS
},
],
isExperimentRunning: [
Expand Down
1 change: 1 addition & 0 deletions mypy-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ posthog/queries/trends/test/test_person.py:0: error: "str" has no attribute "get
posthog/queries/trends/test/test_person.py:0: error: Invalid index type "int" for "HttpResponse"; expected type "str | bytes" [index]
posthog/queries/trends/test/test_person.py:0: error: "str" has no attribute "get" [attr-defined]
posthog/queries/trends/test/test_person.py:0: error: Invalid index type "int" for "HttpResponse"; expected type "str | bytes" [index]
posthog/management/commands/migrate_team.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "BatchExport") [assignment]
posthog/hogql/test/test_query.py:0: error: Argument 1 to "len" has incompatible type "list[Any] | None"; expected "Sized" [arg-type]
posthog/hogql/test/test_query.py:0: error: Value of type "list[QueryTiming] | None" is not indexable [index]
posthog/hogql/test/test_query.py:0: error: Value of type "list[QueryTiming] | None" is not indexable [index]
Expand Down
23 changes: 2 additions & 21 deletions posthog/batch_exports/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@
BatchExportSchema,
BatchExportServiceError,
BatchExportServiceRPCError,
BatchExportServiceScheduleNotFound,
BatchExportWithNoEndNotAllowedError,
backfill_export,
batch_export_delete_schedule,
cancel_running_batch_export_backfill,
disable_and_delete_export,
pause_batch_export,
sync_batch_export,
unpause_batch_export,
Expand All @@ -43,7 +41,6 @@
from posthog.hogql.printer import prepare_ast_for_printing, print_prepared_ast
from posthog.models import (
BatchExport,
BatchExportBackfill,
BatchExportDestination,
BatchExportRun,
Team,
Expand Down Expand Up @@ -436,23 +433,7 @@ def perform_destroy(self, instance: BatchExport):
since we are deleting, we assume that we can recover from this state by finishing the delete operation by calling
instance.save().
"""
temporal = sync_connect()

instance.deleted = True

try:
batch_export_delete_schedule(temporal, str(instance.pk))
except BatchExportServiceScheduleNotFound as e:
logger.warning(
"The Schedule %s could not be deleted as it was not found",
e.schedule_id,
)

instance.save()

for backfill in BatchExportBackfill.objects.filter(batch_export=instance):
if backfill.status == BatchExportBackfill.Status.RUNNING:
cancel_running_batch_export_backfill(temporal, backfill.workflow_id)
disable_and_delete_export(instance)


class BatchExportOrganizationViewSet(BatchExportViewSet):
Expand Down
24 changes: 24 additions & 0 deletions posthog/batch_exports/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import asdict, dataclass, fields
from uuid import UUID

import structlog
import temporalio
from asgiref.sync import async_to_sync
from temporalio.client import (
Expand Down Expand Up @@ -32,6 +33,8 @@
update_schedule,
)

logger = structlog.get_logger(__name__)


class BatchExportField(typing.TypedDict):
"""A field to be queried from ClickHouse.
Expand Down Expand Up @@ -291,6 +294,27 @@ def unpause_batch_export(
backfill_export(temporal, batch_export_id, batch_export.team_id, start_at, end_at)


def disable_and_delete_export(instance: BatchExport):
"""Mark a BatchExport as deleted and delete its Temporal Schedule (including backfills)."""
temporal = sync_connect()

instance.deleted = True

try:
batch_export_delete_schedule(temporal, str(instance.pk))
except BatchExportServiceScheduleNotFound as e:
logger.warning(
"The Schedule %s could not be deleted as it was not found",
e.schedule_id,
)

instance.save()

for backfill in BatchExportBackfill.objects.filter(batch_export=instance):
if backfill.status == BatchExportBackfill.Status.RUNNING:
cancel_running_batch_export_backfill(temporal, backfill.workflow_id)


def batch_export_delete_schedule(temporal: Client, schedule_id: str) -> None:
"""Delete a Temporal Schedule."""
try:
Expand Down
Loading

0 comments on commit a0e03ea

Please sign in to comment.