feat: reorg inspector list rows (#26243)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 124 KiB |
@ -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)"
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -196,7 +196,6 @@ export function PlayerInspectorList(): JSX.Element {
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className="p-2"
|
||||
height={height}
|
||||
width={width}
|
||||
deferredMeasurementCache={cellMeasurerCache}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|