From 039d09b18b54ecb0f33cf447f9eee6637efa67ff Mon Sep 17 00:00:00 2001 From: Rafael Audibert <32079912+rafaeelaudibert@users.noreply.github.com> Date: Tue, 11 Feb 2025 19:12:49 -0300 Subject: [PATCH] feat: Add AI Regex Helper to path cleaning (#28512) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .../PathCleanFilterAddItemButton.tsx | 31 ++-- .../PathCleanFilters/PathCleanFilterItem.tsx | 21 ++- .../PathCleanFilters/PathRegexModal.tsx | 103 +++++++++++++ .../PathCleanFilters/PathRegexPopover.tsx | 73 --------- frontend/src/lib/constants.tsx | 1 + .../AiRegexHelper/AiRegexHelper.tsx | 145 ++++++++++++------ .../AiRegexHelper/aiRegexHelperLogic.ts | 42 ++--- .../settings/environment/ReplayTriggers.tsx | 40 ++--- 8 files changed, 260 insertions(+), 196 deletions(-) create mode 100644 frontend/src/lib/components/PathCleanFilters/PathRegexModal.tsx delete mode 100644 frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx index ffb3b5bc54b09..0aed28232c188 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterAddItemButton.tsx @@ -1,11 +1,10 @@ import { IconPlus } from '@posthog/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Popover } from 'lib/lemon-ui/Popover/Popover' import { useState } from 'react' import { PathCleaningFilter } from '~/types' -import { PathRegexPopover } from './PathRegexPopover' +import { PathRegexModal } from './PathRegexModal' type PathCleanFilterAddItemButtonProps = { onAdd: (filter: PathCleaningFilter) => void @@ -14,22 +13,18 @@ type PathCleanFilterAddItemButtonProps = { export function PathCleanFilterAddItemButton({ onAdd }: PathCleanFilterAddItemButtonProps): JSX.Element { const [visible, setVisible] = useState(false) return ( - setVisible(false)} - overlay={ - { - onAdd(filter) - setVisible(false) - }} - onCancel={() => setVisible(false)} - isNew - /> - } - > + <> + setVisible(false)} + onSave={(filter: PathCleaningFilter) => { + onAdd(filter) + setVisible(false) + }} + /> + setVisible(!visible)} + onClick={() => setVisible(true)} type="secondary" size="small" icon={} @@ -37,6 +32,6 @@ export function PathCleanFilterAddItemButton({ onAdd }: PathCleanFilterAddItemBu > Add rule - + ) } diff --git a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx index 9288082ca5d72..3582ed7f3bdff 100644 --- a/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx +++ b/frontend/src/lib/components/PathCleanFilters/PathCleanFilterItem.tsx @@ -1,14 +1,14 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { IconArrowCircleRight } from '@posthog/icons' -import { LemonSnack, Popover, Tooltip } from '@posthog/lemon-ui' +import { LemonSnack, Tooltip } from '@posthog/lemon-ui' import clsx from 'clsx' import { isValidRegexp } from 'lib/utils/regexp' import { useState } from 'react' import { PathCleaningFilter } from '~/types' -import { PathRegexPopover } from './PathRegexPopover' +import { PathRegexModal } from './PathRegexModal' interface PathCleanFilterItem { filter: PathCleaningFilter @@ -24,21 +24,18 @@ export function PathCleanFilterItem({ filter, onChange, onRemove }: PathCleanFil const isInvalidRegex = !isValidRegexp(regex) return ( - setVisible(false)} - overlay={ - + {visible && ( + setVisible(false)} onSave={(filter: PathCleaningFilter) => { onChange(filter) setVisible(false) }} - onCancel={() => setVisible(false)} /> - } - > - {/* required for popover placement */} + )}
-
+ ) } diff --git a/frontend/src/lib/components/PathCleanFilters/PathRegexModal.tsx b/frontend/src/lib/components/PathCleanFilters/PathRegexModal.tsx new file mode 100644 index 0000000000000..4ef9fc1042b48 --- /dev/null +++ b/frontend/src/lib/components/PathCleanFilters/PathRegexModal.tsx @@ -0,0 +1,103 @@ +import { LemonButton, LemonInput, LemonModal, Link } from '@posthog/lemon-ui' +import { FEATURE_FLAGS } from 'lib/constants' +import { isValidRegexp } from 'lib/utils/regexp' +import { useState } from 'react' +import { AiRegexHelperButton } from 'scenes/session-recordings/components/AiRegexHelper/AiRegexHelper' +import { AiRegexHelper } from 'scenes/session-recordings/components/AiRegexHelper/AiRegexHelper' + +import { PathCleaningFilter } from '~/types' + +import { FlaggedFeature } from '../FlaggedFeature' + +export interface PathRegexModalProps { + isOpen: boolean + onSave: (filter: PathCleaningFilter) => void + onClose: () => void + filter?: PathCleaningFilter +} + +export function PathRegexModal({ filter, isOpen, onSave, onClose }: PathRegexModalProps): JSX.Element { + const [alias, setAlias] = useState(filter?.alias ?? '') + const [regex, setRegex] = useState(filter?.regex ?? '') + + const isNew = !filter + const disabledReason = !alias + ? 'Alias is required' + : !regex + ? 'Regex is required' + : !isValidRegexp(regex) + ? 'Malformed regex' + : null + + return ( + + + {isNew ? Add Path Cleaning Rule : Edit Path Cleaning Rule} + + + +
+
+
+ Alias + setAlias(alias)} + onPressEnter={() => false} + /> +
+ We suggest you use <id> or <slug> to indicate a + dynamic part of the path. +
+
+
+ Regex + setRegex(regex)} + onPressEnter={() => false} + /> +

