Repository: PostHog/posthog
Pull request #31805: feat: UX improvements and hotfixes
Author: login: sortafreel
Created: 2025-05-01T08:30:55Z | Merged: 2025-05-01T15:34:11Z

## Changes
- (BE) Improved fallback logic (based on the first/last event) if LLM predicted segments incorrectly
- (FE) Check for autocapture events present to ensure the summary can be generated
- (UX) Segments `disabled -> enabled` + color indicator to visualize when the content/actions are interactable
- (UX) Tag events happened before the recording as "before start" for clarity
- (UI) Overall improvement (hover state, colors, paddings, borders, etc.)

![CleanShot 2025-05-01 at 15 45 07](https://github.com/user-attachments/assets/7c9bdbb7-50ed-4d54-a603-5bbc35556b97)

<!-- If there are frontend changes, please include screenshots. -->
<!-- If a reference design was involved, include a link to the relevant Figma frame! -->

## Does this work well for both Cloud and self-hosted?

<!-- Yes / no / it doesn't have an impact. -->

## How did you test this code?

<!-- Briefly describe the steps you took. -->
<!-- Include automated tests if possible, otherwise describe the manual testing routine. -->



Diff:
diff --git a/ee/session_recordings/session_summary/output_data.py b/ee/session_recordings/session_summary/output_data.py
index 5a3e26144f01..3aab4e49e9f7 100644
--- a/ee/session_recordings/session_summary/output_data.py
+++ b/ee/session_recordings/session_summary/output_data.py
@@ -250,6 +250,12 @@ def _calculate_segment_meta(
     if segment_index is None or start_event_id is None or end_event_id is None:
         # If segment index, start, or end event ID aren't generated yet - return empty meta
         return SegmentMetaSerializer(data=segment_meta_data)
+    if start_event_id not in simplified_events_mapping or end_event_id not in simplified_events_mapping:
+        # If event id is found, but not in mapping, it's a hallucination
+        raise ValueError(
+            f"Mapping data for start_event_id {start_event_id} or end_event_id {end_event_id} not found "
+            f"when preparing segment summary meta for session_id {session_id} (probably a hallucination): {raw_segment}"
+        )
     # Calculate duration of the segment
     if not session_metadata.duration:
         raise ValueError(f"Session duration is not set when summarizing session_id {session_id}")
@@ -287,15 +293,14 @@ def _calculate_segment_meta(
     segment_key_actions_group = None
     # Find a relevant key actions group for the segment
     for key_actions_group in raw_key_actions:
-        failure_count = 0
         key_group_segment_index = key_actions_group.get("segment_index")
         if key_group_segment_index is None or key_group_segment_index != segment_index:
             # If key group segment index isn't generated yet or doesn't match the segment index - skip this group
             continue
         segment_key_actions_group = key_actions_group
         break
+    # If no relevant key actions group is found
     if not segment_key_actions_group:
-        # If no relevant key actions group is found
         return SegmentMetaSerializer(data=segment_meta_data)
     # Process the relevant events from the key actions group
     key_group_events = segment_key_actions_group.get("events", [])
@@ -304,6 +309,7 @@ def _calculate_segment_meta(
         return SegmentMetaSerializer(data=segment_meta_data)
     segment_meta_data["key_action_count"] = len(key_group_events)
     # Calculate failure count
+    failure_count = 0
     for key_action_event in key_group_events:
         if_failure = key_action_event.get("failure")
         if if_failure is None:
@@ -313,34 +319,35 @@ def _calculate_segment_meta(
             failure_count += 1
     segment_meta_data["failure_count"] = failure_count
     # Fallback - if enough events processed and the data drastically changes - calculate the meta from the key actions
-    if len(key_group_events) < 3:
+    if len(key_group_events) < 2:
         # If not enough events yet
         return SegmentMetaSerializer(data=segment_meta_data)
     # Calculate key action count and failure count
-    start_event_index = key_group_events[0].get("event_index")
-    end_event_index = key_group_events[-1].get("event_index")
-    if start_event_index is None or end_event_index is None:
-        # If event indexes aren't generated yet
+    fallback_start_event_id = key_group_events[0].get("event_id")
+    fallback_end_event_id = key_group_events[-1].get("event_id")
+    if fallback_start_event_id is None or fallback_end_event_id is None:
+        # If event ids aren't generated yet
         return SegmentMetaSerializer(data=segment_meta_data)
     fallback_duration, fallback_duration_percentage = _calculate_segment_duration(
-        start_event_id=start_event_id,
-        end_event_id=end_event_id,
+        start_event_id=fallback_start_event_id,
+        end_event_id=fallback_end_event_id,
         timestamp_index=timestamp_index,
         simplified_events_mapping=simplified_events_mapping,
         session_total_duration=session_metadata.duration,
     )
     fallback_events_count, fallback_events_percentage = _calculate_segment_events_count(
-        start_event_id=start_event_id,
-        end_event_id=end_event_id,
+        start_event_id=fallback_start_event_id,
+        end_event_id=fallback_end_event_id,
         event_index_index=event_index_index,
         simplified_events_mapping=simplified_events_mapping,
     )
-    if fallback_duration == 0 or duration == 0:
-        # If data is 0, calculations are not reliable
+    if fallback_duration == 0:
+        # If fallback_duration is also 0, calculations are not reliable
         return SegmentMetaSerializer(data=segment_meta_data)
-    # If the change is drastic (in both directions) - use the fallback data to avoid reader's confusion
+    # If the change is drastic, or no duration at all - use the fallback data to avoid reader's confusion
+    # Avoiding downsizing the segment as larger segments make more sense visually
     # TODO: Factor of two is arbitrary, find a better solution
-    if fallback_duration // duration > 2 or duration // fallback_duration > 2:
+    if duration <= 0 or fallback_duration // duration > 2:
         # Checking only duration as events are sorted chronologically
         logger.warning(
             f"Duration change is drastic (fallback: {fallback_duration} -> segments: {duration}) - using fallback data for session_id {session_id}"
diff --git a/ee/session_recordings/session_summary/tests/test_output_data.py b/ee/session_recordings/session_summary/tests/test_output_data.py
index 7ed007b4f83d..0695a7986415 100644
--- a/ee/session_recordings/session_summary/tests/test_output_data.py
+++ b/ee/session_recordings/session_summary/tests/test_output_data.py
@@ -260,10 +260,36 @@ def test_enrich_raw_session_summary_missing_event(
         mock_session_metadata: SessionSummaryMetadata,
     ) -> None:
         # Remove one event from mapping
-        del mock_events_mapping["defg4567"]
+        del mock_events_mapping["mnop3456"]
         session_id = "test_session"
         with pytest.raises(
-            ValueError, match=f"Mapping data for event_id defg4567 not found when summarizing session_id {session_id}"
+            ValueError, match=f"Mapping data for event_id mnop3456 not found when summarizing session_id {session_id}"
+        ):
+            enrich_raw_session_summary_with_meta(
+                mock_raw_session_summary,
+                mock_events_mapping,
+                mock_events_columns,
+                mock_url_mapping_reversed,
+                mock_window_mapping_reversed,
+                mock_session_metadata,
+                session_id,
+            )
+
+    def test_calculate_segment_meta_missing_event(
+        self,
+        mock_raw_session_summary: RawSessionSummarySerializer,
+        mock_events_mapping: dict[str, list[Any]],
+        mock_events_columns: list[str],
+        mock_url_mapping_reversed: dict[str, str],
+        mock_window_mapping_reversed: dict[str, str],
+        mock_session_metadata: SessionSummaryMetadata,
+    ) -> None:
+        # Remove one event from mapping (segment end id)
+        del mock_events_mapping["vbgs1287"]
+        session_id = "test_session"
+        with pytest.raises(
+            ValueError,
+            match=f"Mapping data for start_event_id abcd1234 or end_event_id vbgs1287 not found when preparing segment summary meta for session_id {session_id}",
         ):
             enrich_raw_session_summary_with_meta(
                 mock_raw_session_summary,
diff --git a/ee/session_recordings/session_summary/utils.py b/ee/session_recordings/session_summary/utils.py
index dd9e9fe93cd8..4140c54396f8 100644
--- a/ee/session_recordings/session_summary/utils.py
+++ b/ee/session_recordings/session_summary/utils.py
@@ -50,7 +50,7 @@ def load_session_recording_events_from_csv(
         timestamp_index = get_column_index(list(headers_indexes.keys()), "timestamp")
         for raw_row in reader:
             row: list[str | datetime | list[str] | None] = []
-            for header_metadata in headers_indexes.values():
+            for header_index, header_metadata in headers_indexes.items():
                 if len(header_metadata["indexes"]) == 1:
                     raw_row_value = raw_row[header_metadata["indexes"][0]]
                     # Ensure to keep the format for multi-column fields
@@ -62,8 +62,10 @@ def load_session_recording_events_from_csv(
                     else:
                         if header_metadata["multi_column"]:
                             row.append([])
-                        else:
+                        elif header_index in ("$window_id", "$current_url"):
                             row.append(None)
+                        else:
+                            row.append("")
                 # Ensure to combine all values for multi-column fields (like chain texts) into a single list
                 else:
                     # Store only non-empty values
diff --git a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
index a33cd6d1cd8b..b34349633681 100644
--- a/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
+++ b/frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
@@ -1,6 +1,8 @@
 import {
     IconAIText,
     IconClock,
+    IconCollapse,
+    IconExpand,
     IconKeyboard,
     IconMagicWand,
     IconPointer,
@@ -8,13 +10,17 @@ import {
     IconThumbsUp,
     IconWarning,
 } from '@posthog/icons'
-import { LemonBanner, LemonCollapse, LemonDivider, LemonTag, Link, Tooltip } from '@posthog/lemon-ui'
+import { LemonBanner, LemonDivider, LemonTag, Link, Tooltip } from '@posthog/lemon-ui'
+import clsx from 'clsx'
 import { useActions, useValues } from 'kea'
 import { LemonButton } from 'lib/lemon-ui/LemonButton'
 import { Spinner } from 'lib/lemon-ui/Spinner'
-import { useEffect, useState } from 'react'
+import { ReactNode, useEffect, useState } from 'react'
+import { Transition } from 'react-transition-group'
+import { ENTERED, ENTERING } from 'react-transition-group/Transition'
 import { playerMetaLogic } from 'scenes/session-recordings/player/player-meta/playerMetaLogic'
 import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
+import useResizeObserver from 'use-resize-observer'
 
 import { playerInspectorLogic } from '../inspector/playerInspectorLogic'
 import {
@@ -77,6 +83,66 @@ function LoadingTimer({ operation }: { operation?: string }): JSX.Element {
     return <span className="font-mono text-xs text-muted">{elapsedSeconds}s</span>
 }
 
+interface SessionSegmentCollapseProps {
+    header: ReactNode
+    content: ReactNode
+    actionsPresent?: boolean
+    className?: string
+    isFailed?: boolean
+}
+
+function SessionSegmentCollapse({
+    header,
+    content,
+    actionsPresent,
+    className,
+    isFailed,
+}: SessionSegmentCollapseProps): JSX.Element {
+    const [isExpanded, setIsExpanded] = useState(false)
+    const { height: contentHeight, ref: contentRef } = useResizeObserver({ box: 'border-box' })
+
+    return (
+        <div className={clsx('LemonCollapse', className)}>
+            <div className="LemonCollapsePanel" aria-expanded={isExpanded}>
+                <LemonButton
+                    fullWidth
+                    className={clsx(
+                        'LemonCollapsePanel__header hover:bg-primary-alt-highlight border-l-[5px]',
+                        !actionsPresent && 'LemonCollapsePanel__header--disabled',
+                        isFailed && 'border-l-danger'
+                    )}
+                    onClick={actionsPresent ? () => setIsExpanded(!isExpanded) : undefined}
+                    icon={isExpanded ? <IconCollapse /> : <IconExpand />}
+                    size="medium"
+                    disabled={!actionsPresent}
+                >
+                    {header}
+                </LemonButton>
+                <Transition in={isExpanded} timeout={200} mountOnEnter unmountOnExit>
+                    {(status) => (
+                        <div
+                            className="LemonCollapsePanel__body"
+                            // eslint-disable-next-line react/forbid-dom-props
+                            style={
+                                status === ENTERING || status === ENTERED
+                                    ? {
+                                          height: contentHeight,
+                                      }
+                                    : undefined
+                            }
+                            aria-busy={status.endsWith('ing')}
+                        >
+                            <div className="LemonCollapsePanel__content" ref={contentRef}>
+                                {content}
+                            </div>
+                        </div>
+                    )}
+                </Transition>
+            </div>
+        </div>
+    )
+}
+
 function SegmentMetaTable({ meta }: SegmentMetaProps): JSX.Element | null {
     if (!meta) {
         return null
@@ -85,7 +151,7 @@ function SegmentMetaTable({ meta }: SegmentMetaProps): JSX.Element | null {
     return (
         <div className="grid grid-cols-2 gap-2 text-xs mt-2">
             <div className="flex items-center gap-1">
-                <IconKeyboard />
+                <IconKeyboard className={meta.key_action_count && meta.key_action_count > 0 ? 'text-success' : ''} />
                 <span className="text-muted">Key actions:</span>
                 {isValidMetaNumber(meta.key_action_count) && <span>{meta.key_action_count}</span>}
             </div>
@@ -99,8 +165,13 @@ function SegmentMetaTable({ meta }: SegmentMetaProps): JSX.Element | null {
                 <span className="text-muted">Duration:</span>
                 {isValidMetaNumber(meta.duration) && isValidMetaNumber(meta.duration_percentage) && (
                     <span>
-                        {formatMsIntoTime(meta.duration * 1000 || 0)} (
-                        {((meta.duration_percentage || 0) * 100).toFixed(2)}%)
+                        {meta.duration === 0 ? (
+                            <span className="text-muted">...</span>
+                        ) : (
+                            `${formatMsIntoTime(meta.duration * 1000)} (${(
+                                (meta.duration_percentage || 0) * 100
+                            ).toFixed(2)}%)`
+                        )}
                     </span>
                 )}
             </div>
@@ -109,7 +180,11 @@ function SegmentMetaTable({ meta }: SegmentMetaProps): JSX.Element | null {
                 <span className="text-muted">Events:</span>
                 {isValidMetaNumber(meta.events_count) && isValidMetaNumber(meta.events_percentage) && (
                     <span>
-                        {meta.events_count} ({((meta.events_percentage || 0) * 100).toFixed(2)}%)
+                        {meta.events_count === 0 ? (
+                            <span className="text-muted">...</span>
+                        ) : (
+                            `${meta.events_count} (${((meta.events_percentage || 0) * 100).toFixed(2)}%)`
+                        )}
                     </span>
                 )}
             </div>
@@ -132,90 +207,107 @@ function SessionSegmentView({
 }: SessionSegmentViewProps): JSX.Element {
     return (
         <div key={segment.name} className="mb-4">
-            <LemonCollapse
-                size="medium"
-                className={`border-b cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight ${
-                    segmentOutcome && Object.keys(segmentOutcome).length > 0 && segmentOutcome.success === false
-                        ? 'bg-danger-highlight'
-                        : ''
-                }`}
-                panels={[
-                    {
-                        key: 'previous',
-                        header: (
-                            <div className="py-2">
-                                <div className="flex flex-row gap-2">
-                                    <h3 className="mb-1">{segment.name}</h3>
-                                    {segmentOutcome && Object.keys(segmentOutcome).length > 0 ? (
-                                        <div>
-                                            {segmentOutcome.success ? null : (
-                                                <LemonTag size="small" type="default">
-                                                    failed
-                                                </LemonTag>
-                                            )}
-                                        </div>
-                                    ) : (
-                                        <Spinner />
+            <SessionSegmentCollapse
+                className="cursor-pointer"
+                actionsPresent={keyActions && keyActions.length > 0}
+                isFailed={segmentOutcome && Object.keys(segmentOutcome).length > 0 && segmentOutcome.success === false}
+                header={
+                    <div className="py-2">
+                        <div className="flex flex-row gap-2">
+                            <h3 className="mb-1">{segment.name}</h3>
+                            {segmentOutcome && Object.keys(segmentOutcome).length > 0 ? (
+                                <div>
+                                    {segmentOutcome.success ? null : (
+                                        <LemonTag size="small" type="default">
+                                            failed
+                                        </LemonTag>
                                     )}
                                 </div>
-                                {segmentOutcome && (
-                                    <>
-                                        <p className="text-sm font-normal mb-0">{segmentOutcome.summary}</p>
-                                    </>
-                                )}
-                                <SegmentMetaTable
-                                    meta={segment.meta && Object.keys(segment.meta).length > 0 ? segment.meta : null}
-                                />
-                            </div>
-                        ),
-                        content: (
+                            ) : (
+                                <Spinner />
+                            )}
+                        </div>
+                        {segmentOutcome && (
+                            <>
+                                <p className="text-sm font-normal mb-0">{segmentOutcome.summary}</p>
+                            </>
+                        )}
+                        <SegmentMetaTable
+                            meta={segment.meta && Object.keys(segment.meta).length > 0 ? segment.meta : null}
+                        />
+                    </div>
+                }
+                content={
+                    <>
+                        {keyActions && keyActions.length > 0 ? (
                             <>
                                 {keyActions?.map((keyAction) =>
-                                    keyAction.events?.map((event: SessionKeyAction, eventIndex: number) =>
-                                        isValidTimestamp(event.milliseconds_since_start) ? (
-                                            <div
-                                                key={`${segment.name}-${eventIndex}`}
-                                                className={`border-b cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight ${
-                                                    event.failure ? 'bg-danger-highlight' : ''
-                                                }`}
-                                                onClick={() => {
-                                                    if (!isValidTimestamp(event.milliseconds_since_start)) {
-                                                        return
-                                                    }
-                                                    onSeekToTime(event.milliseconds_since_start)
-                                                }}
-                                            >
-                                                <div className="flex flex-row gap-2">
-                                                    <span className="text-muted-alt shrink-0 min-w-[4rem] font-mono text-xs">
-                                                        {formatMsIntoTime(event.milliseconds_since_start)}
-                                                        <div className="flex flex-row gap-2 mt-1">
-                                                            {event.current_url ? (
-                                                                <Link to={event.current_url} target="_blank">
-                                                                    <Tooltip title={event.current_url} placement="top">
-                                                                        <span className="font-mono text-xs text-muted-alt">
-                                                                            url
-                                                                        </span>
-                                                                    </Tooltip>
-                                                                </Link>
-                                                            ) : null}
-                                                            <Tooltip title={formatEventMetaInfo(event)} placement="top">
-                                                                <span className="font-mono text-xs text-muted-alt">
-                                                                    meta
-                                                                </span>
-                                                            </Tooltip>
-                                                        </div>
-                                                    </span>
-
-                                                    <span className="text-xs break-words">{event.description}</span>
+                                    keyAction.events?.map(
+                                        (event: SessionKeyAction, eventIndex: number, events: SessionKeyAction[]) =>
+                                            isValidTimestamp(event.milliseconds_since_start) ? (
+                                                <div
+                                                    key={`${segment.name}-${eventIndex}`}
+                                                    className={clsx(
+                                                        'cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight',
+                                                        // Avoid adding a border to the last event
+                                                        eventIndex !== events.length - 1 && 'border-b',
+                                                        event.failure && 'bg-danger-highlight'
+                                                    )}
+                                                    onClick={() => {
+                                                        if (!isValidTimestamp(event.milliseconds_since_start)) {
+                                                            return
+                                                        }
+                                                        onSeekToTime(event.milliseconds_since_start)
+                                                    }}
+                                                >
+                                                    <div className="flex flex-row gap-2">
+                                                        <span className="text-muted-alt shrink-0 min-w-[4rem] font-mono text-xs">
+                                                            {formatMsIntoTime(event.milliseconds_since_start)}
+                                                            <div className="flex flex-row gap-2 mt-1">
+                                                                {event.current_url ? (
+                                                                    <Link to={event.current_url} target="_blank">
+                                                                        <Tooltip
+                                                                            title={event.current_url}
+                                                                            placement="top"
+                                                                        >
+                                                                            <span className="font-mono text-xs text-muted-alt">
+                                                                                url
+                                                                            </span>
+                                                                        </Tooltip>
+                                                                    </Link>
+                                                                ) : null}
+                                                                <Tooltip
+                                                                    title={formatEventMetaInfo(event)}
+                                                                    placement="top"
+                                                                >
+                                                                    <span className="font-mono text-xs text-muted-alt">
+                                                                        meta
+                                                                    </span>
+                                                                </Tooltip>
+                                                            </div>
+                                                        </span>
+
+                                                        <span className="text-xs break-words">
+                                                            {event.description}&nbsp;{' '}
+                                                            {event.milliseconds_since_start === 0 && (
+                                                                <LemonTag size="small" type="default">
+                                                                    before start
+                                                                </LemonTag>
+                                                            )}
+                                                        </span>
+                                                    </div>
                                                 </div>
-                                            </div>
-                                        ) : null
+                                            ) : null
                                     )
                                 )}
                             </>
-                        ),
-                    },
-                ]}
+                        ) : (
+                            <div className="text-muted-alt">
+                                Waiting for key actions... <Spinner />
+                            </div>
+                        )}
+                    </>
+                }
             />
         </div>
     )
@@ -432,7 +524,18 @@ function LoadSessionSummaryButton(): JSX.Element {
     const { items: inspectorItems } = useValues(inspectorLogic)
     const { summarizeSession } = useActions(playerMetaLogic(logicProps))
 
-    const hasEnoughEvents = inspectorItems && inspectorItems.length > 0
+    // We need $autocapture events to be able to generate a summary
+    const hasEvents = inspectorItems && inspectorItems.length > 0
+    const hasEnoughEvents =
+        hasEvents &&
+        inspectorItems?.some(
+            (item) =>
+                'data' in item &&
+                item.data &&
+                typeof item.data === 'object' &&
+                'event' in item.data &&
+                item.data.event === '$autocapture'
+        )
 
     return (
         <div className="space-y-2">
@@ -455,10 +558,21 @@ function LoadSessionSummaryButton(): JSX.Element {
                 </div>
             ) : (
                 !hasEnoughEvents && (
-                    <div className="text-sm">
-                        Session events not available for summary yet.
-                        <br />
-                        Please, try again in a few minutes.
+                    <div>
+                        {hasEvents ? (
+                            <>
+                                <h4>No autocapture events found for this session</h4>
+                                <p className="text-sm mb-1">
+                                    Please, ensure that Autocapture is enabled in project's settings, or try again in a
+                                    few minutes.
+                                </p>
+                            </>
+                        ) : (
+                            <>
+                                <h4>Session events are not available for summary yet</h4>
+                                <p className="text-sm mb-1">Please, try again in a few minutes.</p>
+                            </>
+                        )}
                     </div>
                 )
             )}


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR enhances session recording analysis with improved metadata calculations and UX refinements in the session player.

- Added fallback duration logic in `_calculate_segment_meta` to handle edge cases when original duration is zero
- Improved empty value handling in CSV loading, differentiating between `$window_id`/`$current_url` (null) and other fields (empty string)
- Enhanced loading states with progress indicators and better error messaging for missing autocapture events
- Added visual indicators for key actions/failures and 'before start' tag for 0ms events in the session player UI
- Replaced LemonCollapse with custom SessionSegmentCollapse component for better animations and accessibility

<sub>3 file(s) reviewed, 2 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_31805)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:36:58Z
user: login: pauldambra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T14:00:20Z
user: login: pauldambra
body: didn't run it but 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-01T14:00:45Z

Review comments:
diff_hunk: @@ -132,90 +207,107 @@ function SessionSegmentView({
 }: SessionSegmentViewProps): JSX.Element {
     return (
         <div key={segment.name} className="mb-4">
-            <LemonCollapse
-                size="medium"
-                className={`border-b cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight ${
-                    segmentOutcome && Object.keys(segmentOutcome).length > 0 && segmentOutcome.success === false
-                        ? 'bg-danger-highlight'
-                        : ''
-                }`}
-                panels={[
-                    {
-                        key: 'previous',
-                        header: (
-                            <div className="py-2">
-                                <div className="flex flex-row gap-2">
-                                    <h3 className="mb-1">{segment.name}</h3>
-                                    {segmentOutcome && Object.keys(segmentOutcome).length > 0 ? (
-                                        <div>
-                                            {segmentOutcome.success ? null : (
-                                                <LemonTag size="small" type="default">
-                                                    failed
-                                                </LemonTag>
-                                            )}
-                                        </div>
-                                    ) : (
-                                        <Spinner />
+            <SessionSegmentCollapse
+                className="cursor-pointer"
+                actionsPresent={keyActions && keyActions.length > 0}
+                isFailed={segmentOutcome && Object.keys(segmentOutcome).length > 0 && segmentOutcome.success === false}
+                header={
+                    <div className="py-2">
+                        <div className="flex flex-row gap-2">
+                            <h3 className="mb-1">{segment.name}</h3>
+                            {segmentOutcome && Object.keys(segmentOutcome).length > 0 ? (
+                                <div>
+                                    {segmentOutcome.success ? null : (
+                                        <LemonTag size="small" type="default">
+                                            failed
+                                        </LemonTag>
                                     )}
                                 </div>
-                                {segmentOutcome && (
-                                    <>
-                                        <p className="text-sm font-normal mb-0">{segmentOutcome.summary}</p>
-                                    </>
-                                )}
-                                <SegmentMetaTable
-                                    meta={segment.meta && Object.keys(segment.meta).length > 0 ? segment.meta : null}
-                                />
-                            </div>
-                        ),
-                        content: (
+                            ) : (
+                                <Spinner />
+                            )}
+                        </div>
+                        {segmentOutcome && (
+                            <>
+                                <p className="text-sm font-normal mb-0">{segmentOutcome.summary}</p>
+                            </>
+                        )}
+                        <SegmentMetaTable
+                            meta={segment.meta && Object.keys(segment.meta).length > 0 ? segment.meta : null}
+                        />
+                    </div>
+                }
+                content={
+                    <>
+                        {keyActions && keyActions.length > 0 ? (
                             <>
                                 {keyActions?.map((keyAction) =>
-                                    keyAction.events?.map((event: SessionKeyAction, eventIndex: number) =>
-                                        isValidTimestamp(event.milliseconds_since_start) ? (
-                                            <div
-                                                key={`${segment.name}-${eventIndex}`}
-                                                className={`border-b cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight ${
-                                                    event.failure ? 'bg-danger-highlight' : ''
-                                                }`}
-                                                onClick={() => {
-                                                    if (!isValidTimestamp(event.milliseconds_since_start)) {
-                                                        return
-                                                    }
-                                                    onSeekToTime(event.milliseconds_since_start)
-                                                }}
-                                            >
-                                                <div className="flex flex-row gap-2">
-                                                    <span className="text-muted-alt shrink-0 min-w-[4rem] font-mono text-xs">
-                                                        {formatMsIntoTime(event.milliseconds_since_start)}
-                                                        <div className="flex flex-row gap-2 mt-1">
-                                                            {event.current_url ? (
-                                                                <Link to={event.current_url} target="_blank">
-                                                                    <Tooltip title={event.current_url} placement="top">
-                                                                        <span className="font-mono text-xs text-muted-alt">
-                                                                            url
-                                                                        </span>
-                                                                    </Tooltip>
-                                                                </Link>
-                                                            ) : null}
-                                                            <Tooltip title={formatEventMetaInfo(event)} placement="top">
-                                                                <span className="font-mono text-xs text-muted-alt">
-                                                                    meta
-                                                                </span>
-                                                            </Tooltip>
-                                                        </div>
-                                                    </span>
-
-                                                    <span className="text-xs break-words">{event.description}</span>
+                                    keyAction.events?.map(
+                                        (event: SessionKeyAction, eventIndex: number, events: SessionKeyAction[]) =>
+                                            isValidTimestamp(event.milliseconds_since_start) ? (
+                                                <div
+                                                    key={`${segment.name}-${eventIndex}`}
+                                                    className={clsx(
+                                                        'cursor-pointer py-2 px-2 hover:bg-primary-alt-highlight',
+                                                        // Avoid adding a border to the last event
+                                                        eventIndex !== events.length - 1 && 'border-b',
+                                                        event.failure && 'bg-danger-highlight'
+                                                    )}
+                                                    onClick={() => {
+                                                        if (!isValidTimestamp(event.milliseconds_since_start)) {
+                                                            return
+                                                        }
path: frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
user: login: greptile-apps[bot]
body: logic: Redundant check - isValidTimestamp was already verified in the parent condition

```suggestion
                                                        onSeekToTime(event.milliseconds_since_start)
```
created_at: 2025-05-01T13:36:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -432,7 +524,18 @@ function LoadSessionSummaryButton(): JSX.Element {
     const { items: inspectorItems } = useValues(inspectorLogic)
     const { summarizeSession } = useActions(playerMetaLogic(logicProps))
 
-    const hasEnoughEvents = inspectorItems && inspectorItems.length > 0
+    // We need $autocapture events to be able to generate a summary
+    const hasEvents = inspectorItems && inspectorItems.length > 0
+    const hasEnoughEvents =
+        hasEvents &&
+        inspectorItems?.some(
+            (item) =>
+                'data' in item &&
+                item.data &&
+                typeof item.data === 'object' &&
+                'event' in item.data &&
+                item.data.event === '$autocapture'
+        )
path: frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
user: login: greptile-apps[bot]
body: style: Consider extracting this complex condition into a separate isAutocaptureEvent helper function for better readability and reuse
created_at: 2025-05-01T13:36:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,7 +151,7 @@ function SegmentMetaTable({ meta }: SegmentMetaProps): JSX.Element | null {
     return (
         <div className="grid grid-cols-2 gap-2 text-xs mt-2">
             <div className="flex items-center gap-1">
-                <IconKeyboard />
+                <IconKeyboard className={meta.key_action_count && meta.key_action_count > 0 ? 'text-success' : ''} />
path: frontend/src/scenes/session-recordings/player/sidebar/PlayerSidebarSessionSummary.tsx
user: login: pauldambra
body: we use a convenience function that was called `cslx` but recently changed to make things like this easier to extend

not blocking since there's a single thing here 

ah, now called `cn` 

https://posthog.slack.com/archives/C0113360FFV/p1741762808953989
created_at: 2025-05-01T14:00:20Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-01T08:43:08Z
body: **Size Change:** 0 B 

**Total Size:** 4.54 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `frontend/dist/toolbar.js` | 4.54 MB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-05-01T14:39:58Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 5](https://github.com/PostHog/posthog/pull/31805/commits/0341ce1c4a0f292b3730c178f84075b66ebc4eb7))
- `webkit`: **0** added, **0** modified, **0** deleted

_Triggered by [this commit](https://github.com/PostHog/posthog/pull/31805/commits/3d85c76a70bf8f415296ec7ac2c687f08adab10e)._

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/31805/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-05-01T14:49:19Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 5](https://github.com/PostHog/posthog/pull/31805/commits/03db47b41581f28ff9443123a99ba06d52ca759d))
- `webkit`: **0** added, **0** modified, **0** deleted

_Triggered by [this commit](https://github.com/PostHog/posthog/pull/31805/commits/6e981a5bdfcdb48529a2be3de9fd8aaa47ceab63)._

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/31805/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-05-01T14:58:56Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 2](https://github.com/PostHog/posthog/pull/31805/commits/77b433599a57776fcabd65e98d78c7f17b7d3a8a))
- `webkit`: **0** added, **0** modified, **0** deleted

_Triggered by [this commit](https://github.com/PostHog/posthog/pull/31805/commits/975a2398af8e1cc4d9c904cba552d4d7062994ed)._

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/31805/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-05-01T15:08:19Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 2](https://github.com/PostHog/posthog/pull/31805/commits/7dd1fb4ea7897a38673457fc5d0683266d818f26))
- `webkit`: **0** added, **0** modified, **0** deleted

_Triggered by [this commit](https://github.com/PostHog/posthog/pull/31805/commits/6ea3676d5d7d5190c3efc9945663e48754f44d18)._

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/31805/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR