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(replay-templates): minor improvements #25778

Merged
merged 20 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3a01e01
template for high activity score
raquelmsmith Oct 23, 2024
c3d0c6b
add template for activity score
raquelmsmith Oct 23, 2024
3a470b8
send an custom event when template used
raquelmsmith Oct 23, 2024
2f860a6
handle unsetting of vars
raquelmsmith Oct 23, 2024
d559925
types and scattershot sorting
pauldambra Oct 24, 2024
639e860
persist set filters
raquelmsmith Oct 24, 2024
6aefd78
Merge branch 'feat/replay-templates-minor-updates' of https://github.…
raquelmsmith Oct 24, 2024
78b1c5c
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
76efc32
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
5fe2829
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
e59b421
make people use the button for a consistent experience
raquelmsmith Oct 24, 2024
63812a7
don't use an enum
raquelmsmith Oct 24, 2024
9f2d6eb
Merge branch 'feat/replay-templates-minor-updates' of https://github.…
raquelmsmith Oct 24, 2024
dc01b8b
update schemas
raquelmsmith Oct 24, 2024
8ac42cb
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
a4fbd57
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 24, 2024
dbbd689
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
de3f680
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 24, 2024
2ed34b1
Update UI snapshots for `chromium` (1)
github-actions[bot] Oct 24, 2024
081a2de
Update UI snapshots for `chromium` (2)
github-actions[bot] Oct 24, 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
47 changes: 17 additions & 30 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10148,6 +10148,21 @@
"required": ["k", "t"],
"type": "object"
},
"RecordingOrder": {
"enum": [
"duration",
"recording_duration",
"inactive_seconds",
"active_seconds",
"start_time",
"console_error_count",
"click_count",
"keypress_count",
"mouse_activity_count",
"activity_score"
],
"type": "string"
},
"RecordingPropertyFilter": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -10237,35 +10252,7 @@
"$ref": "#/definitions/FilterLogicalOperator"
},
"order": {
"anyOf": [
{
"$ref": "#/definitions/DurationType"
},
{
"const": "start_time",
"type": "string"
},
{
"const": "console_error_count",
"type": "string"
},
{
"const": "click_count",
"type": "string"
},
{
"const": "keypress_count",
"type": "string"
},
{
"const": "mouse_activity_count",
"type": "string"
},
{
"const": "activity_score",
"type": "string"
}
]
"$ref": "#/definitions/RecordingOrder"
},
"person_uuid": {
"type": "string"
Expand All @@ -10289,7 +10276,7 @@
"type": "object"
}
},
"required": ["kind", "order"],
"required": ["kind"],
"type": "object"
},
"RecordingsQueryResponse": {
Expand Down
22 changes: 13 additions & 9 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
ChartDisplayType,
CohortPropertyFilter,
CountPerActorMathType,
DurationType,
EventPropertyFilter,
EventType,
FeaturePropertyFilter,
Expand Down Expand Up @@ -311,6 +310,18 @@ export interface RecordingsQueryResponse {
has_next: boolean
}

export type RecordingOrder =
| 'duration'
| 'recording_duration'
| 'inactive_seconds'
| 'active_seconds'
| 'start_time'
| 'console_error_count'
| 'click_count'
| 'keypress_count'
| 'mouse_activity_count'
| 'activity_score'

export interface RecordingsQuery extends DataNode<RecordingsQueryResponse> {
kind: NodeKind.RecordingsQuery
date_from?: string | null
Expand All @@ -324,14 +335,7 @@ export interface RecordingsQuery extends DataNode<RecordingsQueryResponse> {
operand?: FilterLogicalOperator
session_ids?: string[]
person_uuid?: string
order:
| DurationType
| 'start_time'
| 'console_error_count'
| 'click_count'
| 'keypress_count'
| 'mouse_activity_count'
| 'activity_score'
order?: RecordingOrder
limit?: integer
offset?: integer
user_modified_filters?: Record<string, any>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { useNotebookNode } from 'scenes/notebooks/Nodes/NotebookNodeContext'
import { urls } from 'scenes/urls'

import { RecordingsQuery } from '~/queries/schema'
import { RecordingOrder } from '~/queries/schema'
import { RecordingUniversalFilters, ReplayTabs, SessionRecordingType } from '~/types'

import { RecordingsUniversalFilters } from '../filters/RecordingsUniversalFilters'
Expand All @@ -31,7 +31,7 @@ function SortedBy({
filters: RecordingUniversalFilters
setFilters: (filters: Partial<RecordingUniversalFilters>) => void
}): JSX.Element {
const simpleSortingOptions: LemonSelectSection<RecordingsQuery['order']> = {
const simpleSortingOptions: LemonSelectSection<RecordingOrder> = {
options: [
{
value: 'start_time',
Expand All @@ -47,7 +47,7 @@ function SortedBy({
},
],
}
const detailedSortingOptions: LemonSelectSection<RecordingsQuery['order']> = {
const detailedSortingOptions: LemonSelectSection<RecordingOrder> = {
options: [
{
label: 'Longest',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { objectClean, objectsEqual } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'

import { NodeKind, RecordingsQuery, RecordingsQueryResponse } from '~/queries/schema'
import { NodeKind, RecordingOrder, RecordingsQuery, RecordingsQueryResponse } from '~/queries/schema'
import {
EntityTypes,
FilterLogicalOperator,
Expand Down Expand Up @@ -225,17 +225,11 @@ function combineLegacyRecordingFilters(
}
}

function sortRecordings(recordings: SessionRecordingType[], order: RecordingsQuery['order']): SessionRecordingType[] {
const orderKey:
| 'recording_duration'
| 'activity_score'
| 'active_seconds'
| 'inactive_seconds'
| 'console_error_count'
| 'click_count'
| 'keypress_count'
| 'mouse_activity_count'
| 'start_time' = order === 'duration' ? 'recording_duration' : order
function sortRecordings(
recordings: SessionRecordingType[],
order: RecordingsQuery['order'] | 'duration' = 'start_time'
): SessionRecordingType[] {
const orderKey: RecordingOrder = order === 'duration' ? 'recording_duration' : order

return recordings.sort((a, b) => {
const orderA = a[orderKey]
Expand Down Expand Up @@ -741,6 +735,9 @@ export const sessionRecordingsPlaylistLogic = kea<sessionRecordingsPlaylistLogic
if (params.filters && !equal(params.filters, values.filters)) {
actions.setFilters(params.filters)
}
if (params.order && !equal(params.order, values.filters.order)) {
actions.setFilters({ ...values.filters, order: params.order })
}
}
return {
'*': urlToAction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,18 @@ const SingleTemplateVariable = ({
}: RecordingTemplateCardProps & {
variable: ReplayTemplateVariableType
}): JSX.Element | null => {
const { setVariable } = useActions(sessionReplayTemplatesLogic(props))
const { setVariable, resetVariable } = useActions(sessionReplayTemplatesLogic(props))
useMountedLogic(actionsModel)

return variable.type === 'pageview' ? (
<div>
<LemonLabel info={variable.description}>{variable.name}</LemonLabel>
<LemonInput
placeholder={variable.value}
onChange={(e) => setVariable({ ...variable, value: e })}
value={variable.value}
onChange={(e) =>
e ? setVariable({ ...variable, value: e }) : resetVariable({ ...variable, value: undefined })
}
size="small"
/>
</div>
Expand All @@ -91,7 +94,7 @@ const SingleTemplateVariable = ({
rootKey={`session-recordings-${variable.key}`}
group={{
type: FilterLogicalOperator.And,
values: [],
values: variable.filterGroup ? [variable.filterGroup] : [],
}}
taxonomicGroupTypes={
variable.type === 'event'
Expand All @@ -103,9 +106,13 @@ const SingleTemplateVariable = ({
: []
}
onChange={(thisFilterGroup) => {
variable.type === 'flag'
? setVariable({ ...variable, value: (thisFilterGroup.values[0] as FeaturePropertyFilter).key })
: setVariable({ ...variable, filterGroup: thisFilterGroup.values[0] })
if (thisFilterGroup.values.length === 0) {
resetVariable({ ...variable, filterGroup: undefined })
} else if (variable.type === 'flag') {
setVariable({ ...variable, value: (thisFilterGroup.values[0] as FeaturePropertyFilter).key })
} else {
setVariable({ ...variable, filterGroup: thisFilterGroup.values[0] })
}
}}
>
<NestedFilterGroup
Expand All @@ -122,20 +129,20 @@ const SingleTemplateVariable = ({

const TemplateVariables = (props: RecordingTemplateCardProps): JSX.Element => {
const { navigate } = useActions(sessionReplayTemplatesLogic(props))
const { variables, areAnyVariablesTouched } = useValues(sessionReplayTemplatesLogic(props))
const { variables, canApplyFilters } = useValues(sessionReplayTemplatesLogic(props))
return (
<div className="flex flex-col gap-2">
{variables.map((variable) => (
<SingleTemplateVariable key={variable.key} variable={variable} {...props} />
))}
{variables
.filter((v) => !v.noTouch)
.map((variable) => (
<SingleTemplateVariable key={variable.key} variable={variable} {...props} />
))}
<div>
<LemonButton
onClick={() => navigate()}
type="primary"
className="mt-2"
disabledReason={
!areAnyVariablesTouched ? 'Please set a value for at least one variable' : undefined
}
disabledReason={!canApplyFilters ? 'Please set a value for at least one variable' : undefined}
>
Apply filters
</LemonButton>
Expand All @@ -145,14 +152,14 @@ const TemplateVariables = (props: RecordingTemplateCardProps): JSX.Element => {
}

const RecordingTemplateCard = (props: RecordingTemplateCardProps): JSX.Element => {
const { showVariables, hideVariables, navigate } = useActions(sessionReplayTemplatesLogic(props))
const { variablesVisible, editableVariables } = useValues(sessionReplayTemplatesLogic(props))
const { showVariables, hideVariables } = useActions(sessionReplayTemplatesLogic(props))
const { variablesVisible } = useValues(sessionReplayTemplatesLogic(props))

return (
<LemonCard
className="w-80"
onClick={() => {
editableVariables.length > 0 ? showVariables() : navigate()
showVariables()
}}
closeable={variablesVisible}
onClose={hideVariables}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const replayTemplates: ReplayTemplateType[] = [
description: 'Watch all recent replays, and see where users are getting stuck.',
variables: [],
categories: ['More'],
order: 'start_time',
icon: <IconVideoCamera />,
},
{
Expand Down Expand Up @@ -223,4 +224,12 @@ export const replayTemplates: ReplayTemplateType[] = [
categories: ['More'],
icon: <IconPhone />,
},
{
key: 'activity-score',
name: 'Most active users',
description: 'Watch recordings of the most active sessions. Lots of valuable insights, guaranteed!',
order: 'activity_score',
categories: ['More'],
icon: <IconCursorClick />,
},
]
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { actions, events, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import clsx from 'clsx'
import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { router } from 'kea-router'
import posthog from 'posthog-js'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import {
Expand Down Expand Up @@ -64,20 +67,35 @@ export const sessionReplayTemplatesLogic = kea<sessionReplayTemplatesLogicType>(
path(() => ['scenes', 'session-recordings', 'templates', 'sessionReplayTemplatesLogic']),
props({} as ReplayTemplateLogicPropsType),
key((props) => `${props.category}-${props.template.key}`),
connect({
values: [teamLogic, ['currentTeam']],
}),
actions({
setVariables: (variables: ReplayTemplateVariableType[]) => ({ variables }),
setVariables: (variables?: ReplayTemplateVariableType[]) => ({ variables }),
setVariable: (variable: ReplayTemplateVariableType) => ({ variable }),
resetVariable: (variable: ReplayTemplateVariableType) => ({ variable }),
navigate: true,
showVariables: true,
hideVariables: true,
}),
reducers(({ props }) => ({
reducers(({ props, values }) => ({
variables: [
props.template.variables,
props.template.variables ?? [],
{
persist: true,
storageKey: clsx(
'session-recordings.templates.variables',
values.currentTeam?.id,
props.category,
props.template.key
),
},
{
setVariables: (_, { variables }) => variables,
setVariables: (_, { variables }) => variables ?? [],
setVariable: (state, { variable }) =>
state.map((v) => (v.key === variable.key ? { ...variable, touched: true } : v)),
resetVariable: (state, { variable }) =>
state.map((v) => (v.key === variable.key ? { ...variable, touched: false } : v)),
},
],
variablesVisible: [
Expand Down Expand Up @@ -125,21 +143,31 @@ export const sessionReplayTemplatesLogic = kea<sessionReplayTemplatesLogicType>(
return filterGroup
},
],
canApplyFilters: [
(s) => [s.variables, s.areAnyVariablesTouched],
(variables, areAnyVariablesTouched) => areAnyVariablesTouched || variables.length === 0,
],
areAnyVariablesTouched: [
(s) => [s.variables],
(variables) => variables.some((v) => v.touched) || variables.some((v) => v.noTouch),
],
editableVariables: [(s) => [s.variables], (variables) => variables.filter((v) => !v.noTouch)],
}),
listeners(({ values }) => ({
listeners(({ values, props }) => ({
navigate: () => {
posthog.capture('session replay template used', {
template: props.template.key,
category: props.category,
})
const filterGroup = values.variables.length > 0 ? values.filterGroup : undefined
raquelmsmith marked this conversation as resolved.
Show resolved Hide resolved
router.actions.push(urls.replay(ReplayTabs.Home, filterGroup))
router.actions.push(urls.replay(ReplayTabs.Home, filterGroup, undefined, props.template.order))
},
})),
events(({ actions, props }) => ({
events(({ actions, props, values }) => ({
afterMount: () => {
actions.setVariables(props.template.variables)
if (values.variables.length === 0) {
actions.setVariables(props.template.variables)
}
},
})),
])
8 changes: 7 additions & 1 deletion frontend/src/scenes/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ export const urls = {
savedInsights: (tab?: string): string => `/insights${tab ? `?tab=${tab}` : ''}`,
webAnalytics: (): string => `/web`,

replay: (tab?: ReplayTabs, filters?: Partial<RecordingUniversalFilters>, sessionRecordingId?: string): string =>
replay: (
tab?: ReplayTabs,
filters?: Partial<RecordingUniversalFilters>,
sessionRecordingId?: string,
order?: string
): string =>
combineUrl(tab ? `/replay/${tab}` : '/replay/home', {
...(filters ? { filters } : {}),
...(sessionRecordingId ? { sessionRecordingId } : {}),
...(order ? { order } : {}),
}).url,
replayPlaylist: (id: string): string => `/replay/playlists/${id}`,
replaySingle: (id: string): string => `/replay/${id}`,
Expand Down
Loading
Loading