diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--dark.png
index 3c683404aaa08..f8d9ebf34b596 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--light.png
index 5e1ecd3ade756..9391040853b10 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--all-slow--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--default--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--default--dark.png
index 365aee3e010cb..3d58b4fea9f81 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--default--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--default--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--default--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--default--light.png
index c9eda9ea22ff7..5232ed893b2dc 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--default--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--default--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--dark.png
index 365aee3e010cb..3d58b4fea9f81 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--light.png
index c9eda9ea22ff7..5232ed893b2dc 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--expanded--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--dark.png
index c0d848ec2933f..69f229638345c 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--light.png
index 402a521b246b3..67f440939f1cb 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-dom-interactive--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--dark.png
index 65ad534220f53..6ab711ef05d0a 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--light.png
index 4c2b8804e1362..99766bf651e0a 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-fcp--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--dark.png
index c0d04bac886ca..6da90315d9faa 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--light.png
index 3f419e266e06e..5bd2d952043d1 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--really-slow-load-event--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--dark.png
index 4b6a3f1a3021c..4d270ab840bcf 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--light.png
index 9b2e8044a4dd1..50cae88b6dc3f 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-dom-interactive--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--dark.png
index 18269729036eb..8cea1a608ddfe 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--light.png
index 1df2983b4e8d7..af3e1a0be1258 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-fcp--light.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--dark.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--dark.png
index c52430b5d44d2..6e2680e95f4e7 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--dark.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--dark.png differ
diff --git a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--light.png b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--light.png
index 178f9aabba2ae..89f3daec08d84 100644
Binary files a/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--light.png and b/frontend/__snapshots__/components-networkrequest-navigationitem--slow-load-event--light.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--dark.png b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--dark.png
index 19d73bc7ffd6f..4a5d7b827fa26 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--dark.png and b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--dark.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--light.png b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--light.png
index 55895c53e01d8..54925db899a2c 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--light.png and b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-current-url--light.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--dark.png b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--dark.png
index cb2809ec33e2d..950725713e7a0 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--dark.png and b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--dark.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--light.png b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--light.png
index 09bd6c91f512e..853ad7a6329d1 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--light.png and b/frontend/__snapshots__/components-playerinspector-itemevent--page-view-with-path--light.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--dark.png b/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--dark.png
index a764aca1b0182..38a99e796fa43 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--dark.png and b/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--dark.png differ
diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--light.png b/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--light.png
index 5d451dac3493a..c39cb56172b54 100644
Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--light.png and b/frontend/__snapshots__/components-playerinspector-itemevent--web-vitals-event--light.png differ
diff --git a/frontend/src/lib/components/TitledSnack.tsx b/frontend/src/lib/components/TitledSnack.tsx
index 5c5cbacc15be2..c6843cab426cc 100644
--- a/frontend/src/lib/components/TitledSnack.tsx
+++ b/frontend/src/lib/components/TitledSnack.tsx
@@ -4,8 +4,10 @@ export function TitledSnack({
title,
value,
type = 'default',
+ titleSuffix = ':',
}: {
title: string
+ titleSuffix?: string
value: string | JSX.Element
type?: 'default' | 'success'
}): JSX.Element {
@@ -20,7 +22,10 @@ export function TitledSnack({
type === 'success' ? 'bg-success-highlight' : 'bg-primary-highlight'
)}
>
- {title}:
+
+ {title}
+ {titleSuffix}
+
- The First Contentful Paint (FCP) metric measures the time from when the page starts loading to when any
- part of the page's content is rendered on the screen.{' '}
-
- Read more on developer.mozilla.org
-
-
- ),
- key: 'first_contentful_paint',
- scoreBenchmarks: [1800, 3000],
- },
- {
- label: 'DOM Interactive',
- description: (
-
- The document has finished loading and the document has been parsed but sub-resources such as scripts,
- images, stylesheets and frames are still loading.{' '}
-
- Read more on developer.mozilla.org
-
-
- The load event is fired when the whole page has loaded, including all dependent resources such as
- stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM
- has been loaded, without waiting for resources to finish loading.{' '}
-
- Read more on developer.mozilla.org
-
-
+ The First Contentful Paint (FCP) metric measures the time from when the page starts loading to when any part
+ of the page's content is rendered on the screen.{' '}
+
+ Read more on developer.mozilla.org
+
+
+ The document has finished loading and the document has been parsed but sub-resources such as scripts,
+ images, stylesheets and frames are still loading.{' '}
+
+ Read more on developer.mozilla.org
+
+
+ The load event is fired when the whole page has loaded, including all dependent resources such as
+ stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has
+ been loaded, without waiting for resources to finish loading.{' '}
+
+ Read more on developer.mozilla.org
+
+
+ Cumulative layout shift measures the extent to which users encounter unexpected layout shifts, in which
+ elements of the page are moved in an unexpected way: that is, that are not the result of a user action like
+ pressing a button or part of an animation.{' '}
+
+ Read more on developer.mozilla.org
+
+
+ The Largest Contentful Paint (LCP) performance metric provides the render time of the largest image or text
+ block visible within the viewport, recorded from when the page first begins to load.{' '}
+
+ Read more on developer.mozilla.org
+
+
+ INP is a metric that assesses a page's overall responsiveness to user interactions by observing the latency
+ of all click, tap, and keyboard interactions that occur throughout the lifespan of a user's visit to a page.
+ The final INP value is the longest interaction observed, ignoring outliers.{' '}
+
+ Read more on web.dev
+
+
+ >
+ )
+}
diff --git a/frontend/src/scenes/session-recordings/apm/performance-event-utils.test.ts b/frontend/src/scenes/session-recordings/apm/performance-event-utils.test.ts
index 10339390c6ee2..d7a77778b1a2c 100644
--- a/frontend/src/scenes/session-recordings/apm/performance-event-utils.test.ts
+++ b/frontend/src/scenes/session-recordings/apm/performance-event-utils.test.ts
@@ -1,4 +1,4 @@
-import { matchNetworkEvents } from 'scenes/session-recordings/apm/performance-event-utils'
+import { getPerformanceEvents } from 'scenes/session-recordings/apm/performance-event-utils'
const aSingleSnapshotWithNetworkPayloads = {
windowId: '018d5247-079c-7126-8e43-464605576a62',
@@ -277,7 +277,7 @@ describe('performance-event-utils', () => {
])
// there are 13 requests in the sample data
// only 5 should remain after collapsing server timings
- const actual = matchNetworkEvents(someData)
+ const actual = getPerformanceEvents(someData)
// we're collapsing server timings into their parent, so we'll have no top-level server timings
expect(actual.map((a) => a.entry_type)).toEqual(['navigation', 'resource', 'resource', 'resource', 'resource'])
diff --git a/frontend/src/scenes/session-recordings/apm/performance-event-utils.ts b/frontend/src/scenes/session-recordings/apm/performance-event-utils.ts
index 9efaed46c9a5a..2e91c3c583f05 100644
--- a/frontend/src/scenes/session-recordings/apm/performance-event-utils.ts
+++ b/frontend/src/scenes/session-recordings/apm/performance-event-utils.ts
@@ -222,7 +222,7 @@ export function mapRRWebNetworkRequest(
return data as PerformanceEvent
}
-export function matchNetworkEvents(snapshotsByWindowId: Record): PerformanceEvent[] {
+export function getPerformanceEvents(snapshotsByWindowId: Record): PerformanceEvent[] {
// we only support rrweb/network@1 events or posthog/network@1 events in any one recording
// apart from during testing, where we might have both
// if we have both, we only display posthog/network@1 events
diff --git a/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts b/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts
index f10016fd0cc0a..e6a27e9ed22a6 100644
--- a/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts
+++ b/frontend/src/scenes/session-recordings/apm/performanceEventDataLogic.ts
@@ -1,8 +1,8 @@
import { connect, kea, key, path, props, selectors } from 'kea'
import {
+ getPerformanceEvents,
initiatorToAssetTypeMapping,
itemSizeInfo,
- matchNetworkEvents,
} from 'scenes/session-recordings/apm/performance-event-utils'
import { InspectorListItemBase } from 'scenes/session-recordings/player/inspector/playerInspectorLogic'
import { playerSettingsLogic } from 'scenes/session-recordings/player/playerSettingsLogic'
@@ -11,7 +11,7 @@ import {
SessionRecordingDataLogicProps,
} from 'scenes/session-recordings/player/sessionRecordingDataLogic'
-import { PerformanceEvent, SessionRecordingPlayerTab } from '~/types'
+import { PerformanceEvent, RecordingEventType, SessionRecordingPlayerTab } from '~/types'
import type { performanceEventDataLogicType } from './performanceEventDataLogicType'
@@ -24,14 +24,19 @@ export interface PerformanceEventDataLogicProps extends SessionRecordingDataLogi
key?: string
}
+/** it's pretty quick to sort an already sorted list */
+function sortPerformanceEvents(events: PerformanceEvent[]): PerformanceEvent[] {
+ return events.sort((a, b) => (a.timestamp.valueOf() > b.timestamp.valueOf() ? 1 : -1))
+}
+
/**
* If we have paint events we should add them to the appropriate navigation event
* this makes it easier to draw performance cards for navigation events
*/
function matchPaintEvents(performanceEvents: PerformanceEvent[]): PerformanceEvent[] {
- // KLUDGE: this assumes that the input is sorted by timestamp and relies on the identity of the events to mutate them
+ // NB: this relies on the input being sorted by timestamp and relies on the identity of the events to mutate them
let lastNavigationEvent: PerformanceEvent | null = null
- for (const event of performanceEvents) {
+ for (const event of sortPerformanceEvents(performanceEvents)) {
if (event.entry_type === 'navigation') {
lastNavigationEvent = event
} else if (event.entry_type === 'paint' && event.name === 'first-contentful-paint' && lastNavigationEvent) {
@@ -42,6 +47,58 @@ function matchPaintEvents(performanceEvents: PerformanceEvent[]): PerformanceEve
return performanceEvents
}
+function matchWebVitalsEvents(
+ performanceEvents: PerformanceEvent[],
+ webVitalsEvents: RecordingEventType[]
+): PerformanceEvent[] {
+ // NB: this relies on the input being sorted by timestamp and relies on the identity of the events to mutate them
+
+ if (!webVitalsEvents.length) {
+ return performanceEvents
+ }
+
+ // first we get the timestamps of each navigation event,
+ // any web vitals events that occur between these timestamps
+ // can be associated to the navigation event
+ const navigationTimestamps: number[] = []
+ for (const event of performanceEvents) {
+ if (event.entry_type === 'navigation') {
+ // TRICKY: this is typed as string|number but it is always number
+ // TODO: fix this in the types
+ navigationTimestamps.push(event.timestamp as number)
+ }
+ }
+
+ let lastNavigationEvent: PerformanceEvent | null = null
+ let nextTimestamp: number | null = null
+ for (const event of sortPerformanceEvents(performanceEvents)) {
+ if (event.entry_type === 'navigation') {
+ lastNavigationEvent = event
+ nextTimestamp = navigationTimestamps.find((t) => t > event.timestamp) ?? null
+ } else {
+ if (!lastNavigationEvent) {
+ continue
+ }
+
+ for (const webVital of webVitalsEvents) {
+ if (webVital.properties.$current_url !== lastNavigationEvent.name) {
+ continue
+ }
+
+ const webVitalUnixTimestamp = new Date(webVital.timestamp).valueOf()
+ const isAfterLastNavigation = webVitalUnixTimestamp > lastNavigationEvent.timestamp
+ const isBeforeNextNavigation = webVitalUnixTimestamp < (nextTimestamp ?? Infinity)
+ if (isAfterLastNavigation && isBeforeNextNavigation) {
+ lastNavigationEvent.web_vitals = lastNavigationEvent.web_vitals || new Set()
+ lastNavigationEvent.web_vitals.add(webVital)
+ }
+ }
+ }
+ }
+
+ return performanceEvents
+}
+
export const performanceEventDataLogic = kea([
path(['scenes', 'session-recordings', 'apm', 'performanceEventDataLogic']),
props({} as PerformanceEventDataLogicProps),
@@ -52,32 +109,25 @@ export const performanceEventDataLogic = kea([
playerSettingsLogic,
['showOnlyMatching', 'tab', 'miniFiltersByKey', 'searchQuery'],
sessionRecordingDataLogic(props),
- [
- 'sessionPlayerData',
- 'sessionPlayerMetaDataLoading',
- 'snapshotsLoading',
- 'sessionEventsData',
- 'sessionEventsDataLoading',
- 'windowIds',
- 'start',
- 'end',
- 'durationMs',
- ],
+ ['sessionPlayerData', 'webVitalsEvents'],
],
})),
selectors(() => ({
allPerformanceEvents: [
- (s) => [s.sessionPlayerData],
- (sessionPlayerData): PerformanceEvent[] => {
+ (s) => [s.sessionPlayerData, s.webVitalsEvents],
+ (sessionPlayerData, webVitalsEvents): PerformanceEvent[] => {
+ // TRICKY: we listen to webVitalsEventsLoading to trigger a recompute once all the data is present
+
// performanceEvents used to come from the API,
// but we decided to instead store them in the recording data
// we gather more info than rrweb, so we mix the two back together here
- return matchPaintEvents(
- deduplicatePerformanceEvents(
- filterUnwanted(matchNetworkEvents(sessionPlayerData.snapshotsByWindowId))
- ).sort((a, b) => (a.timestamp.valueOf() > b.timestamp.valueOf() ? 1 : -1))
- )
+ const performanceEvents = getPerformanceEvents(sessionPlayerData.snapshotsByWindowId)
+ const filteredPerformanceEvents = filterUnwanted(performanceEvents)
+ const deduplicatedPerformanceEvents = deduplicatePerformanceEvents(filteredPerformanceEvents)
+ const sortedEvents = sortPerformanceEvents(deduplicatedPerformanceEvents)
+ const withMatchedPaintEvents = matchPaintEvents(sortedEvents)
+ return matchWebVitalsEvents(withMatchedPaintEvents, webVitalsEvents)
},
],
sizeBreakdown: [
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx
index 864b0b8782d38..f5cfe244cb075 100644
--- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx
+++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx
@@ -5,7 +5,7 @@ import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { TitledSnack } from 'lib/components/TitledSnack'
import { IconOpenInNew } from 'lib/lemon-ui/icons'
import { Spinner } from 'lib/lemon-ui/Spinner'
-import { autoCaptureEventToDescription, capitalizeFirstLetter } from 'lib/utils'
+import { autoCaptureEventToDescription, capitalizeFirstLetter, isString } from 'lib/utils'
import { insightUrlForEvent } from 'scenes/insights/utils'
import { InspectorListItemEvent } from '../playerInspectorLogic'
@@ -17,13 +17,14 @@ export interface ItemEventProps {
setExpanded: (expanded: boolean) => void
}
-function webVitalEventSummary(event: Record): JSX.Element {
+function WebVitalEventSummary({ event }: { event: Record }): JSX.Element {
return (
<>
{event ? (
{event.rating}: {event.value.toFixed(2)}
@@ -35,15 +36,15 @@ function webVitalEventSummary(event: Record): JSX.Element {
)
}
-function summarizeWebVitals(properties: Record): JSX.Element {
+function SummarizeWebVitals({ properties }: { properties: Record }): JSX.Element {
const { $web_vitals_FCP_event, $web_vitals_CLS_event, $web_vitals_INP_event, $web_vitals_LCP_event } = properties
return (