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: session summary config #19887

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
59efc83
add config for summaries
pauldambra Jan 20, 2024
e4e5dc8
front end guard summaries based on opt in
pauldambra Jan 20, 2024
1add22e
use config in the backend
pauldambra Jan 20, 2024
cecacdd
add defaults and validation tests
pauldambra Jan 20, 2024
ef9113d
Update query snapshots
github-actions[bot] Jan 20, 2024
d41d488
Update query snapshots
github-actions[bot] Jan 20, 2024
c218734
Update query snapshots
github-actions[bot] Jan 20, 2024
c6f064c
Update query snapshots
github-actions[bot] Jan 20, 2024
e81a78a
Update query snapshots
github-actions[bot] Jan 20, 2024
8357bda
fiddling
pauldambra Jan 20, 2024
7b77b02
Merge branch 'master' into feat/session-summary-config
pauldambra Jan 21, 2024
c09bf25
update snapshot
pauldambra Jan 21, 2024
d1a1637
Merge branch 'master' into feat/session-summary-config
daibhin Jan 25, 2024
752a98a
fix tests
daibhin Jan 25, 2024
685979a
Update query snapshots
github-actions[bot] Jan 25, 2024
ec1bc24
start adding important user properties
daibhin Jan 25, 2024
2dba074
feat: Add `LemonSlider` and use it in flag rollout conditions (#19958)
Twixes Jan 25, 2024
b9ed2be
fix(frontend): Fix clipped date picker (#19972)
Twixes Jan 25, 2024
872a830
feat(insights): pretty print more sql (#19963)
mariusandra Jan 26, 2024
4e491fb
chore: Add eslint no-useless-rename rule (#19966)
webjunkie Jan 26, 2024
6338662
fix(web-analytics): Add more debugging when test filters do not pass …
robbie-c Jan 26, 2024
123f26b
feat(insights): actor breakdown options in data table (#19968)
mariusandra Jan 26, 2024
be626f6
fix(web-analytics): validate web analytics filters from url (#19967)
robbie-c Jan 26, 2024
9db37a9
chore: make PropertySelect component generic (#19971)
daibhin Jan 26, 2024
5d08ff5
chore: LemonButton cleanup (#19579)
daibhin Jan 26, 2024
330631b
working user property select
daibhin Jan 26, 2024
ee97cc0
Merge branch 'master' into feat/session-summary-config
daibhin Jan 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function RecordingsLists(): JSX.Element {
recordingsCount,
sessionSummaryLoading,
sessionBeingSummarized,
sessionSummaryOptedIn,
} = useValues(sessionRecordingsPlaylistLogic)
const {
setSelectedRecordingId,
Expand All @@ -115,9 +116,11 @@ function RecordingsLists(): JSX.Element {
setFilters(defaultPageviewPropertyEntityFilter(filters, property, value))
}

const onSummarizeClick = (recording: SessionRecordingType): void => {
summarizeSession(recording.id)
}
const onSummarizeClick = sessionSummaryOptedIn
? (recording: SessionRecordingType): void => {
summarizeSession(recording.id)
}
: null

const lastScrollPositionRef = useRef(0)
const contentRef = useRef<HTMLDivElement | null>(null)
Expand Down Expand Up @@ -255,7 +258,7 @@ function RecordingsLists(): JSX.Element {
onPropertyClick={onPropertyClick}
isActive={activeSessionRecordingId === rec.id}
pinned={false}
summariseFn={onSummarizeClick}
summariseFn={onSummarizeClick ?? undefined}
sessionSummaryLoading={
sessionSummaryLoading && sessionBeingSummarized === rec.id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { objectClean, objectsEqual } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import posthog from 'posthog-js'
import { teamLogic } from 'scenes/teamLogic'

import {
AnyPropertyFilter,
Expand Down Expand Up @@ -229,6 +230,8 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
['featureFlags'],
playerSettingsLogic,
['autoplayDirection', 'hideViewedRecordings'],
teamLogic,
['currentTeam'],
],
}),
actions({
Expand Down Expand Up @@ -263,7 +266,7 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
loaders(({ props, values, actions }) => ({
sessionSummary: {
summarizeSession: async ({ id }): Promise<SessionSummaryResponse | null> => {
if (!id) {
if (!id || !values.sessionSummaryOptedIn) {
return null
}
const response = await api.recordings.summarize(id)
Expand Down Expand Up @@ -548,6 +551,10 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
},
})),
selectors({
sessionSummaryOptedIn: [
(s) => [s.currentTeam],
(currentTeam) => currentTeam?.session_replay_config?.ai_summary?.opt_in ?? false,
],
logicProps: [() => [(_, props) => props], (props): SessionRecordingPlaylistLogicProps => props],
shouldShowEmptyState: [
(s) => [
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/scenes/settings/SettingsMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {
ProjectVariables,
WebSnippet,
} from './project/ProjectSettings'
import { ReplayAuthorizedDomains, ReplayCostControl, ReplayGeneral } from './project/SessionRecordingSettings'
import {
ReplayAuthorizedDomains,
ReplayCostControl,
ReplayGeneral,
ReplaySummarySettings,
} from './project/SessionRecordingSettings'
import { SettingPersonsOnEvents } from './project/SettingPersonsOnEvents'
import { SlackIntegration } from './project/SlackIntegration'
import { SurveySettings } from './project/SurveySettings'
Expand Down Expand Up @@ -167,6 +172,12 @@ export const SettingsMap: SettingSection[] = [
AvailableFeature.RECORDING_DURATION_MINIMUM,
],
},
{
id: 'replay-ai-summary',
title: 'AI Recording Summary',
component: <ReplaySummarySettings />,
flag: 'AI_SESSION_SUMMARY',
},
],
},
{
Expand Down
155 changes: 153 additions & 2 deletions frontend/src/scenes/settings/project/SessionRecordingSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { LemonButton, LemonSelect, LemonSwitch, LemonTag, Link } from '@posthog/
import { useActions, useValues } from 'kea'
import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList'
import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { EventSelect } from 'lib/components/EventSelect/EventSelect'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FlagSelector } from 'lib/components/FlagSelector'
import { PropertySelect } from 'lib/components/PropertySelect/PropertySelect'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { FEATURE_FLAGS, SESSION_REPLAY_MINIMUM_DURATION_OPTIONS } from 'lib/constants'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { IconCancel } from 'lib/lemon-ui/icons'
import { IconAutoAwesome, IconCancel, IconPlus, IconSelectEvents } from 'lib/lemon-ui/icons'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

import { AvailableFeature } from '~/types'
import { AvailableFeature, SessionRecordingSummaryConfig } from '~/types'

export function ReplayGeneral(): JSX.Element {
const { updateCurrentTeam } = useActions(teamLogic)
Expand Down Expand Up @@ -214,6 +217,154 @@ export function ReplayAuthorizedDomains(): JSX.Element {
)
}

export function ReplaySummarySettings(): JSX.Element | null {
const { updateCurrentTeam } = useActions(teamLogic)

const { currentTeam } = useValues(teamLogic)

if (!currentTeam) {
return null
}

const defaultConfig = {
opt_in: false,
preferred_events: [],
excluded_events: ['$feature_flag_called'],
included_event_properties: ['elements_chain', '$window_id', '$current_url', '$event_type'],
important_user_properties: [],
}
const sessionReplayConfig = currentTeam.session_replay_config || {}
const currentConfig: SessionRecordingSummaryConfig = sessionReplayConfig.ai_summary || defaultConfig

const updateSummaryConfig = (summaryConfig: SessionRecordingSummaryConfig): void => {
updateCurrentTeam({
session_replay_config: { ...sessionReplayConfig, ai_summary: summaryConfig },
})
}

return (
<div className="flex flex-col gap-2">
<div>
<LemonButton type="secondary" onClick={() => updateSummaryConfig(defaultConfig)}>
Reset to default
</LemonButton>
</div>
<div>
<p>
We use Open AI to summarise sessions. No data is sent to OpenAI without an explicit instruction to
do so. Only by clicking the <IconAutoAwesome /> "Summary" button will selected event data be shared
with a third party. We only send the data selected below.{' '}
<strong>Data submitted is not used to train Open AI's models</strong>
</p>
<LemonSwitch
checked={currentConfig.opt_in}
onChange={(checked) => {
updateSummaryConfig({
...currentConfig,
opt_in: checked,
})
}}
bordered
label="Opt in to enable AI suggested summaries"
/>
</div>
{currentConfig.opt_in && (
<>
<div>
<h3 className="flex items-center gap-2">
<IconSelectEvents className="text-lg" />
Preferred events
</h3>
<p>
These events are treated as more interesting when generating a summary. We recommend you
include events that represent value for your user
</p>
<EventSelect
onChange={(includedEvents) => {
updateSummaryConfig({
...currentConfig,
preferred_events: includedEvents,
})
}}
selectedEvents={currentConfig.preferred_events || []}
addElement={
<LemonButton size="small" type="secondary" icon={<IconPlus />} sideIcon={null}>
Add event
</LemonButton>
}
/>
</div>
<div>
<h3 className="flex items-center gap-2">
<IconSelectEvents className="text-lg" />
Excluded events
</h3>
<p>These events are never submitted even when they are present in the session.</p>
<EventSelect
onChange={(excludedEvents) => {
updateSummaryConfig({
...currentConfig,
excluded_events: excludedEvents,
})
}}
selectedEvents={currentConfig.excluded_events || []}
addElement={
<LemonButton size="small" type="secondary" icon={<IconPlus />} sideIcon={null}>
Exclude event
</LemonButton>
}
/>
</div>
<div>
<h3 className="flex items-center gap-2">
<IconSelectEvents className="text-lg" />
Included event properties
</h3>
<p>
We always send the event name and timestamp. The only event data sent are values of the
properties selected here.
</p>
<PropertySelect
taxonomicFilterGroup={TaxonomicFilterGroupType.EventProperties}
sortable={false}
onChange={(properties: string[]) => {
updateSummaryConfig({
...currentConfig,
included_event_properties: properties,
})
}}
selectedProperties={currentConfig.included_event_properties || []}
addText="Add property"
/>
</div>
<div>
<h3 className="flex items-center gap-2">
<IconSelectEvents className="text-lg" />
Important user properties
</h3>
<p>
We always send the first and last seen dates. The only user data sent are values of the
properties selected here.
</p>
<PropertySelect
taxonomicFilterGroup={TaxonomicFilterGroupType.PersonProperties}
sortable={false}
onChange={(properties) => {
updateSummaryConfig({
...currentConfig,
important_user_properties: properties,
})
}}
selectedProperties={currentConfig.important_user_properties || []}
addText="Add property"
/>
</div>
</>
)}
</div>
)
}

export function ReplayCostControl(): JSX.Element | null {
const { updateCurrentTeam } = useActions(teamLogic)
const { currentTeam } = useValues(teamLogic)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type SettingId =
| 'notifications'
| 'optout'
| 'theme'
| 'replay-ai-summary'

export type Setting = {
id: SettingId
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ export interface CorrelationConfigType {
excluded_event_names?: string[]
}

export interface SessionRecordingSummaryConfig {
opt_in: boolean
preferred_events: string[]
excluded_events: string[]
included_event_properties: string[]
important_user_properties: string[]
}

export interface TeamType extends TeamBasicType {
created_at: string
updated_at: string
Expand All @@ -360,7 +368,7 @@ export interface TeamType extends TeamBasicType {
| { recordHeaders?: boolean; recordBody?: boolean }
| undefined
| null
session_replay_config: { record_canvas?: boolean } | undefined | null
session_replay_config: { record_canvas?: boolean; ai_summary?: SessionRecordingSummaryConfig } | undefined | null
autocapture_exceptions_opt_in: boolean
surveys_opt_in?: boolean
autocapture_exceptions_errors_to_ignore: string[]
Expand Down
26 changes: 24 additions & 2 deletions posthog/api/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,33 @@
if not isinstance(value, Dict):
raise exceptions.ValidationError("Must provide a dictionary or None.")

if not all(key in ["record_canvas"] for key in value.keys()):
raise exceptions.ValidationError("Must provide a dictionary with only 'record_canvas' key.")
if not all(key in ["record_canvas", "ai_summary"] for key in value.keys()):
raise exceptions.ValidationError(
"Must provide a dictionary with only 'record_canvas' and 'ai_summary' keys."
)

if "ai_summary" in value:
self.validate_session_replay_ai_summary_config(value["ai_summary"])

return value

def validate_session_replay_ai_summary_config(self, value) -> Dict | None:

Check failure on line 230 in posthog/api/team.py

View workflow job for this annotation

GitHub Actions / Python code quality checks

Missing return statement
if value is not None:
if not isinstance(value, Dict):
raise exceptions.ValidationError("Must provide a dictionary or None.")

allowed_keys = [
"included_event_properties",
"opt_in",
"preferred_events",
"excluded_events",
"important_user_properties",
]
if not all(key in allowed_keys for key in value.keys()):
raise exceptions.ValidationError(
"Must provide a dictionary with only allowed keys: {}".format(allowed_keys)
)

def validate(self, attrs: Any) -> Any:
if "primary_dashboard" in attrs and attrs["primary_dashboard"].team != self.instance:
raise exceptions.PermissionDenied("Dashboard does not belong to this team.")
Expand Down
Loading
Loading