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 (