Skip to content

Commit

Permalink
Merge branch 'master' into update-channel-type-to-match-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
robbie-c authored Mar 28, 2024
2 parents 05835e0 + c677074 commit 62d144f
Show file tree
Hide file tree
Showing 27 changed files with 905 additions and 759 deletions.
4 changes: 2 additions & 2 deletions cypress/e2e/dashboard.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ describe('Dashboard', () => {
cy.get('.InsightCard [data-attr=more-button]').first().click()
cy.get('button').contains('Rename').click()

cy.get('[data-attr=modal-prompt]').clear().type('Test Name')
cy.contains('OK').click()
cy.get('[data-attr=insight-name]').clear().type('Test Name')
cy.contains('Submit').click()
cy.contains('Test Name').should('exist')
})

Expand Down
10 changes: 5 additions & 5 deletions ee/session_recordings/ai/error_clustering.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from prometheus_client import Histogram
from django.conf import settings
from posthog.clickhouse.client import sync_execute
from posthog.models import Team, User
from posthog.models import Team
from sklearn.cluster import DBSCAN
import pandas as pd
import numpy as np
Expand All @@ -25,7 +25,7 @@
DBSCAN_MIN_SAMPLES = settings.REPLAY_EMBEDDINGS_CLUSTERING_DBSCAN_MIN_SAMPLES


def error_clustering(team: Team, user: User):
def error_clustering(team: Team):
results = fetch_error_embeddings(team.pk)

if not results:
Expand All @@ -37,7 +37,7 @@ def error_clustering(team: Team, user: User):

CLUSTER_REPLAY_ERRORS_CLUSTER_COUNT.labels(team_id=team.pk).observe(df["cluster"].nunique())

return construct_response(df, team, user)
return construct_response(df, team)


def fetch_error_embeddings(team_id: int):
Expand Down Expand Up @@ -67,9 +67,9 @@ def cluster_embeddings(embeddings):
return dbscan.labels_


def construct_response(df: pd.DataFrame, team: Team, user: User):
def construct_response(df: pd.DataFrame, team: Team):
viewed_session_ids = list(
SessionRecordingViewed.objects.filter(team=team, user=user, session_id__in=df["session_id"].unique())
SessionRecordingViewed.objects.filter(team=team, session_id__in=df["session_id"].unique())
.values_list("session_id", flat=True)
.distinct()
)
Expand Down
9 changes: 8 additions & 1 deletion ee/tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
handle_subscription_value_change,
schedule_all_subscriptions,
)
from .replay import embed_batch_of_recordings_task, generate_recordings_embeddings_batch
from .replay import (
embed_batch_of_recordings_task,
generate_recordings_embeddings_batch,
generate_replay_embedding_error_clusters,
cluster_replay_error_embeddings,
)

# As our EE tasks are not included at startup for Celery, we need to ensure they are declared here so that they are imported by posthog/settings/celery.py

Expand All @@ -19,4 +24,6 @@
"handle_subscription_value_change",
"embed_batch_of_recordings_task",
"generate_recordings_embeddings_batch",
"generate_replay_embedding_error_clusters",
"cluster_replay_error_embeddings",
]
33 changes: 30 additions & 3 deletions ee/tasks/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
ErrorEmbeddingsPreparation,
SessionEventsEmbeddingsPreparation,
)
from ee.session_recordings.ai.error_clustering import error_clustering
from posthog import settings
from posthog.models import Team
from posthog.tasks.utils import CeleryQueue
from django.core.cache import cache

logger = structlog.get_logger(__name__)

Expand Down Expand Up @@ -64,9 +66,34 @@ def generate_recordings_embeddings_batch() -> None:
team_id=team_id,
)
embed_batch_of_recordings_task.si(recordings, int(team_id)).apply_async()
except Team.DoesNotExist:
logger.info(f"[generate_recordings_embeddings_batch] Team {team_id} does not exist. Skipping.")
pass
except Exception as e:
logger.error(f"[generate_recordings_embeddings_batch] Error: {e}.", exc_info=True, error=e)
pass


@shared_task(ignore_result=True)
def generate_replay_embedding_error_clusters() -> None:
for team_id in settings.REPLAY_EMBEDDINGS_ALLOWED_TEAMS:
try:
cluster_replay_error_embeddings.si(int(team_id)).apply_async()
except Exception as e:
logger.error(f"[generate_replay_error_clusters] Error: {e}.", exc_info=True, error=e)
pass


@shared_task(ignore_result=True, queue=CeleryQueue.SESSION_REPLAY_EMBEDDINGS.value)
def cluster_replay_error_embeddings(team_id: int) -> None:
try:
team = Team.objects.get(id=team_id)
clusters = error_clustering(team)

cache.set(f"cluster_errors_{team.pk}", clusters, settings.CACHED_RESULTS_TTL)

