Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdp): improve testing interface #27054

Merged
merged 11 commits into from
Dec 19, 2024
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 12 additions & 7 deletions frontend/src/lib/lemon-ui/LemonButton/More.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { IconEllipsis } from '@posthog/icons'

import { PopoverProps } from '../Popover/Popover'
import { LemonButtonWithDropdown } from '.'
import { LemonButtonProps } from './LemonButton'
import { LemonButtonDropdown, LemonButtonProps } from './LemonButton'

export type MoreProps = Partial<Pick<PopoverProps, 'overlay' | 'placement'>> & LemonButtonProps
export type MoreProps = Partial<Pick<PopoverProps, 'overlay' | 'placement'>> &
LemonButtonProps & { dropdown?: Partial<LemonButtonDropdown> }

export function More({
overlay,
dropdown,
'data-attr': dataAttr,
placement = 'bottom-end',
...buttonProps
Expand All @@ -17,11 +19,14 @@ export function More({
aria-label="more"
data-attr={dataAttr ?? 'more-button'}
icon={<IconEllipsis />}
dropdown={{
placement: placement,
actionable: true,
overlay,
}}
dropdown={
{
placement: placement,
actionable: true,
...dropdown,
overlay,
} as LemonButtonDropdown
}
size="small"
{...buttonProps}
disabled={!overlay}
Expand Down
168 changes: 127 additions & 41 deletions frontend/src/scenes/pipeline/hogfunctions/HogFunctionTest.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { TZLabel } from '@posthog/apps-common'
import { IconInfo, IconX } from '@posthog/icons'
import { LemonButton, LemonLabel, LemonSwitch, LemonTable, LemonTag, Tooltip } from '@posthog/lemon-ui'
import {
LemonButton,
LemonDivider,
LemonLabel,
LemonSwitch,
LemonTable,
LemonTag,
Spinner,
Tooltip,
} from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { More } from 'lib/lemon-ui/LemonButton/More'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { CodeEditorResizeable } from 'lib/monaco/CodeEditorResizable'

Expand Down Expand Up @@ -62,11 +72,25 @@ export function HogFunctionTestPlaceholder({
}

export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element {
const { isTestInvocationSubmitting, testResult, expanded, sampleGlobalsLoading, sampleGlobalsError, type } =
useValues(hogFunctionTestLogic(props))
const { submitTestInvocation, setTestResult, toggleExpanded, loadSampleGlobals } = useActions(
hogFunctionTestLogic(props)
)
const {
isTestInvocationSubmitting,
testResult,
expanded,
sampleGlobalsLoading,
sampleGlobalsError,
type,
savedGlobals,
testInvocation,
} = useValues(hogFunctionTestLogic(props))
const {
submitTestInvocation,
setTestResult,
toggleExpanded,
loadSampleGlobals,
deleteSavedGlobals,
setSampleGlobals,
saveGlobals,
} = useActions(hogFunctionTestLogic(props))

return (
<Form logic={hogFunctionTestLogic} props={props} formKey="testInvocation" enableFormOnSubmit>
Expand All @@ -75,7 +99,10 @@ export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element {
>
<div className="flex items-center gap-2 justify-end">
<div className="flex-1 space-y-2">
<h2 className="mb-0">Testing</h2>
<h2 className="mb-0 flex gap-2 items-center">
<span>Testing</span>
{sampleGlobalsLoading ? <Spinner /> : null}
</h2>
{!expanded &&
(type === 'email' ? (
<p>Click here to test the provider with a sample e-mail</p>
Expand All @@ -87,7 +114,7 @@ export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element {
</div>

{!expanded ? (
<LemonButton type="secondary" onClick={() => toggleExpanded()}>
<LemonButton data-attr="expand-hog-testing" type="secondary" onClick={() => toggleExpanded()}>
Start testing
</LemonButton>
) : (
Expand All @@ -97,46 +124,100 @@ export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element {
type="primary"
onClick={() => setTestResult(null)}
loading={isTestInvocationSubmitting}
data-attr="clear-hog-test-result"
>
Clear test result
</LemonButton>
) : (
<>
<LemonButton
type="secondary"
onClick={loadSampleGlobals}
loading={sampleGlobalsLoading}
tooltip="Find the last event matching filters, and use it to populate the globals below."
>
Refresh globals
</LemonButton>
<LemonField name="mock_async_functions">
{({ value, onChange }) => (
<LemonSwitch
bordered
onChange={onChange}
checked={value}
label={
<Tooltip
title={
<>
When selected, async functions such as `fetch` will not
actually be called but instead will be mocked out with
the fetch content logged instead
</>
}
<More
dropdown={{ closeOnClickInside: false }}
overlay={
<>
<LemonField name="mock_async_functions">
{({ value, onChange }) => (
<LemonSwitch
onChange={(v) => onChange(!v)}
checked={!value}
data-attr="toggle-hog-test-mocking"
className="px-2 py-1"
label={
<Tooltip
title={
<>
When disabled, async functions such as
`fetch` will not be called. Instead they
will be mocked out and logged.
</>
}
>
<span className="flex gap-2">
Make real HTTP requests
<IconInfo className="text-lg" />
</span>
</Tooltip>
}
/>
)}
</LemonField>
<LemonDivider />
<LemonButton
fullWidth
onClick={loadSampleGlobals}
loading={sampleGlobalsLoading}
tooltip="Find the last event matching filters, and use it to populate the globals below."
>
Fetch new event
</LemonButton>
<LemonDivider />
{savedGlobals.map(({ name, globals }, index) => (
<div className="flex w-full justify-between" key={index}>
<LemonButton
data-attr="open-hog-test-data"
key={index}
onClick={() => setSampleGlobals(globals)}
fullWidth
className="flex-1"
>
{name}
</LemonButton>
<LemonButton
data-attr="delete-hog-test-data"
size="small"
icon={<IconX />}
onClick={() => deleteSavedGlobals(index)}
tooltip="Delete saved test data"
/>
</div>
))}
{testInvocation.globals && (
<LemonButton
fullWidth
data-attr="save-hog-test-data"
onClick={() => {
const name = prompt('Name this test data')
if (name) {
saveGlobals(name, JSON.parse(testInvocation.globals))
}
}}
disabledReason={(() => {
try {
JSON.parse(testInvocation.globals)
} catch (e) {
return 'Invalid globals JSON'
}
return undefined
})()}
>
<span className="flex gap-2">
Mock out HTTP requests
<IconInfo className="text-lg" />
</span>
</Tooltip>
}
/>
)}
</LemonField>
Save test data
</LemonButton>
)}
</>
}
/>
<LemonButton
type="primary"
data-attr="test-hog-function"
onClick={submitTestInvocation}
loading={isTestInvocationSubmitting}
>
Expand All @@ -145,7 +226,12 @@ export function HogFunctionTest(props: HogFunctionTestLogicProps): JSX.Element {
</>
)}

<LemonButton icon={<IconX />} onClick={() => toggleExpanded()} tooltip="Hide testing" />
<LemonButton
data-attr="hide-hog-testing"
icon={<IconX />}
onClick={() => toggleExpanded()}
tooltip="Hide testing"
/>
</>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { lemonToast } from '@posthog/lemon-ui'
import equal from 'fast-deep-equal'
import { actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { actions, afterMount, connect, isBreakpoint, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { forms } from 'kea-forms'
import { loaders } from 'kea-loaders'
import { beforeUnload, router } from 'kea-router'
Expand Down Expand Up @@ -231,8 +231,15 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
setUnsavedConfiguration: (configuration: HogFunctionConfigurationType | null) => ({ configuration }),
persistForUnload: true,
setSampleGlobalsError: (error) => ({ error }),
setSampleGlobals: (sampleGlobals: HogFunctionInvocationGlobals | null) => ({ sampleGlobals }),
}),
reducers(({ props }) => ({
sampleGlobals: [
null as HogFunctionInvocationGlobals | null,
{
setSampleGlobals: (_, { sampleGlobals }) => sampleGlobals,
},
],
showSource: [
// Show source by default for blank templates when creating a new function
!!(!props.id && props.templateId?.startsWith('template-blank-')),
Expand Down Expand Up @@ -440,7 +447,9 @@ export const hogFunctionConfigurationLogic = kea<hogFunctionConfigurationLogicTy
}
return globals
} catch (e: any) {
actions.setSampleGlobalsError(e.message ?? errorMessage)
if (!isBreakpoint(e)) {
actions.setSampleGlobalsError(e.message ?? errorMessage)
}
return values.exampleInvocationGlobals
}
},
Expand Down
21 changes: 18 additions & 3 deletions frontend/src/scenes/pipeline/hogfunctions/hogFunctionTestLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducer
import { forms } from 'kea-forms'
import api from 'lib/api'
import { tryJsonParse } from 'lib/utils'
import { getCurrentTeamId } from 'lib/utils/getAppContext'

import { groupsModel } from '~/models/groupsModel'
import { LogEntry } from '~/types'
import { HogFunctionInvocationGlobals, LogEntry } from '~/types'

import { hogFunctionConfigurationLogic, sanitizeConfiguration } from './hogFunctionConfigurationLogic'
import type { hogFunctionTestLogicType } from './hogFunctionTestLogicType'
Expand Down Expand Up @@ -45,18 +46,20 @@ export const hogFunctionTestLogic = kea<hogFunctionTestLogicType>([
],
actions: [
hogFunctionConfigurationLogic({ id: props.id }),
['touchConfigurationField', 'loadSampleGlobalsSuccess', 'loadSampleGlobals'],
['touchConfigurationField', 'loadSampleGlobalsSuccess', 'loadSampleGlobals', 'setSampleGlobals'],
],
})),
actions({
setTestResult: (result: HogFunctionTestInvocationResult | null) => ({ result }),
toggleExpanded: (expanded?: boolean) => ({ expanded }),
saveGlobals: (name: string, globals: HogFunctionInvocationGlobals) => ({ name, globals }),
deleteSavedGlobals: (index: number) => ({ index }),
}),
reducers({
expanded: [
false as boolean,
{
toggleExpanded: (_, { expanded }) => (expanded === undefined ? !_ : expanded),
toggleExpanded: (state, { expanded }) => (expanded === undefined ? !state : expanded),
},
],

Expand All @@ -66,11 +69,23 @@ export const hogFunctionTestLogic = kea<hogFunctionTestLogicType>([
setTestResult: (_, { result }) => result,
},
],

savedGlobals: [
[] as { name: string; globals: HogFunctionInvocationGlobals }[],
{ persist: true, prefix: `${getCurrentTeamId()}__` },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think this is fine. I'd say, lets add some tracking to this. If we find a lot of usage then we can consider a more robust persistence?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a bunch of data-attr-s all over the place and called these things "test data" everywhere. I guess good enough?

{
saveGlobals: (state, { name, globals }) => [...state, { name, globals }],
deleteSavedGlobals: (state, { index }) => state.filter((_, i) => i !== index),
},
],
}),
listeners(({ values, actions }) => ({
loadSampleGlobalsSuccess: () => {
actions.setTestInvocationValue('globals', JSON.stringify(values.sampleGlobals, null, 2))
},
setSampleGlobals: ({ sampleGlobals }) => {
actions.setTestInvocationValue('globals', JSON.stringify(sampleGlobals, null, 2))
},
})),
forms(({ props, actions, values }) => ({
testInvocation: {
Expand Down
1 change: 1 addition & 0 deletions plugin-server/src/cdp/cdp-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export class CdpApi {
id: team.id,
name: team.name,
url: `${this.hub.SITE_URL ?? 'http://localhost:8000'}/project/${team.id}`,
...globals.project,
},
},
compoundConfiguration,
Expand Down
4 changes: 2 additions & 2 deletions posthog/api/hog_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,13 @@ def invocations(self, request: Request, *args, **kwargs):
# Remove the team from the config
configuration.pop("team")

globals = serializer.validated_data["globals"]
hog_globals = serializer.validated_data["globals"]
mock_async_functions = serializer.validated_data["mock_async_functions"]

res = create_hog_invocation_test(
team_id=hog_function.team_id,
hog_function_id=hog_function.id,
globals=globals,
globals=hog_globals,
configuration=configuration,
mock_async_functions=mock_async_functions,
)
Expand Down
Loading