+ {item.type === FilterableInspectorListItemTypes.NETWORK ? (
- ) : item.type === SessionRecordingPlayerTab.CONSOLE ? (
+ ) : item.type === FilterableInspectorListItemTypes.CONSOLE ? (
- ) : item.type === SessionRecordingPlayerTab.EVENTS ? (
+ ) : item.type === FilterableInspectorListItemTypes.EVENTS ? (
+
{item.offline ? 'Browser went offline' : 'Browser returned online'}
) : item.type === 'browser-visibility' ? (
-
+
Window became {item.status}
- ) : item.type === SessionRecordingPlayerTab.DOCTOR ? (
+ ) : item.type === FilterableInspectorListItemTypes.DOCTOR ? (
) : item.type === 'comment' ? (
+ ) : item.type === 'inspector-summary' ? (
+
+ ) : item.type === 'inactivity' ? (
+
) : null}
)
@@ -142,14 +156,14 @@ function RowItemDetail({
}): JSX.Element | null {
return (
- {item.type === SessionRecordingPlayerTab.NETWORK ? (
+ {item.type === FilterableInspectorListItemTypes.NETWORK ? (
- ) : item.type === SessionRecordingPlayerTab.CONSOLE ? (
+ ) : item.type === FilterableInspectorListItemTypes.CONSOLE ? (
- ) : item.type === SessionRecordingPlayerTab.EVENTS ? (
+ ) : item.type === FilterableInspectorListItemTypes.EVENTS ? (
) : item.type === 'offline-status' ? null : item.type === 'browser-visibility' ? null : item.type ===
- SessionRecordingPlayerTab.DOCTOR ? (
+ FilterableInspectorListItemTypes.DOCTOR ? (
) : item.type === 'comment' ? (
@@ -172,11 +186,9 @@ export function PlayerInspectorListItem({
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { seekToTime } = useActions(sessionRecordingPlayerLogic)
- const { tab, end, expandedItems } = useValues(playerInspectorLogic(logicProps))
+ const { end, expandedItems } = useValues(playerInspectorLogic(logicProps))
const { setItemExpanded } = useActions(playerInspectorLogic(logicProps))
- const showIcon = tab === SessionRecordingPlayerTab.ALL
-
const isExpanded = expandedItems.includes(index)
// NOTE: We offset by 1 second so that the playback starts just before the event occurs.
@@ -215,6 +227,8 @@ export function PlayerInspectorListItem({
const isHovering = useIsHovering(hoverRef)
+ const TypeIcon = typeToIconAndDescription[item.type].Icon
+
return (
) : null}
-
+ {item.type !== 'inspector-summary' && item.type !== 'inactivity' &&
}
+
+ {TypeIcon ?
:
}
-
+
-
:
}
- size="small"
- noPadding
- onClick={() => setItemExpanded(index, !isExpanded)}
- data-attr="expand-inspector-row"
- disabledReason={
- item.type === 'offline-status' || item.type === 'browser-visibility'
- ? 'This event type does not have a detail view'
- : undefined
- }
- />
+ {item.type !== 'inspector-summary' && item.type !== 'inactivity' && (
+
:
}
+ size="small"
+ noPadding
+ onClick={() => setItemExpanded(index, !isExpanded)}
+ data-attr="expand-inspector-row"
+ disabledReason={
+ item.type === 'offline-status' || item.type === 'browser-visibility'
+ ? 'This event type does not have a detail view'
+ : undefined
+ }
+ />
+ )}
{isExpanded ? (
diff --git a/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.test.ts b/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.test.ts
index 409535ef946b5..05858ffe5eae5 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.test.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.test.ts
@@ -2,181 +2,138 @@ import { filterInspectorListItems } from 'scenes/session-recordings/player/inspe
import { SharedListMiniFilter } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
import {
InspectorListBrowserVisibility,
+ InspectorListItemComment,
InspectorListItemDoctor,
InspectorListItemEvent,
InspectorListOfflineStatusChange,
} from 'scenes/session-recordings/player/inspector/playerInspectorLogic'
-import { PerformanceEvent, SessionRecordingPlayerTab } from '~/types'
+import { FilterableInspectorListItemTypes, PerformanceEvent } from '~/types'
describe('filtering inspector list items', () => {
- describe('the all tab', () => {
- it('includes browser visibility', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: 'browser-visibility',
- } as InspectorListBrowserVisibility,
- ],
- tab: SessionRecordingPlayerTab.ALL,
- miniFiltersByKey: { 'all-everything': { enabled: true } as unknown as SharedListMiniFilter },
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(1)
- })
-
- it('hides doctor items in everything mode', () => {
- const filteredItems = filterInspectorListItems({
+ it('hides context events when no other events', () => {
+ expect(
+ filterInspectorListItems({
allItems: [
{
type: 'browser-visibility',
} as InspectorListBrowserVisibility,
{
- type: 'doctor',
- } as InspectorListItemDoctor,
+ type: 'offline-status',
+ } as unknown as InspectorListOfflineStatusChange,
+ {
+ type: 'comment',
+ } as unknown as InspectorListItemComment,
],
- tab: SessionRecordingPlayerTab.ALL,
- miniFiltersByKey: { 'all-everything': { enabled: true } as unknown as SharedListMiniFilter },
+ miniFiltersByKey: { 'events-posthog': { enabled: false } as unknown as SharedListMiniFilter },
showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
+ allowMatchingEventsFilter: false,
+ trackedWindow: null,
})
- expect(filteredItems.map((item) => item.type)).toEqual(['browser-visibility'])
- })
+ ).toHaveLength(0)
})
- describe('the events tab', () => {
- it('filters by window id', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: SessionRecordingPlayerTab.EVENTS,
- windowId: 'this window',
- data: { event: '$exception' } as unknown as PerformanceEvent,
- } as unknown as InspectorListItemEvent,
- {
- type: SessionRecordingPlayerTab.EVENTS,
- windowId: 'a different window',
- data: { event: '$exception' } as unknown as PerformanceEvent,
- } as unknown as InspectorListItemEvent,
- ],
- tab: SessionRecordingPlayerTab.EVENTS,
- miniFiltersByKey: { 'events-all': { enabled: true } as unknown as SharedListMiniFilter },
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: 'a different window',
- })
- ).toHaveLength(1)
- })
-
- it('excludes browser visibility on console filter', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: 'browser-visibility',
- } as InspectorListBrowserVisibility,
- ],
- tab: SessionRecordingPlayerTab.EVENTS,
- miniFiltersByKey: { 'all-everything': { enabled: false } as unknown as SharedListMiniFilter },
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(0)
- })
-
- it('excludes browser visibility when show only matching', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: 'browser-visibility',
- } as InspectorListBrowserVisibility,
- ],
- tab: SessionRecordingPlayerTab.EVENTS,
- miniFiltersByKey: { 'all-everything': { enabled: true } as unknown as SharedListMiniFilter },
- showOnlyMatching: true,
- showMatchingEventsFilter: true,
- windowIdFilter: null,
- })
- ).toHaveLength(0)
- })
+ it('shows context events when other events', () => {
+ expect(
+ filterInspectorListItems({
+ allItems: [
+ {
+ type: 'browser-visibility',
+ } as InspectorListBrowserVisibility,
+ {
+ type: 'offline-status',
+ } as unknown as InspectorListOfflineStatusChange,
+ {
+ type: 'comment',
+ } as unknown as InspectorListItemComment,
+ {
+ data: { event: '$pageview' },
+ type: 'events',
+ } as InspectorListItemEvent,
+ ],
+ miniFiltersByKey: { 'events-pageview': { enabled: true } as unknown as SharedListMiniFilter },
+ showOnlyMatching: false,
+ allowMatchingEventsFilter: false,
+ trackedWindow: null,
+ }).map((item) => item.type)
+ ).toEqual(['browser-visibility', 'offline-status', 'comment', 'events'])
})
- describe('the doctor tab', () => {
- it('ignores events that are not exceptions', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: SessionRecordingPlayerTab.EVENTS,
- data: { event: 'an event' } as unknown as PerformanceEvent,
- } as unknown as InspectorListItemEvent,
- ],
- tab: SessionRecordingPlayerTab.DOCTOR,
- miniFiltersByKey: {},
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(0)
+ it.each([
+ [true, 1],
+ [false, 0],
+ ])('hides/shows doctor items when %s', (enabled, expectedLength) => {
+ const filteredItems = filterInspectorListItems({
+ allItems: [
+ {
+ type: 'doctor',
+ } as InspectorListItemDoctor,
+ ],
+ miniFiltersByKey: { doctor: { enabled } as unknown as SharedListMiniFilter },
+ showOnlyMatching: false,
+ allowMatchingEventsFilter: false,
+ trackedWindow: null,
})
+ expect(filteredItems).toHaveLength(expectedLength)
+ })
- it('includes events that are exceptions', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: SessionRecordingPlayerTab.EVENTS,
- data: { event: '$exception' } as unknown as PerformanceEvent,
- } as unknown as InspectorListItemEvent,
- ],
- tab: SessionRecordingPlayerTab.DOCTOR,
- miniFiltersByKey: {},
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(1)
- })
+ it('filters by window id', () => {
+ expect(
+ filterInspectorListItems({
+ allItems: [
+ {
+ type: FilterableInspectorListItemTypes.EVENTS,
+ windowId: 'this window',
+ data: { event: '$exception' } as unknown as PerformanceEvent,
+ } as unknown as InspectorListItemEvent,
+ {
+ type: FilterableInspectorListItemTypes.EVENTS,
+ windowId: 'a different window',
+ data: { event: '$exception' } as unknown as PerformanceEvent,
+ } as unknown as InspectorListItemEvent,
+ ],
+ miniFiltersByKey: { 'events-exceptions': { enabled: true } as unknown as SharedListMiniFilter },
+ showOnlyMatching: false,
+ allowMatchingEventsFilter: false,
+ trackedWindow: 'a different window',
+ })
+ ).toHaveLength(1)
+ })
- it('includes browser offline status', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: 'offline-status',
- } as unknown as InspectorListOfflineStatusChange,
- ],
- tab: SessionRecordingPlayerTab.DOCTOR,
- miniFiltersByKey: {},
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(1)
- })
+ it('empty mini filters hides everything', () => {
+ expect(
+ filterInspectorListItems({
+ allItems: [
+ {
+ type: FilterableInspectorListItemTypes.EVENTS,
+ data: { event: 'an event' } as unknown as PerformanceEvent,
+ } as unknown as InspectorListItemEvent,
+ ],
+ miniFiltersByKey: {},
+ showOnlyMatching: false,
+ allowMatchingEventsFilter: false,
+ trackedWindow: null,
+ })
+ ).toHaveLength(0)
+ })
- it('includes browser visibility status', () => {
- expect(
- filterInspectorListItems({
- allItems: [
- {
- type: 'browser-visibility',
- } as InspectorListBrowserVisibility,
- ],
- tab: SessionRecordingPlayerTab.DOCTOR,
- miniFiltersByKey: {},
- showOnlyMatching: false,
- showMatchingEventsFilter: false,
- windowIdFilter: null,
- })
- ).toHaveLength(1)
- })
+ it.each([
+ [true, 1],
+ [false, 0],
+ ])('hides/shows exceptions when %s', (enabled, expectedLength) => {
+ expect(
+ filterInspectorListItems({
+ allItems: [
+ {
+ type: FilterableInspectorListItemTypes.EVENTS,
+ data: { event: '$exception' } as unknown as PerformanceEvent,
+ } as unknown as InspectorListItemEvent,
+ ],
+ miniFiltersByKey: { 'events-exceptions': { enabled } as unknown as SharedListMiniFilter },
+ showOnlyMatching: false,
+ allowMatchingEventsFilter: false,
+ trackedWindow: null,
+ })
+ ).toHaveLength(expectedLength)
})
})
diff --git a/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.ts b/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.ts
index 071351202798f..e7a0859f9c000 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/inspectorListFiltering.ts
@@ -1,17 +1,14 @@
import { InspectorListItemPerformance } from 'scenes/session-recordings/apm/performanceEventDataLogic'
-import { SharedListMiniFilter } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
+import { MiniFilterKey, SharedListMiniFilter } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
import {
IMAGE_WEB_EXTENSIONS,
- InspectorListBrowserVisibility,
InspectorListItem,
- InspectorListItemComment,
InspectorListItemConsole,
InspectorListItemDoctor,
InspectorListItemEvent,
- InspectorListOfflineStatusChange,
} from 'scenes/session-recordings/player/inspector/playerInspectorLogic'
-import { SessionRecordingPlayerTab } from '~/types'
+import { FilterableInspectorListItemTypes } from '~/types'
const PostHogMobileEvents = [
'Deep Link Opened',
@@ -31,27 +28,15 @@ function isPostHogEvent(item: InspectorListItem): boolean {
}
function isNetworkEvent(item: InspectorListItem): item is InspectorListItemPerformance {
- return item.type === SessionRecordingPlayerTab.NETWORK
-}
-
-function isOfflineStatusChange(item: InspectorListItem): item is InspectorListOfflineStatusChange {
- return item.type === 'offline-status'
-}
-
-function isBrowserVisibilityEvent(item: InspectorListItem): item is InspectorListBrowserVisibility {
- return item.type === 'browser-visibility'
+ return item.type === FilterableInspectorListItemTypes.NETWORK
}
function isNavigationEvent(item: InspectorListItem): boolean {
return isNetworkEvent(item) && ['navigation'].includes(item.data.entry_type || '')
}
-function isNetworkError(item: InspectorListItem): boolean {
- return isNetworkEvent(item) && (item.data.response_status || -1) >= 400
-}
-
function isEvent(item: InspectorListItem): item is InspectorListItemEvent {
- return item.type === SessionRecordingPlayerTab.EVENTS
+ return item.type === FilterableInspectorListItemTypes.EVENTS
}
function isPageviewOrScreen(item: InspectorListItem): boolean {
@@ -63,7 +48,7 @@ function isAutocapture(item: InspectorListItem): boolean {
}
function isConsoleEvent(item: InspectorListItem): item is InspectorListItemConsole {
- return item.type === SessionRecordingPlayerTab.CONSOLE
+ return item.type === FilterableInspectorListItemTypes.CONSOLE
}
function isConsoleError(item: InspectorListItem): boolean {
@@ -82,107 +67,120 @@ function isDoctorEvent(item: InspectorListItem): item is InspectorListItemDoctor
return item.type === 'doctor'
}
-function isComment(item: InspectorListItem): item is InspectorListItemComment {
- return item.type === 'comment'
+function isContextItem(item: InspectorListItem): boolean {
+ return ['browser-visibility', 'offline-status', 'comment', 'inspector-summary', 'inactivity'].includes(item.type)
+}
+
+const eventsMatch = (
+ item: InspectorListItemEvent,
+ miniFiltersByKey: { [p: MiniFilterKey]: SharedListMiniFilter }
+): SharedListMiniFilter | null => {
+ if (isException(item) || isErrorEvent(item)) {
+ return miniFiltersByKey['events-exceptions']
+ } else if (isAutocapture(item)) {
+ return miniFiltersByKey['events-autocapture']
+ } else if (isPageviewOrScreen(item)) {
+ return miniFiltersByKey['events-pageview']
+ } else if (isPostHogEvent(item)) {
+ return miniFiltersByKey['events-posthog']
+ } else if (!isPostHogEvent(item)) {
+ return miniFiltersByKey['events-custom']
+ }
+ return null
+}
+
+const consoleMatch = (
+ item: InspectorListItemConsole,
+ miniFiltersByKey: { [p: MiniFilterKey]: SharedListMiniFilter }
+): SharedListMiniFilter | null => {
+ if (['log', 'info'].includes(item.data.level)) {
+ return miniFiltersByKey['console-info']
+ } else if (item.data.level === 'warn') {
+ return miniFiltersByKey['console-warn']
+ } else if (isConsoleError(item)) {
+ return miniFiltersByKey['console-error']
+ }
+ return null
}
-const inspectorTabFilters: Record<
- SessionRecordingPlayerTab,
- (
- item: InspectorListItem,
- miniFiltersByKey: {
- [key: string]: SharedListMiniFilter
- }
- ) => boolean
-> = {
- [SessionRecordingPlayerTab.ALL]: (item, miniFiltersByKey) => {
- // even in everything mode we don't show doctor events
- const isAllEverything = miniFiltersByKey['all-everything']?.enabled === true && !isDoctorEvent(item)
- const isAllAutomatic =
- !!miniFiltersByKey['all-automatic']?.enabled &&
- (isOfflineStatusChange(item) || isBrowserVisibilityEvent(item) || isEvent(item) || isComment(item))
- const isAllErrors =
- !!miniFiltersByKey['all-errors']?.enabled &&
- (isNetworkError(item) || isConsoleError(item) || isException(item) || isErrorEvent(item))
- return isAllEverything || isAllAutomatic || isAllErrors
- },
- [SessionRecordingPlayerTab.EVENTS]: (item, miniFiltersByKey) => {
- if (item.type !== SessionRecordingPlayerTab.EVENTS) {
- return false
- }
- return (
- !!miniFiltersByKey['events-all']?.enabled ||
- (!!miniFiltersByKey['events-posthog']?.enabled && isPostHogEvent(item)) ||
- (!!miniFiltersByKey['events-custom']?.enabled && !isPostHogEvent(item)) ||
- (!!miniFiltersByKey['events-pageview']?.enabled && isPageviewOrScreen(item)) ||
- (!!miniFiltersByKey['events-autocapture']?.enabled && isAutocapture(item)) ||
- (!!miniFiltersByKey['events-exceptions']?.enabled && isException(item))
- )
- },
- [SessionRecordingPlayerTab.CONSOLE]: (item, miniFiltersByKey) => {
- if (item.type !== SessionRecordingPlayerTab.CONSOLE) {
- return false
- }
- return (
- !!miniFiltersByKey['console-all']?.enabled ||
- (!!miniFiltersByKey['console-info']?.enabled && ['log', 'info'].includes(item.data.level)) ||
- (!!miniFiltersByKey['console-warn']?.enabled && item.data.level === 'warn') ||
- (!!miniFiltersByKey['console-error']?.enabled && isConsoleError(item))
- )
- },
- [SessionRecordingPlayerTab.NETWORK]: (item, miniFiltersByKey) => {
- if (item.type !== SessionRecordingPlayerTab.NETWORK) {
- return false
- }
- return (
- !!miniFiltersByKey['performance-all']?.enabled === true ||
- (!!miniFiltersByKey['performance-document']?.enabled && isNavigationEvent(item)) ||
- (!!miniFiltersByKey['performance-fetch']?.enabled &&
- item.data.entry_type === 'resource' &&
- ['fetch', 'xmlhttprequest'].includes(item.data.initiator_type || '')) ||
- (!!miniFiltersByKey['performance-assets-js']?.enabled &&
- item.data.entry_type === 'resource' &&
- (item.data.initiator_type === 'script' ||
- (['link', 'other'].includes(item.data.initiator_type || '') && item.data.name?.includes('.js')))) ||
- (!!miniFiltersByKey['performance-assets-css']?.enabled &&
- item.data.entry_type === 'resource' &&
- (item.data.initiator_type === 'css' ||
- (['link', 'other'].includes(item.data.initiator_type || '') &&
- item.data.name?.includes('.css')))) ||
- (!!miniFiltersByKey['performance-assets-img']?.enabled &&
- item.data.entry_type === 'resource' &&
- (item.data.initiator_type === 'img' ||
- (['link', 'other'].includes(item.data.initiator_type || '') &&
- !!IMAGE_WEB_EXTENSIONS.some((ext) => item.data.name?.includes(`.${ext}`))))) ||
- (!!miniFiltersByKey['performance-other']?.enabled &&
- item.data.entry_type === 'resource' &&
- ['other'].includes(item.data.initiator_type || '') &&
- ![...IMAGE_WEB_EXTENSIONS, 'css', 'js'].some((ext) => item.data.name?.includes(`.${ext}`)))
- )
- },
- [SessionRecordingPlayerTab.DOCTOR]: (item) => {
- return isOfflineStatusChange(item) || isBrowserVisibilityEvent(item) || isException(item) || isDoctorEvent(item)
- },
+function networkMatch(
+ item: InspectorListItemPerformance,
+ miniFiltersByKey: {
+ [p: MiniFilterKey]: SharedListMiniFilter
+ }
+): SharedListMiniFilter | null {
+ if (isNavigationEvent(item)) {
+ return miniFiltersByKey['performance-document']
+ } else if (
+ item.data.entry_type === 'resource' &&
+ ['fetch', 'xmlhttprequest'].includes(item.data.initiator_type || '')
+ ) {
+ return miniFiltersByKey['performance-fetch']
+ } else if (
+ item.data.entry_type === 'resource' &&
+ (item.data.initiator_type === 'script' ||
+ (['link', 'other'].includes(item.data.initiator_type || '') && item.data.name?.includes('.js')))
+ ) {
+ return miniFiltersByKey['performance-assets-js']
+ } else if (
+ item.data.entry_type === 'resource' &&
+ (item.data.initiator_type === 'css' ||
+ (['link', 'other'].includes(item.data.initiator_type || '') && item.data.name?.includes('.css')))
+ ) {
+ return miniFiltersByKey['performance-assets-css']
+ } else if (
+ item.data.entry_type === 'resource' &&
+ (item.data.initiator_type === 'img' ||
+ (['link', 'other'].includes(item.data.initiator_type || '') &&
+ !!IMAGE_WEB_EXTENSIONS.some((ext) => item.data.name?.includes(`.${ext}`))))
+ ) {
+ return miniFiltersByKey['performance-assets-img']
+ } else if (
+ item.data.entry_type === 'resource' &&
+ ['other'].includes(item.data.initiator_type || '') &&
+ ![...IMAGE_WEB_EXTENSIONS, 'css', 'js'].some((ext) => item.data.name?.includes(`.${ext}`))
+ ) {
+ return miniFiltersByKey['performance-other']
+ }
+ return null
+}
+
+export function itemToMiniFilter(
+ item: InspectorListItem,
+ miniFiltersByKey: { [p: MiniFilterKey]: SharedListMiniFilter }
+): SharedListMiniFilter | null {
+ switch (item.type) {
+ case FilterableInspectorListItemTypes.EVENTS:
+ return eventsMatch(item, miniFiltersByKey)
+ case FilterableInspectorListItemTypes.CONSOLE:
+ return consoleMatch(item, miniFiltersByKey)
+ case FilterableInspectorListItemTypes.NETWORK:
+ return networkMatch(item, miniFiltersByKey)
+ case FilterableInspectorListItemTypes.DOCTOR:
+ if (isDoctorEvent(item)) {
+ return miniFiltersByKey['doctor']
+ }
+ break
+ }
+ return null
}
export function filterInspectorListItems({
allItems,
- tab,
miniFiltersByKey,
- showMatchingEventsFilter,
+ allowMatchingEventsFilter,
showOnlyMatching,
- windowIdFilter,
+ trackedWindow,
}: {
allItems: InspectorListItem[]
- tab: SessionRecordingPlayerTab
miniFiltersByKey:
| {
- [key: string]: SharedListMiniFilter
+ [key: MiniFilterKey]: SharedListMiniFilter
}
| undefined
- showMatchingEventsFilter: boolean
+ allowMatchingEventsFilter: boolean
showOnlyMatching: boolean
- windowIdFilter: string | null
+ trackedWindow: string | null
}): InspectorListItem[] {
const items: InspectorListItem[] = []
@@ -200,15 +198,18 @@ export function filterInspectorListItems({
continue
}
- include = inspectorTabFilters[tab](item, miniFiltersByKey)
+ const itemFilter = itemToMiniFilter(item, miniFiltersByKey)
+ include = isContextItem(item) || !!itemFilter?.enabled
+
+ // what about isOfflineStatusChange(item) || isBrowserVisibilityEvent(item) || isComment(item)
- if (showMatchingEventsFilter && showOnlyMatching) {
+ if (allowMatchingEventsFilter && showOnlyMatching) {
// Special case - overrides the others
include = include && item.highlightColor === 'primary'
}
const itemWindowId = item.windowId // how do we use sometimes properties $window_id... maybe we just shouldn't need to :shrug:
- const excludedByWindowFilter = !!windowIdFilter && !!itemWindowId && itemWindowId !== windowIdFilter
+ const excludedByWindowFilter = !!trackedWindow && !!itemWindowId && itemWindowId !== trackedWindow
if (!include || excludedByWindowFilter) {
continue
@@ -217,5 +218,5 @@ export function filterInspectorListItems({
items.push(item)
}
- return items
+ return items.every((i) => isContextItem(i)) ? [] : items
}
diff --git a/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.test.ts b/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.test.ts
index 1418f6a13a7f6..2f1b83ebca39d 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.test.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.test.ts
@@ -3,7 +3,6 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { miniFiltersLogic } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
import { initKeaTests } from '~/test/init'
-import { SessionRecordingPlayerTab } from '~/types'
describe('miniFiltersLogic', () => {
let logic: ReturnType
@@ -19,9 +18,17 @@ describe('miniFiltersLogic', () => {
describe('initialState', () => {
it('sets default values', () => {
expectLogic(logic).toMatchValues({
- tab: SessionRecordingPlayerTab.ALL,
showOnlyMatching: false,
- selectedMiniFilters: ['all-automatic', 'console-all', 'events-all', 'performance-all'],
+ selectedMiniFilters: [
+ 'events-posthog',
+ 'events-custom',
+ 'events-pageview',
+ 'events-autocapture',
+ 'events-exceptions',
+ 'console-info',
+ 'console-warn',
+ 'console-error',
+ ],
})
})
})
@@ -48,66 +55,20 @@ describe('miniFiltersLogic', () => {
localStorage.clear()
})
- it('should start with the first entry selected', () => {
- expect(logic.values.selectedMiniFilters).toEqual([
- 'all-automatic',
- 'console-all',
- 'events-all',
- 'performance-all',
- ])
- })
-
- it('should remove other selected filters if alone', () => {
- logic.actions.setMiniFilter('all-errors', true)
-
- expect(logic.values.selectedMiniFilters.sort()).toEqual([
- 'all-errors',
- 'console-all',
- 'events-all',
- 'performance-all',
- ])
- })
-
- it('should allow multiple filters if not alone', () => {
- logic.actions.setMiniFilter('console-warn', true)
- logic.actions.setMiniFilter('console-info', true)
-
- expect(logic.values.selectedMiniFilters.sort()).toEqual([
- 'all-automatic',
- 'console-info',
- 'console-warn',
- 'events-all',
- 'performance-all',
- ])
- })
-
- it('should reset to first in tab if empty', () => {
- expect(logic.values.selectedMiniFilters.sort()).toEqual([
- 'all-automatic',
- 'console-all',
- 'events-all',
- 'performance-all',
- ])
- logic.actions.setMiniFilter('console-warn', true)
- logic.actions.setMiniFilter('console-info', true)
-
- expect(logic.values.selectedMiniFilters.sort()).toEqual([
- 'all-automatic',
- 'console-info',
- 'console-warn',
- 'events-all',
- 'performance-all',
- ])
-
- logic.actions.setMiniFilter('console-warn', false)
- logic.actions.setMiniFilter('console-info', false)
-
- expect(logic.values.selectedMiniFilters.sort()).toEqual([
- 'all-automatic',
- 'console-all',
- 'events-all',
- 'performance-all',
- ])
+ it('can unselect', async () => {
+ await expectLogic(logic, () => {
+ logic.actions.setMiniFilter('events-posthog', false)
+ }).toMatchValues({
+ selectedMiniFilters: [
+ 'events-custom',
+ 'events-pageview',
+ 'events-autocapture',
+ 'events-exceptions',
+ 'console-info',
+ 'console-warn',
+ 'console-error',
+ ],
+ })
})
})
})
diff --git a/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.ts
index e6f6d92d77752..e7388ba569c83 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/miniFiltersLogic.ts
@@ -2,177 +2,115 @@ import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { teamLogic } from 'scenes/teamLogic'
-import { SessionRecordingPlayerTab } from '~/types'
+import { FilterableInspectorListItemTypes } from '~/types'
import type { miniFiltersLogicType } from './miniFiltersLogicType'
export type SharedListMiniFilter = {
- tab: SessionRecordingPlayerTab
+ type: FilterableInspectorListItemTypes
key: string
name: string
- // If alone, then enabling it will disable all the others
- alone?: boolean
tooltip?: string
enabled?: boolean
}
const MiniFilters: SharedListMiniFilter[] = [
{
- tab: SessionRecordingPlayerTab.ALL,
- key: 'all-automatic',
- name: 'Auto',
- alone: true,
- tooltip: 'Curated list of key PostHog events, custom events, error logs etc.',
- },
- {
- tab: SessionRecordingPlayerTab.ALL,
- key: 'all-errors',
- name: 'Errors',
- alone: true,
- tooltip: 'Events containing "error" or "exception" in their name and console errors',
- },
- {
- tab: SessionRecordingPlayerTab.ALL,
- key: 'all-everything',
- name: 'Everything',
- alone: true,
- tooltip: 'Everything that happened in this session',
- },
- {
- tab: SessionRecordingPlayerTab.EVENTS,
- key: 'events-all',
- name: 'All',
- alone: true,
- tooltip: 'All events tracked during this session',
- },
- {
- tab: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
key: 'events-posthog',
name: 'PostHog',
- tooltip: 'Standard PostHog events like Pageviews, Autocapture etc.',
+ tooltip: 'Standard PostHog events except Pageviews, Autocapture, and Exceptions.',
},
{
- tab: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
key: 'events-custom',
name: 'Custom',
tooltip: 'Custom events tracked by your app',
},
{
- tab: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
key: 'events-pageview',
name: 'Pageview / Screen',
tooltip: 'Pageview (or Screen for mobile) events',
},
{
- tab: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
key: 'events-autocapture',
name: 'Autocapture',
tooltip: 'Autocapture events such as clicks and inputs',
},
{
- tab: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
key: 'events-exceptions',
name: 'Exceptions',
tooltip: 'Exception events from PostHog or its Sentry integration',
},
{
- tab: SessionRecordingPlayerTab.CONSOLE,
- key: 'console-all',
- name: 'All',
- alone: true,
- },
- {
- tab: SessionRecordingPlayerTab.CONSOLE,
+ type: FilterableInspectorListItemTypes.CONSOLE,
key: 'console-info',
name: 'Info',
},
{
- tab: SessionRecordingPlayerTab.CONSOLE,
+ type: FilterableInspectorListItemTypes.CONSOLE,
key: 'console-warn',
name: 'Warn',
},
{
- tab: SessionRecordingPlayerTab.CONSOLE,
+ type: FilterableInspectorListItemTypes.CONSOLE,
key: 'console-error',
name: 'Error',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
- key: 'performance-all',
- name: 'All',
- alone: true,
- tooltip: 'All network performance information collected during the session',
- },
- {
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-fetch',
name: 'Fetch/XHR',
tooltip: 'Requests during the session to external resources like APIs via XHR or Fetch',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-document',
name: 'Doc',
tooltip: 'Page load information collected on a fresh browser page load, refresh, or page paint.',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-assets-js',
name: 'JS',
tooltip: 'Scripts loaded during the session.',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-assets-css',
name: 'CSS',
tooltip: 'CSS loaded during the session.',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-assets-img',
name: 'Img',
tooltip: 'Images loaded during the session.',
},
{
- tab: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
key: 'performance-other',
name: 'Other',
tooltip: 'Any other network requests that do not fall into the other categories',
},
-
- // NOTE: The below filters use the `response_status` property which is currently experiemental
- // and as such doesn't show for many browsers: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus
- // We should only add these in if the recording in question has those values (otherwiseit is a confusing experience for the user)
-
- // {
- // tab: SessionRecordingPlayerTab.PERFORMANCE,
- // key: 'performance-2xx',
- // name: '2xx',
- // tooltip:
- // 'Requests that returned a HTTP status code of 2xx. The request was successfully received, understood, and accepted.',
- // },
- // {
- // tab: SessionRecordingPlayerTab.PERFORMANCE,
- // key: 'performance-4xx',
- // name: '4xx',
- // tooltip:
- // 'Requests that returned a HTTP status code of 4xx. The request contains bad syntax or cannot be fulfilled.',
- // },
- // {
- // tab: SessionRecordingPlayerTab.PERFORMANCE,
- // key: 'performance-5xx',
- // name: '5xx',
- // tooltip:
- // 'Requests that returned a HTTP status code of 5xx. The server failed to fulfil an apparently valid request.',
- // },
+ {
+ type: FilterableInspectorListItemTypes.DOCTOR,
+ key: 'doctor',
+ name: 'Doctor',
+ tooltip:
+ 'Doctor events are special events that are automatically detected by PostHog to help diagnose issues in replay.',
+ },
]
+export type MiniFilterKey = (typeof MiniFilters)[number]['key']
export const miniFiltersLogic = kea([
path(['scenes', 'session-recordings', 'player', 'miniFiltersLogic']),
actions({
setShowOnlyMatching: (showOnlyMatching: boolean) => ({ showOnlyMatching }),
- setTab: (tab: SessionRecordingPlayerTab) => ({ tab }),
- setMiniFilter: (key: string, enabled: boolean) => ({ key, enabled }),
+ setMiniFilter: (key: MiniFilterKey, enabled: boolean) => ({ key, enabled }),
setSearchQuery: (search: string) => ({ search }),
}),
connect({
@@ -187,55 +125,28 @@ export const miniFiltersLogic = kea([
},
],
- tab: [
- SessionRecordingPlayerTab.ALL as SessionRecordingPlayerTab,
- { persist: true },
- {
- setTab: (_, { tab }) => tab,
- },
- ],
-
selectedMiniFilters: [
- ['all-automatic', 'console-all', 'events-all', 'performance-all'] as string[],
+ [
+ 'events-posthog',
+ 'events-custom',
+ 'events-pageview',
+ 'events-autocapture',
+ 'events-exceptions',
+ 'console-info',
+ 'console-warn',
+ 'console-error',
+ ] as MiniFilterKey[],
{ persist: true },
{
setMiniFilter: (state, { key, enabled }) => {
- const selectedFilter = MiniFilters.find((x) => x.key === key)
-
- if (!selectedFilter) {
- return state
- }
- const filtersInTab = MiniFilters.filter((x) => x.tab === selectedFilter.tab)
-
- const newFilters = state.filter((existingSelected) => {
- const filterInTab = filtersInTab.find((x) => x.key === existingSelected)
- if (!filterInTab) {
- return true
- }
-
- if (enabled) {
- if (selectedFilter.alone) {
- return false
- }
- return filterInTab.alone ? false : true
- }
-
- if (existingSelected !== key) {
- return true
- }
- return false
- })
-
+ const stateWithoutKey = state.filter((x) => x !== key)
if (enabled) {
- newFilters.push(key)
- } else {
- // Ensure the first one is checked if no others
- if (filtersInTab.every((x) => !newFilters.includes(x.key))) {
- newFilters.push(filtersInTab[0].key)
- }
+ // ensure it's in the array
+ // remove it if it's there and then add it back
+ return stateWithoutKey.concat(key)
}
-
- return newFilters
+ // ensure it's not in the array
+ return stateWithoutKey
},
},
],
@@ -249,11 +160,11 @@ export const miniFiltersLogic = kea([
})),
selectors({
- miniFiltersForTab: [
+ miniFiltersForType: [
(s) => [s.selectedMiniFilters],
- (selectedMiniFilters): ((tab: SessionRecordingPlayerTab) => SharedListMiniFilter[]) => {
- return (tab: SessionRecordingPlayerTab) => {
- return MiniFilters.filter((filter) => filter.tab === tab).map((x) => ({
+ (selectedMiniFilters): ((tab: FilterableInspectorListItemTypes) => SharedListMiniFilter[]) => {
+ return (tab: FilterableInspectorListItemTypes) => {
+ return MiniFilters.filter((filter) => filter.type === tab).map((x) => ({
...x,
enabled: selectedMiniFilters.includes(x.key),
}))
@@ -262,9 +173,12 @@ export const miniFiltersLogic = kea([
],
miniFilters: [
- (s) => [s.tab, s.miniFiltersForTab],
- (tab, miniFiltersForTab): SharedListMiniFilter[] => {
- return miniFiltersForTab(tab)
+ (s) => [s.selectedMiniFilters],
+ (selectedMiniFilters): SharedListMiniFilter[] => {
+ return MiniFilters.map((x) => ({
+ ...x,
+ enabled: selectedMiniFilters.includes(x.key),
+ }))
},
],
@@ -278,11 +192,13 @@ export const miniFiltersLogic = kea([
},
],
- miniFiltersForTabByKey: [
- (s) => [s.miniFiltersForTab],
- (miniFiltersForTab): ((tab: SessionRecordingPlayerTab) => { [key: string]: SharedListMiniFilter }) => {
+ miniFiltersForTypeByKey: [
+ (s) => [s.miniFiltersForType],
+ (
+ miniFiltersForType
+ ): ((tab: FilterableInspectorListItemTypes) => { [key: string]: SharedListMiniFilter }) => {
return (tab) => {
- return miniFiltersForTab(tab).reduce((acc, filter) => {
+ return miniFiltersForType(tab).reduce((acc, filter) => {
acc[filter.key] = filter
return acc
}, {})
@@ -290,13 +206,10 @@ export const miniFiltersLogic = kea([
},
],
}),
- listeners(({ values }) => ({
- setTab: ({ tab }) => {
- eventUsageLogic.actions.reportRecordingInspectorTabViewed(tab)
- },
+ listeners(() => ({
setMiniFilter: ({ key, enabled }) => {
if (enabled) {
- eventUsageLogic.actions.reportRecordingInspectorMiniFilterViewed(values.tab, key)
+ eventUsageLogic.actions.reportRecordingInspectorMiniFilterViewed(key, enabled)
}
},
})),
diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts
index dafd05c304ff3..e2ac2aa0e96e0 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.test.ts
@@ -65,26 +65,27 @@ describe('playerInspectorLogic', () => {
})
})
- describe('setWindowIdFilter', () => {
- it('happy case', async () => {
- await expectLogic(logic).toMatchValues({
- windowIdFilter: null,
- })
+ describe('setTrackedWindow', () => {
+ it('starts with no tracked window', async () => {
await expectLogic(logic, () => {
- logic.actions.setWindowIdFilter('nightly')
+ logic.actions.setTrackedWindow(null as unknown as string)
})
- .toDispatchActions(['setWindowIdFilter'])
+ .toDispatchActions(['setTrackedWindow'])
.toMatchValues({
- windowIdFilter: 'nightly',
+ trackedWindow: null,
})
})
- it('default all', async () => {
+
+ it('can set tracked window', async () => {
+ await expectLogic(logic).toMatchValues({
+ trackedWindow: null,
+ })
await expectLogic(logic, () => {
- logic.actions.setWindowIdFilter(null as unknown as string)
+ logic.actions.setTrackedWindow('nightly')
})
- .toDispatchActions(['setWindowIdFilter'])
+ .toDispatchActions(['setTrackedWindow'])
.toMatchValues({
- windowIdFilter: null,
+ trackedWindow: 'nightly',
})
})
})
diff --git a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts
index 1b94f2db41f08..f3d380dbc14f7 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts
+++ b/frontend/src/scenes/session-recordings/player/inspector/playerInspectorLogic.ts
@@ -12,20 +12,23 @@ import {
InspectorListItemPerformance,
performanceEventDataLogic,
} from 'scenes/session-recordings/apm/performanceEventDataLogic'
-import { filterInspectorListItems } from 'scenes/session-recordings/player/inspector/inspectorListFiltering'
-import { miniFiltersLogic } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
+import {
+ filterInspectorListItems,
+ itemToMiniFilter,
+} from 'scenes/session-recordings/player/inspector/inspectorListFiltering'
+import { MiniFilterKey, miniFiltersLogic } from 'scenes/session-recordings/player/inspector/miniFiltersLogic'
import {
convertUniversalFiltersToRecordingsQuery,
MatchingEventsMatchType,
} from 'scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic'
import {
+ FilterableInspectorListItemTypes,
MatchedRecordingEvent,
PerformanceEvent,
RecordingConsoleLogV2,
RecordingEventType,
RRWebRecordingConsoleLogPayload,
- SessionRecordingPlayerTab,
} from '~/types'
import { sessionRecordingDataLogic } from '../sessionRecordingDataLogic'
@@ -54,6 +57,14 @@ export const IMAGE_WEB_EXTENSIONS = [
// Helping kea-typegen navigate the exported default class for Fuse
export interface Fuse extends FuseClass {}
+export type RecordingComment = {
+ id: string
+ notebookShortId: string
+ notebookTitle: string
+ comment: string
+ timeInRecording: number
+}
+
export type InspectorListItemBase = {
timestamp: Dayjs
timeInRecording: number
@@ -63,17 +74,16 @@ export type InspectorListItemBase = {
windowNumber?: number | '?' | undefined
}
+export type InspectorListItemType = InspectorListItem['type']
+
export type InspectorListItemEvent = InspectorListItemBase & {
- type: SessionRecordingPlayerTab.EVENTS
+ type: FilterableInspectorListItemTypes.EVENTS
data: RecordingEventType
}
-export type RecordingComment = {
- id: string
- notebookShortId: string
- notebookTitle: string
- comment: string
- timeInRecording: number
+export type InspectorListItemInactivity = InspectorListItemBase & {
+ type: 'inactivity'
+ durationMs: number
}
export type InspectorListItemComment = InspectorListItemBase & {
@@ -82,7 +92,7 @@ export type InspectorListItemComment = InspectorListItemBase & {
}
export type InspectorListItemConsole = InspectorListItemBase & {
- type: SessionRecordingPlayerTab.CONSOLE
+ type: FilterableInspectorListItemTypes.CONSOLE
data: RecordingConsoleLogV2
}
@@ -97,12 +107,19 @@ export type InspectorListBrowserVisibility = InspectorListItemBase & {
}
export type InspectorListItemDoctor = InspectorListItemBase & {
- type: SessionRecordingPlayerTab.DOCTOR
+ type: FilterableInspectorListItemTypes.DOCTOR
tag: string
data?: Record
window_id?: string
}
+export type InspectorListItemSummary = InspectorListItemBase & {
+ type: 'inspector-summary'
+ clickCount: number | null
+ keypressCount: number | null
+ errorCount: number | null
+}
+
export type InspectorListItem =
| InspectorListItemEvent
| InspectorListItemConsole
@@ -111,6 +128,8 @@ export type InspectorListItem =
| InspectorListItemDoctor
| InspectorListBrowserVisibility
| InspectorListItemComment
+ | InspectorListItemSummary
+ | InspectorListItemInactivity
export interface PlayerInspectorLogicProps extends SessionRecordingPlayerLogicProps {
matchingEventsMatchType?: MatchingEventsMatchType
@@ -141,7 +160,12 @@ function snapshotDescription(snapshot: eventWithTime): string {
}
function timeRelativeToStart(
- thingWithTime: eventWithTime | PerformanceEvent | RecordingConsoleLogV2 | RecordingEventType,
+ thingWithTime:
+ | eventWithTime
+ | PerformanceEvent
+ | RecordingConsoleLogV2
+ | RecordingEventType
+ | { timestamp: number },
start: Dayjs | null
): {
timeInRecording: number
@@ -193,15 +217,15 @@ export const playerInspectorLogic = kea([
connect((props: PlayerInspectorLogicProps) => ({
actions: [
miniFiltersLogic,
- ['setTab', 'setMiniFilter', 'setSearchQuery'],
+ ['setMiniFilter', 'setSearchQuery'],
eventUsageLogic,
['reportRecordingInspectorItemExpanded'],
sessionRecordingDataLogic(props),
- ['loadFullEventData'],
+ ['loadFullEventData', 'setTrackedWindow'],
],
values: [
miniFiltersLogic,
- ['showOnlyMatching', 'tab', 'miniFiltersByKey', 'searchQuery', 'miniFiltersForTabByKey'],
+ ['showOnlyMatching', 'miniFiltersByKey', 'searchQuery', 'miniFiltersForTypeByKey', 'miniFilters'],
sessionRecordingDataLogic(props),
[
'sessionPlayerData',
@@ -215,43 +239,37 @@ export const playerInspectorLogic = kea([
'durationMs',
'sessionComments',
'windowIdForTimestamp',
+ 'sessionPlayerMetaData',
+ 'segments',
],
sessionRecordingPlayerLogic(props),
['currentPlayerTime'],
performanceEventDataLogic({ key: props.playerKey, sessionRecordingId: props.sessionRecordingId }),
['allPerformanceEvents'],
+ sessionRecordingDataLogic(props),
+ ['trackedWindow'],
],
})),
actions(() => ({
- setWindowIdFilter: (windowId: string | null) => ({ windowId }),
setItemExpanded: (index: number, expanded: boolean) => ({ index, expanded }),
setSyncScrollPaused: (paused: boolean) => ({ paused }),
})),
reducers(() => ({
- windowIdFilter: [
- null as string | null,
- {
- setWindowIdFilter: (_, { windowId }) => windowId || null,
- },
- ],
expandedItems: [
[] as number[],
{
setItemExpanded: (items, { index, expanded }) => {
return expanded ? [...items, index] : items.filter((item) => item !== index)
},
-
- setTab: () => [],
setMiniFilter: () => [],
setSearchQuery: () => [],
- setWindowIdFilter: () => [],
+ setTrackedWindow: () => [],
},
],
syncScrollPaused: [
false,
{
- setTab: () => false,
setSyncScrollPaused: (_, { paused }) => paused,
setItemExpanded: () => true,
},
@@ -290,10 +308,13 @@ export const playerInspectorLogic = kea([
],
})),
selectors(({ props }) => ({
- showMatchingEventsFilter: [
- (s) => [s.tab],
- (tab): boolean => {
- return tab === SessionRecordingPlayerTab.EVENTS && props.matchingEventsMatchType?.matchType !== 'none'
+ allowMatchingEventsFilter: [
+ (s) => [s.miniFilters],
+ (miniFilters): boolean => {
+ return (
+ miniFilters.some((mf) => mf.type === FilterableInspectorListItemTypes.EVENTS && mf.enabled) &&
+ props.matchingEventsMatchType?.matchType !== 'none'
+ )
},
],
@@ -413,13 +434,12 @@ export const playerInspectorLogic = kea([
const { timestamp, timeInRecording } = timeRelativeToStart(snapshot, start)
items.push({
- type: SessionRecordingPlayerTab.DOCTOR,
+ type: FilterableInspectorListItemTypes.DOCTOR,
timestamp,
timeInRecording,
tag: niceify(tag),
search: niceify(tag),
window_id: windowId,
- // TODO why both?
windowId: windowId,
windowNumber: windowNumberForID(windowId),
data: getPayloadFor(customEvent, tag),
@@ -429,13 +449,12 @@ export const playerInspectorLogic = kea([
const { timestamp, timeInRecording } = timeRelativeToStart(snapshot, start)
items.push({
- type: SessionRecordingPlayerTab.DOCTOR,
+ type: FilterableInspectorListItemTypes.DOCTOR,
timestamp,
timeInRecording,
tag: 'full snapshot event',
search: 'full snapshot event',
window_id: windowId,
- // TODO why both?
windowId: windowId,
windowNumber: windowNumberForID(windowId),
data: { snapshotSize: humanizeBytes(estimateSize(snapshot)) },
@@ -445,7 +464,7 @@ export const playerInspectorLogic = kea([
})
items.push({
- type: SessionRecordingPlayerTab.DOCTOR,
+ type: FilterableInspectorListItemTypes.DOCTOR,
timestamp: start,
timeInRecording: 0,
tag: 'count of snapshot types by window',
@@ -505,39 +524,50 @@ export const playerInspectorLogic = kea([
},
],
- allItems: [
+ allContextItems: [
(s) => [
s.start,
- s.allPerformanceEvents,
- s.consoleLogs,
- s.sessionEventsData,
- s.matchingEventUUIDs,
s.offlineStatusChanges,
s.doctorEvents,
s.browserVisibilityChanges,
s.sessionComments,
s.windowIdForTimestamp,
s.windowNumberForID,
+ s.sessionPlayerMetaData,
+ s.segments,
],
(
start,
- performanceEvents,
- consoleLogs,
- eventsData,
- matchingEventUUIDs,
offlineStatusChanges,
doctorEvents,
browserVisibilityChanges,
sessionComments,
windowIdForTimestamp,
- windowNumberForID
- ): InspectorListItem[] => {
- // NOTE: Possible perf improvement here would be to have a selector to parse the items
- // and then do the filtering of what items are shown, elsewhere
- // ALSO: We could move the individual filtering logic into the MiniFilters themselves
- // WARNING: Be careful of dayjs functions - they can be slow due to the size of the loop.
+ windowNumberForID,
+ sessionPlayerMetaData,
+ segments
+ ) => {
const items: InspectorListItem[] = []
+ segments
+ .filter((segment) => segment.kind === 'gap')
+ .filter((segment) => segment.durationMs > 15000)
+ .map((segment) => {
+ const { timestamp, timeInRecording } = timeRelativeToStart(
+ { timestamp: segment.startTimestamp },
+ start
+ )
+ items.push({
+ type: 'inactivity',
+ durationMs: segment.durationMs,
+ windowId: segment.windowId,
+ windowNumber: windowNumberForID(segment.windowId),
+ timestamp,
+ timeInRecording,
+ search: 'inactiv',
+ })
+ })
+
// no conversion needed for offlineStatusChanges, they're ready to roll
for (const event of offlineStatusChanges || []) {
items.push(event)
@@ -553,11 +583,72 @@ export const playerInspectorLogic = kea([
items.push(event)
}
+ for (const comment of sessionComments || []) {
+ const { timestamp, timeInRecording } = commentTimestamp(comment, start)
+ if (timestamp) {
+ items.push({
+ highlightColor: 'primary',
+ type: 'comment',
+ timeInRecording: timeInRecording,
+ timestamp: timestamp,
+ search: comment.comment,
+ data: comment,
+ windowId: windowIdForTimestamp(timestamp.valueOf()),
+ windowNumber: windowNumberForID(windowIdForTimestamp(timestamp.valueOf())),
+ })
+ }
+ }
+
+ // now we've calculated everything else
+ // always start with a context row that has a little summary
+ if (start) {
+ items.push({
+ type: 'inspector-summary',
+ timestamp: start,
+ timeInRecording: 0,
+ search: '',
+ clickCount: sessionPlayerMetaData?.click_count || null,
+ keypressCount: sessionPlayerMetaData?.keypress_count || null,
+ errorCount: 0,
+ })
+ }
+
+ // NOTE: Native JS sorting is relatively slow here - be careful changing this
+ items.sort((a, b) => (a.timestamp.valueOf() > b.timestamp.valueOf() ? 1 : -1))
+
+ return items
+ },
+ ],
+
+ allItems: [
+ (s) => [
+ s.start,
+ s.allPerformanceEvents,
+ s.consoleLogs,
+ s.sessionEventsData,
+ s.matchingEventUUIDs,
+ s.windowNumberForID,
+ s.allContextItems,
+ ],
+ (
+ start,
+ performanceEvents,
+ consoleLogs,
+ eventsData,
+ matchingEventUUIDs,
+ windowNumberForID,
+ allContextItems
+ ): InspectorListItem[] => {
+ // NOTE: Possible perf improvement here would be to have a selector to parse the items
+ // and then do the filtering of what items are shown, elsewhere
+ // ALSO: We could move the individual filtering logic into the MiniFilters themselves
+ // WARNING: Be careful of dayjs functions - they can be slow due to the size of the loop.
+ const items: InspectorListItem[] = []
+
// PERFORMANCE EVENTS
const performanceEventsArr = performanceEvents || []
for (const event of performanceEventsArr) {
- // TODO should we be defaulting to 200 here :shrug:
- const responseStatus = event.response_status || 200
+ const responseStatus = event.response_status || null
if (event.entry_type === 'paint') {
// We don't include paint events as they are covered in the navigation events
@@ -566,12 +657,12 @@ export const playerInspectorLogic = kea([
const { timestamp, timeInRecording } = timeRelativeToStart(event, start)
items.push({
- type: SessionRecordingPlayerTab.NETWORK,
+ type: FilterableInspectorListItemTypes.NETWORK,
timestamp,
timeInRecording,
search: event.name || '',
data: event,
- highlightColor: responseStatus >= 400 ? 'danger' : undefined,
+ highlightColor: (responseStatus || 0) >= 400 ? 'danger' : undefined,
windowId: event.window_id,
windowNumber: windowNumberForID(event.window_id),
})
@@ -581,7 +672,7 @@ export const playerInspectorLogic = kea([
for (const event of consoleLogs || []) {
const { timestamp, timeInRecording } = timeRelativeToStart(event, start)
items.push({
- type: SessionRecordingPlayerTab.CONSOLE,
+ type: FilterableInspectorListItemTypes.CONSOLE,
timestamp,
timeInRecording,
search: event.content,
@@ -593,9 +684,14 @@ export const playerInspectorLogic = kea([
})
}
+ let errorCount = 0
for (const event of eventsData || []) {
let isMatchingEvent = false
+ if (event.event === '$exception') {
+ errorCount += 1
+ }
+
if (matchingEventUUIDs?.length) {
isMatchingEvent = !!matchingEventUUIDs.find((x) => x.uuid === String(event.id))
} else if (props.matchingEventsMatchType?.matchType === 'name') {
@@ -610,7 +706,7 @@ export const playerInspectorLogic = kea([
const { timestamp, timeInRecording } = timeRelativeToStart(event, start)
items.push({
- type: SessionRecordingPlayerTab.EVENTS,
+ type: FilterableInspectorListItemTypes.EVENTS,
timestamp,
timeInRecording,
search: search,
@@ -625,90 +721,92 @@ export const playerInspectorLogic = kea([
})
}
- for (const comment of sessionComments || []) {
- const { timestamp, timeInRecording } = commentTimestamp(comment, start)
- if (timestamp) {
- items.push({
- highlightColor: 'primary',
- type: 'comment',
- timeInRecording: timeInRecording,
- timestamp: timestamp,
- search: comment.comment,
- data: comment,
- windowId: windowIdForTimestamp(timestamp.valueOf()),
- windowNumber: windowNumberForID(windowIdForTimestamp(timestamp.valueOf())),
- })
- }
+ for (const event of allContextItems || []) {
+ items.push(event)
}
// NOTE: Native JS sorting is relatively slow here - be careful changing this
items.sort((a, b) => (a.timestamp.valueOf() > b.timestamp.valueOf() ? 1 : -1))
+ // ensure that item with type 'inspector-summary' is always at the top
+ const summary = items.find((item) => item.type === 'inspector-summary')
+ if (summary) {
+ ;(summary as InspectorListItemSummary).errorCount = errorCount
+ items.splice(items.indexOf(summary), 1)
+ items.unshift(summary)
+ }
+ if (items.length > 0) {
+ items[0].windowNumber = items[1]?.windowNumber
+ items[0].windowId = items[1]?.windowId
+ }
+
return items
},
],
filteredItems: [
- (s) => [
- s.allItems,
- s.tab,
- s.miniFiltersByKey,
- s.showOnlyMatching,
- s.showMatchingEventsFilter,
- s.windowIdFilter,
- ],
+ (s) => [s.allItems, s.miniFiltersByKey, s.showOnlyMatching, s.allowMatchingEventsFilter, s.trackedWindow],
(
allItems,
- tab,
miniFiltersByKey,
showOnlyMatching,
- showMatchingEventsFilter,
- windowIdFilter
+ allowMatchingEventsFilter,
+ trackedWindow
): InspectorListItem[] => {
- return filterInspectorListItems({
+ const filteredItems = filterInspectorListItems({
allItems,
- tab,
miniFiltersByKey,
- showMatchingEventsFilter,
+ allowMatchingEventsFilter,
showOnlyMatching,
- windowIdFilter,
+ trackedWindow,
})
+ // need to collapse adjacent inactivity items
+ // they look werong next to each other
+ return filteredItems.reduce((acc, item, index) => {
+ if (item.type === 'inactivity') {
+ const previousItem = filteredItems[index - 1]
+ if (previousItem?.type === 'inactivity') {
+ previousItem.durationMs += item.durationMs
+ return acc
+ }
+ }
+ acc.push(item)
+ return acc
+ }, [] as InspectorListItem[])
},
],
seekbarItems: [
(s) => [
s.allItems,
- s.miniFiltersForTabByKey,
+ s.miniFiltersForTypeByKey,
s.showOnlyMatching,
- s.showMatchingEventsFilter,
- s.windowIdFilter,
+ s.allowMatchingEventsFilter,
+ s.trackedWindow,
],
(
allItems,
- miniFiltersForTabByKey,
+ miniFiltersForTypeByKey,
showOnlyMatching,
- showMatchingEventsFilter,
- windowIdFilter
+ allowMatchingEventsFilter,
+ trackedWindow
): (InspectorListItemEvent | InspectorListItemComment)[] => {
- const eventsTabFilters = miniFiltersForTabByKey(SessionRecordingPlayerTab.EVENTS)
- const eventTabFilteredItems = filterInspectorListItems({
+ const eventFilteredItems = filterInspectorListItems({
allItems,
- tab: SessionRecordingPlayerTab.EVENTS,
- miniFiltersByKey: eventsTabFilters,
- showMatchingEventsFilter,
+ miniFiltersByKey: miniFiltersForTypeByKey(FilterableInspectorListItemTypes.EVENTS),
+ allowMatchingEventsFilter,
showOnlyMatching,
- windowIdFilter,
+ trackedWindow,
})
- let items: (InspectorListItemEvent | InspectorListItemComment)[] = eventTabFilteredItems.filter(
+ let items: (InspectorListItemEvent | InspectorListItemComment)[] = eventFilteredItems.filter(
(item): item is InspectorListItemEvent | InspectorListItemComment => {
- if (item.type === SessionRecordingPlayerTab.EVENTS) {
- return !(showMatchingEventsFilter && showOnlyMatching && item.highlightColor !== 'primary')
+ if (item.type === FilterableInspectorListItemTypes.EVENTS) {
+ return !(allowMatchingEventsFilter && showOnlyMatching && item.highlightColor !== 'primary')
}
if (item.type === 'comment') {
- return !showMatchingEventsFilter
+ return !allowMatchingEventsFilter
}
return false
@@ -719,7 +817,7 @@ export const playerInspectorLogic = kea([
items = items.filter((item) => {
const isPrimary = item.highlightColor === 'primary'
const isPageView =
- item.type === SessionRecordingPlayerTab.EVENTS && item.data.event === '$pageview'
+ item.type === FilterableInspectorListItemTypes.EVENTS && item.data.event === '$pageview'
const isComment = item.type === 'comment'
return isPrimary || isPageView || isComment
})
@@ -733,7 +831,7 @@ export const playerInspectorLogic = kea([
},
],
- tabsState: [
+ inspectorDataState: [
(s) => [
s.sessionEventsDataLoading,
s.sessionPlayerMetaDataLoading,
@@ -751,36 +849,31 @@ export const playerInspectorLogic = kea([
logs,
performanceEvents,
doctorEvents
- ): Record => {
- const tabEventsState = sessionEventsDataLoading ? 'loading' : events?.length ? 'ready' : 'empty'
- const tabConsoleState =
+ ): Record => {
+ const dataForEventsState = sessionEventsDataLoading ? 'loading' : events?.length ? 'ready' : 'empty'
+ const dataForConsoleState =
sessionPlayerMetaDataLoading || snapshotsLoading || !logs
? 'loading'
: logs.length
? 'ready'
: 'empty'
- const tabNetworkState =
+ const dataForNetworkState =
sessionPlayerMetaDataLoading || snapshotsLoading || !performanceEvents
? 'loading'
: performanceEvents.length
? 'ready'
: 'empty'
- const tabDoctorState =
+ const dataForDoctorState =
sessionPlayerMetaDataLoading || snapshotsLoading || !performanceEvents
? 'loading'
: doctorEvents.length
? 'ready'
: 'empty'
return {
- [SessionRecordingPlayerTab.ALL]: [tabEventsState, tabConsoleState, tabNetworkState].every(
- (x) => x === 'loading'
- )
- ? 'loading'
- : 'ready',
- [SessionRecordingPlayerTab.EVENTS]: tabEventsState,
- [SessionRecordingPlayerTab.CONSOLE]: tabConsoleState,
- [SessionRecordingPlayerTab.NETWORK]: tabNetworkState,
- [SessionRecordingPlayerTab.DOCTOR]: tabDoctorState,
+ [FilterableInspectorListItemTypes.EVENTS]: dataForEventsState,
+ [FilterableInspectorListItemTypes.CONSOLE]: dataForConsoleState,
+ [FilterableInspectorListItemTypes.NETWORK]: dataForNetworkState,
+ [FilterableInspectorListItemTypes.DOCTOR]: dataForDoctorState,
}
},
],
@@ -825,15 +918,80 @@ export const playerInspectorLogic = kea([
return fuse.search(searchQuery).map((x: any) => x.item)
},
],
+
+ /**
+ * All items by mini-filter key, not filtered items, so that we can count the unfiltered sets
+ */
+ allItemsByMiniFilterKey: [
+ (s) => [s.allItems, s.miniFiltersByKey],
+ (allItems, miniFiltersByKey): Record => {
+ const itemsByMiniFilterKey: Record = {
+ 'events-posthog': [],
+ 'events-custom': [],
+ 'events-pageview': [],
+ 'events-autocapture': [],
+ 'events-exceptions': [],
+ 'console-info': [],
+ 'console-warn': [],
+ 'console-error': [],
+ 'performance-fetch': [],
+ 'performance-document': [],
+ 'performance-assets-js': [],
+ 'performance-assets-css': [],
+ 'performance-assets-img': [],
+ 'performance-other': [],
+ doctor: [],
+ }
+
+ for (const item of allItems) {
+ const miniFilter = itemToMiniFilter(item, miniFiltersByKey)
+ if (miniFilter) {
+ itemsByMiniFilterKey[miniFilter.key].push(item)
+ }
+ }
+
+ return itemsByMiniFilterKey
+ },
+ ],
+
+ /**
+ * All items by item type, not filtered items, so that we can count the unfiltered sets
+ */
+ allItemsByItemType: [
+ (s) => [s.allItems],
+ (allItems): Record => {
+ const itemsByType: Record = {
+ [FilterableInspectorListItemTypes.EVENTS]: [],
+ [FilterableInspectorListItemTypes.CONSOLE]: [],
+ [FilterableInspectorListItemTypes.NETWORK]: [],
+ [FilterableInspectorListItemTypes.DOCTOR]: [],
+ context: [],
+ }
+
+ for (const item of allItems) {
+ itemsByType[
+ [
+ FilterableInspectorListItemTypes.EVENTS,
+ FilterableInspectorListItemTypes.CONSOLE,
+ FilterableInspectorListItemTypes.NETWORK,
+ FilterableInspectorListItemTypes.DOCTOR,
+ ].includes(item.type as FilterableInspectorListItemTypes)
+ ? item.type
+ : 'context'
+ ].push(item)
+ }
+
+ return itemsByType
+ },
+ ],
})),
listeners(({ values, actions }) => ({
setItemExpanded: ({ index, expanded }) => {
if (expanded) {
- eventUsageLogic.actions.reportRecordingInspectorItemExpanded(values.tab, index)
-
const item = values.items[index]
+ eventUsageLogic.actions.reportRecordingInspectorItemExpanded(item.type, index)
- if (item.type === SessionRecordingPlayerTab.EVENTS) {
+ if (item.type === FilterableInspectorListItemTypes.EVENTS) {
actions.loadFullEventData(item.data)
}
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 4c2ceab0faafe..b19486531a11c 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -997,8 +997,7 @@ export enum SessionRecordingSidebarStacking {
Horizontal = 'horizontal',
}
-export enum SessionRecordingPlayerTab {
- ALL = 'all',
+export enum FilterableInspectorListItemTypes {
EVENTS = 'events',
CONSOLE = 'console',
NETWORK = 'network',