logger.info(
f"[generate_replay_error_clusters] Completed for team",
flow="embeddings",
team_id=team_id,
)
except Team.DoesNotExist:
logger.info(f"[generate_replay_error_clusters] Team {team} does not exist. Skipping.")
pass
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 31 additions & 2 deletions frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Link } from '@posthog/lemon-ui'
import { LemonInput, Link } from '@posthog/lemon-ui'
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { LemonButton } from 'lib/lemon-ui/LemonButton'

import { LemonDialog, LemonDialogProps } from './LemonDialog'
import { LemonField } from '../LemonField'
import { LemonDialog, LemonDialogProps, LemonFormDialog, LemonFormDialogProps } from './LemonDialog'

type Story = StoryObj<typeof LemonDialog>
const meta: Meta<typeof LemonDialog> = {
Expand Down Expand Up @@ -98,3 +99,31 @@ Customised.args = {
onClick: () => alert('Organization and all events deleted!'),
},
}

export const Form: StoryObj = (props: LemonFormDialogProps): JSX.Element => {
const onClick = (): void => {
LemonDialog.openForm(props)
}
return (
<div>
<div className="bg-default p-4">
<LemonFormDialog {...props} inline />
</div>
<LemonButton type="primary" onClick={() => onClick()} className="mx-auto mt-2">
Open as modal
</LemonButton>
</div>
)
}
Form.args = {
title: 'This is a test',
initialValues: { name: 'one' },
description: undefined,
tertiaryButton: undefined,
content: (
<LemonField name="name">
<LemonInput placeholder="Please enter the new name" autoFocus />
</LemonField>
),
}
Form.storyName = 'Category - Elements'
68 changes: 62 additions & 6 deletions frontend/src/lib/lemon-ui/LemonDialog/LemonDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { useValues } from 'kea'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { router } from 'kea-router'
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
import { LemonModal, LemonModalProps } from 'lib/lemon-ui/LemonModal'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { createRoot, Root } from 'react-dom/client'

import { LemonDialogFormPropsType, lemonDialogLogic } from './lemonDialogLogic'

export type LemonFormDialogProps = LemonDialogFormPropsType &
Omit<LemonDialogProps, 'primaryButton' | 'secondaryButton' | 'tertiaryButton'> & {
initialValues: Record<string, any>
onSubmit: (values: Record<string, any>) => void | Promise<void>
}

export type LemonDialogProps = Pick<
LemonModalProps,
Expand All @@ -12,6 +21,7 @@ export type LemonDialogProps = Pick<
primaryButton?: LemonButtonProps | null
secondaryButton?: LemonButtonProps | null
tertiaryButton?: LemonButtonProps | null
initialFormValues?: Record<string, any>
content?: ReactNode
onClose?: () => void
onAfterClose?: () => void
Expand All @@ -25,6 +35,7 @@ export function LemonDialog({
tertiaryButton,
secondaryButton,
content,
initialFormValues,
closeOnNavigate = true,
footer,
...props
Expand Down Expand Up @@ -90,7 +101,43 @@ export function LemonDialog({
)
}

LemonDialog.open = (props: LemonDialogProps) => {
export const LemonFormDialog = ({
initialValues = {},
onSubmit,
errors,
...props
}: LemonFormDialogProps): JSX.Element => {
const logic = lemonDialogLogic({ errors })
const { form, isFormValid, formValidationErrors } = useValues(logic)
const { setFormValues } = useActions(logic)

const firstError = useMemo(() => Object.values(formValidationErrors)[0] as string, [formValidationErrors])

const primaryButton: LemonDialogProps['primaryButton'] = {
type: 'primary',
children: 'Submit',
htmlType: 'submit',
onClick: () => void onSubmit(form),
disabledReason: !isFormValid ? firstError : undefined,
}

const secondaryButton: LemonDialogProps['secondaryButton'] = {
type: 'secondary',
children: 'Cancel',
}

useEffect(() => {
setFormValues(initialValues)
}, [])

return (
<Form logic={lemonDialogLogic} formKey="form">
<LemonDialog {...props} primaryButton={primaryButton} secondaryButton={secondaryButton} />
</Form>
)
}

function createAndInsertRoot(): { root: Root; onDestroy: () => void } {
const div = document.createElement('div')
const root = createRoot(div)
function destroy(): void {
Expand All @@ -101,6 +148,15 @@ LemonDialog.open = (props: LemonDialogProps) => {
}

document.body.appendChild(div)
root.render(<LemonDialog {...props} onAfterClose={destroy} />)
return
return { root, onDestroy: destroy }
}

LemonDialog.open = (props: LemonDialogProps) => {
const { root, onDestroy } = createAndInsertRoot()
root.render(<LemonDialog {...props} onAfterClose={onDestroy} />)
}

LemonDialog.openForm = (props: LemonFormDialogProps) => {
const { root, onDestroy } = createAndInsertRoot()
root.render(<LemonFormDialog {...props} onAfterClose={onDestroy} />)
}
25 changes: 25 additions & 0 deletions frontend/src/lib/lemon-ui/LemonDialog/lemonDialogLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { kea, path, props } from 'kea'
import { forms } from 'kea-forms'

import type { lemonDialogLogicType } from './lemonDialogLogicType'

export type LemonDialogFormPropsType = {
errors?: Record<string, (value: string) => string | undefined>
}

export const lemonDialogLogic = kea<lemonDialogLogicType>([
path(['components', 'lemon-dialog', 'lemonDialogLogic']),
props({} as LemonDialogFormPropsType),
forms(({ props }) => ({
form: {
defaults: {},
errors: (values) => {
const entries = Object.entries(props.errors || []).map(([key, valueOf]) => {
const result = valueOf(values[key])
return [key, result]
})
return Object.fromEntries(entries)
},
},
})),
])
25 changes: 15 additions & 10 deletions frontend/src/models/insightsModel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { LemonDialog, LemonInput } from '@posthog/lemon-ui'
import { actions, connect, kea, listeners, path } from 'kea'
import api from 'lib/api'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
import { promptLogic } from 'lib/logic/promptLogic'
import { teamLogic } from 'scenes/teamLogic'

import { InsightModel } from '~/types'
Expand All @@ -10,7 +11,7 @@ import type { insightsModelType } from './insightsModelType'

export const insightsModel = kea<insightsModelType>([
path(['models', 'insightsModel']),
connect([promptLogic({ key: 'rename-insight' }), teamLogic]),
connect([teamLogic]),
actions(() => ({
renameInsight: (item: InsightModel) => ({ item }),
renameInsightSuccess: (item: InsightModel) => ({ item }),
Expand All @@ -27,17 +28,21 @@ export const insightsModel = kea<insightsModelType>([
})),
listeners(({ actions }) => ({
renameInsight: async ({ item }) => {
promptLogic({ key: 'rename-insight' }).actions.prompt({
LemonDialog.openForm({
title: 'Rename insight',
placeholder: 'Please enter the new name',
value: item.name,
error: 'You must enter name',
success: async (name: string) => {
initialValues: { name: item.name },
content: (
<LemonField name="name">
<LemonInput data-attr="insight-name" placeholder="Please enter the new name" autoFocus />
</LemonField>
),
errors: {
name: (name) => (!name ? 'You must enter a name' : undefined),
},
onSubmit: async ({ name }) => {
const updatedItem = await api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${item.id}`,
{
name,
}
{ name }
)
lemonToast.success(
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { lemonToast } from '@posthog/lemon-ui'
import { eventWithTime } from '@rrweb/types'
import { BuiltLogic, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { beforeUnload } from 'kea-router'
Expand All @@ -10,7 +9,7 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { Scene } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'

import { Breadcrumb, PersonType, RecordingSnapshot, ReplayTabs, SessionRecordingType } from '~/types'
import { Breadcrumb, ReplayTabs } from '~/types'

import {
deduplicateSnapshots,
Expand All @@ -19,23 +18,7 @@ import {
} from '../player/sessionRecordingDataLogic'
import type { sessionRecordingDataLogicType } from '../player/sessionRecordingDataLogicType'
import type { sessionRecordingFilePlaybackLogicType } from './sessionRecordingFilePlaybackLogicType'

export type ExportedSessionRecordingFileV1 = {
version: '2022-12-02'
data: {
person: PersonType | null
snapshotsByWindowId: Record<string, eventWithTime[]>
}
}

export type ExportedSessionRecordingFileV2 = {
version: '2023-04-28'
data: {
id: SessionRecordingType['id']
person: SessionRecordingType['person']
snapshots: RecordingSnapshot[]
}
}
import { ExportedSessionRecordingFileV1, ExportedSessionRecordingFileV2 } from './types'

export const createExportedSessionRecording = (
logic: BuiltLogic<sessionRecordingDataLogicType>,
Expand Down Expand Up @@ -95,7 +78,7 @@ export const parseExportedSessionRecording = (fileData: string): ExportedSession
* in practice, it will only wait for 1-2 retries
* but a timeout is provided to avoid waiting forever when something breaks
*/
const waitForDataLogic = async (playerKey: string): Promise<BuiltLogic<any>> => {
const waitForDataLogic = async (playerKey: string): Promise<BuiltLogic<sessionRecordingDataLogicType>> => {
const maxRetries = 20 // 2 seconds / 100 ms per retry
let retries = 0
let dataLogic = null
Expand Down
Loading

0 comments on commit 62d144f

Please sign in to comment.