Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: network payload capture draft 2 #18736

Merged
merged 19 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified frontend/__snapshots__/components-command-bar--actions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/components-networkrequesttiming--basic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions frontend/src/loadPostHogJS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export function loadPostHogJS(): void {
bootstrap: window.POSTHOG_USER_IDENTITY_WITH_FLAGS ? window.POSTHOG_USER_IDENTITY_WITH_FLAGS : {},
opt_in_site_apps: true,
loaded: (posthog) => {
if (posthog.webPerformance) {
posthog.webPerformance._forceAllowLocalhost = true
if (posthog.sessionRecording) {
posthog.sessionRecording._forceAllowLocalhostNetworkCapture = true
}

if (window.IMPERSONATED_SESSION) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function ItemPerformanceEvent({
expanded,
setExpanded,
}: ItemPerformanceEvent): JSX.Element {
const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body'>('timings')
const [activeTab, setActiveTab] = useState<'timings' | 'headers' | 'payload' | 'response_body' | 'raw'>('timings')

const bytes = humanizeBytes(item.encoded_body_size || item.decoded_body_size || 0)
const startTime = item.start_time || item.fetch_start || 0
Expand Down Expand Up @@ -176,7 +176,11 @@ export function ItemPerformanceEvent({
return acc
}

if (['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status'].includes(key)) {
if (
['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status', 'raw'].includes(
key
)
) {
return acc
}

Expand Down Expand Up @@ -392,6 +396,17 @@ export function ItemPerformanceEvent({
),
}
: false,
// raw is only available if the feature flag is enabled
// TODO before proper release we should put raw behind its own flag
{
key: 'raw',
label: 'Json',
content: (
<CodeSnippet language={Language.JSON} wrap thing="performance event">
{JSON.stringify(item.raw, null, 2)}
</CodeSnippet>
),
},
]}
/>
</FlaggedFeature>
Expand Down Expand Up @@ -470,6 +485,11 @@ function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null {
let statusRow = null
let methodRow = null

let fromDiskCache = false
if (item.transfer_size === 0 && item.response_body && item.response_status && item.response_status < 400) {
fromDiskCache = true
}

if (item.response_status) {
const statusDescription = `${item.response_status} ${friendlyHttpStatus[item.response_status] || ''}`

Expand All @@ -483,7 +503,10 @@ function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null {
statusRow = (
<div className="flex gap-4 items-center justify-between overflow-hidden">
<div className="font-semibold">Status code</div>
<LemonTag type={statusType}>{statusDescription}</LemonTag>
<div>
<LemonTag type={statusType}>{statusDescription}</LemonTag>
{fromDiskCache && <span className={'text-muted'}> (from cache)</span>}
</div>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,30 @@ export function Basic(): JSX.Element {
<NetworkRequestTiming
performanceEvent={
{
connect_end: 9525.599999964237,
connect_start: 9525.599999964237,
// fake an event with every step visible
start_time: 10,
redirect_start: 10,
redirect_end: 20,
fetch_start: 20,
domain_lookup_start: 30,
domain_lookup_end: 40,
connect_start: 40,
secure_connection_start: 45,
connect_end: 50,
request_start: 60,
response_start: 70,
response_end: 80,
load_event_end: 90,
duration: 90,

decoded_body_size: 18260,
domain_lookup_end: 9525.599999964237,
domain_lookup_start: 9525.599999964237,
duration: 935.5,
encoded_body_size: 18260,
entry_type: 'resource',
fetch_start: 9525.599999964237,
initiator_type: 'fetch',
name: 'http://localhost:8000/api/organizations/@current/plugins/repository/',
next_hop_protocol: 'http/1.1',
redirect_end: 0,
redirect_start: 0,
render_blocking_status: 'non-blocking',
request_start: 9803.099999964237,
response_end: 10461.099999964237,
response_start: 10428.399999976158,
response_status: 200,
secure_connection_start: 0,
start_time: 9525.599999964237,
time_origin: '1699990397357',
timestamp: 1699990406882,
transfer_size: 18560,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { getSeriesColor } from 'lib/colors'
import { humanFriendlyMilliseconds } from 'lib/utils'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { useState } from 'react'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { SimpleKeyValueList } from 'scenes/session-recordings/player/inspector/components/SimpleKeyValueList'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
Expand Down Expand Up @@ -67,6 +66,8 @@ function colorForSection(section: (typeof perfSections)[number]): string {
}
}

type PerformanceMeasures = Record<string, EventPerformanceMeasure>

/**
* There are defined sections to performance measurement. We may have data for some or all of them
*
Expand Down Expand Up @@ -109,85 +110,114 @@ function colorForSection(section: (typeof perfSections)[number]): string {
*
* see https://nicj.net/resourcetiming-in-practice/
*/
function calculatePerformanceParts(perfEntry: PerformanceEvent): Record<string, EventPerformanceMeasure> {
export function calculatePerformanceParts(perfEntry: PerformanceEvent): PerformanceMeasures {
const performanceParts: Record<string, EventPerformanceMeasure> = {}

if (perfEntry.redirect_start && perfEntry.redirect_end) {
performanceParts['redirect'] = {
start: perfEntry.redirect_start,
end: perfEntry.redirect_end,
color: colorForSection('redirect'),
if (isPresent(perfEntry.redirect_start) && isPresent(perfEntry.redirect_end)) {
if (perfEntry.redirect_end - perfEntry.redirect_start > 0) {
performanceParts['redirect'] = {
start: perfEntry.redirect_start,
end: perfEntry.redirect_end,
color: colorForSection('redirect'),
}
}
}

if (perfEntry.fetch_start && perfEntry.domain_lookup_start) {
performanceParts['app cache'] = {
start: perfEntry.fetch_start,
end: perfEntry.domain_lookup_start,
color: colorForSection('app cache'),
if (isPresent(perfEntry.fetch_start) && isPresent(perfEntry.domain_lookup_start)) {
if (perfEntry.domain_lookup_start - perfEntry.fetch_start > 0) {
performanceParts['app cache'] = {
start: perfEntry.fetch_start,
end: perfEntry.domain_lookup_start,
color: colorForSection('app cache'),
}
}
}

if (perfEntry.domain_lookup_end && perfEntry.domain_lookup_start) {
performanceParts['dns lookup'] = {
start: perfEntry.domain_lookup_start,
end: perfEntry.domain_lookup_end,
color: colorForSection('dns lookup'),
if (isPresent(perfEntry.domain_lookup_end) && isPresent(perfEntry.domain_lookup_start)) {
if (perfEntry.domain_lookup_end - perfEntry.domain_lookup_start > 0) {
performanceParts['dns lookup'] = {
start: perfEntry.domain_lookup_start,
end: perfEntry.domain_lookup_end,
color: colorForSection('dns lookup'),
}
}
}

if (perfEntry.connect_end && perfEntry.connect_start) {
performanceParts['connection time'] = {
start: perfEntry.connect_start,
end: perfEntry.connect_end,
color: colorForSection('connection time'),
}

if (perfEntry.secure_connection_start) {
performanceParts['tls time'] = {
start: perfEntry.secure_connection_start,
if (isPresent(perfEntry.connect_end) && isPresent(perfEntry.connect_start)) {
if (perfEntry.connect_end - perfEntry.connect_start > 0) {
performanceParts['connection time'] = {
start: perfEntry.connect_start,
end: perfEntry.connect_end,
color: colorForSection('tls time'),
reducedHeight: true,
color: colorForSection('connection time'),
}

if (isPresent(perfEntry.secure_connection_start) && perfEntry.secure_connection_start > 0) {
performanceParts['tls time'] = {
start: perfEntry.secure_connection_start,
end: perfEntry.connect_end,
color: colorForSection('tls time'),
reducedHeight: true,
}
}
}
}

if (perfEntry.connect_end && perfEntry.request_start && perfEntry.connect_end !== perfEntry.request_start) {
performanceParts['request queuing time'] = {
start: perfEntry.connect_end,
end: perfEntry.request_start,
color: colorForSection('request queuing time'),
if (
isPresent(perfEntry.connect_end) &&
isPresent(perfEntry.request_start) &&
perfEntry.connect_end !== perfEntry.request_start
) {
if (perfEntry.request_start - perfEntry.connect_end > 0) {
performanceParts['request queuing time'] = {
start: perfEntry.connect_end,
end: perfEntry.request_start,
color: colorForSection('request queuing time'),
}
}
}

if (perfEntry.response_start && perfEntry.request_start) {
performanceParts['waiting for first byte'] = {
start: perfEntry.request_start,
end: perfEntry.response_start,
color: colorForSection('waiting for first byte'),
if (isPresent(perfEntry.response_start) && isPresent(perfEntry.request_start)) {
if (perfEntry.response_start - perfEntry.request_start > 0) {
performanceParts['waiting for first byte'] = {
start: perfEntry.request_start,
end: perfEntry.response_start,
color: colorForSection('waiting for first byte'),
}
}
}

if (perfEntry.response_start && perfEntry.response_end) {
performanceParts['receiving response'] = {
start: perfEntry.response_start,
end: perfEntry.response_end,
color: colorForSection('receiving response'),
if (isPresent(perfEntry.response_start) && isPresent(perfEntry.response_end)) {
if (perfEntry.response_end - perfEntry.response_start > 0) {
// if loading from disk cache then response_start is 0 but fetch_start is not
let start = perfEntry.response_start
if (perfEntry.response_start === 0 && isPresent(perfEntry.fetch_start)) {
start = perfEntry.fetch_start
}
performanceParts['receiving response'] = {
start: start,
end: perfEntry.response_end,
color: colorForSection('receiving response'),
}
}
}

if (perfEntry.response_end && perfEntry.load_event_end) {
performanceParts['document processing'] = {
start: perfEntry.response_end,
end: perfEntry.load_event_end,
color: colorForSection('document processing'),
if (isPresent(perfEntry.response_end) && isPresent(perfEntry.load_event_end)) {
if (perfEntry.load_event_end - perfEntry.response_end > 0) {
performanceParts['document processing'] = {
start: perfEntry.response_end,
end: perfEntry.load_event_end,
color: colorForSection('document processing'),
}
}
}

return performanceParts
}

function percentage(partDuration: number, totalDuration: number, min: number): number {
return Math.min(Math.max(min, (partDuration / totalDuration) * 100), 100)
}

function percentagesWithinEventRange({
partStart,
partEnd,
Expand All @@ -203,20 +233,20 @@ function percentagesWithinEventRange({
const partStartRelativeToTimeline = partStart - rangeStart
const partDuration = partEnd - partStart

const partPercentage = Math.max(0.1, (partDuration / totalDuration) * 100) //less than 0.1% is not visible
const partStartPercentage = (partStartRelativeToTimeline / totalDuration) * 100
const partPercentage = percentage(partDuration, totalDuration, 0.1)
const partStartPercentage = percentage(partStartRelativeToTimeline, totalDuration, 0)
return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` }
}

const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => {
const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element | null => {
const rangeStart = performanceEvent.start_time
const rangeEnd = performanceEvent.response_end
const rangeEnd = performanceEvent.load_event_end ? performanceEvent.load_event_end : performanceEvent.response_end
if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') {
const performanceParts = calculatePerformanceParts(performanceEvent)
const timings = calculatePerformanceParts(performanceEvent)
return (
<div className={'font-semibold text-xs'}>
{perfSections.map((section) => {
const matchedSection = performanceParts[section]
const matchedSection = timings[section]
const start = matchedSection?.start
const end = matchedSection?.end
const partDuration = end - start
Expand Down Expand Up @@ -263,7 +293,7 @@ const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent
</div>
)
}
return <LemonBanner type={'warning'}>Cannot render performance timeline for this request</LemonBanner>
return null
}

const TableView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => {
Expand All @@ -283,11 +313,15 @@ export const NetworkRequestTiming = ({
}): JSX.Element | null => {
const [timelineMode, setTimelineMode] = useState<boolean>(true)

// if timeline view renders null then we fall back to table view
const timelineView = timelineMode ? <TimeLineView performanceEvent={performanceEvent} /> : null

return (
<div className={'flex flex-col space-y-2'}>
<div className={'flex flex-row justify-end'}>
<LemonButton
type={'secondary'}
size={'xsmall'}
status={'stealth'}
onClick={() => setTimelineMode(!timelineMode)}
data-attr={`switch-timing-to-${timelineMode ? 'table' : 'timeline'}-view`}
Expand All @@ -296,11 +330,11 @@ export const NetworkRequestTiming = ({
</LemonButton>
</div>
<LemonDivider dashed={true} />
{timelineMode ? (
<TimeLineView performanceEvent={performanceEvent} />
) : (
<TableView performanceEvent={performanceEvent} />
)}
{timelineMode && timelineView ? timelineView : <TableView performanceEvent={performanceEvent} />}
</div>
)
}

function isPresent(x: number | undefined): x is number {
return typeof x === 'number'
}
Loading
Loading