diff --git a/frontend/__snapshots__/components-properties-table--basic--dark.png b/frontend/__snapshots__/components-properties-table--basic--dark.png index 3890636e34f8a..a561e6ed86b77 100644 Binary files a/frontend/__snapshots__/components-properties-table--basic--dark.png and b/frontend/__snapshots__/components-properties-table--basic--dark.png differ diff --git a/frontend/__snapshots__/components-properties-table--basic--light.png b/frontend/__snapshots__/components-properties-table--basic--light.png index de0f90740eb37..43498d9e17326 100644 Binary files a/frontend/__snapshots__/components-properties-table--basic--light.png and b/frontend/__snapshots__/components-properties-table--basic--light.png differ diff --git a/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--dark.png b/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--dark.png index fdec449544189..80868ba79c924 100644 Binary files a/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--dark.png and b/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--dark.png differ diff --git a/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--light.png b/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--light.png index 4208f3bc30e2e..37d723ef28b8c 100644 Binary files a/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--light.png and b/frontend/__snapshots__/components-properties-table--dollar-properties-on-event--light.png differ diff --git a/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--dark.png b/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--dark.png index e6488189d84e4..d13cdb0246334 100644 Binary files a/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--dark.png and b/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--dark.png differ diff --git a/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--light.png b/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--light.png index 84d14a856b752..d1064c7d548bf 100644 Binary files a/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--light.png and b/frontend/__snapshots__/components-properties-table--dollar-properties-on-person--light.png differ diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx index daa07771ac1df..b245d5db8bae4 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx @@ -8,10 +8,16 @@ import { combineUrl } from 'kea-router' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonTable, LemonTableColumns, LemonTableProps } from 'lib/lemon-ui/LemonTable' import { userPreferencesLogic } from 'lib/logic/userPreferencesLogic' -import { CORE_FILTER_DEFINITIONS_BY_GROUP, PROPERTY_KEYS } from 'lib/taxonomy' +import { + CORE_FILTER_DEFINITIONS_BY_GROUP, + getCoreFilterDefinition, + NON_DOLLAR_POSTHOG_PROPERTY_KEYS, + PROPERTY_KEYS, +} from 'lib/taxonomy' import { isURL } from 'lib/utils' import { useMemo, useState } from 'react' import { NewProperty } from 'scenes/persons/NewProperty' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { urls } from 'scenes/urls' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' @@ -209,12 +215,26 @@ export function PropertiesTable({ const [searchTerm, setSearchTerm] = useState('') const { hidePostHogPropertiesInTable } = useValues(userPreferencesLogic) const { setHidePostHogPropertiesInTable } = useActions(userPreferencesLogic) + const { isCloudOrDev } = useValues(preflightLogic) const objectProperties = useMemo(() => { if (!properties || Array.isArray(properties)) { return [] } - let entries = Object.entries(properties) + let entries = Object.entries(properties).sort((a, b) => { + // if this is a posthog property we want to sort by its label + const left = getCoreFilterDefinition(a[0], TaxonomicFilterGroupType.EventProperties)?.label || a[0] + const right = getCoreFilterDefinition(b[0], TaxonomicFilterGroupType.EventProperties)?.label || b[0] + + if (left < right) { + return -1 + } + if (left > right) { + return 1 + } + return 0 + }) + if (searchTerm) { const normalizedSearchTerm = searchTerm.toLowerCase() entries = entries.filter(([key, value]) => { @@ -228,7 +248,11 @@ export function PropertiesTable({ } if (filterable && hidePostHogPropertiesInTable) { - entries = entries.filter(([key]) => !key.startsWith('$') && !PROPERTY_KEYS.includes(key)) + entries = entries.filter(([key]) => { + const isPostHogProperty = key.startsWith('$') && PROPERTY_KEYS.includes(key) + const isNonDollarPostHogProperty = isCloudOrDev && NON_DOLLAR_POSTHOG_PROPERTY_KEYS.includes(key) + return !isPostHogProperty && !isNonDollarPostHogProperty + }) } if (sortProperties) { diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index 75b55ca7f705c..b8a333f2d2fa8 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -249,6 +249,80 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { description: 'If autocapture has been disabled server-side.', system: true, }, + $feature_flag_payloads: { + label: 'Feature Flag Payloads', + description: 'Feature flag payloads active in the environment.', + }, + $capture_failed_request: { + label: 'Capture Failed Request', + description: '', + }, + $lib_rate_limit_remaining_tokens: { + label: 'Clientside rate limit remaining tokens', + description: ( + + Remaining rate limit tokens for the posthog-js library client-side rate limiting implementation. + + ), + examples: ['100'], + }, + token: { + label: 'Token', + description: Token used for authentication., + examples: ['ph_abcdefg'], + }, + $ce_version: { + label: '$ce_version', + description: '', + system: true, + }, + $anon_distinct_id: { + label: 'Anon Distinct ID', + description: 'If the user was previously anonymous, their anonymous ID will be set here.', + examples: ['16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767'], + system: true, + }, + $event_type: { + label: 'Event Type', + description: + 'When the event is an $autocapture event, this specifies what the action was against the element.', + examples: ['click', 'submit', 'change'], + }, + $insert_id: { + label: 'Insert ID', + description: 'Unique insert ID for the event.', + system: true, + }, + $time: { + label: '$time (deprecated)', + description: + 'Use the HogQL field `timestamp` instead. This field was previously set on some client side events.', + system: true, + examples: ['1681211521.345'], + }, + $device_id: { + label: 'Device ID', + description: 'Unique ID for that device, consistent even if users are logging in/out.', + examples: ['16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767'], + system: true, + }, + $browser_type: { + label: 'Browser Type', + description: 'This is only added when posthog-js config.opt_out_useragent_filter is true.', + examples: ['browser', 'bot'], + }, + + // session recording + $replay_minimum_duration: { + label: 'Replay config - minimum duration', + description: Config for minimum duration before emitting a session recording., + examples: ['1000'], + }, + $replay_sample_rate: { + label: 'Replay config - sample rate', + description: Config for sampling rate of session recordings., + examples: ['0.1'], + }, $console_log_recording_enabled_server_side: { label: 'Console Log Recording Enabled Server-Side', description: 'If console log recording has been enabled server-side.', @@ -260,14 +334,44 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { examples: ['v2'], system: true, }, - $feature_flag_payloads: { - label: 'Feature Flag Payloads', - description: 'Feature flag payloads active in the environment.', + $session_recording_start_reason: { + label: 'Session recording start reason', + description: ( + + Reason for starting the session recording. Useful for e.g. if you have sampling enabled and want to + see on batch exported events which sessions have recordings available. + + ), + examples: ['sampling_override', 'recording_initialized', 'linked_flag_match'], }, - $capture_failed_request: { - label: 'Capture Failed Request', - description: '', + $session_recording_canvas_recording: { + label: 'Session recording canvas recording', + description: Session recording canvas capture config., + examples: ['{"enabled": false}'], }, + $session_recording_network_payload_capture: { + label: 'Session recording network payload capture', + description: Session recording network payload capture config., + examples: ['{"recordHeaders": false}'], + }, + $session_recording_url_trigger_activated_session: { + label: 'Session recording URL trigger activated session', + description: ( + + Session recording URL trigger activated session config. Used by posthog-js to track URL activation + of session replay. + + ), + }, + $session_recording_url_trigger_status: { + label: 'Session recording URL trigger status', + description: ( + + Session recording URL trigger status. Used by posthog-js to track URL activation of session replay. + + ), + }, + // exception tracking $sentry_exception: { label: 'Sentry exception', description: 'Raw Sentry exception data', @@ -324,41 +428,21 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { label: 'Exception person URL', description: 'The PostHog person that experienced the exception', }, - $ce_version: { - label: '$ce_version', - description: '', - system: true, - }, - $anon_distinct_id: { - label: 'Anon Distinct ID', - description: 'If the user was previously anonymous, their anonymous ID will be set here.', - examples: ['16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767'], - system: true, - }, - $event_type: { - label: 'Event Type', - description: - 'When the event is an $autocapture event, this specifies what the action was against the element.', - examples: ['click', 'submit', 'change'], - }, - $insert_id: { - label: 'Insert ID', - description: 'Unique insert ID for the event.', - system: true, + $exception_capture_endpoint: { + label: 'Exception capture endpoint', + description: Endpoint used by posthog-js exception autocapture., + examples: ['/e/'], }, - $time: { - label: '$time (deprecated)', - description: - 'Use the HogQL field `timestamp` instead. This field was previously set on some client side events.', - system: true, - examples: ['1681211521.345'], + $exception_capture_endpoint_suffix: { + label: 'Exception capture endpoint', + description: Endpoint used by posthog-js exception autocapture., + examples: ['/e/'], }, - $device_id: { - label: 'Device ID', - description: 'Unique ID for that device, consistent even if users are logging in/out.', - examples: ['16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767'], - system: true, + $exception_capture_enabled_server_side: { + label: 'Exception capture enabled server side', + description: Whether exception autocapture was enabled in remote config., }, + // GeoIP $geoip_city_name: { label: 'City Name', @@ -435,6 +519,27 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { label: 'GeoIP Disabled', description: `Whether to skip GeoIP processing for the event.`, }, + $geoip_city_confidence: { + label: 'GeoIP detection city confidence', + description: "Confidence level of the city matched to this event's IP address.", + examples: ['0.5'], + }, + $geoip_country_confidence: { + label: 'GeoIP detection country confidence', + description: "Confidence level of the country matched to this event's IP address.", + examples: ['0.5'], + }, + $geoip_accuracy_radius: { + label: 'GeoIP detection accuracy radius', + description: "Accuracy radius of the location matched to this event's IP address.", + examples: ['50'], + }, + $geoip_subdivision_1_confidence: { + label: 'GeoIP detection subdivision 1 confidence', + description: "Confidence level of the first subdivision matched to this event's IP address.", + examples: ['0.5'], + }, + $el_text: { label: 'Element Text', description: `The text of the element that was clicked. Only sent with Autocapture events.`, @@ -1017,7 +1122,10 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { label: 'Is Identified', description: 'When the person was identified', }, - + $initial_person_info: { + label: 'Initial Person Info', + description: 'posthog-js initial person information. used in the $set_once flow', + }, // web vitals properties $web_vitals_enabled_server_side: { label: 'Web vitals enabled server side', @@ -1047,6 +1155,63 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { $web_vitals_CLS_value: { label: 'Web vitals CLS value', }, + $web_vitals_allowed_metrics: { + label: 'Web vitals allowed metrics', + description: Allowed web vitals metrics config., + examples: ['["LCP", "CLS"]'], + }, + + // page leave properties + $prev_pageview_last_scroll: { + label: 'Previous pageview last scroll', + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + examples: [0], + }, + $prev_pageview_last_scroll_percentage: { + label: 'Previous pageview last scroll percentage', + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + examples: [0], + }, + $prev_pageview_max_scroll: { + examples: [0], + label: 'Previous pageview max scroll', + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + }, + $prev_pageview_max_scroll_percentage: { + examples: [0], + label: 'Previous pageview max scroll percentage', + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + }, + $prev_pageview_last_content: { + examples: [0], + label: 'Previous pageview last content', + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + }, + $prev_pageview_last_content_percentage: { + examples: [0], + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + label: 'Previous pageview last content percentage', + }, + $prev_pageview_max_content: { + examples: [0], + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + label: 'Previous pageview max content', + }, + $prev_pageview_max_content_percentage: { + examples: [0], + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + label: 'Previous pageview max content percentage', + }, + $prev_pageview_pathname: { + examples: ['/pricing', '/about-us/team'], + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + label: 'Previous pageview pathname', + }, + $prev_pageview_duration: { + examples: [0], + description: 'posthog-js adds these to the page leave event, they are used in web analytics calculations', + label: 'Previous pageview duration', + }, }, numerical_event_properties: {}, // Same as event properties, see assignment below person_properties: {}, // Currently person properties are the same as event properties, see assignment below @@ -1143,89 +1308,6 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { ), examples: ['2.2'], }, - $session_recording_start_reason: { - label: 'Session recording start reason', - description: ( - - Reason for starting the session recording. Useful for e.g. if you have sampling enabled and want to - see on batch exported events which sessions have recordings available. - - ), - examples: ['sampling_override', 'recording_initialized', 'linked_flag_match'], - }, - $replay_minimum_duration: { - label: 'Replay config - minimum duration', - description: Config for minimum duration before emitting a session recording., - examples: ['1000'], - }, - $replay_sample_rate: { - label: 'Replay config - sample rate', - description: Config for sampling rate of session recordings., - examples: ['0.1'], - }, - $exception_capture_endpoint: { - label: 'Exception capture endpoint', - description: Endpoint used by posthog-js exception autocapture., - examples: ['/e/'], - }, - $exception_capture_endpoint_suffix: { - label: 'Exception capture endpoint', - description: Endpoint used by posthog-js exception autocapture., - examples: ['/e/'], - }, - $exception_capture_enabled_server_side: { - label: 'Exception capture enabled server side', - description: Whether exception autocapture was enabled in remote config., - }, - $lib_rate_limit_remaining_tokens: { - label: 'Clientside rate limit remaining tokens', - description: ( - - Remaining rate limit tokens for the posthog-js library client-side rate limiting implementation. - - ), - examples: ['100'], - }, - $session_recording_canvas_recording: { - label: 'Session recording canvas recording', - description: Session recording canvas capture config., - examples: ['{"enabled": false}'], - }, - $session_recording_network_payload_capture: { - label: 'Session recording network payload capture', - description: Session recording network payload capture config., - examples: ['{"recordHeaders": false}'], - }, - token: { - label: 'Token', - description: Token used for authentication., - examples: ['ph_abcdefg'], - }, - $web_vitals_allowed_metrics: { - label: 'Web vitals allowed metrics', - description: Allowed web vitals metrics config., - examples: ['["LCP", "CLS"]'], - }, - $geoip_city_confidence: { - label: 'GeoIP detection city confidence', - description: "Confidence level of the city matched to this event's IP address.", - examples: ['0.5'], - }, - $geoip_country_confidence: { - label: 'GeoIP detection country confidence', - description: "Confidence level of the country matched to this event's IP address.", - examples: ['0.5'], - }, - $geoip_accuracy_radius: { - label: 'GeoIP detection accuracy radius', - description: "Accuracy radius of the location matched to this event's IP address.", - examples: ['50'], - }, - $geoip_subdivision_1_confidence: { - label: 'GeoIP detection subdivision 1 confidence', - description: "Confidence level of the first subdivision matched to this event's IP address.", - examples: ['0.5'], - }, }, groups: { $group_key: { @@ -1325,6 +1407,77 @@ CORE_FILTER_DEFINITIONS_BY_GROUP.event_properties.$session_duration = export const PROPERTY_KEYS = Object.keys(CORE_FILTER_DEFINITIONS_BY_GROUP.event_properties) +/** + * these are properties that PostHog add to events they track for their own purposes + * not part of the general taxonomy + * but often more numerous than actual properties set on events and useful to hide + * to make those properties discoverable + */ +export const NON_DOLLAR_POSTHOG_PROPERTY_KEYS = [ + 'billing_period_end', + 'billing_period_start', + 'current_amount_usd.data_warehouse', + 'current_amount_usd.feature_flags', + 'current_amount_usd.integrations', + 'current_amount_usd.platform_and_support', + 'current_amount_usd.product_analytics', + 'current_amount_usd.session_replay', + 'current_amount_usd.surveys', + 'current_total_amount_usd', + 'current_usage.data_warehouse', + 'current_usage.feature_flags', + 'current_usage.integrations', + 'current_usage.platform_and_support', + 'current_usage.product_analytics', + 'current_usage.session_replay', + 'current_usage.surveys', + 'customer_deactivated', + 'custom_limits_usd.data_warehouse', + 'free_allocation.data_warehouse', + 'free_allocation.feature_flags', + 'free_allocation.integrations', + 'free_allocation.platform_and_support', + 'free_allocation.product_analytics', + 'free_allocation.session_replay', + 'free_allocation.surveys', + 'has_billing_plan', + 'percentage_usage.data_warehouse', + 'percentage_usage.feature_flags', + 'percentage_usage.integrations', + 'percentage_usage.platform_and_support', + 'percentage_usage.product_analytics', + 'percentage_usage.session_replay', + 'percentage_usage.surveys', + 'projected_usage.data_warehouse', + 'projected_usage.feature_flags', + 'projected_usage.integrations', + 'projected_usage.platform_and_support', + 'projected_usage.product_analytics', + 'projected_usage.session_replay', + 'projected_usage.surveys', + 'unit_amount_usd.data_warehouse', + 'unit_amount_usd.feature_flags', + 'unit_amount_usd.integrations', + 'unit_amount_usd.platform_and_support', + 'unit_amount_usd.product_analytics', + 'unit_amount_usd.session_replay', + 'unit_amount_usd.surveys', + 'usage_limit.data_warehouse', + 'usage_limit.feature_flags', + 'usage_limit.integrations', + 'usage_limit.platform_and_support', + 'usage_limit.product_analytics', + 'usage_limit.session_replay', + 'usage_limit.surveys', + 'is_demo_project', + 'realm', + 'email_service_available', + 'slack_service_available', + 'commit_sha', + 'token', + 'distinct_id', +] + /** Return whether a given filter key is part of PostHog's core (marked by the PostHog logo). */ export function isCoreFilter(key: string): boolean { return Object.values(CORE_FILTER_DEFINITIONS_BY_GROUP).some((mapping) => Object.keys(mapping).includes(key)) diff --git a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx index 9dd41215f9190..c56eeaf3a9683 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/PlayerInspectorControls.tsx @@ -5,6 +5,7 @@ import { FEATURE_FLAGS } from 'lib/constants' import { IconUnverifiedEvent } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { userPreferencesLogic } from 'lib/logic/userPreferencesLogic' import { capitalizeFirstLetter } from 'lib/utils' import { IconWindow } from 'scenes/session-recordings/player/icons' @@ -19,6 +20,81 @@ import { import { InspectorSearchInfo } from './components/InspectorSearchInfo' import { playerInspectorLogic } from './playerInspectorLogic' +function HideProperties(): JSX.Element | null { + const { logicProps } = useValues(sessionRecordingPlayerLogic) + const inspectorLogic = playerInspectorLogic(logicProps) + const { tab } = useValues(inspectorLogic) + const { hidePostHogPropertiesInTable } = useValues(userPreferencesLogic) + const { setHidePostHogPropertiesInTable } = useActions(userPreferencesLogic) + + return tab === SessionRecordingPlayerTab.EVENTS ? ( + + ) : null +} + +function MiniFilters(): JSX.Element { + const { miniFilters } = useValues(playerSettingsLogic) + const { setMiniFilter } = useActions(playerSettingsLogic) + + return ( +
+ {miniFilters.map((filter) => ( + { + // "alone" should always be a select-to-true action + setMiniFilter(filter.key, filter.alone || !filter.enabled) + }} + tooltip={filter.tooltip} + > + {filter.name} + + ))} +
+ ) +} + +function WindowSelector(): JSX.Element { + const { logicProps } = useValues(sessionRecordingPlayerLogic) + const inspectorLogic = playerInspectorLogic(logicProps) + const { windowIdFilter, windowIds } = useValues(inspectorLogic) + const { setWindowIdFilter } = useActions(inspectorLogic) + + return windowIds.length > 1 ? ( + setWindowIdFilter(val || null)} + options={[ + { + value: null, + label: 'All windows', + icon: , + }, + ...windowIds.map((windowId, index) => ({ + value: windowId, + label: `Window ${index + 1}`, + icon: , + })), + ]} + tooltip="Each recording window translates to a distinct browser tab or window." + /> + ) : ( + // returns an empty div to keep spacing/positioning consistent +
+ ) +} + export const TabToIcon = { [SessionRecordingPlayerTab.ALL]: undefined, [SessionRecordingPlayerTab.EVENTS]: IconUnverifiedEvent, @@ -68,10 +144,10 @@ function TabButtons({ export function PlayerInspectorControls(): JSX.Element { const { logicProps } = useValues(sessionRecordingPlayerLogic) const inspectorLogic = playerInspectorLogic(logicProps) - const { tab, windowIdFilter, windowIds, showMatchingEventsFilter } = useValues(inspectorLogic) - const { setWindowIdFilter, setTab } = useActions(inspectorLogic) - const { showOnlyMatching, miniFilters, searchQuery } = useValues(playerSettingsLogic) - const { setShowOnlyMatching, setMiniFilter, setSearchQuery } = useActions(playerSettingsLogic) + const { tab, showMatchingEventsFilter } = useValues(inspectorLogic) + const { setTab } = useActions(inspectorLogic) + const { showOnlyMatching, searchQuery } = useValues(playerSettingsLogic) + const { setShowOnlyMatching, setSearchQuery } = useActions(playerSettingsLogic) const mode = logicProps.mode ?? SessionRecordingPlayerMode.Standard @@ -108,67 +184,27 @@ export function PlayerInspectorControls(): JSX.Element {
-
- setSearchQuery(e)} - placeholder="Search..." - type="search" - value={searchQuery} - fullWidth - className="min-w-60" - suffix={ - }> - - - } - /> -
+ setSearchQuery(e)} + placeholder="Search..." + type="search" + value={searchQuery} + fullWidth + className="min-w-60" + suffix={ + }> + + + } + /> -
- {miniFilters.map((filter) => ( - { - // "alone" should always be a select-to-true action - setMiniFilter(filter.key, filter.alone || !filter.enabled) - }} - tooltip={filter.tooltip} - > - {filter.name} - - ))} -
+ - {windowIds.length > 1 ? ( -
- setWindowIdFilter(val || null)} - options={[ - { - value: null, - label: 'All windows', - icon: , - }, - ...windowIds.map((windowId, index) => ({ - value: windowId, - label: `Window ${index + 1}`, - icon: , - })), - ]} - tooltip="Each recording window translates to a distinct browser tab or window." - /> -
- ) : null} +
+ + +
{showMatchingEventsFilter ? (
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx index 97a7dc544e11c..9590dd5be3e5c 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx @@ -1,4 +1,5 @@ import { LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { useValues } from 'kea' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' @@ -7,6 +8,7 @@ import { IconOpenInNew } from 'lib/lemon-ui/icons' import { Spinner } from 'lib/lemon-ui/Spinner' import { autoCaptureEventToDescription, capitalizeFirstLetter, isString } from 'lib/utils' import { insightUrlForEvent } from 'scenes/insights/utils' +import { eventPropertyFilteringLogic } from 'scenes/session-recordings/player/inspector/components/eventPropertyFilteringLogic' import { InspectorListItemEvent } from '../playerInspectorLogic' import { SimpleKeyValueList } from './SimpleKeyValueList' @@ -51,6 +53,7 @@ function SummarizeWebVitals({ properties }: { properties: Record }) export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX.Element { const insightUrl = insightUrlForEvent(item.data) + const { promoteProperties, filterProperties } = useValues(eventPropertyFilteringLogic) const subValue = item.data.event === '$pageview' ? ( @@ -61,25 +64,7 @@ export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX. ) : undefined - let promotedKeys: string[] | undefined = undefined - if (item.data.event === '$pageview') { - promotedKeys = ['$current_url', '$title', '$referrer'] - } else if (item.data.event === '$groupidentify') { - promotedKeys = ['$group_type', '$group_key', '$group_set'] - } else if (item.data.event === '$screen') { - promotedKeys = ['$screen_name'] - } else if (item.data.event === '$web_vitals') { - promotedKeys = [ - '$web_vitals_FCP_value', - '$web_vitals_CLS_value', - '$web_vitals_INP_value', - '$web_vitals_LCP_value', - '$web_vitals_FCP_event', - '$web_vitals_CLS_event', - '$web_vitals_INP_event', - '$web_vitals_LCP_event', - ] - } + const promotedKeys = promoteProperties(item.data.event) return (
@@ -129,7 +114,10 @@ export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX. item.data.event === '$exception' ? ( ) : ( - + ) ) : (
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/SimpleKeyValueList.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/SimpleKeyValueList.tsx index 7f51b758bd253..9b530fce41d81 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/SimpleKeyValueList.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/SimpleKeyValueList.tsx @@ -1,6 +1,8 @@ // A React component that renders a list of key-value pairs in a simple way. import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { getCoreFilterDefinition } from 'lib/taxonomy' import { useEffect, useState } from 'react' export interface SimpleKeyValueListProps { @@ -22,10 +24,14 @@ export function SimpleKeyValueList({ useEffect(() => { const sortedItems = Object.entries(item).sort((a, b) => { - if (a[0] < b[0]) { + // if this is a posthog property we want to sort by its label + const left = getCoreFilterDefinition(a[0], TaxonomicFilterGroupType.EventProperties)?.label || a[0] + const right = getCoreFilterDefinition(b[0], TaxonomicFilterGroupType.EventProperties)?.label || b[0] + + if (left < right) { return -1 } - if (a[0] > b[0]) { + if (left > right) { return 1 } return 0 diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/eventPropertyFilteringLogic.ts b/frontend/src/scenes/session-recordings/player/inspector/components/eventPropertyFilteringLogic.ts new file mode 100644 index 0000000000000..2f3ca7b84ae6c --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/components/eventPropertyFilteringLogic.ts @@ -0,0 +1,61 @@ +import { connect, kea, path, selectors } from 'kea' +import { userPreferencesLogic } from 'lib/logic/userPreferencesLogic' +import { NON_DOLLAR_POSTHOG_PROPERTY_KEYS, PROPERTY_KEYS } from 'lib/taxonomy' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import type { eventPropertyFilteringLogicType } from './eventPropertyFilteringLogicType' + +export const eventPropertyFilteringLogic = kea([ + path(['scenes', 'session-recordings', 'player', 'inspector', 'components', 'eventPropertyFilteringLogic']), + connect({ + values: [userPreferencesLogic, ['hidePostHogPropertiesInTable'], preflightLogic, ['isCloudOrDev']], + }), + selectors({ + promoteProperties: [ + () => [], + () => { + return (event: string): string[] | undefined => { + if (['$pageview', '$pageleave'].includes(event)) { + return ['$current_url', '$title', '$referrer'] + } else if (event === '$groupidentify') { + return ['$group_type', '$group_key', '$group_set'] + } else if (event === '$screen') { + return ['$screen_name'] + } else if (event === '$web_vitals') { + return [ + '$web_vitals_FCP_value', + '$web_vitals_CLS_value', + '$web_vitals_INP_value', + '$web_vitals_LCP_value', + '$web_vitals_FCP_event', + '$web_vitals_CLS_event', + '$web_vitals_INP_event', + '$web_vitals_LCP_event', + ] + } else if (event === '$set') { + return ['$set', '$set_once'] + } + } + }, + ], + filterProperties: [ + (s) => [s.hidePostHogPropertiesInTable, s.isCloudOrDev], + (hidePostHogPropertiesInTable, isCloudOrDev) => { + return (props: Record) => { + if (!hidePostHogPropertiesInTable) { + return props + } + + return Object.fromEntries( + Object.entries(props).filter(([key]) => { + const isPostHogProperty = key.startsWith('$') && PROPERTY_KEYS.includes(key) + const isNonDollarPostHogProperty = + isCloudOrDev && NON_DOLLAR_POSTHOG_PROPERTY_KEYS.includes(key) + return !isPostHogProperty && !isNonDollarPostHogProperty + }) + ) + } + }, + ], + }), +])