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: create playlists from errors #21037

Merged
merged 10 commits into from
Mar 21, 2024
6 changes: 3 additions & 3 deletions ee/session_recordings/ai/error_clustering.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ def construct_response(df: pd.DataFrame, team: Team, user: User):
clusters = []
for cluster, rows in df.groupby("cluster"):
session_ids = rows["session_id"].unique()
sample = rows.sample(n=1)[["session_id", "input"]].rename(columns={"input": "error"}).to_dict("records")
sample = rows.sample(n=1)[["session_id", "input"]].rename(columns={"input": "error"}).to_dict("records")[0]
Copy link
Member

Choose a reason for hiding this comment

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

silly check do we know we have a non-empty thing to get [0] from at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep good question. The cluster will only be generated if it meets the MIN_SAMPLES so we know that each cluster has at least 10 rows

clusters.append(
{
"cluster": cluster,
"sample": sample,
"session_ids": session_ids,
"sample": sample.get("error"),
"session_ids": np.random.choice(session_ids, size=DBSCAN_MIN_SAMPLES - 1),
"occurrences": rows.size,
"unique_sessions": len(session_ids),
"viewed": len(np.intersect1d(session_ids, viewed_session_ids, assume_unique=True)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const MAX_TITLE_LENGTH = 75
export function SessionRecordingErrors(): JSX.Element {
const { openSessionPlayer } = useActions(sessionPlayerModalLogic)
const { errors, errorsLoading } = useValues(sessionRecordingErrorsLogic)
const { loadErrorClusters } = useActions(sessionRecordingErrorsLogic)
const { loadErrorClusters, createPlaylist } = useActions(sessionRecordingErrorsLogic)

if (errorsLoading) {
return <Spinner />
Expand All @@ -36,7 +36,7 @@ export function SessionRecordingErrors(): JSX.Element {
title: 'Error',
dataIndex: 'cluster',
render: (_, cluster) => {
const displayTitle = parseTitle(cluster.sample.error)
const displayTitle = parseTitle(cluster.sample)
return (
<div title={displayTitle} className="font-semibold text-sm text-default line-clamp-1">
{displayTitle}
Expand Down Expand Up @@ -68,23 +68,40 @@ export function SessionRecordingErrors(): JSX.Element {
title: 'Actions',
render: function Render(_, cluster) {
return (
<LemonButton
to={urls.replaySingle(cluster.sample.session_id)}
onClick={(e) => {
e.preventDefault()
openSessionPlayer({ id: cluster.sample.session_id })
}}
className="p-2 whitespace-nowrap"
type="primary"
>
Watch example
</LemonButton>
<div className="p-2 flex space-x-2">
<LemonButton
to={urls.replaySingle(cluster.session_ids[0])}
onClick={(e) => {
e.preventDefault()
openSessionPlayer({ id: cluster.session_ids[0] })
}}
className="whitespace-nowrap"
type="primary"
>
Watch example
</LemonButton>
<LemonButton
onClick={() => {
createPlaylist(
`Examples of '${parseTitle(cluster.sample)}'`,
cluster.session_ids
)
}}
className="whitespace-nowrap"
type="secondary"
tooltip="Create a playlist of recordings containing this issue"
>
Create playlist
</LemonButton>
</div>
)
},
},
]}
dataSource={errors}
expandable={{ expandedRowRender: (cluster) => <ExpandedError error={cluster.sample.error} /> }}
expandable={{
expandedRowRender: (cluster) => <ExpandedError error={cluster.sample} />,
}}
/>
<SessionPlayerModal />
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { afterMount, kea, path } from 'kea'
import { actions, afterMount, kea, listeners, path } from 'kea'
import { loaders } from 'kea-loaders'
import { router } from 'kea-router'
import api from 'lib/api'
import { urls } from 'scenes/urls'

import { ErrorClusterResponse } from '~/types'

import { createPlaylist } from '../playlist/playlistUtils'
import type { sessionRecordingErrorsLogicType } from './sessionRecordingErrorsLogicType'

export const sessionRecordingErrorsLogic = kea<sessionRecordingErrorsLogicType>([
path(['scenes', 'session-recordings', 'detail', 'sessionRecordingErrorsLogic']),
actions({
createPlaylist: (name: string, sessionIds: string[]) => ({ name, sessionIds }),
}),
loaders(() => ({
errors: [
null as ErrorClusterResponse,
Expand All @@ -19,7 +25,19 @@ export const sessionRecordingErrorsLogic = kea<sessionRecordingErrorsLogicType>(
},
],
})),
listeners(() => ({
createPlaylist: async ({ name, sessionIds }) => {
const playlist = await createPlaylist({ name: name })

if (playlist) {
const samples = sessionIds.slice(0, 10)
await Promise.all(
samples.map((sessionId) => api.recordings.addRecordingToPlaylist(playlist.short_id, sessionId))
)
router.actions.push(urls.replayPlaylist(playlist.short_id))
}
},
})),
afterMount(({ actions }) => {
actions.loadErrorClusters(false)
}),
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,8 +903,9 @@ export interface SessionRecordingsResponse {

export type ErrorCluster = {
cluster: number
sample: { session_id: string; error: string }
sample: string
occurrences: number
session_ids: string[]
unique_sessions: number
viewed: number
}
Expand Down
Loading