Skip to content

Commit

Permalink
feat: Persons Feed (#18183)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Oct 26, 2023
1 parent 367b44a commit 8a188cc
Show file tree
Hide file tree
Showing 18 changed files with 27,518 additions and 17 deletions.
Binary file modified frontend/__snapshots__/lemon-ui-icons--shelf-a.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/lemon-ui-icons--shelf-c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const FEATURE_FLAGS = {
PERSONS_HOGQL_QUERY: 'persons-hogql-query', // owner: @mariusandra
NOTEBOOK_CANVASES: 'notebook-canvases', // owner: #team-monitoring
SESSION_RECORDING_SAMPLING: 'session-recording-sampling', // owner: #team-monitoring
PERSON_FEED_CANVAS: 'person-feed-canvas', // owner: #project-canvas
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

Expand Down
22 changes: 22 additions & 0 deletions frontend/src/lib/lemon-ui/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2472,3 +2472,25 @@ export function IconNotebook(props: LemonIconProps): JSX.Element {
</LemonIconBase>
)
}

export function IconCode(props: LemonIconProps): JSX.Element {
return (
<LemonIconBase {...props}>
<path
d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"
fill="currentColor"
/>
</LemonIconBase>
)
}

export function IconAdsClick(props: LemonIconProps): JSX.Element {
return (
<LemonIconBase {...props}>
<path
d="M11.71,17.99C8.53,17.84,6,15.22,6,12c0-3.31,2.69-6,6-6c3.22,0,5.84,2.53,5.99,5.71l-2.1-0.63C15.48,9.31,13.89,8,12,8 c-2.21,0-4,1.79-4,4c0,1.89,1.31,3.48,3.08,3.89L11.71,17.99z M22,12c0,0.3-0.01,0.6-0.04,0.9l-1.97-0.59C20,12.21,20,12.1,20,12 c0-4.42-3.58-8-8-8s-8,3.58-8,8s3.58,8,8,8c0.1,0,0.21,0,0.31-0.01l0.59,1.97C12.6,21.99,12.3,22,12,22C6.48,22,2,17.52,2,12 C2,6.48,6.48,2,12,2S22,6.48,22,12z M18.23,16.26L22,15l-10-3l3,10l1.26-3.77l4.27,4.27l1.98-1.98L18.23,16.26z"
fill="currentColor"
/>
</LemonIconBase>
)
}
1 change: 1 addition & 0 deletions frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const NotebookNodePerson = createPostHogWidgetNode<NotebookNodePersonAttr
Component,
heightEstimate: 300,
minHeight: '5rem',
startExpanded: false,
href: (attrs) => urls.personByDistinctId(attrs.id),
resizeable: true,
attributes: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { EventType } from '~/types'

import { Tooltip } from '@posthog/lemon-ui'
import { IconAdsClick, IconExclamation, IconEyeHidden, IconEyeVisible, IconCode } from 'lib/lemon-ui/icons'
import { KEY_MAPPING } from 'lib/taxonomy'

type EventIconProps = { event: EventType }

export const EventIcon = ({ event }: EventIconProps): JSX.Element => {
let Component: React.ComponentType<{ className: string }>
switch (event.event) {
case '$pageview':
Component = IconEyeVisible
break
case '$pageleave':
Component = IconEyeHidden
break
case '$autocapture':
Component = IconAdsClick
break
case '$rageclick':
Component = IconExclamation
break
default:
Component = IconCode
}
return (
<Tooltip title={`${KEY_MAPPING.event[event.event]?.label || 'Custom'} event`}>
<Component className="text-2xl text-muted" />
</Tooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useValues } from 'kea'

import { LemonSkeleton } from '@posthog/lemon-ui'
import { NotFound } from 'lib/components/NotFound'
import { NotebookNodeType, PersonType } from '~/types'
// import { TimelineEntry } from '~/queries/schema'
import { NotebookNodeProps } from 'scenes/notebooks/Notebook/utils'
import { personLogic } from 'scenes/persons/personLogic'
import { createPostHogWidgetNode } from '../NodeWrapper'
import { notebookNodePersonFeedLogic } from './notebookNodePersonFeedLogic'
import { Session } from './Session'

const FeedSkeleton = (): JSX.Element => (
<div className="space-y-4 p-4">
<LemonSkeleton className="h-8" repeat={10} />
</div>
)

type FeedProps = {
person: PersonType
}

const Feed = ({ person }: FeedProps): JSX.Element => {
const id = person.id ?? 'missing'
const { sessions, sessionsLoading } = useValues(notebookNodePersonFeedLogic({ personId: id }))

if (!sessions && sessionsLoading) {
return <FeedSkeleton />
} else if (sessions === null) {
return <NotFound object="person" />
}

return (
<div className="p-2">
{sessions.map((session: any) => (
<Session key={session.sessionId} session={session} />
))}
</div>
)
}

const Component = ({ attributes }: NotebookNodeProps<NotebookNodePersonFeedAttributes>): JSX.Element => {
const { id } = attributes

const logic = personLogic({ id })
const { person, personLoading } = useValues(logic)

if (personLoading) {
return <FeedSkeleton />
} else if (!person) {
return <NotFound object="person" />
}

return <Feed person={person} />
}

type NotebookNodePersonFeedAttributes = {
id: string
}

export const NotebookNodePersonFeed = createPostHogWidgetNode<NotebookNodePersonFeedAttributes>({
nodeType: NotebookNodeType.PersonFeed,
titlePlaceholder: 'Feed',
Component,
resizeable: false,
expandable: false,
attributes: {
id: {},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useState } from 'react'
import { useActions, useValues } from 'kea'

import { LemonButton } from '@posthog/lemon-ui'
import { IconRewindPlay } from '@posthog/icons'
import { dayjs } from 'lib/dayjs'
// import { TimelineEntry } from '~/queries/schema'
import { NotebookNodeType } from '~/types'
import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons'
import { humanFriendlyDetailedTime, humanFriendlyDuration } from 'lib/utils'
import { SessionEvent } from './SessionEvent'
import { notebookNodeLogic } from '../notebookNodeLogic'

type SessionProps = {
session: any // TimelineEntry
}

export const Session = ({ session }: SessionProps): JSX.Element => {
const { children, nodeId } = useValues(notebookNodeLogic)
const { updateAttributes } = useActions(notebookNodeLogic)

const startTime = dayjs(session.events[0].timestamp)
const endTime = dayjs(session.events[session.events.length - 1].timestamp)
const durationSeconds = endTime.diff(startTime, 'second')

const [isFolded, setIsFolded] = useState(false)

const onOpenReplay = (): void => {
const newChildren = [...children] || []

const existingChild = newChildren.find((child) => child.attrs?.nodeId === `${nodeId}-active-replay`)

if (existingChild) {
existingChild.attrs.id = session.sessionId
} else {
newChildren.splice(0, 0, {
type: NotebookNodeType.Recording,
attrs: {
id: session.sessionId,
nodeId: `${nodeId}-active-replay`,
height: '5rem',
__init: {
expanded: true,
},
},
})
}

updateAttributes({
children: newChildren,
})
}

return (
<div className="flex flex-col rounded bg-side border overflow-hidden mb-3" title={session.sessionId}>
<div className="flex items-center justify-between bg-bg-light p-0.5 pr-2 text-xs">
<div className="flex items-center">
<LemonButton
size="small"
icon={isFolded ? <IconUnfoldMore /> : <IconUnfoldLess />}
status="stealth"
onClick={() => setIsFolded((state) => !state)}
/>
<span className="font-bold ml-2">{humanFriendlyDetailedTime(startTime)}</span>
</div>
<div className="flex items-center">
<span>
<b>{session.events.length} events</b> in <b>{humanFriendlyDuration(durationSeconds)}</b>
</span>
{session.recording_duration_s ? (
<LemonButton
className="ml-1"
size="small"
icon={<IconRewindPlay />}
onClick={() => onOpenReplay()}
/>
) : null}
</div>
</div>
{!isFolded && (
<div className="p-1 border-t space-y-1">
{session.events.map((event: any) => (
<SessionEvent key={event.id} event={event} />
))}
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EventType } from '~/types'
import { eventToDescription } from 'lib/utils'
import { dayjs } from 'lib/dayjs'
import { EventIcon } from './EventIcon'

type SessionEventProps = { event: EventType }

export const SessionEvent = ({ event }: SessionEventProps): JSX.Element => (
<div className="relative flex items-center justify-between border rounded pl-3 pr-4 py-1 bg-bg-light text-xs">
<div className="flex items-center">
<EventIcon event={event} />
<b className="ml-3">{eventToDescription(event)}</b>
</div>
<div className="flex items-center text-muted font-bold">
<span>{dayjs(event.timestamp).format('h:mm:ss A')}</span>
</div>
</div>
)
Loading

0 comments on commit 8a188cc

Please sign in to comment.