0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat: reorg inspector list rows (#26243)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Paul D'Ambra 2024-11-20 19:36:46 +01:00 committed by GitHub
parent 3742adc14c
commit 402fbf1852
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 1442 additions and 574 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,490 @@
{
"cache_key": "cache_1c8fec99704fa4258dee014aae86657d",
"cache_target_age": "2024-11-18T15:55:20.644358Z",
"calculation_trigger": null,
"columns": [
"uuid",
"event",
"timestamp",
"elements_chain",
"properties.$window_id",
"properties.$current_url",
"properties.$event_type"
],
"error": null,
"hasMore": false,
"hogql": "SELECT\n uuid,\n event,\n timestamp,\n elements_chain,\n properties.$window_id,\n properties.$current_url,\n properties.$event_type\nFROM\n events\nWHERE\n and(equals(properties.$session_id, '01932f1e-cf6a-7e77-8837-23366207a24b'), less(timestamp, toDateTime('2024-11-15 09:26:01.000000')), greater(timestamp, toDateTime('2024-11-15 09:18:28.000000')))\nORDER BY\n timestamp ASC\nLIMIT 50000\nOFFSET 0",
"is_cached": false,
"last_refresh": "2024-11-18T15:55:05.644358Z",
"limit": 50000,
"modifiers": {
"bounceRatePageViewMode": "count_pageviews",
"customChannelTypeRules": null,
"dataWarehouseEventsModifiers": null,
"debug": null,
"inCohortVia": "auto",
"materializationMode": "legacy_null_as_null",
"optimizeJoinedFilters": false,
"personsArgMaxVersion": "auto",
"personsJoinMode": null,
"personsOnEventsMode": "person_id_override_properties_joined",
"propertyGroupsMode": null,
"s3TableUseInvalidColumns": null,
"sessionTableVersion": "auto",
"useMaterializedViews": true
},
"next_allowed_client_refresh": "2024-11-18T15:56:05.644358Z",
"offset": 0,
"query_status": null,
"results": [
[
"01932f1e-dd30-7ff4-951d-e8eede5444ef",
"$pageleave",
"2024-11-15T09:19:32.149000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-dfe6-7e71-b89d-92a5faa0ac9b",
"$opt_in",
"2024-11-15T09:19:32.861000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-dfeb-7448-95ba-1eae8b23992a",
"$pageview",
"2024-11-15T09:19:32.871000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-dffa-797b-82e0-d5fa24a558fd",
"$set",
"2024-11-15T09:19:32.876000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-dffd-7c3b-8df2-2be5213171f3",
"$groupidentify",
"2024-11-15T09:19:32.879000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-dfff-770a-967d-1a3a76edcf75",
"$groupidentify",
"2024-11-15T09:19:32.881000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e057-7be8-9c69-fa4b4e7c26d9",
"$pageview",
"2024-11-15T09:19:32.969000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e068-7373-a33a-75b895e586dc",
"$groupidentify",
"2024-11-15T09:19:32.986000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e172-7829-879c-b51c9f80b7f0",
"$feature_flag_called",
"2024-11-15T09:19:33.252000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e176-7d4d-bb3a-478d0c7f3b70",
"$feature_flag_called",
"2024-11-15T09:19:33.256000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e17b-7d9a-a79c-51d367c77797",
"$feature_flag_called",
"2024-11-15T09:19:33.261000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e17d-7fe9-82cd-94e267bd2a93",
"$feature_flag_called",
"2024-11-15T09:19:33.263000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e17d-7fe9-82cd-94e1c0eb96e4",
"$feature_flag_called",
"2024-11-15T09:19:33.263000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e17e-716e-9756-40053c7e0291",
"$feature_flag_called",
"2024-11-15T09:19:33.264000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e17f-744c-bbe0-15e364b61c60",
"$feature_flag_called",
"2024-11-15T09:19:33.265000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e180-741f-99ad-b39813521345",
"$feature_flag_called",
"2024-11-15T09:19:33.266000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e180-741f-99ad-b397dc90f72a",
"$feature_flag_called",
"2024-11-15T09:19:33.266000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e181-76ea-a1d2-9e48b3bd1ca5",
"$feature_flag_called",
"2024-11-15T09:19:33.267000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e188-71de-b7ab-1dbb643922df",
"$feature_flag_called",
"2024-11-15T09:19:33.274000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e189-7b6b-af45-b4be29b97765",
"$feature_flag_called",
"2024-11-15T09:19:33.275000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e194-70de-b3e0-b0c3608eebed",
"$feature_flag_called",
"2024-11-15T09:19:33.286000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e1a3-7a0d-a463-b51ecf1a63d8",
"$feature_flag_called",
"2024-11-15T09:19:33.301000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e1e6-78dc-ad38-4e2dc10747b2",
"query completed",
"2024-11-15T09:19:33.368000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e6d0-7c6e-915c-5cb7734446d1",
"$feature_flag_called",
"2024-11-15T09:19:34.626000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1e-e7e0-75a6-afc5-6dccde5419f0",
"client_request_failure",
"2024-11-15T09:19:34.898000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1f-1f89-77d3-a8b9-eb93fd25d7d0",
"command bar status changed",
"2024-11-15T09:19:49.138000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1f-1f92-7dab-b6ee-a808229c0224",
"palette shown",
"2024-11-15T09:19:49.147000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/settings/environment-replay",
null
],
[
"01932f1f-3998-788b-ae71-93dd2aef3207",
"command bar status changed",
"2024-11-15T09:19:55.808000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
],
[
"01932f1f-399a-789e-98ad-d74da0ad5ca6",
"palette command executed",
"2024-11-15T09:19:55.810000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
],
[
"01932f1f-399d-74d4-a254-5b4463d25a6a",
"command bar search result executed",
"2024-11-15T09:19:55.813000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
],
[
"01932f1f-39e3-7d64-94df-12c5ba3fc347",
"$pageview",
"2024-11-15T09:19:55.884000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
],
[
"01932f1f-3a06-7441-9863-2acac1fe3e62",
"$exception",
"2024-11-15T09:19:55.918000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
],
[
"01932f1f-525d-7672-bccb-9630f4576422",
"$web_vitals",
"2024-11-15T09:20:02.151000Z",
"",
"01932f1e-cf6a-7e77-8837-2337cb916511",
"http://localhost:8000/project/1/dashboard",
null
]
],
"timezone": "UTC",
"timings": [
{
"k": "./build_ast/columns/parse_expr_cpp",
"t": 0.0010820409952430055
},
{
"k": "./build_ast/columns",
"t": 0.0011979579940089025
},
{
"k": "./build_ast/aggregations",
"t": 0.0004660420017899014
},
{
"k": "./build_ast/filters/where",
"t": 0.0000359579935320653
},
{
"k": "./build_ast/filters/properties",
"t": 0.00018483300664229318
},
{
"k": "./build_ast/filters",
"t": 0.000295000005280599
},
{
"k": "./build_ast/timestamps/parse_expr_cpp/replace_placeholders",
"t": 0.00021462500444613397
},
{
"k": "./build_ast/timestamps/parse_expr_cpp",
"t": 0.006467084000178147
},
{
"k": "./build_ast/timestamps",
"t": 0.011939292002352886
},
{
"k": "./build_ast/where",
"t": 0.006279791996348649
},
{
"k": "./build_ast/order/parse_order_expr_cpp",
"t": 0.0009526250069029629
},
{
"k": "./build_ast/order",
"t": 0.0010012079947046004
},
{
"k": "./build_ast/select",
"t": 0.0001021249991026707
},
{
"k": "./build_ast",
"t": 0.021405540996056516
},
{
"k": "./query",
"t": 0.00019283300207462162
},
{
"k": "./variables",
"t": 0.00003975000436184928
},
{
"k": "./replace_placeholders",
"t": 0.006989917004830204
},
{
"k": "./max_limit",
"t": 0.00007029200060060248
},
{
"k": "./hogql/prepare_ast/clone",
"t": 0.000481041002785787
},
{
"k": "./hogql/prepare_ast/create_hogql_database",
"t": 0.14176429100189125
},
{
"k": "./hogql/prepare_ast/resolve_types",
"t": 0.0010933330049738288
},
{
"k": "./hogql/prepare_ast",
"t": 0.14348054199945182
},
{
"k": "./hogql/print_ast/printer",
"t": 0.0016133750032167882
},
{
"k": "./hogql/print_ast",
"t": 0.0016721670035622083
},
{
"k": "./hogql",
"t": 0.2308277080010157
},
{
"k": "./print_ast/create_hogql_database",
"t": 0.2334512080051354
},
{
"k": "./print_ast/resolve_types",
"t": 0.001193624993902631
},
{
"k": "./print_ast/resolve_property_types",
"t": 0.007970916994963773
},
{
"k": "./print_ast/resolve_lazy_tables",
"t": 0.003332209002110176
},
{
"k": "./print_ast/swap_properties",
"t": 0.0006330420001177117
},
{
"k": "./print_ast/printer",
"t": 0.002056582998193335
},
{
"k": "./print_ast",
"t": 0.24880249999841908
},
{
"k": "./clickhouse_execute",
"t": 0.24669937499857042
},
{
"k": "./parse_expr_cpp",
"t": 0.0013694569934159517
},
{
"k": ".",
"t": 0.8226239580035326
}
],
"types": [
"UUID",
"String",
"DateTime64(6, 'UTC')",
"String",
"Nullable(String)",
"Nullable(String)",
"Nullable(String)"
]
}

View File

@ -0,0 +1,155 @@
{
"id": "01932f1e-cf6a-7e77-8837-23366207a24b",
"distinct_id": "MDl4qb3tG4yWMSwc5zKnMwuw3u0ZpHFMxt3ANLuujyq",
"viewed": false,
"recording_duration": 333,
"active_seconds": 18,
"inactive_seconds": 314,
"start_time": "2024-11-15T09:19:28.620000Z",
"end_time": "2024-11-15T09:25:01.649000Z",
"click_count": 1,
"keypress_count": 0,
"mouse_activity_count": 32,
"console_log_count": 6,
"console_warn_count": 3,
"console_error_count": 2,
"start_url": "http://localhost:8000/project/1/settings/environment-replay",
"person": {
"id": 1,
"name": "paul@posthog.com",
"distinct_ids": [
"MDl4qb3tG4yWMSwc5zKnMwuw3u0ZpHFMxt3ANLuujyq",
"01928ce6-8fd0-771c-be21-b6ada50a1d74",
"01929a4d-f720-7830-8e95-f4402de424e8",
"01929a4e-65ef-7301-b3c3-4a3f9b0195f1",
"01929a4e-7de1-7437-8528-5dbcfb5ed19c",
"01929a4f-a6ca-7f6b-b6b8-326bcd04be2a",
"01929a4f-bb37-7efd-a3be-10f1afc72114",
"01929a50-31e2-7c5b-88d2-84c58bffd1a3",
"01929a52-1e2c-7897-a8e1-16a986d94ce3",
"01929a52-1e53-777b-93a5-e394ea98a138"
],
"properties": {
"$os": "Mac OS X",
"name": "secret name",
"dclid": null,
"email": "paul@posthog.com",
"gclid": null,
"realm": "hosted-clickhouse",
"fbclid": null,
"gbraid": null,
"gclsrc": null,
"igshid": null,
"mc_cid": null,
"ttclid": null,
"twclid": null,
"wbraid": null,
"msclkid": null,
"rdt_cid": null,
"$browser": "Chrome",
"utm_term": null,
"$pathname": "/",
"$referrer": "$direct",
"joined_at": "2024-10-14T21:19:31.774344+00:00",
"li_fat_id": null,
"strapi_id": null,
"gad_source": null,
"project_id": "01928ce6-8a43-0000-1f02-d1916f05086c",
"utm_medium": null,
"utm_source": null,
"$initial_os": "Mac OS X",
"$os_version": "10.15.7",
"homeAddress": "******",
"utm_content": null,
"$current_url": "http://localhost:8000/",
"$device_type": "Desktop",
"initial_name": "secret name",
"instance_tag": "none",
"instance_url": "http://localhost:8000",
"is_signed_up": true,
"utm_campaign": null,
"$initial_host": "localhost:8000",
"project_count": 1,
"$initial_dclid": null,
"$initial_gclid": null,
"anonymize_data": false,
"$geoip_latitude": -33.8715,
"$initial_fbclid": null,
"$initial_gbraid": null,
"$initial_gclsrc": null,
"$initial_igshid": null,
"$initial_mc_cid": null,
"$initial_ttclid": null,
"$initial_twclid": null,
"$initial_wbraid": null,
"has_social_auth": false,
"organization_id": "01928ce6-84d0-0000-19b3-4fe9812b33c5",
"$browser_version": 130,
"$geoip_city_name": "Sydney",
"$geoip_longitude": 151.2006,
"$geoip_time_zone": "Australia/Sydney",
"$initial_browser": "Chrome",
"$initial_msclkid": null,
"$initial_rdt_cid": null,
"has_password_set": true,
"social_providers": [],
"$initial_pathname": "/",
"$initial_referrer": "http://localhost:8000/signup",
"$initial_utm_term": null,
"$referring_domain": "$direct",
"is_email_verified": false,
"$geoip_postal_code": "2000",
"$initial_li_fat_id": null,
"organization_count": 1,
"$creator_event_uuid": "01928ce6-8fd5-7c3f-9437-e1aa4638aba1",
"$geoip_country_code": "AU",
"$geoip_country_name": "Australia",
"$initial_gad_source": null,
"$initial_os_version": "10.15.7",
"$initial_utm_medium": null,
"$initial_utm_source": null,
"$initial_current_url": "http://localhost:8000/",
"$initial_device_type": "Desktop",
"$initial_utm_content": null,
"initial_home_address": "******",
"$geoip_continent_code": "OC",
"$geoip_continent_name": "Oceania",
"$initial_utm_campaign": null,
"team_member_count_all": 1,
"project_setup_complete": false,
"$initial_geoip_latitude": -33.8715,
"$initial_browser_version": 129,
"$initial_geoip_city_name": "Sydney",
"$initial_geoip_longitude": 151.2006,
"$initial_geoip_time_zone": "Australia/Sydney",
"$geoip_subdivision_1_code": "NSW",
"$geoip_subdivision_1_name": "New South Wales",
"$initial_referring_domain": "localhost:8000",
"completed_onboarding_once": false,
"$initial_geoip_postal_code": "2000",
"has_seen_product_intro_for": {
"cohorts": true,
"surveys": true,
"feature_flags": true
},
"$initial_geoip_country_code": "AU",
"$initial_geoip_country_name": "Australia",
"$initial_geoip_continent_code": "OC",
"$initial_geoip_continent_name": "Oceania",
"$initial_geoip_subdivision_1_code": "NSW",
"$initial_geoip_subdivision_1_name": "New South Wales",
"current_organization_membership_level": 15,
"$survey_dismissed/01929a53-90a3-0000-0611-842282989aaf": true,
"$survey_dismissed/0192dd4c-3891-0000-e0f8-8d6635b282a1": true,
"$survey_responded/0192dd4c-3891-0000-e0f8-8d6635b282a1": true,
"$survey_dismissed/019302b9-39bd-0000-736e-eeb9e761c343/3": true,
"$survey_responded/019302b9-39bd-0000-736e-eeb9e761c343/1": true
},
"created_at": "2024-10-14T21:19:33.368000Z",
"uuid": "1bcc7e91-f665-5f29-996e-8f9f6cfe9212"
},
"storage": "object_storage",
"snapshot_source": "web",
"ongoing": null,
"activity_score": null
}

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import {
BodyDisplay,
HeadersDisplay,
ItemPerformanceEvent,
ItemPerformanceEventDetail,
ItemPerformanceEventProps,
} from 'scenes/session-recordings/apm/playerInspector/ItemPerformanceEvent'
@ -25,17 +26,16 @@ export default meta
const BasicTemplate: StoryFn<typeof ItemPerformanceEvent> = (props: Partial<ItemPerformanceEventProps>) => {
props.item = props.item || undefined
props.setExpanded = props.setExpanded || (() => {})
const propsToUse = props as ItemPerformanceEventProps
return (
<div className="flex flex-col gap-2 min-w-96">
<h3>Collapsed</h3>
<ItemPerformanceEvent {...propsToUse} expanded={false} />
<ItemPerformanceEvent {...propsToUse} />
<LemonDivider />
<h3>Expanded</h3>
<ItemPerformanceEvent {...propsToUse} expanded={true} />
<ItemPerformanceEventDetail {...propsToUse} />
</div>
)
}

View File

@ -1,4 +1,4 @@
import { LemonButton, LemonDivider, LemonTabs, LemonTag, LemonTagType, Link } from '@posthog/lemon-ui'
import { LemonDivider, LemonTabs, LemonTag, LemonTagType, Link } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useValues } from 'kea'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
@ -63,8 +63,6 @@ const friendlyHttpStatus = {
export interface ItemPerformanceEventProps {
item: PerformanceEvent
expanded: boolean
setExpanded: (expanded: boolean) => void
finalTimestamp: Dayjs | null
}
@ -151,19 +149,7 @@ function SizeDescription({ sizeInfo }: { sizeInfo: PerformanceEventSizeInfo }):
)
}
export function ItemPerformanceEvent({
item,
finalTimestamp,
expanded,
setExpanded,
}: ItemPerformanceEventProps): JSX.Element {
const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body' | 'raw'>('timings')
const { currentTeam } = useValues(teamLogic)
const payloadCaptureIsEnabled =
currentTeam?.capture_performance_opt_in &&
currentTeam?.session_recording_network_payload_capture_config?.recordBody
export function ItemPerformanceEvent({ item, finalTimestamp }: ItemPerformanceEventProps): JSX.Element {
const sizeInfo = itemSizeInfo(item)
const startTime = item.start_time || item.fetch_start || 0
@ -191,6 +177,70 @@ export function ItemPerformanceEvent({
...otherProps
} = item
return (
<div data-attr="item-performance-event" className="font-light w-full">
<div className="flex-1 overflow-hidden">
<div
className="absolute bg-primary rounded-sm opacity-75 h-1 bottom-0.5"
// eslint-disable-next-line react/forbid-dom-props
style={{
left: `${(startTime / contextLengthMs) * 100}%`,
width: `${Math.max((duration / contextLengthMs) * 100, 0.5)}%`,
}}
/>
{item.entry_type === 'navigation' ? (
<NavigationItem item={item} expanded={false} navigationURL={shortEventName} />
) : (
<div className="flex gap-2 p-2 text-xs cursor-pointer items-center">
<MethodTag item={item} />
<PerformanceEventLabel name={item.name} expanded={false} />
{/* We only show the status if it exists and is an error status */}
{otherProps.response_status && otherProps.response_status >= 400 ? (
<span
className={clsx(
'font-semibold',
otherProps.response_status >= 400 &&
otherProps.response_status < 500 &&
'text-warning-dark',
otherProps.response_status >= 500 && 'text-danger-dark'
)}
>
{otherProps.response_status}
</span>
) : null}
{renderTimeBenchmark(duration)}
<span className={clsx('font-semibold')}>{sizeInfo.formattedBytes}</span>
</div>
)}
</div>
</div>
)
}
export function ItemPerformanceEventDetail({ item }: ItemPerformanceEventProps): JSX.Element {
const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body' | 'raw'>('timings')
const { currentTeam } = useValues(teamLogic)
const payloadCaptureIsEnabled =
currentTeam?.capture_performance_opt_in &&
currentTeam?.session_recording_network_payload_capture_config?.recordBody
const sizeInfo = itemSizeInfo(item)
const {
timestamp,
uuid,
name,
session_id,
window_id,
pageview_id,
distinct_id,
time_origin,
entry_type,
current_url,
...otherProps
} = item
// NOTE: This is a bit of a quick-fix for the fact that each event has all values despite most not applying.
// We should probably do a specific mapping depending on the event type to display it properly (and probably give an info indicator what it all means...)
@ -224,138 +274,83 @@ export function ItemPerformanceEvent({
}, {} as Record<string, any>)
return (
<div>
<LemonButton
noPadding
onClick={() => setExpanded(!expanded)}
fullWidth
data-attr="item-performance-event"
className="font-normal"
>
<div className="flex-1 overflow-hidden">
<div
className="absolute bg-primary rounded-sm opacity-75 h-1 bottom-0.5"
// eslint-disable-next-line react/forbid-dom-props
style={{
left: `${(startTime / contextLengthMs) * 100}%`,
width: `${Math.max((duration / contextLengthMs) * 100, 0.5)}%`,
}}
/>
{item.entry_type === 'navigation' ? (
<NavigationItem item={item} expanded={expanded} navigationURL={shortEventName} />
) : (
<div className="flex gap-2 items-start p-2 text-xs cursor-pointer items-center">
<MethodTag item={item} />
<PerformanceEventLabel expanded={expanded} name={item.name} />
{/* We only show the status if it exists and is an error status */}
{otherProps.response_status && otherProps.response_status >= 400 ? (
<span
className={clsx(
'font-semibold',
otherProps.response_status >= 400 &&
otherProps.response_status < 500 &&
'text-warning-dark',
otherProps.response_status >= 500 && 'text-danger-dark'
)}
>
{otherProps.response_status}
</span>
) : null}
{renderTimeBenchmark(duration)}
<span className={clsx('font-semibold')}>{sizeInfo.formattedBytes}</span>
</div>
)}
</div>
</LemonButton>
<div className="p-2 text-xs border-t font-light w-full">
<>
<StatusRow item={item} />
<p>
Request <StartedAt item={item} /> <DurationDescription item={item} />
<SizeDescription sizeInfo={sizeInfo} />.
</p>
</>
<LemonDivider dashed />
{expanded && (
<div className="p-2 text-xs border-t">
<>
<StatusRow item={item} />
<p>
Request <StartedAt item={item} /> <DurationDescription item={item} />
<SizeDescription sizeInfo={sizeInfo} />.
</p>
</>
<LemonDivider dashed />
<LemonTabs
activeKey={activeTab}
onChange={(newKey) => setActiveTab(newKey)}
tabs={[
{
key: 'timings',
label: 'Timings',
content: (
<>
<SimpleKeyValueList item={sanitizedProps} />
<LemonDivider dashed />
<NetworkRequestTiming performanceEvent={item} />
</>
),
},
item.request_headers || item.response_headers
? {
key: 'headers',
label: 'Headers',
content: (
<HeadersDisplay
request={item.request_headers}
response={item.response_headers}
isInitial={item.is_initial}
/>
),
}
: false,
item.entry_type !== 'navigation' &&
// if we're missing the initiator type, but we do have a body then we should show it
(['fetch', 'xmlhttprequest'].includes(item.initiator_type || '') || !!item.request_body)
? {
key: 'payload',
label: 'Payload',
content: (
<BodyDisplay
content={item.request_body}
headers={item.request_headers}
emptyMessage={emptyPayloadMessage(
payloadCaptureIsEnabled,
item,
'Request'
)}
/>
),
}
: false,
item.entry_type !== 'navigation' && item.response_body
? {
key: 'response_body',
label: 'Response',
content: (
<BodyDisplay
content={item.response_body}
headers={item.response_headers}
emptyMessage={emptyPayloadMessage(
payloadCaptureIsEnabled,
item,
'Response'
)}
/>
),
}
: false,
{
key: 'raw',
label: 'Json',
content: (
<CodeSnippet language={Language.JSON} wrap thing="performance event">
{JSON.stringify(item.raw || 'no item to display', null, 2)}
</CodeSnippet>
),
},
]}
/>
</div>
)}
<LemonTabs
activeKey={activeTab}
onChange={(newKey) => setActiveTab(newKey)}
tabs={[
{
key: 'timings',
label: 'Timings',
content: (
<>
<SimpleKeyValueList item={sanitizedProps} />
<LemonDivider dashed />
<NetworkRequestTiming performanceEvent={item} />
</>
),
},
item.request_headers || item.response_headers
? {
key: 'headers',
label: 'Headers',
content: (
<HeadersDisplay
request={item.request_headers}
response={item.response_headers}
isInitial={item.is_initial}
/>
),
}
: false,
item.entry_type !== 'navigation' &&
// if we're missing the initiator type, but we do have a body then we should show it
(['fetch', 'xmlhttprequest'].includes(item.initiator_type || '') || !!item.request_body)
? {
key: 'payload',
label: 'Payload',
content: (
<BodyDisplay
content={item.request_body}
headers={item.request_headers}
emptyMessage={emptyPayloadMessage(payloadCaptureIsEnabled, item, 'Request')}
/>
),
}
: false,
item.entry_type !== 'navigation' && item.response_body
? {
key: 'response_body',
label: 'Response',
content: (
<BodyDisplay
content={item.response_body}
headers={item.response_headers}
emptyMessage={emptyPayloadMessage(payloadCaptureIsEnabled, item, 'Response')}
/>
),
}
: false,
{
key: 'raw',
label: 'Json',
content: (
<CodeSnippet language={Language.JSON} wrap thing="performance event">
{JSON.stringify(item.raw || 'no item to display', null, 2)}
</CodeSnippet>
),
},
]}
/>
</div>
)
}
@ -385,7 +380,7 @@ export function BodyDisplay({
language = Language.JSON
}
const isAutoRedaction = /(\[SessionRecording\].*redacted)/.test(displayContent)
const isAutoRedaction = /(\[SessionRecording].*redacted)/.test(displayContent)
return isAutoRedaction ? (
<>

View File

@ -1,9 +1,10 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { BindLogic, useActions, useValues } from 'kea'
import { useEffect } from 'react'
import recordingEventsJson from 'scenes/session-recordings/__mocks__/recording_events_query'
import recordingMetaJson from 'scenes/session-recordings/__mocks__/recording_meta.json'
import { snapshotsAsJSONLines } from 'scenes/session-recordings/__mocks__/recording_snapshots'
import { largeRecordingJSONL } from 'scenes/session-recordings/__mocks__/large_recording_blob_one'
import largeRecordingEventsJson from 'scenes/session-recordings/__mocks__/large_recording_load_events_one.json'
import largeRecordingMetaJson from 'scenes/session-recordings/__mocks__/large_recording_meta.json'
import largeRecordingWebVitalsEventsPropertiesJson from 'scenes/session-recordings/__mocks__/large_recording_web_vitals_props.json'
import { PlayerInspector } from 'scenes/session-recordings/player/inspector/PlayerInspector'
import { sessionRecordingDataLogic } from 'scenes/session-recordings/player/sessionRecordingDataLogic'
import { sessionRecordingPlayerLogic } from 'scenes/session-recordings/player/sessionRecordingPlayerLogic'
@ -17,11 +18,11 @@ const meta: Meta<typeof PlayerInspector> = {
decorators: [
mswDecorator({
get: {
'/api/environments/:team_id/session_recordings/:id': recordingMetaJson,
'/api/environments/:team_id/session_recordings/:id': largeRecordingMetaJson,
'/api/environments/:team_id/session_recordings/:id/snapshots': (req, res, ctx) => {
// with no sources, returns sources...
if (req.url.searchParams.get('source') === 'blob') {
return res(ctx.text(snapshotsAsJSONLines()))
return res(ctx.text(largeRecordingJSONL))
}
// with no source requested should return sources
return [
@ -43,7 +44,11 @@ const meta: Meta<typeof PlayerInspector> = {
'/api/environments/:team_id/query': (req, res, ctx) => {
const body = req.body as Record<string, any>
if (body.query.kind === 'EventsQuery' && body.query.properties.length === 1) {
return res(ctx.json(recordingEventsJson))
return res(ctx.json(largeRecordingEventsJson))
}
if (body.query.kind === 'HogQLQuery' && body.query.query.includes("event in ['$web_vitals']")) {
return res(ctx.json(largeRecordingWebVitalsEventsPropertiesJson))
}
// default to an empty response or we duplicate information

View File

@ -1,14 +1,14 @@
#PlayerInspectorListMarker {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
width: 1rem;
height: 0.5rem;
margin-top: 0.25rem;
pointer-events: none;
background-color: var(--primary-3000);
border-radius: var(--radius) 0 0 var(--radius);
border-radius: 0 var(--radius) var(--radius) 0;
transition: transform 200ms linear;
will-change: transform;
}

View File

@ -196,7 +196,6 @@ export function PlayerInspectorList(): JSX.Element {
<AutoSizer>
{({ height, width }) => (
<List
className="p-2"
height={height}
width={width}
deferredMeasurementCache={cellMeasurerCache}

View File

@ -1,7 +1,11 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { now } from 'lib/dayjs'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { ItemComment, ItemCommentProps } from 'scenes/session-recordings/player/inspector/components/ItemComment'
import {
ItemComment,
ItemCommentDetail,
ItemCommentProps,
} from 'scenes/session-recordings/player/inspector/components/ItemComment'
import {
InspectorListItemComment,
RecordingComment,
@ -44,21 +48,20 @@ function makeItem(
const BasicTemplate: StoryFn<typeof ItemComment> = (props: Partial<ItemCommentProps>) => {
props.item = props.item || makeItem()
props.setExpanded = props.setExpanded || (() => {})
const propsToUse = props as ItemCommentProps
return (
<div className="flex flex-col gap-2 min-w-96">
<h3>Collapsed</h3>
<ItemComment {...propsToUse} expanded={false} />
<ItemComment {...propsToUse} />
<LemonDivider />
<h3>Expanded</h3>
<ItemComment {...propsToUse} expanded={true} />
<ItemCommentDetail {...propsToUse} />
<LemonDivider />
<h3>Collapsed with overflowing comment</h3>
<div className="w-52">
<ItemComment {...propsToUse} expanded={false} />
<ItemComment {...propsToUse} />
</div>
</div>
)

View File

@ -5,43 +5,42 @@ import { InspectorListItemComment } from 'scenes/session-recordings/player/inspe
export interface ItemCommentProps {
item: InspectorListItemComment
expanded: boolean
setExpanded: (expanded: boolean) => void
}
export function ItemComment({ item, expanded, setExpanded }: ItemCommentProps): JSX.Element {
const { selectNotebook } = useActions(notebookPanelLogic)
export function ItemComment({ item }: ItemCommentProps): JSX.Element {
return (
<div data-attr="item-comment">
<LemonButton noPadding onClick={() => setExpanded(!expanded)} fullWidth className="font-normal">
{expanded ? (
<div className="p-2 text-xs border-t w-full flex justify-end">
<LemonButton
type="secondary"
onClick={(e) => {
selectNotebook(item.data.notebookShortId)
e.stopPropagation()
e.preventDefault()
}}
>
Continue in {item.data.notebookTitle}
</LemonButton>
</div>
) : (
<div className="flex flex-row w-full justify-between gap-2 items-center p-2 text-xs cursor-pointer">
<div className="font-medium truncate">{item.data.comment}</div>
</div>
)}
</LemonButton>
{expanded && (
<div className="p-2 text-xs border-t">
<div className="flex flex-row w-full justify-between gap-2 items-center p-2 text-xs cursor-pointer truncate">
<div className="font-medium shrink-0">{item.data.comment}</div>
</div>
</div>
)}
<div data-attr="item-comment" className="font-light w-full">
<div className="flex flex-row w-full justify-between gap-2 items-center px-2 py-1 text-xs cursor-pointer">
<div className="font-medium truncate">{item.data.comment}</div>
</div>
</div>
)
}
export function ItemCommentDetail({ item }: ItemCommentProps): JSX.Element {
const { selectNotebook } = useActions(notebookPanelLogic)
return (
<div data-attr="item-comment" className="font-light w-full">
<div className="px-2 py-1 text-xs border-t w-full flex justify-end">
<LemonButton
type="secondary"
onClick={(e) => {
selectNotebook(item.data.notebookShortId)
e.stopPropagation()
e.preventDefault()
}}
size="xsmall"
>
Continue in {item.data.notebookTitle}
</LemonButton>
</div>
<div className="p-2 text-xs border-t">
<div className="flex flex-row w-full justify-between gap-2 items-center px-2 py-1 text-xs cursor-pointer truncate">
<div className="font-medium shrink-0">{item.data.comment}</div>
</div>
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { LemonButton, LemonDivider } from '@posthog/lemon-ui'
import { LemonDivider } from '@posthog/lemon-ui'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
@ -6,59 +6,55 @@ import { InspectorListItemConsole } from '../playerInspectorLogic'
export interface ItemConsoleLogProps {
item: InspectorListItemConsole
expanded: boolean
setExpanded: (expanded: boolean) => void
}
export function ItemConsoleLog({ item, expanded, setExpanded }: ItemConsoleLogProps): JSX.Element {
export function ItemConsoleLog({ item }: ItemConsoleLogProps): JSX.Element {
return (
<>
<LemonButton
noPadding
onClick={() => setExpanded(!expanded)}
fullWidth
data-attr="item-console-log"
className="font-normal"
>
<div className="p-2 text-xs cursor-pointer truncate font-mono flex-1">{item.data.content}</div>
{(item.data.count || 1) > 1 ? (
<span
className={`${
item.highlightColor ? 'bg-' + item.highlightColor : 'bg-primary-alt'
} rounded-lg px-1 mx-2 text-white text-xs font-semibold`}
>
{item.data.count}
</span>
) : null}
</LemonButton>
{expanded && (
<div className="p-2 text-xs border-t">
{(item.data.count || 1) > 1 ? (
<>
<div className="italic">
This log occurred <b>{item.data.count}</b> times in a row.
</div>
<LemonDivider dashed />
</>
) : null}
{item.data.lines?.length && (
<CodeSnippet language={Language.JavaScript} wrap thing="console log">
{item.data.lines.join(' ')}
</CodeSnippet>
)}
{item.data.trace?.length ? (
<>
<LemonDivider dashed />
<LemonLabel>Stack trace</LemonLabel>
<CodeSnippet language={Language.Markup} wrap thing="stack trace">
{item.data.trace.join('\n')}
</CodeSnippet>
</>
) : null}
</div>
)}
</>
<div className="w-full font-light" data-attr="item-console-log">
<div className="px-2 py-1 text-xs cursor-pointer truncate font-mono flex-1">{item.data.content}</div>
{(item.data.count || 1) > 1 ? (
<span
className={`${
item.highlightColor ? 'bg-' + item.highlightColor : 'bg-primary-alt'
} rounded-lg px-1 mx-2 text-white text-xs font-semibold`}
>
{item.data.count}
</span>
) : null}
</div>
)
}
export function ItemConsoleLogDetail({ item }: ItemConsoleLogProps): JSX.Element {
return (
<div className="w-full font-light" data-attr="item-console-log">
<div className="px-2 py-1 text-xs cursor-pointer truncate font-mono flex-1">{item.data.content}</div>
<div className="px-2 py-1 text-xs border-t">
{(item.data.count || 1) > 1 ? (
<>
<div className="italic">
This log occurred <b>{item.data.count}</b> times in a row.
</div>
<LemonDivider dashed />
</>
) : null}
{item.data.lines?.length && (
<CodeSnippet language={Language.JavaScript} wrap thing="console log">
{item.data.lines.join(' ')}
</CodeSnippet>
)}
{item.data.trace?.length ? (
<>
<LemonDivider dashed />
<LemonLabel>Stack trace</LemonLabel>
<CodeSnippet language={Language.Markup} wrap thing="stack trace">
{item.data.trace.join('\n')}
</CodeSnippet>
</>
) : null}
</div>
</div>
)
}

View File

@ -1,30 +1,23 @@
import { LemonButton } from '@posthog/lemon-ui'
import { SimpleKeyValueList } from 'scenes/session-recordings/player/inspector/components/SimpleKeyValueList'
import { InspectorListItemDoctor } from '../playerInspectorLogic'
export interface ItemDoctorProps {
item: InspectorListItemDoctor
expanded: boolean
setExpanded: (expanded: boolean) => void
}
export function ItemDoctor({ item, expanded, setExpanded }: ItemDoctorProps): JSX.Element {
export function ItemDoctor({ item }: ItemDoctorProps): JSX.Element {
return (
<>
<LemonButton
noPadding
onClick={() => setExpanded(!expanded)}
fullWidth
data-attr="item-doctor-item"
className="font-normal"
>
<div className="p-2 text-xs cursor-pointer truncate font-mono flex-1">{item.tag}</div>
</LemonButton>
{expanded && (
<div className="p-2 text-xs border-t">{item.data && <SimpleKeyValueList item={item.data} />}</div>
)}
</>
<div data-attr="item-doctor-item" className="font-light w-full">
<div className="px-2 py-1 text-xs cursor-pointer truncate font-mono flex-1">{item.tag}</div>
</div>
)
}
export function ItemDoctorDetail({ item }: ItemDoctorProps): JSX.Element {
return (
<div data-attr="item-doctor-item" className="font-light w-full">
<div className="px-2 py-1 text-xs border-t">{item.data && <SimpleKeyValueList item={item.data} />}</div>
</div>
)
}

View File

@ -1,7 +1,11 @@
import { Meta, StoryFn, StoryObj } from '@storybook/react'
import { now } from 'lib/dayjs'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { ItemEvent, ItemEventProps } from 'scenes/session-recordings/player/inspector/components/ItemEvent'
import {
ItemEvent,
ItemEventDetail,
ItemEventProps,
} from 'scenes/session-recordings/player/inspector/components/ItemEvent'
import { InspectorListItemEvent } from 'scenes/session-recordings/player/inspector/playerInspectorLogic'
import { mswDecorator } from '~/mocks/browser'
@ -50,21 +54,20 @@ function makeItem(
const BasicTemplate: StoryFn<typeof ItemEvent> = (props: Partial<ItemEventProps>) => {
props.item = props.item || makeItem(undefined, { event: 'A long event name if no other name is provided' })
props.setExpanded = props.setExpanded || (() => {})
const propsToUse = props as ItemEventProps
return (
<div className="flex flex-col gap-2 min-w-96">
<h3>Collapsed</h3>
<ItemEvent {...propsToUse} expanded={false} />
<ItemEvent {...propsToUse} />
<LemonDivider />
<h3>Expanded</h3>
<ItemEvent {...propsToUse} expanded={true} />
<ItemEventDetail {...propsToUse} />
<LemonDivider />
<h3>Collapsed with overflowing text</h3>
<div className="w-20">
<ItemEvent {...propsToUse} expanded={false} />
<ItemEvent {...propsToUse} />
</div>
</div>
)

View File

@ -16,8 +16,6 @@ import { SimpleKeyValueList } from './SimpleKeyValueList'
export interface ItemEventProps {
item: InspectorListItemEvent
expanded: boolean
setExpanded: (expanded: boolean) => void
}
function WebVitalEventSummary({ event }: { event: Record<string, any> }): JSX.Element {
@ -52,10 +50,7 @@ function SummarizeWebVitals({ properties }: { properties: Record<string, any> })
)
}
export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX.Element {
const insightUrl = insightUrlForEvent(item.data)
const { filterProperties } = useValues(eventPropertyFilteringLogic)
export function ItemEvent({ item }: ItemEventProps): JSX.Element {
const subValue =
item.data.event === '$pageview' ? (
item.data.properties.$pathname || item.data.properties.$current_url
@ -65,69 +60,69 @@ export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX.
<SummarizeWebVitals properties={item.data.properties} />
) : undefined
const promotedKeys = POSTHOG_EVENT_PROMOTED_PROPERTIES[item.data.event]
return (
<div data-attr="item-event">
<LemonButton noPadding onClick={() => setExpanded(!expanded)} fullWidth className="font-normal">
<div className="flex flex-row w-full justify-between gap-2 items-center p-2 text-xs cursor-pointer">
<div className="truncate">
<PropertyKeyInfo
className="font-medium"
disablePopover
ellipsis={true}
value={capitalizeFirstLetter(autoCaptureEventToDescription(item.data))}
type={TaxonomicFilterGroupType.Events}
/>
{item.data.event === '$autocapture' ? (
<span className="text-muted-alt">(Autocapture)</span>
) : null}
<div data-attr="item-event" className="font-light w-full">
<div className="flex flex-row w-full justify-between gap-2 items-center px-2 py-1 text-xs cursor-pointer">
<div className="truncate">
<PropertyKeyInfo
className="font-medium"
disablePopover
ellipsis={true}
value={capitalizeFirstLetter(autoCaptureEventToDescription(item.data))}
type={TaxonomicFilterGroupType.Events}
/>
{item.data.event === '$autocapture' ? <span className="text-muted-alt">(Autocapture)</span> : null}
</div>
{subValue ? (
<div className="text-muted-alt truncate" title={isString(subValue) ? subValue : undefined}>
{subValue}
</div>
{subValue ? (
<div className="text-muted-alt truncate" title={isString(subValue) ? subValue : undefined}>
{subValue}
</div>
) : null}
</div>
</LemonButton>
{expanded && (
<div className="p-2 text-xs border-t">
{insightUrl ? (
<>
<div className="flex justify-end">
<LemonButton
size="small"
type="secondary"
sideIcon={<IconOpenInNew />}
data-attr="recordings-event-to-insights"
to={insightUrl}
targetBlank
>
Try out in Insights
</LemonButton>
</div>
<LemonDivider dashed />
</>
) : null}
{item.data.fullyLoaded ? (
item.data.event === '$exception' ? (
<ErrorDisplay eventProperties={item.data.properties} />
) : (
<SimpleKeyValueList
item={filterProperties(item.data.properties)}
promotedKeys={promotedKeys}
/>
)
) : (
<div className="text-muted-alt flex gap-1 items-center">
<Spinner textColored />
Loading...
</div>
)}
</div>
)}
) : null}
</div>
</div>
)
}
export function ItemEventDetail({ item }: ItemEventProps): JSX.Element {
const insightUrl = insightUrlForEvent(item.data)
const { filterProperties } = useValues(eventPropertyFilteringLogic)
const promotedKeys = POSTHOG_EVENT_PROMOTED_PROPERTIES[item.data.event]
return (
<div data-attr="item-event" className="font-light w-full">
<div className="px-2 py-1 text-xs border-t">
{insightUrl ? (
<>
<div className="flex justify-end">
<LemonButton
size="xsmall"
type="secondary"
sideIcon={<IconOpenInNew />}
data-attr="recordings-event-to-insights"
to={insightUrl}
targetBlank
>
Try out in Insights
</LemonButton>
</div>
<LemonDivider dashed />
</>
) : null}
{item.data.fullyLoaded ? (
item.data.event === '$exception' ? (
<ErrorDisplay eventProperties={item.data.properties} />
) : (
<SimpleKeyValueList item={filterProperties(item.data.properties)} promotedKeys={promotedKeys} />
)
) : (
<div className="text-muted-alt flex gap-1 items-center">
<Spinner textColored />
Loading...
</div>
)}
</div>
</div>
)
}

View File

@ -16,7 +16,7 @@ export type NavigationItemProps = {
export function NavigationItem({ item, expanded, navigationURL }: NavigationItemProps): JSX.Element | null {
return (
<>
<div className="flex gap-2 items-start p-2 text-xs">
<div className="flex gap-2 items-start px-2 py-1 text-xs">
<PerformanceEventLabel label="navigated to " expanded={expanded} name={navigationURL} />
</div>
<LemonDivider className="my-0" />

View File

@ -1,25 +1,27 @@
import { IconDashboard, IconEye, IconGear, IconTerminal } from '@posthog/icons'
import { IconDashboard, IconEye, IconGear, IconMinusSquare, IconPlusSquare, IconTerminal } from '@posthog/icons'
import { LemonButton, LemonDivider } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { IconComment, IconOffline, IconUnverifiedEvent } from 'lib/lemon-ui/icons'
import { Dayjs } from 'lib/dayjs'
import useIsHovering from 'lib/hooks/useIsHovering'
import { IconComment, IconOffline } from 'lib/lemon-ui/icons'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { ceilMsToClosestSecond, colonDelimitedDuration } from 'lib/utils'
import { useEffect } from 'react'
import { ItemComment } from 'scenes/session-recordings/player/inspector/components/ItemComment'
import { useEffect, useRef } from 'react'
import { ItemComment, ItemCommentDetail } from 'scenes/session-recordings/player/inspector/components/ItemComment'
import { useDebouncedCallback } from 'use-debounce'
import useResizeObserver from 'use-resize-observer'
import { SessionRecordingPlayerTab } from '~/types'
import { ItemPerformanceEvent } from '../../../apm/playerInspector/ItemPerformanceEvent'
import { ItemPerformanceEvent, ItemPerformanceEventDetail } from '../../../apm/playerInspector/ItemPerformanceEvent'
import { IconWindow } from '../../icons'
import { playerSettingsLogic, TimestampFormat } from '../../playerSettingsLogic'
import { sessionRecordingPlayerLogic } from '../../sessionRecordingPlayerLogic'
import { InspectorListItem, playerInspectorLogic } from '../playerInspectorLogic'
import { ItemConsoleLog } from './ItemConsoleLog'
import { ItemDoctor } from './ItemDoctor'
import { ItemEvent } from './ItemEvent'
import { ItemConsoleLog, ItemConsoleLogDetail } from './ItemConsoleLog'
import { ItemDoctor, ItemDoctorDetail } from './ItemDoctor'
import { ItemEvent, ItemEventDetail } from './ItemEvent'
const typeToIconAndDescription = {
[SessionRecordingPlayerTab.ALL]: {
@ -27,7 +29,7 @@ const typeToIconAndDescription = {
tooltip: 'All events',
},
[SessionRecordingPlayerTab.EVENTS]: {
Icon: IconUnverifiedEvent,
Icon: undefined,
tooltip: 'Recording event',
},
[SessionRecordingPlayerTab.CONSOLE]: {
@ -59,7 +61,102 @@ const typeToIconAndDescription = {
tooltip: 'A user commented on this timestamp in the recording',
},
}
const PLAYER_INSPECTOR_LIST_ITEM_MARGIN = 4
const PLAYER_INSPECTOR_LIST_ITEM_MARGIN = 1
function ItemTimeDisplay({ item }: { item: InspectorListItem }): JSX.Element {
const { timestampFormat } = useValues(playerSettingsLogic)
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { durationMs } = useValues(playerInspectorLogic(logicProps))
const fixedUnits = durationMs / 1000 > 3600 ? 3 : 2
return (
<span className="px-2 py-1 text-xs min-w-12">
{timestampFormat != TimestampFormat.Relative ? (
(timestampFormat === TimestampFormat.UTC ? item.timestamp.tz('UTC') : item.timestamp).format(
'DD, MMM HH:mm:ss'
)
) : (
<>
{item.timeInRecording < 0 ? (
<Tooltip
title="This event occured before the recording started, likely as the page was loading."
placement="left"
>
<span className="text-muted">load</span>
</Tooltip>
) : (
colonDelimitedDuration(item.timeInRecording / 1000, fixedUnits)
)}
</>
)}
</span>
)
}
function RowItemTitle({
item,
finalTimestamp,
showIcon,
}: {
item: InspectorListItem
finalTimestamp: Dayjs | null
showIcon?: boolean
}): JSX.Element {
const TypeIcon = typeToIconAndDescription[item.type].Icon
return (
<div className="flex gap-1 items-center">
{showIcon && TypeIcon ? <TypeIcon /> : null}
{item.type === SessionRecordingPlayerTab.NETWORK ? (
<ItemPerformanceEvent item={item.data} finalTimestamp={finalTimestamp} />
) : item.type === SessionRecordingPlayerTab.CONSOLE ? (
<ItemConsoleLog item={item} />
) : item.type === SessionRecordingPlayerTab.EVENTS ? (
<ItemEvent item={item} />
) : item.type === 'offline-status' ? (
<div className="flex items-start p-2 text-xs font-light font-mono">
{item.offline ? 'Browser went offline' : 'Browser returned online'}
</div>
) : item.type === 'browser-visibility' ? (
<div className="flex items-start px-2 py-1 font-light font-mono text-xs">
Window became {item.status}
</div>
) : item.type === SessionRecordingPlayerTab.DOCTOR ? (
<ItemDoctor item={item} />
) : item.type === 'comment' ? (
<ItemComment item={item} />
) : null}
</div>
)
}
function RowItemDetail({
item,
finalTimestamp,
onClick,
}: {
item: InspectorListItem
finalTimestamp: Dayjs | null
onClick: () => void
}): JSX.Element | null {
return (
<div onClick={onClick}>
{item.type === SessionRecordingPlayerTab.NETWORK ? (
<ItemPerformanceEventDetail item={item.data} finalTimestamp={finalTimestamp} />
) : item.type === SessionRecordingPlayerTab.CONSOLE ? (
<ItemConsoleLogDetail item={item} />
) : item.type === SessionRecordingPlayerTab.EVENTS ? (
<ItemEventDetail item={item} />
) : item.type === 'offline-status' ? null : item.type === 'browser-visibility' ? null : item.type ===
SessionRecordingPlayerTab.DOCTOR ? (
<ItemDoctorDetail item={item} />
) : item.type === 'comment' ? (
<ItemCommentDetail item={item} />
) : null}
</div>
)
}
export function PlayerInspectorListItem({
item,
@ -70,14 +167,15 @@ export function PlayerInspectorListItem({
index: number
onLayout: (layout: { width: number; height: number }) => void
}): JSX.Element {
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { tab, durationMs, end, expandedItems, windowIds } = useValues(playerInspectorLogic(logicProps))
const { timestampFormat } = useValues(playerSettingsLogic)
const hoverRef = useRef<HTMLDivElement>(null)
const { logicProps } = useValues(sessionRecordingPlayerLogic)
const { seekToTime } = useActions(sessionRecordingPlayerLogic)
const { tab, end, expandedItems } = useValues(playerInspectorLogic(logicProps))
const { setItemExpanded } = useActions(playerInspectorLogic(logicProps))
const showIcon = tab === SessionRecordingPlayerTab.ALL
const fixedUnits = durationMs / 1000 > 3600 ? 3 : 2
const isExpanded = expandedItems.includes(index)
@ -85,149 +183,132 @@ export function PlayerInspectorListItem({
// Ceiling second is used since this is what's displayed to the user.
const seekToEvent = (): void => seekToTime(ceilMsToClosestSecond(item.timeInRecording) - 1000)
const itemProps = {
setExpanded: () => {
setItemExpanded(index, !isExpanded)
if (!isExpanded) {
seekToEvent()
}
},
expanded: isExpanded,
}
const onLayoutDebounced = useDebouncedCallback(onLayout, 500)
const { ref, width, height } = useResizeObserver({})
const totalHeight = height ? height + PLAYER_INSPECTOR_LIST_ITEM_MARGIN : height
// Height changes should layout immediately but width ones (browser resize can be much slower)
useEffect(() => {
if (!width || !totalHeight) {
return
}
onLayoutDebounced({ width, height: totalHeight })
}, [width])
useEffect(() => {
if (!width || !totalHeight) {
return
}
onLayout({ width, height: totalHeight })
}, [totalHeight])
// Height changes should lay out immediately but width ones (browser resize can be much slower)
useEffect(
() => {
if (!width || !totalHeight) {
return
}
onLayoutDebounced({ width, height: totalHeight })
},
// purposefully only triggering on width
// eslint-disable-next-line react-hooks/exhaustive-deps
[width]
)
const windowNumber =
windowIds.length > 1 ? (item.windowId ? windowIds.indexOf(item.windowId) + 1 || '?' : '?') : undefined
useEffect(
() => {
if (!width || !totalHeight) {
return
}
onLayout({ width, height: totalHeight })
},
// purposefully only triggering on total height
// eslint-disable-next-line react-hooks/exhaustive-deps
[totalHeight]
)
const TypeIcon = typeToIconAndDescription[item.type].Icon
const isHovering = useIsHovering(hoverRef)
return (
<div
ref={ref}
className={clsx('flex flex-1 overflow-hidden gap-2 relative items-start')}
className={clsx(
'ml-1 flex flex-col items-center',
isExpanded && 'border border-primary',
isExpanded && item.highlightColor && `border border-${item.highlightColor}-dark`,
isHovering && 'bg-bg-light'
)}
// eslint-disable-next-line react/forbid-dom-props
style={{
// Style as we need it for the layout optimisation
marginTop: PLAYER_INSPECTOR_LIST_ITEM_MARGIN / 2,
marginBottom: PLAYER_INSPECTOR_LIST_ITEM_MARGIN / 2,
zIndex: isExpanded ? 1 : 0,
}}
>
{!isExpanded && (showIcon || windowNumber) && (
<Tooltip
placement="left"
title={
<>
<b>{typeToIconAndDescription[item.type]?.tooltip}</b>
{windowNumber ? (
<>
<br />
{windowNumber !== '?' ? (
<>
{' '}
occurred in Window <b>{windowNumber}</b>
</>
) : (
<>
{' '}
not linked to any specific window. Either an event tracked from the backend
or otherwise not able to be linked to a given window.
</>
)}
</>
) : null}
</>
}
<div className="flex flex-row items-center w-full px-1">
<div
className="flex flex-row flex-1 items-center overflow-hidden cursor-pointer"
ref={hoverRef}
onClick={() => seekToEvent()}
>
<div className="shrink-0 text-2xl h-8 text-muted-alt flex items-center justify-center gap-1">
{showIcon && TypeIcon ? <TypeIcon /> : null}
{windowNumber ? <IconWindow size="small" value={windowNumber} /> : null}
</div>
</Tooltip>
)}
{/*TODO this tooltip doesn't trigger whether its inside or outside of this hover container */}
{item.windowNumber ? (
<Tooltip
placement="left"
title={
<>
<b>{typeToIconAndDescription[item.type]?.tooltip}</b>
<span
className={clsx(
'flex-1 overflow-hidden rounded border',
isExpanded && 'border-primary',
item.highlightColor && `border-${item.highlightColor}-dark bg-${item.highlightColor}-highlight`,
!item.highlightColor && 'bg-bg-light'
)}
>
{item.type === SessionRecordingPlayerTab.NETWORK ? (
<ItemPerformanceEvent item={item.data} finalTimestamp={end} {...itemProps} />
) : item.type === SessionRecordingPlayerTab.CONSOLE ? (
<ItemConsoleLog item={item} {...itemProps} />
) : item.type === SessionRecordingPlayerTab.EVENTS ? (
<ItemEvent item={item} {...itemProps} />
) : item.type === 'offline-status' ? (
<div className="flex items-start p-2 text-xs">
{item.offline ? 'Browser went offline' : 'Browser returned online'}
</div>
) : item.type === 'browser-visibility' ? (
<div className="flex items-start p-2 text-xs">Window became {item.status}</div>
) : item.type === SessionRecordingPlayerTab.DOCTOR ? (
<ItemDoctor item={item} {...itemProps} />
) : item.type === 'comment' ? (
<ItemComment item={item} {...itemProps} />
) : null}
<>
<br />
{item.windowNumber !== '?' ? (
<>
{' '}
occurred in Window <b>{item.windowNumber}</b>
</>
) : (
<>
{' '}
not linked to any specific window. Either an event tracked from the
backend or otherwise not able to be linked to a given window.
</>
)}
</>
</>
}
>
<IconWindow size="small" value={item.windowNumber || '?'} />
</Tooltip>
) : null}
{isExpanded ? (
<ItemTimeDisplay item={item} />
<div
className={clsx(
'flex-1 overflow-hidden',
item.highlightColor && `bg-${item.highlightColor}-highlight`
)}
>
<RowItemTitle item={item} finalTimestamp={end} showIcon={showIcon} />
</div>
</div>
<LemonButton
icon={isExpanded ? <IconMinusSquare /> : <IconPlusSquare />}
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
}
/>
</div>
{isExpanded ? (
<div
className={clsx(
'w-full mx-2 overflow-hidden',
item.highlightColor && `bg-${item.highlightColor}-highlight`
)}
>
<div className="text-xs">
<RowItemDetail item={item} finalTimestamp={end} onClick={() => seekToEvent()} />
<LemonDivider dashed />
<div
className="flex gap-2 justify-end cursor-pointer m-2"
className="flex justify-end cursor-pointer mx-2 my-1"
onClick={() => setItemExpanded(index, false)}
>
<span className="text-muted-alt">Collapse</span>
</div>
</div>
) : null}
</span>
{!isExpanded ? (
<LemonButton size="small" noPadding onClick={() => seekToEvent()}>
<span className="p-1 text-xs">
{timestampFormat != TimestampFormat.Relative ? (
(timestampFormat === TimestampFormat.UTC
? item.timestamp.tz('UTC')
: item.timestamp
).format('DD, MMM HH:mm:ss')
) : (
<>
{item.timeInRecording < 0 ? (
<Tooltip
title="This event occured before the recording started, likely as the page was loading."
placement="left"
>
<span className="text-muted">load</span>
</Tooltip>
) : (
colonDelimitedDuration(item.timeInRecording / 1000, fixedUnits)
)}
</>
)}
</span>
</LemonButton>
</div>
) : null}
</div>
)

View File

@ -50,10 +50,6 @@ function isNetworkError(item: InspectorListItem): boolean {
return isNetworkEvent(item) && (item.data.response_status || -1) >= 400
}
function isSlowNetwork(item: InspectorListItem): boolean {
return isNetworkEvent(item) && (item.data.duration || -1) >= 1000
}
function isEvent(item: InspectorListItem): item is InspectorListItemEvent {
return item.type === SessionRecordingPlayerTab.EVENTS
}
@ -90,6 +86,85 @@ function isComment(item: InspectorListItem): item is InspectorListItemComment {
return item.type === 'comment'
}
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)
},
}
export function filterInspectorListItems({
allItems,
tab,
@ -118,94 +193,6 @@ export function filterInspectorListItems({
return []
}
const inspectorTabFilters: Record<SessionRecordingPlayerTab, (item: InspectorListItem) => boolean> = {
[SessionRecordingPlayerTab.ALL]: (item: InspectorListItem) => {
// 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) ||
isNavigationEvent(item) ||
isNetworkError(item) ||
isSlowNetwork(item) ||
isPostHogMobileEvent(item) ||
isPageviewOrScreen(item) ||
isAutocapture(item))) ||
isComment(item)
const isAllErrors =
(!!miniFiltersByKey['all-errors']?.enabled && isNetworkError(item)) ||
isConsoleError(item) ||
isException(item) ||
isErrorEvent(item)
return isAllEverything || isAllAutomatic || isAllErrors
},
[SessionRecordingPlayerTab.EVENTS]: (item: InspectorListItem) => {
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 &&
['$pageview', '$screen'].includes(item.data.event)) ||
(!!miniFiltersByKey['events-autocapture']?.enabled && item.data.event === '$autocapture') ||
(!!miniFiltersByKey['events-exceptions']?.enabled && item.data.event === '$exception')
)
},
[SessionRecordingPlayerTab.CONSOLE]: (item: InspectorListItem) => {
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: InspectorListItem) => {
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: InspectorListItem) => {
return (
isOfflineStatusChange(item) ||
isBrowserVisibilityEvent(item) ||
isException(item) ||
isDoctorEvent(item)
)
},
}
for (const item of allItems) {
let include = false
@ -213,7 +200,7 @@ export function filterInspectorListItems({
continue
}
include = inspectorTabFilters[tab](item)
include = inspectorTabFilters[tab](item, miniFiltersByKey)
if (showMatchingEventsFilter && showOnlyMatching) {
// Special case - overrides the others

View File

@ -60,6 +60,7 @@ export type InspectorListItemBase = {
search: string
highlightColor?: 'danger' | 'warning' | 'primary'
windowId?: string
windowNumber?: number | '?' | undefined
}
export type InspectorListItemEvent = InspectorListItemBase & {
@ -296,9 +297,18 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
},
],
windowNumberForID: [
(s) => [s.windowIds],
(windowIds) => {
return (windowId: string | undefined): number | '?' | undefined => {
return windowIds.length > 1 ? (windowId ? windowIds.indexOf(windowId) + 1 || '?' : '?') : undefined
}
},
],
offlineStatusChanges: [
(s) => [s.start, s.sessionPlayerData],
(start, sessionPlayerData): InspectorListOfflineStatusChange[] => {
(s) => [s.start, s.sessionPlayerData, s.windowNumberForID],
(start, sessionPlayerData, windowNumberForID): InspectorListOfflineStatusChange[] => {
const logs: InspectorListOfflineStatusChange[] = []
Object.entries(sessionPlayerData.snapshotsByWindowId).forEach(([windowId, snapshots]) => {
@ -318,6 +328,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
timeInRecording: timeInRecording,
search: tag,
windowId: windowId,
windowNumber: windowNumberForID(windowId),
highlightColor: 'warning',
} satisfies InspectorListOfflineStatusChange)
}
@ -330,8 +341,8 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
],
browserVisibilityChanges: [
(s) => [s.start, s.sessionPlayerData],
(start, sessionPlayerData): InspectorListBrowserVisibility[] => {
(s) => [s.start, s.sessionPlayerData, s.windowNumberForID],
(start, sessionPlayerData, windowNumberForID): InspectorListBrowserVisibility[] => {
const logs: InspectorListBrowserVisibility[] = []
Object.entries(sessionPlayerData.snapshotsByWindowId).forEach(([windowId, snapshots]) => {
@ -351,6 +362,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
timeInRecording: timeInRecording,
search: tag,
windowId: windowId,
windowNumber: windowNumberForID(windowId),
highlightColor: 'warning',
} satisfies InspectorListBrowserVisibility)
}
@ -363,8 +375,8 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
],
doctorEvents: [
(s) => [s.start, s.sessionPlayerData],
(start, sessionPlayerData): InspectorListItemDoctor[] => {
(s) => [s.start, s.sessionPlayerData, s.windowNumberForID],
(start, sessionPlayerData, windowNumberForID): InspectorListItemDoctor[] => {
if (!start) {
return []
}
@ -407,6 +419,9 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
tag: niceify(tag),
search: niceify(tag),
window_id: windowId,
// TODO why both?
windowId: windowId,
windowNumber: windowNumberForID(windowId),
data: getPayloadFor(customEvent, tag),
})
}
@ -420,6 +435,9 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
tag: 'full snapshot event',
search: 'full snapshot event',
window_id: windowId,
// TODO why both?
windowId: windowId,
windowNumber: windowNumberForID(windowId),
data: { snapshotSize: humanizeBytes(estimateSize(snapshot)) },
})
}
@ -440,8 +458,8 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
],
consoleLogs: [
(s) => [s.sessionPlayerData],
(sessionPlayerData): RecordingConsoleLogV2[] => {
(s) => [s.sessionPlayerData, s.windowNumberForID],
(sessionPlayerData, windowNumberForID): RecordingConsoleLogV2[] => {
const logs: RecordingConsoleLogV2[] = []
const seenCache = new Set<string>()
@ -472,6 +490,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
logs.push({
timestamp: snapshot.timestamp,
windowId: windowId,
windowNumber: windowNumberForID(windowId),
content,
lines,
level,
@ -498,6 +517,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
s.browserVisibilityChanges,
s.sessionComments,
s.windowIdForTimestamp,
s.windowNumberForID,
],
(
start,
@ -509,7 +529,8 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
doctorEvents,
browserVisibilityChanges,
sessionComments,
windowIdForTimestamp
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
@ -552,6 +573,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
data: event,
highlightColor: responseStatus >= 400 ? 'danger' : undefined,
windowId: event.window_id,
windowNumber: windowNumberForID(event.window_id),
})
}
@ -567,6 +589,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
highlightColor:
event.level === 'error' ? 'danger' : event.level === 'warn' ? 'warning' : undefined,
windowId: event.windowId,
windowNumber: windowNumberForID(event.windowId),
})
}
@ -598,6 +621,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
? 'danger'
: undefined,
windowId: event.properties?.$window_id,
windowNumber: windowNumberForID(event.properties?.$window_id),
})
}
@ -612,6 +636,7 @@ export const playerInspectorLogic = kea<playerInspectorLogicType>([
search: comment.comment,
data: comment,
windowId: windowIdForTimestamp(timestamp.valueOf()),
windowNumber: windowNumberForID(windowIdForTimestamp(timestamp.valueOf())),
})
}
}

View File

@ -887,6 +887,7 @@ export type RecordingConsoleLog = RecordingConsoleLogBase & RecordingTimeMixinTy
export type RecordingConsoleLogV2 = {
timestamp: number
windowId: string | undefined
windowNumber?: number | '?' | undefined
level: LogLevel
content: string
// JS code associated with the log - implicitly the empty array when not provided