+ + Example:{' '} + + /merchant/\d+/dashboard$ + {' '} + (no need to escape slashes) + {' '} +
+ + We use the{' '} + + re2 + {' '} + syntax. + +

+
+
+ +
+ + + + + +
+ + Cancel + + onSave({ alias, regex })} + disabledReason={disabledReason} + > + Save + +
+
+
+
+
+ ) +} diff --git a/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx b/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx deleted file mode 100644 index c6657214499c4..0000000000000 --- a/frontend/src/lib/components/PathCleanFilters/PathRegexPopover.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' -import { isValidRegexp } from 'lib/utils/regexp' -import { useState } from 'react' - -import { PathCleaningFilter } from '~/types' - -interface PathRegexPopoverProps { - filter?: PathCleaningFilter - onSave: (filter: PathCleaningFilter) => void - onCancel: () => void - /** Wether we're editing an existing filter or adding a new one */ - isNew?: boolean -} - -export function PathRegexPopover({ filter = {}, onSave, onCancel, isNew = false }: PathRegexPopoverProps): JSX.Element { - const [alias, setAlias] = useState(filter.alias) - const [regex, setRegex] = useState(filter.regex) - - const disabledReason = !alias - ? 'Alias is required' - : !regex - ? 'Regex is required' - : !isValidRegexp(regex) - ? 'Malformed regex' - : null - - return ( -
- {isNew ? Add Path Cleaning Rule : Edit Path Cleaning Rule} - -
-
- Alias - setAlias(alias)} onPressEnter={() => false} /> -
- We suggest you use <id> or <slug> to indicate a dynamic - part of the path. -
-
-
- Regex - setRegex(regex)} onPressEnter={() => false} /> -

- - Example:{' '} - - /merchant/\d+/dashboard$ - {' '} - (no need to escape slashes) - {' '} -
- - We use the{' '} - - re2 - {' '} - syntax. - -

