From a5e2ca1361fd69bb058e7a883b812a8cb400ecb1 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 21 Nov 2023 18:29:00 +0000 Subject: [PATCH] feat: network payload capture draft 2 (#18736) requires: PostHog/posthog-js#902 We don't want to support running posthog/network and rrweb/network plugin at the same time. These changes support that change improve timeline display allow raw json view for debugging --- ...components-networkrequesttiming--basic.png | Bin 10527 -> 10613 bytes frontend/src/loadPostHogJS.tsx | 4 +- .../components/ItemPerformanceEvent.tsx | 29 ++- .../Timing/NetworkRequestTiming.stories.tsx | 29 +-- .../Timing/NetworkRequestTiming.tsx | 156 ++++++++------ .../Timing/calculatePerformanceParts.test.ts | 192 ++++++++++++++++++ .../inspector/performance-event-utils.ts | 191 +++++++++-------- frontend/src/types.ts | 4 + package.json | 2 +- pnpm-lock.yaml | 8 +- 10 files changed, 444 insertions(+), 171 deletions(-) create mode 100644 frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts diff --git a/frontend/__snapshots__/components-networkrequesttiming--basic.png b/frontend/__snapshots__/components-networkrequesttiming--basic.png index 8247b433f71c8f2b4e1682d1e2f887655045bc2b..effc91c21a0a6da960f47b9bba209a4214394205 100644 GIT binary patch literal 10613 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 literal 10527 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 { - if (posthog.webPerformance) { - posthog.webPerformance._forceAllowLocalhost = true + if (posthog.sessionRecording) { + posthog.sessionRecording._forceAllowLocalhostNetworkCapture = true } if (window.IMPERSONATED_SESSION) { diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx index 96c4a9e01a143..592d926958224 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -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 @@ -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 } @@ -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: ( + + {JSON.stringify(item.raw, null, 2)} + + ), + }, ]} /> @@ -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] || ''}` @@ -483,7 +503,10 @@ function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null { statusRow = (
Status code
- {statusDescription} +
+ {statusDescription} + {fromDiskCache && (from cache)} +
) } diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx index a02e9bf3dce03..815eefed8bdfb 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.stories.tsx @@ -19,27 +19,30 @@ export function Basic(): JSX.Element { + /** * There are defined sections to performance measurement. We may have data for some or all of them * @@ -109,85 +110,114 @@ function colorForSection(section: (typeof perfSections)[number]): string { * * see https://nicj.net/resourcetiming-in-practice/ */ -function calculatePerformanceParts(perfEntry: PerformanceEvent): Record { +export function calculatePerformanceParts(perfEntry: PerformanceEvent): PerformanceMeasures { const performanceParts: Record = {} - 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, @@ -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 (
{perfSections.map((section) => { - const matchedSection = performanceParts[section] + const matchedSection = timings[section] const start = matchedSection?.start const end = matchedSection?.end const partDuration = end - start @@ -263,7 +293,7 @@ const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent
) } - return Cannot render performance timeline for this request + return null } const TableView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element => { @@ -283,11 +313,15 @@ export const NetworkRequestTiming = ({ }): JSX.Element | null => { const [timelineMode, setTimelineMode] = useState(true) + // if timeline view renders null then we fall back to table view + const timelineView = timelineMode ? : null + return (
setTimelineMode(!timelineMode)} data-attr={`switch-timing-to-${timelineMode ? 'table' : 'timeline'}-view`} @@ -296,11 +330,11 @@ export const NetworkRequestTiming = ({
- {timelineMode ? ( - - ) : ( - - )} + {timelineMode && timelineView ? timelineView : }
) } + +function isPresent(x: number | undefined): x is number { + return typeof x === 'number' +} diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts new file mode 100644 index 0000000000000..24f59d7f4af3d --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts @@ -0,0 +1,192 @@ +import { mapRRWebNetworkRequest } from 'scenes/session-recordings/player/inspector/performance-event-utils' +import { InitiatorType } from 'posthog-js' +import { calculatePerformanceParts } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' + +jest.mock('lib/colors', () => { + return { + getSeriesColor: jest.fn(() => '#000000'), + } +}) + +describe('calculatePerformanceParts', () => { + it('can calculate TTFB', () => { + const perfEvent = { + connect_end: 9525.599999964237, + connect_start: 9525.599999964237, + 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, + window_id: '018bcf51-b1f0-7fe0-ac05-10543621f4f2', + worker_start: 0, + uuid: '12345', + distinct_id: '23456', + session_id: 'abcde', + pageview_id: 'fghij', + current_url: 'http://localhost:8000/insights', + } + + expect(calculatePerformanceParts(perfEvent)).toEqual({ + 'request queuing time': { + color: '#000000', + end: 9803.099999964237, + start: 9525.599999964237, + }, + + 'waiting for first byte': { + color: '#000000', + end: 10428.399999976158, + start: 9803.099999964237, + }, + 'receiving response': { + color: '#000000', + end: 10461.099999964237, + start: 10428.399999976158, + }, + }) + }) + + it('can handle gravatar timings', () => { + const gravatarReqRes = { + name: 'https://www.gravatar.com/avatar/2e7d95b60efbe947f71009a1af1ba8d0?s=96&d=404', + entryType: 'resource', + initiatorType: 'fetch' as InitiatorType, + deliveryType: '', + nextHopProtocol: '', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + domainLookupStart: 0, + domainLookupEnd: 0, + connectStart: 0, + secureConnectionStart: 0, + connectEnd: 0, + requestStart: 0, + responseStart: 0, + firstInterimResponseStart: 0, + // only fetch start and response end + // and transfer size is 0 + // loaded from disk cache + startTime: 18229, + fetchStart: 18228.5, + responseEnd: 18267.5, + endTime: 18268, + duration: 39, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + responseStatus: 200, + serverTiming: [], + timeOrigin: 1700296048424, + timestamp: 1700296066652, + method: 'GET', + status: 200, + requestHeaders: {}, + requestBody: null, + responseHeaders: { + 'cache-control': 'max-age=300', + 'content-length': '13127', + 'content-type': 'image/png', + expires: 'Sat, 18 Nov 2023 08:32:46 GMT', + 'last-modified': 'Wed, 02 Feb 2022 09:11:05 GMT', + }, + responseBody: '�PNGblah', + } + const mappedToPerfEvent = mapRRWebNetworkRequest(gravatarReqRes, 'windowId', 1700296066652) + expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + // 'app cache' not included - end would be before beginning + // 'connection time' has 0 length + // 'dns lookup' has 0 length + // 'redirect has 0 length + // 'tls time' has 0 length + // TTFB has 0 length + 'receiving response': { + color: '#000000', + end: 18267.5, + start: 18228.5, + }, + }) + }) + + it('can handle no TLS connection timing', () => { + const tlsFreeReqRes = { + name: 'http://localhost:8000/decide/?v=3&ip=1&_=1700319068450&ver=1.91.1', + entryType: 'resource', + startTime: 6648, + duration: 93.40000003576279, + initiatorType: 'xmlhttprequest' as InitiatorType, + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 6647.699999988079, + domainLookupStart: 6648.800000011921, + domainLookupEnd: 6648.800000011921, + connectStart: 6648.800000011921, + secureConnectionStart: 0, + connectEnd: 6649.300000011921, + requestStart: 6649.5, + responseStart: 6740.800000011921, + firstInterimResponseStart: 0, + responseEnd: 6741.100000023842, + transferSize: 2383, + encodedBodySize: 2083, + decodedBodySize: 2083, + responseStatus: 200, + serverTiming: [], + endTime: 6741, + timeOrigin: 1700319061802, + timestamp: 1700319068449, + isInitial: true, + } + const mappedToPerfEvent = mapRRWebNetworkRequest(tlsFreeReqRes, 'windowId', 1700319068449) + expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + 'app cache': { + color: '#000000', + end: 6648.800000011921, + start: 6647.699999988079, + }, + 'connection time': { + color: '#000000', + end: 6649.300000011921, + start: 6648.800000011921, + }, + 'waiting for first byte': { + color: '#000000', + end: 6740.800000011921, + start: 6649.5, + }, + 'receiving response': { + color: '#000000', + end: 6741.100000023842, + start: 6740.800000011921, + }, + 'request queuing time': { + color: '#000000', + end: 6649.5, + start: 6649.300000011921, + }, + }) + }) +}) diff --git a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts index 43d0b2ef616b8..564b96fb865db 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts @@ -1,10 +1,9 @@ import { eventWithTime } from '@rrweb/types' -// import posthog from 'posthog-js' +import { CapturedNetworkRequest } from 'posthog-js' import { PerformanceEvent } from '~/types' const NETWORK_PLUGIN_NAME = 'posthog/network@1' -// const RRWEB_NETWORK_PLUGIN_NAME = 'rrweb/network@1' -// const IGNORED_POSTHOG_PATHS = ['/s/', '/e/', '/i/v0/e/'] +const RRWEB_NETWORK_PLUGIN_NAME = 'rrweb/network@1' export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceEvent } = { // BASE_PERFORMANCE_EVENT_COLUMNS @@ -58,8 +57,92 @@ export const PerformanceEventReverseMapping: { [key: number]: keyof PerformanceE 40: 'timestamp', } +export const RRWebPerformanceEventReverseMapping: Record = { + // BASE_PERFORMANCE_EVENT_COLUMNS + entryType: 'entry_type', + timeOrigin: 'time_origin', + name: 'name', + + // RESOURCE_EVENT_COLUMNS + startTime: 'start_time', + redirectStart: 'redirect_start', + redirectEnd: 'redirect_end', + workerStart: 'worker_start', + fetchStart: 'fetch_start', + domainLookupStart: 'domain_lookup_start', + domainLookupEnd: 'domain_lookup_end', + connectStart: 'connect_start', + secureConnectionStart: 'secure_connection_start', + connectEnd: 'connect_end', + requestStart: 'request_start', + responseStart: 'response_start', + responseEnd: 'response_end', + decodedBodySize: 'decoded_body_size', + encodedBodySize: 'encoded_body_size', + initiatorType: 'initiator_type', + nextHopProtocol: 'next_hop_protocol', + renderBlockingStatus: 'render_blocking_status', + responseStatus: 'response_status', + transferSize: 'transfer_size', + + // LARGEST_CONTENTFUL_PAINT_EVENT_COLUMNS + largestContentfulPaintElement: 'largest_contentful_paint_element', + largestContentfulPaintRenderTime: 'largest_contentful_paint_render_time', + largestContentfulPaintLoadTime: 'largest_contentful_paint_load_time', + largestContentfulPaintSize: 'largest_contentful_paint_size', + largestContentfulPaintId: 'largest_contentful_paint_id', + largestContentfulPaintUrl: 'largest_contentful_paint_url', + + // NAVIGATION_EVENT_COLUMNS + domComplete: 'dom_complete', + domContentLoadedEvent: 'dom_content_loaded_event', + domInteractive: 'dom_interactive', + loadEventEnd: 'load_event_end', + loadEventStart: 'load_event_start', + redirectCount: 'redirect_count', + navigationType: 'navigation_type', + unloadEventEnd: 'unload_event_end', + unloadEventStart: 'unload_event_start', + + // Added after v1 + duration: 'duration', + timestamp: 'timestamp', + + //rrweb/network@1 + isInitial: 'is_initial', + requestHeaders: 'request_headers', + responseHeaders: 'response_headers', + requestBody: 'request_body', + responseBody: 'response_body', + method: 'method', +} + +export function mapRRWebNetworkRequest( + capturedRequest: CapturedNetworkRequest, + windowId: string, + timestamp: PerformanceEvent['timestamp'] +): PerformanceEvent { + const data: Partial = { + timestamp: timestamp, + window_id: windowId, + raw: capturedRequest, + } + + Object.entries(RRWebPerformanceEventReverseMapping).forEach(([key, value]) => { + if (key in capturedRequest) { + data[value] = capturedRequest[key] + } + }) + + return data as PerformanceEvent +} + export function matchNetworkEvents(snapshotsByWindowId: Record): PerformanceEvent[] { - const eventsMapping: Record> = {} + // 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 + const events: PerformanceEvent[] = [] + const rrwebEvents: PerformanceEvent[] = [] // we could do this in one pass, but it's easier to log missing events // when we have all the posthog/network@1 events first @@ -83,93 +166,27 @@ export function matchNetworkEvents(snapshotsByWindowId: Record { + const data: PerformanceEvent = mapRRWebNetworkRequest(capturedRequest, windowId, snapshot.timestamp) - eventsMapping[eventName] = eventsMapping[eventName] || {} - eventsMapping[eventName][startTime] = eventsMapping[eventName][startTime] || [] - eventsMapping[eventName][startTime].push(mappedData) + rrwebEvents.push(data) + }) } }) }) - // // now we have all the posthog/network@1 events we can try to match any rrweb/network@1 events - // Object.entries(snapshotsByWindowId).forEach((snapshotsByWindowId) => { - // const snapshots = snapshotsByWindowId[1] - // snapshots.forEach((snapshot: eventWithTime) => { - // if ( - // snapshot.type === 6 && // RRWeb plugin event type - // snapshot.data.plugin === RRWEB_NETWORK_PLUGIN_NAME - // ) { - // const payload = snapshot.data.payload as any - // if (!Array.isArray(payload.requests) || payload.requests.length === 0) { - // return - // } - // - // payload.requests.forEach((capturedRequest: any) => { - // const matchedURL = eventsMapping[capturedRequest.url] - // - // const matchedStartTime = matchedURL ? matchedURL[capturedRequest.startTime] : null - // - // if (matchedStartTime && matchedStartTime.length === 1) { - // matchedStartTime[0].response_status = capturedRequest.status - // matchedStartTime[0].request_headers = capturedRequest.requestHeaders - // matchedStartTime[0].request_body = capturedRequest.requestBody - // matchedStartTime[0].response_headers = capturedRequest.responseHeaders - // matchedStartTime[0].response_body = capturedRequest.responseBody - // matchedStartTime[0].method = capturedRequest.method - // } else if (matchedStartTime && matchedStartTime.length > 1) { - // // find in eventsMapping[capturedRequest.url][capturedRequest.startTime] by matching capturedRequest.endTime and element.response_end - // const matchedEndTime = matchedStartTime.find( - // (x) => - // typeof x.response_end === 'number' && - // Math.round(x.response_end) === capturedRequest.endTime - // ) - // if (matchedEndTime) { - // matchedEndTime.response_status = capturedRequest.status - // matchedEndTime.request_headers = capturedRequest.requestHeaders - // matchedEndTime.request_body = capturedRequest.requestBody - // matchedEndTime.response_headers = capturedRequest.responseHeaders - // matchedEndTime.response_body = capturedRequest.responseBody - // matchedEndTime.method = capturedRequest.method - // } else { - // const capturedURL = new URL(capturedRequest.url) - // const capturedPath = capturedURL.pathname - // - // if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) { - // posthog.capture('Had matches but still could not match rrweb/network@1 event', { - // rrwebNetworkEvent: payload, - // possibleMatches: matchedStartTime, - // totalMatchedURLs: Object.keys(eventsMapping).length, - // }) - // } - // } - // } else { - // const capturedURL = new URL(capturedRequest.url) - // const capturedPath = capturedURL.pathname - // if (!IGNORED_POSTHOG_PATHS.some((x) => capturedPath === x)) { - // posthog.capture('Could not match rrweb/network@1 event', { - // rrwebNetworkEvent: payload, - // possibleMatches: eventsMapping[capturedRequest.url], - // totalMatchedURLs: Object.keys(eventsMapping).length, - // }) - // } - // } - // }) - // } - // }) - // }) - - // now flatten the eventsMapping into a single array - return Object.values(eventsMapping).reduce((acc: PerformanceEvent[], eventsByURL) => { - Object.values(eventsByURL).forEach((eventsByTime) => { - acc.push(...eventsByTime) - }) - return acc - }, []) + return events.length ? events : rrwebEvents } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index e10cf5628fc73..1273d98ef07e7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1151,6 +1151,10 @@ export interface PerformanceEvent { request_body?: Body response_body?: Body method?: string + + //rrweb/network@1 - i.e. not in ClickHouse table + is_initial?: boolean + raw?: Record } export interface CurrentBillCycleType { diff --git a/package.json b/package.json index 4587978c3a728..e510b90210c08 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "monaco-editor": "^0.39.0", "papaparse": "^5.4.1", "pmtiles": "^2.11.0", - "posthog-js": "1.92.0", + "posthog-js": "1.92.1", "posthog-js-lite": "2.0.0-alpha5", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49a65b92bf93e..bb52460b7206a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,8 +216,8 @@ dependencies: specifier: ^2.11.0 version: 2.11.0 posthog-js: - specifier: 1.92.0 - version: 1.92.0 + specifier: 1.92.1 + version: 1.92.1 posthog-js-lite: specifier: 2.0.0-alpha5 version: 2.0.0-alpha5 @@ -15787,8 +15787,8 @@ packages: resolution: {integrity: sha512-tlkBdypJuvK/s00n4EiQjwYVfuuZv6vt8BF3g1ooIQa2Gz9Vz80p8q3qsPLZ0V5ErGRy6i3Q4fWC9TDzR7GNRQ==} dev: false - /posthog-js@1.92.0: - resolution: {integrity: sha512-87bZ/qwBbIqvkIV4YYn65oIPEsRcWihA3jX7WV33LvZWaU1InlE6cwj95SleIVLiND4Ofm+cKXZeWwcRnrXkKA==} + /posthog-js@1.92.1: + resolution: {integrity: sha512-xtuTfM/acfDauiEfIdKF6d911KUZQ7RLii2COAYEoPWr3cVUFoNUoRQz9QJvgDlV2j22Zwl+mnXacUeua+Yi1A==} dependencies: fflate: 0.4.8 dev: false