-
-
- -
- - Cancel - - onSave({ alias, regex })} disabledReason={disabledReason}> - Save - -
-
- ) -} diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 9def3543d8865..de8afaffe60db 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -243,6 +243,7 @@ export const FEATURE_FLAGS = { WEB_ANALYTICS_IMPROVED_PATH_CLEANING: 'web-analytics-improved-path-cleaning', // owner: @rafaeelaudibert #team-web-analytics EXPERIMENTAL_DASHBOARD_ITEM_RENDERING: 'experimental-dashboard-item-rendering', // owner: @thmsobrmlr #team-product-analytics RECORDINGS_AI_FILTER: 'recordings-ai-filter', // owner: @veryayskiy #team-replay + PATH_CLEANING_AI_REGEX: 'path-cleaning-ai-regex', // owner: @rafaeelaudibert #team-web-analytics } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/scenes/session-recordings/components/AiRegexHelper/AiRegexHelper.tsx b/frontend/src/scenes/session-recordings/components/AiRegexHelper/AiRegexHelper.tsx index 71c2569ee97c1..f6b2475907034 100644 --- a/frontend/src/scenes/session-recordings/components/AiRegexHelper/AiRegexHelper.tsx +++ b/frontend/src/scenes/session-recordings/components/AiRegexHelper/AiRegexHelper.tsx @@ -2,87 +2,134 @@ * @fileoverview A component that helps you to generate regex for your settings using Max AI */ -import { IconCopy, IconPlus } from '@posthog/icons' +import { IconAI, IconCopy, IconPlus } from '@posthog/icons' import { LemonBanner, LemonButton, LemonModal, LemonTextArea } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' +import posthog from 'posthog-js' import { maxGlobalLogic } from 'scenes/max/maxGlobalLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { AIConsentPopoverWrapper } from 'scenes/settings/organization/AIConsentPopoverWrapper' -import { AiConsentPopover } from '../AiConsentPopover' import { aiRegexHelperLogic } from './aiRegexHelperLogic' -export function AiRegexHelper({ type }: { type: 'trigger' | 'blocklist' }): JSX.Element { - const logic = aiRegexHelperLogic() - const { isOpen, input, generatedRegex, error, isLoading } = useValues(logic) - const { setInput, handleGenerateRegex, handleApplyRegex, onClose, handleCopyToClipboard } = useActions(logic) - const { dataProcessingAccepted } = useValues(maxGlobalLogic) +type AiRegexHelperProps = { + onApply: (regex: string) => void +} + +export function AiRegexHelper({ onApply }: AiRegexHelperProps): JSX.Element { + const { isOpen, input, generatedRegex, error, isLoading } = useValues(aiRegexHelperLogic) + const { setInput, handleGenerateRegex, onClose, handleCopyToClipboard } = useActions(aiRegexHelperLogic) + const { dataProcessingAccepted, dataProcessingApprovalDisabledReason } = useValues(maxGlobalLogic) + + const { preflight } = useValues(preflightLogic) + const aiAvailable = preflight?.openai_available + + const disabledReason = !aiAvailable + ? 'To use AI features, set environment variable OPENAI_API_KEY for this instance of PostHog' + : !dataProcessingAccepted + ? dataProcessingApprovalDisabledReason || 'You must accept the data processing agreement to use AI features' + : isLoading + ? 'Generating...' + : !input.length + ? 'Provide a prompt first' + : null return ( <> Explain your regex in natural language: setInput(value)} /> - -
- {!generatedRegex && ( - - Cancel - - )} - - {generatedRegex ? 'Regenerate' : 'Generate Regex'} - -
- {generatedRegex && ( -
-

Your regex is:

-
- - {generatedRegex} - -
- } - /> +
+ {generatedRegex && ( +
+

Your regex is:

+
+ + {generatedRegex} + +
+ } + /> +
-
- - Cancel + )} + +
+ + Close + + + + + {generatedRegex ? 'Regenerate' : 'Generate Regex'} + + + {generatedRegex && ( { - handleApplyRegex(type) + posthog.capture('path_cleaning_regex_ai_applied', { + prompt: input, + regex: generatedRegex, + }) + onApply(generatedRegex) + onClose() }} tooltip="Apply" icon={} > Apply -
+ )}
- )} - {error && ( - - {error} - - )} + + {error && {error}} +
) } + +export function AiRegexHelperButton(): JSX.Element { + const { setIsOpen } = useActions(aiRegexHelperLogic) + const { dataProcessingAccepted, dataProcessingApprovalDisabledReason } = useValues(maxGlobalLogic) + + const disabledReason = !dataProcessingAccepted + ? dataProcessingApprovalDisabledReason || 'You must accept the data processing agreement to use AI features' + : null + + return ( + + } + onClick={() => { + setIsOpen(true) + posthog.capture('ai_regex_helper_open') + }} + disabledReason={disabledReason} + > + Help me with Regex + + + ) +} diff --git a/frontend/src/scenes/session-recordings/components/AiRegexHelper/aiRegexHelperLogic.ts b/frontend/src/scenes/session-recordings/components/AiRegexHelper/aiRegexHelperLogic.ts index ad45210f9c148..657f78915c406 100644 --- a/frontend/src/scenes/session-recordings/components/AiRegexHelper/aiRegexHelperLogic.ts +++ b/frontend/src/scenes/session-recordings/components/AiRegexHelper/aiRegexHelperLogic.ts @@ -1,22 +1,17 @@ -import { actions, connect, kea, listeners, path, reducers } from 'kea' +import { actions, kea, listeners, path, reducers } from 'kea' import api from 'lib/api' import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { copyToClipboard } from 'lib/utils/copyToClipboard' import posthog from 'posthog-js' -import { replayTriggersLogic } from 'scenes/settings/environment/replayTriggersLogic' - -import { SessionReplayUrlTriggerConfig } from '~/types' import type { aiRegexHelperLogicType } from './aiRegexHelperLogicType' export const aiRegexHelperLogic = kea([ - connect(replayTriggersLogic), path(['lib', 'components', 'AiRegexHelper', 'aiRegexHelperLogic']), actions({ setIsOpen: (isOpen: boolean) => ({ isOpen }), setInput: (input: string) => ({ input }), handleGenerateRegex: true, - handleApplyRegex: (type: 'trigger' | 'blocklist') => ({ type }), handleCopyToClipboard: true, setIsLoading: (isLoading: boolean) => ({ isLoading }), setGeneratedRegex: (generatedRegex: string) => ({ generatedRegex }), @@ -62,15 +57,22 @@ export const aiRegexHelperLogic = kea([ actions.setError('') actions.setGeneratedRegex('') - const content = await api.recordings.aiRegex(values.input) + try { + const content = await api.recordings.aiRegex(values.input) - if (content.hasOwnProperty('result') && content.result === 'success') { - posthog.capture('ai_regex_helper_generate_regex_success') - actions.setGeneratedRegex(content.data.output) - } - if (content.hasOwnProperty('result') && content.result === 'error') { - posthog.capture('ai_regex_helper_generate_regex_error') - actions.setError(content.data.output) + if (content.hasOwnProperty('result') && content.result === 'success') { + posthog.capture('ai_regex_helper_generate_regex_success') + actions.setGeneratedRegex(content.data.output) + } else if (content.hasOwnProperty('result') && content.result === 'error') { + posthog.capture('ai_regex_helper_generate_regex_error') + actions.setError(content.data.output) + } else { + posthog.capture('ai_regex_helper_generate_regex_unknown_error') + actions.setError('Failed to generate regex. Try again?') + } + } catch { + posthog.capture('ai_regex_helper_generate_regex_unknown_error') + actions.setError('Failed to generate regex. Try again?') } actions.setIsLoading(false) @@ -82,18 +84,6 @@ export const aiRegexHelperLogic = kea([ lemonToast.error('Failed to copy regex to clipboard') } }, - handleApplyRegex: async ({ type }) => { - try { - const payload: SessionReplayUrlTriggerConfig = { url: values.generatedRegex, matching: 'regex' } - if (type === 'trigger') { - await replayTriggersLogic.asyncActions.addUrlTrigger(payload) - } else { - await replayTriggersLogic.asyncActions.addUrlBlocklist(payload) - } - } catch (error) { - lemonToast.error('Failed to apply regex') - } - }, onClose: () => { actions.setIsOpen(false) actions.setInput('') diff --git a/frontend/src/scenes/settings/environment/ReplayTriggers.tsx b/frontend/src/scenes/settings/environment/ReplayTriggers.tsx index 59d6ef9e4ca37..793c65df73e82 100644 --- a/frontend/src/scenes/settings/environment/ReplayTriggers.tsx +++ b/frontend/src/scenes/settings/environment/ReplayTriggers.tsx @@ -1,4 +1,4 @@ -import { IconAI, IconPencil, IconPlus, IconTrash } from '@posthog/icons' +import { IconPencil, IconPlus, IconTrash } from '@posthog/icons' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' @@ -12,9 +12,8 @@ import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonInput } from 'lib/lemon-ui/LemonInput' import { LemonLabel } from 'lib/lemon-ui/LemonLabel' -import posthog from 'posthog-js' -import { AiRegexHelper } from 'scenes/session-recordings/components/AiRegexHelper/AiRegexHelper' -import { aiRegexHelperLogic } from 'scenes/session-recordings/components/AiRegexHelper/aiRegexHelperLogic' +import { lemonToast } from 'lib/lemon-ui/LemonToast' +import { AiRegexHelper, AiRegexHelperButton } from 'scenes/session-recordings/components/AiRegexHelper/AiRegexHelper' import { replayTriggersLogic } from 'scenes/settings/environment/replayTriggersLogic' import { SupportedPlatforms } from 'scenes/settings/environment/SessionRecordingSettings' @@ -29,8 +28,7 @@ function UrlConfigForm({ onCancel: () => void isSubmitting: boolean }): JSX.Element { - const filterLogic = aiRegexHelperLogic() - const { setIsOpen } = useActions(filterLogic) + const { addUrlTrigger, addUrlBlocklist } = useActions(replayTriggersLogic) return (
- - -
We always wrap the URL regex with anchors to avoid unexpected behavior (if you do not). This is @@ -58,18 +53,27 @@ function UrlConfigForm({
- } - onClick={() => { - setIsOpen(true) - posthog.capture('ai_regex_helper_open') + { + try { + const payload: SessionReplayUrlTriggerConfig = { + url: regex, + matching: 'regex', + } + if (type === 'trigger') { + addUrlTrigger(payload) + } else { + addUrlBlocklist(payload) + } + } catch (error) { + lemonToast.error('Failed to apply regex') + } }} - > - Help me with Regex - + /> +
+
Cancel