From f23055d66589a94f878cd62f5bfb8c900f82a2b9 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 24 Jul 2023 13:24:26 -0700 Subject: [PATCH] ref(replay): Refactor Replay Details>Network table to use new *Frame types (#53384) --- .../replays/virtualizedGrid/headerCell.tsx | 8 +- .../utils/replays/hooks/useCrumbHandlers.tsx | 9 +- static/app/utils/replays/replayReader.tsx | 14 - static/app/utils/replays/resourceFrame.tsx | 42 +++ .../views/replays/detail/layout/focusArea.tsx | 2 +- .../detail/network/details/components.tsx | 2 +- .../detail/network/details/content.spec.tsx | 49 +++- .../detail/network/details/content.tsx | 5 +- .../detail/network/details/getOutputType.tsx | 24 +- .../replays/detail/network/details/index.tsx | 4 +- .../network/details/onboarding.spec.tsx | 13 +- .../detail/network/details/onboarding.tsx | 4 +- .../detail/network/details/sections.tsx | 79 +++--- .../views/replays/detail/network/index.tsx | 25 +- .../replays/detail/network/networkFilters.tsx | 8 +- .../detail/network/networkTableCell.tsx | 56 ++-- .../detail/network/useNetworkFilters.spec.tsx | 250 ++++++++---------- .../detail/network/useNetworkFilters.tsx | 48 ++-- .../detail/network/useSortNetwork.spec.tsx | 205 +++++++------- .../replays/detail/network/useSortNetwork.tsx | 10 +- static/app/views/replays/types.tsx | 2 - 21 files changed, 434 insertions(+), 425 deletions(-) create mode 100644 static/app/utils/replays/resourceFrame.tsx diff --git a/static/app/components/replays/virtualizedGrid/headerCell.tsx b/static/app/components/replays/virtualizedGrid/headerCell.tsx index e05a3319181d01..e9d4c9b8247dba 100644 --- a/static/app/components/replays/virtualizedGrid/headerCell.tsx +++ b/static/app/components/replays/virtualizedGrid/headerCell.tsx @@ -6,18 +6,12 @@ import {IconArrow, IconInfo} from 'sentry/icons'; import {space} from 'sentry/styles/space'; import type {Crumb} from 'sentry/types/breadcrumbs'; import type {BreadcrumbFrame, SpanFrame} from 'sentry/utils/replays/types'; -import type {NetworkSpan} from 'sentry/views/replays/types'; interface SortCrumbs { asc: boolean; by: keyof Crumb | string; getValue: (row: Crumb) => any; } -interface SortSpans { - asc: boolean; - by: keyof NetworkSpan | string; - getValue: (row: NetworkSpan) => any; -} interface SortBreadcrumbFrame { asc: boolean; @@ -34,7 +28,7 @@ type Props = { field: string; handleSort: (fieldName: string) => void; label: string; - sortConfig: SortCrumbs | SortSpans | SortBreadcrumbFrame | SortSpanFrame; + sortConfig: SortCrumbs | SortBreadcrumbFrame | SortSpanFrame; style: CSSProperties; tooltipTitle: undefined | ReactNode; }; diff --git a/static/app/utils/replays/hooks/useCrumbHandlers.tsx b/static/app/utils/replays/hooks/useCrumbHandlers.tsx index e8d33004c48db8..135eee000b1af3 100644 --- a/static/app/utils/replays/hooks/useCrumbHandlers.tsx +++ b/static/app/utils/replays/hooks/useCrumbHandlers.tsx @@ -6,7 +6,6 @@ import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs'; import getFrameDetails from 'sentry/utils/replays/getFrameDetails'; import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab'; import {ReplayFrame} from 'sentry/utils/replays/types'; -import type {NetworkSpan} from 'sentry/views/replays/types'; function useCrumbHandlers(startTimestampMs: number = 0) { const { @@ -19,7 +18,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) { const {setActiveTab} = useActiveReplayTab(); const mouseEnterCallback = useRef<{ - id: Crumb | NetworkSpan | ReplayFrame | null; + id: Crumb | ReplayFrame | null; timeoutId: NodeJS.Timeout | null; }>({ id: null, @@ -27,7 +26,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) { }); const handleMouseEnter = useCallback( - (item: Crumb | NetworkSpan | ReplayFrame) => { + (item: Crumb | ReplayFrame) => { // this debounces the mouseEnter callback in unison with mouseLeave // we ensure the pointer remains over the target element before dispatching state events in order to minimize unnecessary renders // this helps during scrolling or mouse move events which would otherwise fire in rapid succession slowing down our app @@ -56,7 +55,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) { ); const handleMouseLeave = useCallback( - (item: Crumb | NetworkSpan | ReplayFrame) => { + (item: Crumb | ReplayFrame) => { // if there is a mouseEnter callback queued and we're leaving it we can just cancel the timeout if (mouseEnterCallback.current.id === item) { if (mouseEnterCallback.current.timeoutId) { @@ -78,7 +77,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) { ); const handleClick = useCallback( - (crumb: Crumb | NetworkSpan | ReplayFrame) => { + (crumb: Crumb | ReplayFrame) => { if ('offsetMs' in crumb) { const frame = crumb; // Finding `offsetMs` means we have a frame, not a crumb or span diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 45b92c8d8ecdf8..e79171ac37d876 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -3,7 +3,6 @@ import memoize from 'lodash/memoize'; import {duration} from 'moment'; import type {Crumb} from 'sentry/types/breadcrumbs'; -import {BreadcrumbType} from 'sentry/types/breadcrumbs'; import domId from 'sentry/utils/domId'; import localStorageWrapper from 'sentry/utils/localStorage'; import extractDomNodes from 'sentry/utils/replays/extractDomNodes'; @@ -34,7 +33,6 @@ import type { } from 'sentry/utils/replays/types'; import {isDeadClick, isDeadRageClick} from 'sentry/utils/replays/types'; import type { - NetworkSpan, RecordingEvent, ReplayCrumb, ReplayError, @@ -311,16 +309,4 @@ export default class ReplayReader { getNonConsoleCrumbs = memoize(() => this.breadcrumbs.filter(crumb => crumb.category !== 'console') ); - - getNavCrumbs = memoize(() => - this.breadcrumbs.filter(crumb => - [BreadcrumbType.INIT, BreadcrumbType.NAVIGATION].includes(crumb.type) - ) - ); - - getNetworkSpans = memoize(() => this.sortedSpans.filter(isNetworkSpan)); } - -const isNetworkSpan = (span: ReplaySpan): span is NetworkSpan => { - return span.op?.startsWith('navigation.') || span.op?.startsWith('resource.'); -}; diff --git a/static/app/utils/replays/resourceFrame.tsx b/static/app/utils/replays/resourceFrame.tsx new file mode 100644 index 00000000000000..25df09ed918c41 --- /dev/null +++ b/static/app/utils/replays/resourceFrame.tsx @@ -0,0 +1,42 @@ +import type {RequestFrame, ResourceFrame, SpanFrame} from 'sentry/utils/replays/types'; + +export function isRequestFrame(frame: SpanFrame): frame is RequestFrame { + return ['resource.fetch', 'resource.xhr'].includes(frame.op); +} + +function isResourceFrame(frame: SpanFrame): frame is ResourceFrame { + return [ + 'resource.css', + 'resource.iframe', + 'resource.img', + 'resource.link', + 'resource.other', + 'resource.script', + ].includes(frame.op); +} + +export function getFrameMethod(frame: SpanFrame) { + return isRequestFrame(frame) ? frame.data.method ?? 'GET' : 'GET'; +} + +export function getFrameStatus(frame: SpanFrame) { + return isRequestFrame(frame) + ? frame.data.statusCode + : isResourceFrame(frame) + ? frame.data.statusCode + : undefined; +} + +export function getResponseBodySize(frame: SpanFrame) { + if (isRequestFrame(frame)) { + // `data.responseBodySize` is from SDK version 7.44-7.45 + return frame.data.response?.size ?? frame.data.responseBodySize; + } + if (isResourceFrame(frame)) { + // What about these? + // frame.data.decodedBodySize + // frame.data.encodedBodySize + return frame.data.size; + } + return undefined; +} diff --git a/static/app/views/replays/detail/layout/focusArea.tsx b/static/app/views/replays/detail/layout/focusArea.tsx index f914d6c9a5fb84..5c75c3656e0b9b 100644 --- a/static/app/views/replays/detail/layout/focusArea.tsx +++ b/static/app/views/replays/detail/layout/focusArea.tsx @@ -21,7 +21,7 @@ function FocusArea({}: Props) { return ( diff --git a/static/app/views/replays/detail/network/details/components.tsx b/static/app/views/replays/detail/network/details/components.tsx index 278917dfe47a4f..19409a095e7832 100644 --- a/static/app/views/replays/detail/network/details/components.tsx +++ b/static/app/views/replays/detail/network/details/components.tsx @@ -45,7 +45,7 @@ export function SizeTooltip({children}: {children: ReactNode}) { } export function keyValueTableOrNotFound( - data: undefined | Record, + data: undefined | Record, notFoundText: string ) { return data ? ( diff --git a/static/app/views/replays/detail/network/details/content.spec.tsx b/static/app/views/replays/detail/network/details/content.spec.tsx index 406f86582e9ff5..4860f2f6fdd146 100644 --- a/static/app/views/replays/detail/network/details/content.spec.tsx +++ b/static/app/views/replays/detail/network/details/content.spec.tsx @@ -1,5 +1,6 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; +import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; import NetworkDetailsContent from 'sentry/views/replays/detail/network/details/content'; import type {TabKey} from 'sentry/views/replays/detail/network/details/tabs'; @@ -14,21 +15,30 @@ function mockNeedsUpdate(needsUpdate: boolean) { mockUseProjectSdkNeedsUpdate.mockReturnValue({isFetching: false, needsUpdate}); } -const mockItems = { - img: TestStubs.ReplaySpanPayload({ +const [ + img, + fetchNoDataObj, + fetchUrlSkipped, + fetchBodySkipped, + fetchWithHeaders, + fetchWithRespBody, +] = hydrateSpans(TestStubs.ReplayRecord(), [ + TestStubs.Replay.ResourceFrame({ op: 'resource.img', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/static/img/logo.png', - data: { - method: 'GET', - statusCode: 200, - }, }), - fetchNoDataObj: TestStubs.ReplaySpanPayload({ + TestStubs.Replay.RequestFrame({ op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/api/0/issues/1234', }), - fetchUrlSkipped: TestStubs.ReplaySpanPayload({ + TestStubs.Replay.RequestFrame({ op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/api/0/issues/1234', data: { method: 'GET', @@ -37,24 +47,30 @@ const mockItems = { response: {_meta: {warnings: ['URL_SKIPPED']}, headers: {}}, }, }), - fetchBodySkipped: TestStubs.ReplaySpanPayload({ + TestStubs.Replay.RequestFrame({ op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/api/0/issues/1234', data: { method: 'GET', statusCode: 200, request: { + // @ts-expect-error _meta: {warnings: ['BODY_SKIPPED']}, headers: {accept: 'application/json'}, }, response: { + // @ts-expect-error _meta: {warnings: ['BODY_SKIPPED']}, headers: {'content-type': 'application/json'}, }, }, }), - fetchWithHeaders: TestStubs.ReplaySpanPayload({ + TestStubs.Replay.RequestFrame({ op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/api/0/issues/1234', data: { method: 'GET', @@ -69,8 +85,10 @@ const mockItems = { }, }, }), - fetchWithRespBody: TestStubs.ReplaySpanPayload({ + TestStubs.Replay.RequestFrame({ op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), description: '/api/0/issues/1234', data: { method: 'GET', @@ -86,6 +104,15 @@ const mockItems = { }, }, }), +]); + +const mockItems = { + img, + fetchNoDataObj, + fetchUrlSkipped, + fetchBodySkipped, + fetchWithHeaders, + fetchWithRespBody, }; function basicSectionProps() { diff --git a/static/app/views/replays/detail/network/details/content.tsx b/static/app/views/replays/detail/network/details/content.tsx index cbff5851d288ac..dcd92c57c66f97 100644 --- a/static/app/views/replays/detail/network/details/content.tsx +++ b/static/app/views/replays/detail/network/details/content.tsx @@ -2,6 +2,7 @@ import {useEffect} from 'react'; import styled from '@emotion/styled'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {getFrameMethod, getFrameStatus} from 'sentry/utils/replays/resourceFrame'; import useOrganization from 'sentry/utils/useOrganization'; import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight'; import getOutputType, { @@ -34,8 +35,8 @@ export default function NetworkDetailsContent(props: Props) { is_sdk_setup: isSetup, organization, output, - resource_method: item.data.method, - resource_status: item.data.statusCode, + resource_method: getFrameMethod(item), + resource_status: String(getFrameStatus(item)), resource_type: item.op, tab: visibleTab, }); diff --git a/static/app/views/replays/detail/network/details/getOutputType.tsx b/static/app/views/replays/detail/network/details/getOutputType.tsx index 451de5f431b0e3..d0eab57fa07ad4 100644 --- a/static/app/views/replays/detail/network/details/getOutputType.tsx +++ b/static/app/views/replays/detail/network/details/getOutputType.tsx @@ -1,3 +1,4 @@ +import {isRequestFrame} from 'sentry/utils/replays/resourceFrame'; import type {SectionProps} from 'sentry/views/replays/detail/network/details/sections'; import type {TabKey} from 'sentry/views/replays/detail/network/details/tabs'; @@ -16,8 +17,7 @@ type Args = { }; export default function getOutputType({isSetup, item, visibleTab}: Args): Output { - const isSupportedOp = ['resource.fetch', 'resource.xhr'].includes(item.op); - if (!isSupportedOp) { + if (!isRequestFrame(item)) { return Output.UNSUPPORTED; } @@ -25,23 +25,23 @@ export default function getOutputType({isSetup, item, visibleTab}: Args): Output return Output.SETUP; } - const request = item.data?.request ?? {}; - const response = item.data?.response ?? {}; + const request = item.data.request; + const response = item.data.response; const hasHeaders = - Object.keys(request.headers || {}).length || - Object.keys(response.headers || {}).length; + Object.keys(request?.headers ?? {}).length || + Object.keys(response?.headers ?? {}).length; if (hasHeaders && visibleTab === 'details') { return Output.DATA; } - const hasBody = request.body || response.body; + const hasBody = request?.body || response?.body; if (hasBody && ['request', 'response'].includes(visibleTab)) { return Output.DATA; } - const reqWarnings = request._meta?.warnings ?? ['URL_SKIPPED']; - const respWarnings = response._meta?.warnings ?? ['URL_SKIPPED']; + const reqWarnings = request?._meta?.warnings ?? ['URL_SKIPPED']; + const respWarnings = response?._meta?.warnings ?? ['URL_SKIPPED']; const isReqUrlSkipped = reqWarnings?.includes('URL_SKIPPED'); const isRespUrlSkipped = respWarnings?.includes('URL_SKIPPED'); if (isReqUrlSkipped || isRespUrlSkipped) { @@ -49,8 +49,10 @@ export default function getOutputType({isSetup, item, visibleTab}: Args): Output } if (['request', 'response'].includes(visibleTab)) { - const isReqBodySkipped = reqWarnings?.includes('BODY_SKIPPED'); - const isRespBodySkipped = respWarnings?.includes('BODY_SKIPPED'); + // @ts-expect-error: is BODY_SKIPPED really emitted from the SDK? + const isReqBodySkipped = reqWarnings.includes('BODY_SKIPPED'); + // @ts-expect-error: is BODY_SKIPPED really emitted from the SDK? + const isRespBodySkipped = respWarnings.includes('BODY_SKIPPED'); if (isReqBodySkipped || isRespBodySkipped) { return Output.BODY_SKIPPED; } diff --git a/static/app/views/replays/detail/network/details/index.tsx b/static/app/views/replays/detail/network/details/index.tsx index 33a5b43d478829..e64ed48343696d 100644 --- a/static/app/views/replays/detail/network/details/index.tsx +++ b/static/app/views/replays/detail/network/details/index.tsx @@ -6,6 +6,7 @@ import Stacked from 'sentry/components/replays/breadcrumbs/stacked'; import {IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import {useResizableDrawer} from 'sentry/utils/useResizableDrawer'; import useUrlParams from 'sentry/utils/useUrlParams'; import SplitDivider from 'sentry/views/replays/detail/layout/splitDivider'; @@ -13,11 +14,10 @@ import NetworkDetailsContent from 'sentry/views/replays/detail/network/details/c import NetworkDetailsTabs, { TabKey, } from 'sentry/views/replays/detail/network/details/tabs'; -import type {NetworkSpan} from 'sentry/views/replays/types'; type Props = { isSetup: boolean; - item: null | NetworkSpan; + item: null | SpanFrame; onClose: () => void; projectId: undefined | string; startTimestampMs: number; diff --git a/static/app/views/replays/detail/network/details/onboarding.spec.tsx b/static/app/views/replays/detail/network/details/onboarding.spec.tsx index f0a771e9623474..154528e65129c0 100644 --- a/static/app/views/replays/detail/network/details/onboarding.spec.tsx +++ b/static/app/views/replays/detail/network/details/onboarding.spec.tsx @@ -1,6 +1,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {textWithMarkupMatcher} from 'sentry-test/utils'; +import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; import {Output} from 'sentry/views/replays/detail/network/details/getOutputType'; @@ -12,10 +13,14 @@ const mockUseProjectSdkNeedsUpdate = useProjectSdkNeedsUpdate as jest.MockedFunc import {Setup} from 'sentry/views/replays/detail/network/details/onboarding'; -const MOCK_ITEM = TestStubs.ReplaySpanPayload({ - op: 'resource.fetch', - description: '/api/0/issues/1234', -}); +const [MOCK_ITEM] = hydrateSpans(TestStubs.ReplayRecord(), [ + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + startTimestamp: new Date(), + endTimestamp: new Date(), + description: '/api/0/issues/1234', + }), +]); describe('Setup', () => { mockUseProjectSdkNeedsUpdate.mockReturnValue({isFetching: false, needsUpdate: false}); diff --git a/static/app/views/replays/detail/network/details/onboarding.tsx b/static/app/views/replays/detail/network/details/onboarding.tsx index 896e90ec2ed470..d5e45387e6b527 100644 --- a/static/app/views/replays/detail/network/details/onboarding.tsx +++ b/static/app/views/replays/detail/network/details/onboarding.tsx @@ -7,12 +7,12 @@ import ExternalLink from 'sentry/components/links/externalLink'; import {IconClose, IconInfo} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import useDismissAlert from 'sentry/utils/useDismissAlert'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; import {Output} from 'sentry/views/replays/detail/network/details/getOutputType'; import type {TabKey} from 'sentry/views/replays/detail/network/details/tabs'; -import type {NetworkSpan} from 'sentry/views/replays/types'; export const useDismissReqRespBodiesAlert = () => { const organization = useOrganization(); @@ -116,7 +116,7 @@ export function Setup({ showSnippet, visibleTab, }: { - item: NetworkSpan; + item: SpanFrame; projectId: string; showSnippet: Output; visibleTab: TabKey; diff --git a/static/app/views/replays/detail/network/details/sections.tsx b/static/app/views/replays/detail/network/details/sections.tsx index 2abc2b1c90b669..288d3057f12db2 100644 --- a/static/app/views/replays/detail/network/details/sections.tsx +++ b/static/app/views/replays/detail/network/details/sections.tsx @@ -1,10 +1,16 @@ -import {MouseEvent, useEffect} from 'react'; +import {MouseEvent, useEffect, useMemo} from 'react'; import queryString from 'query-string'; import ObjectInspector from 'sentry/components/objectInspector'; -import {t, tct} from 'sentry/locale'; +import {t} from 'sentry/locale'; import {formatBytesBase10} from 'sentry/utils'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; +import { + getFrameMethod, + getFrameStatus, + isRequestFrame, +} from 'sentry/utils/replays/resourceFrame'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import { Indent, keyValueTableOrNotFound, @@ -14,32 +20,39 @@ import { } from 'sentry/views/replays/detail/network/details/components'; import {useDismissReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding'; import TimestampButton from 'sentry/views/replays/detail/timestampButton'; -import type {NetworkSpan} from 'sentry/views/replays/types'; export type SectionProps = { - item: NetworkSpan; + item: SpanFrame; projectId: string; startTimestampMs: number; }; +const UNKNOWN_STATUS = 'unknown'; + export function GeneralSection({item, startTimestampMs}: SectionProps) { const {handleClick} = useCrumbHandlers(startTimestampMs); - const startMs = item.startTimestamp * 1000; - const endMs = item.endTimestamp * 1000; + const requestFrame = isRequestFrame(item) ? item : null; + + // TODO[replay]: what about: + // `requestFrame?.data?.request?.size` vs. `requestFrame?.data?.requestBodySize` const data = { [t('URL')]: item.description, [t('Type')]: item.op, - [t('Method')]: item.data?.method ?? '', - [t('Status Code')]: item.data?.statusCode ?? '', + [t('Method')]: getFrameMethod(item), + [t('Status Code')]: String(getFrameStatus(item) ?? UNKNOWN_STATUS), [t('Request Body Size')]: ( - {formatBytesBase10(item.data?.request?.size ?? 0)} + + {formatBytesBase10(requestFrame?.data?.request?.size ?? 0)} + ), [t('Response Body Size')]: ( - {formatBytesBase10(item.data?.response?.size ?? 0)} + + {formatBytesBase10(requestFrame?.data?.response?.size ?? 0)} + ), - [t('Duration')]: `${(endMs - startMs).toFixed(2)}ms`, + [t('Duration')]: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`, [t('Timestamp')]: ( ), }; @@ -61,17 +74,19 @@ export function GeneralSection({item, startTimestampMs}: SectionProps) { } export function RequestHeadersSection({item}: SectionProps) { + const data = isRequestFrame(item) ? item.data : {}; return ( - {keyValueTableOrNotFound(item.data?.request?.headers, t('Headers not captured'))} + {keyValueTableOrNotFound(data.request?.headers, t('Headers not captured'))} ); } export function ResponseHeadersSection({item}: SectionProps) { + const data = isRequestFrame(item) ? item.data : {}; return ( - {keyValueTableOrNotFound(item.data?.request?.headers, t('Headers not captured'))} + {keyValueTableOrNotFound(data.request?.headers, t('Headers not captured'))} ); } @@ -90,28 +105,28 @@ export function QueryParamsSection({item}: SectionProps) { export function RequestPayloadSection({item}: SectionProps) { const {dismiss, isDismissed} = useDismissReqRespBodiesAlert(); - const hasRequest = 'request' in item.data; + const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]); useEffect(() => { - if (!isDismissed && hasRequest) { + if (!isDismissed && 'request' in data) { dismiss(); } - }, [dismiss, hasRequest, isDismissed]); + }, [dismiss, data, isDismissed]); return ( - {t('Size:')} {formatBytesBase10(item.data?.request?.size ?? 0)} + {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)} } > - - {hasRequest ? ( - + + {'request' in data ? ( + ) : ( - tct('Request body not found.', item.data) + t('Request body not found.') )} @@ -121,32 +136,28 @@ export function RequestPayloadSection({item}: SectionProps) { export function ResponsePayloadSection({item}: SectionProps) { const {dismiss, isDismissed} = useDismissReqRespBodiesAlert(); - const hasResponse = 'response' in item.data; + const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]); useEffect(() => { - if (!isDismissed && hasResponse) { + if (!isDismissed && 'response' in data) { dismiss(); } - }, [dismiss, hasResponse, isDismissed]); + }, [dismiss, data, isDismissed]); return ( - {t('Size:')} {formatBytesBase10(item.data?.response?.size ?? 0)} + {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)} } > - - {hasResponse ? ( - + + {'response' in data ? ( + ) : ( - tct('Response body not found.', item.data) + t('Response body not found.') )} diff --git a/static/app/views/replays/detail/network/index.tsx b/static/app/views/replays/detail/network/index.tsx index 357be4bf4a6722..01b712911662a8 100644 --- a/static/app/views/replays/detail/network/index.tsx +++ b/static/app/views/replays/detail/network/index.tsx @@ -7,6 +7,8 @@ import {useReplayContext} from 'sentry/components/replays/replayContext'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; +import {getFrameMethod, getFrameStatus} from 'sentry/utils/replays/resourceFrame'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import useOrganization from 'sentry/utils/useOrganization'; import {useResizableDrawer} from 'sentry/utils/useResizableDrawer'; import useUrlParams from 'sentry/utils/useUrlParams'; @@ -22,7 +24,6 @@ import useNetworkFilters from 'sentry/views/replays/detail/network/useNetworkFil import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork'; import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer'; import useVirtualizedGrid from 'sentry/views/replays/detail/useVirtualizedGrid'; -import type {NetworkSpan} from 'sentry/views/replays/types'; const HEADER_HEIGHT = 25; const BODY_HEIGHT = 28; @@ -31,7 +32,7 @@ const RESIZEABLE_HANDLE_HEIGHT = 90; type Props = { isNetworkDetailsSetup: boolean; - networkSpans: undefined | NetworkSpan[]; + networkFrames: undefined | SpanFrame[]; projectId: undefined | string; startTimestampMs: number; }; @@ -44,7 +45,7 @@ const cellMeasurer = { function NetworkList({ isNetworkDetailsSetup, - networkSpans, + networkFrames, projectId, startTimestampMs, }: Props) { @@ -53,7 +54,7 @@ function NetworkList({ const [scrollToRow, setScrollToRow] = useState(undefined); - const filterProps = useNetworkFilters({networkSpans: networkSpans || []}); + const filterProps = useNetworkFilters({networkFrames: networkFrames || []}); const {items: filteredItems, searchTerm, setSearchTerm} = filterProps; const clearSearchTerm = () => setSearchTerm(''); const {handleSort, items, sortConfig} = useSortNetwork({items: filteredItems}); @@ -92,7 +93,7 @@ function NetworkList({ const maxContainerHeight = (containerRef.current?.clientHeight || window.innerHeight) - RESIZEABLE_HANDLE_HEIGHT; const splitSize = - networkSpans && detailDataIndex + networkFrames && detailDataIndex ? Math.min(maxContainerHeight, containerSize) : undefined; @@ -113,8 +114,8 @@ function NetworkList({ trackAnalytics('replay.details-network-panel-opened', { is_sdk_setup: isNetworkDetailsSetup, organization, - resource_method: item.data.method, - resource_status: item.data.statusCode, + resource_method: getFrameMethod(item), + resource_status: String(getFrameStatus(item)), resource_type: item.op, }); } @@ -160,7 +161,7 @@ function NetworkList({ ref={e => e && registerChild?.(e)} rowIndex={rowIndex} sortConfig={sortConfig} - span={network} + frame={network} startTimestampMs={startTimestampMs} style={{...style, height: BODY_HEIGHT}} /> @@ -172,7 +173,7 @@ function NetworkList({ return ( - + - {networkSpans ? ( + {networkFrames ? ( {({height, width}) => ( @@ -196,7 +197,7 @@ function NetworkList({ height={height} noContentRenderer={() => ( {t('No network requests recorded')} @@ -224,7 +225,7 @@ function NetworkList({ { setDetailRow(''); trackAnalytics('replay.details-network-panel-closed', { diff --git a/static/app/views/replays/detail/network/networkFilters.tsx b/static/app/views/replays/detail/network/networkFilters.tsx index f8fe1c2f9cab70..ccb21fae0fc558 100644 --- a/static/app/views/replays/detail/network/networkFilters.tsx +++ b/static/app/views/replays/detail/network/networkFilters.tsx @@ -1,19 +1,19 @@ import {CompactSelect, SelectOption} from 'sentry/components/compactSelect'; import SearchBar from 'sentry/components/searchBar'; import {t} from 'sentry/locale'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import FiltersGrid from 'sentry/views/replays/detail/filtersGrid'; import useNetworkFilters from 'sentry/views/replays/detail/network/useNetworkFilters'; -import type {NetworkSpan} from 'sentry/views/replays/types'; type Props = { - networkSpans: undefined | NetworkSpan[]; + networkFrames: undefined | SpanFrame[]; } & ReturnType; function NetworkFilters({ getMethodTypes, getResourceTypes, getStatusTypes, - networkSpans, + networkFrames, searchTerm, selectValue, setFilters, @@ -53,7 +53,7 @@ function NetworkFilters({ onChange={setSearchTerm} placeholder={t('Search Network Requests')} query={searchTerm} - disabled={!networkSpans || !networkSpans.length} + disabled={!networkFrames || !networkFrames.length} /> ); diff --git a/static/app/views/replays/detail/network/networkTableCell.tsx b/static/app/views/replays/detail/network/networkTableCell.tsx index 12435fe9885ac8..71a1bbc56deabf 100644 --- a/static/app/views/replays/detail/network/networkTableCell.tsx +++ b/static/app/views/replays/detail/network/networkTableCell.tsx @@ -1,18 +1,22 @@ -import {ComponentProps, CSSProperties, forwardRef, MouseEvent, useMemo} from 'react'; +import {ComponentProps, CSSProperties, forwardRef, MouseEvent} from 'react'; import classNames from 'classnames'; import FileSize from 'sentry/components/fileSize'; -import {relativeTimeInMs} from 'sentry/components/replays/utils'; import { Cell, StyledTimestampButton, Text, } from 'sentry/components/replays/virtualizedGrid/bodyCell'; import {Tooltip} from 'sentry/components/tooltip'; +import { + getFrameMethod, + getFrameStatus, + getResponseBodySize, +} from 'sentry/utils/replays/resourceFrame'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import useUrlParams from 'sentry/utils/useUrlParams'; import useSortNetwork from 'sentry/views/replays/detail/network/useSortNetwork'; import {operationName} from 'sentry/views/replays/detail/utils'; -import type {NetworkSpan} from 'sentry/views/replays/types'; const EMPTY_CELL = '--'; @@ -20,13 +24,13 @@ type Props = { columnIndex: number; currentHoverTime: number | undefined; currentTime: number; + frame: SpanFrame; onClickCell: (props: {dataIndex: number; rowIndex: number}) => void; - onClickTimestamp: (crumb: NetworkSpan) => void; - onMouseEnter: (span: NetworkSpan) => void; - onMouseLeave: (span: NetworkSpan) => void; + onClickTimestamp: (crumb: SpanFrame) => void; + onMouseEnter: (span: SpanFrame) => void; + onMouseLeave: (span: SpanFrame) => void; rowIndex: number; sortConfig: ReturnType['sortConfig']; - span: NetworkSpan; startTimestampMs: number; style: CSSProperties; }; @@ -43,7 +47,7 @@ const NetworkTableCell = forwardRef( onClickTimestamp, rowIndex, sortConfig, - span, + frame, startTimestampMs, style, }: Props, @@ -55,19 +59,13 @@ const NetworkTableCell = forwardRef( const {getParamValue} = useUrlParams('n_detail_row', ''); const isSelected = getParamValue() === String(dataIndex); - const startMs = span.startTimestamp * 1000; - const endMs = span.endTimestamp * 1000; - const method = span.data.method; - const statusCode = span.data.statusCode; - // `data.responseBodySize` is from SDK version 7.44-7.45 - const size = span.data.size ?? span.data.response?.size ?? span.data.responseBodySize; + const method = getFrameMethod(frame); + const statusCode = getFrameStatus(frame); + const size = getResponseBodySize(frame); - const spanTime = useMemo( - () => relativeTimeInMs(span.startTimestamp * 1000, startTimestampMs), - [span.startTimestamp, startTimestampMs] - ); - const hasOccurred = currentTime >= spanTime; - const isBeforeHover = currentHoverTime === undefined || currentHoverTime >= spanTime; + const hasOccurred = currentTime >= frame.offsetMs; + const isBeforeHover = + currentHoverTime === undefined || currentHoverTime >= frame.offsetMs; const isByTimestamp = sortConfig.by === 'startTimestamp'; const isAsc = isByTimestamp ? sortConfig.asc : undefined; @@ -100,8 +98,8 @@ const NetworkTableCell = forwardRef( isSelected, isStatusError: typeof statusCode === 'number' && statusCode >= 400, onClick: () => onClickCell({dataIndex, rowIndex}), - onMouseEnter: () => onMouseEnter(span), - onMouseLeave: () => onMouseLeave(span), + onMouseEnter: () => onMouseEnter(frame), + onMouseLeave: () => onMouseLeave(frame), ref, style, } as ComponentProps; @@ -120,19 +118,19 @@ const NetworkTableCell = forwardRef( () => ( - {span.description || EMPTY_CELL} + {frame.description || EMPTY_CELL} ), () => ( - - {operationName(span.op)} + + {operationName(frame.op)} ), @@ -145,7 +143,7 @@ const NetworkTableCell = forwardRef( ), () => ( - {`${(endMs - startMs).toFixed(2)}ms`} + {`${(frame.endTimestampMs - frame.timestampMs).toFixed(2)}ms`} ), () => ( @@ -154,10 +152,10 @@ const NetworkTableCell = forwardRef( format="mm:ss.SSS" onClick={(event: MouseEvent) => { event.stopPropagation(); - onClickTimestamp(span); + onClickTimestamp(frame); }} startTimestampMs={startTimestampMs} - timestampMs={startMs} + timestampMs={frame.timestampMs} /> ), diff --git a/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx b/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx index 89a4ea4270db4c..8f81b8d450e3fe 100644 --- a/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx +++ b/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx @@ -3,8 +3,8 @@ import type {Location} from 'history'; import {reactHooks} from 'sentry-test/reactTestingLibrary'; +import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; import {useLocation} from 'sentry/utils/useLocation'; -import type {NetworkSpan} from 'sentry/views/replays/types'; import useNetworkFilters, {FilterFields, NetworkSelectOption} from './useNetworkFilters'; @@ -16,118 +16,91 @@ const mockBrowserHistoryPush = browserHistory.push as jest.MockedFunction< typeof browserHistory.push >; -const SPAN_0_NAVIGATE = { - id: '0', - timestamp: 1663131080555.4, - op: 'navigation.navigate', - description: 'http://localhost:3000/', - startTimestamp: 1663131080.5554, - endTimestamp: 1663131080.6947, - data: { - size: 1334, - }, -}; - -const SPAN_1_LINK = { - id: '1', - timestamp: 1663131080576.7, - op: 'resource.link', - description: 'http://localhost:3000/static/css/main.1856e8e3.chunk.css', - startTimestamp: 1663131080.5767, - endTimestamp: 1663131080.5951, - data: { - size: 300, - }, -}; - -const SPAN_2_SCRIPT = { - id: '2', - timestamp: 1663131080577.0998, - op: 'resource.script', - description: 'http://localhost:3000/static/js/2.3b866bed.chunk.js', - startTimestamp: 1663131080.5770998, - endTimestamp: 1663131080.5979, - data: { - size: 300, - }, -}; - -const SPAN_3_FETCH = { - id: '3', - timestamp: 1663131080641, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon', - startTimestamp: 1663131080.641, - endTimestamp: 1663131080.65, - data: { - method: 'GET', - statusCode: 200, - }, -}; - -const SPAN_4_IMG = { - id: '4', - timestamp: 1663131080642.2, - op: 'resource.img', - description: 'http://localhost:3000/static/media/logo.ddd5084d.png', - startTimestamp: 1663131080.6422, - endTimestamp: 1663131080.6441, - data: { - size: 300, - }, -}; - -const SPAN_5_CSS = { - id: '5', - timestamp: 1663131080644.7997, - op: 'resource.css', - description: - 'http://localhost:3000/static/media/glyphicons-halflings-regular.448c34a5.woff2', - startTimestamp: 1663131080.6447997, - endTimestamp: 1663131080.6548998, - data: { - size: 300, - }, -}; - -const SPAN_6_PUSH = { - id: '6', - timestamp: 1663131082346, - op: 'navigation.push', - description: '/mypokemon', - startTimestamp: 1663131082.346, - endTimestamp: 1663131082.346, - data: {}, -}; - -const SPAN_7_FETCH_GET = { - id: '7', - timestamp: 1663131092471, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon/pikachu', - startTimestamp: 1663131092.471, - endTimestamp: 1663131092.48, - data: { - method: 'GET', - statusCode: 200, - }, -}; - -const SPAN_8_FETCH_POST = { - id: '8', - timestamp: 1663131120198, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon/mewtu', - startTimestamp: 1663131120.198, - endTimestamp: 1663131122.693, - data: { - method: 'POST', - statusCode: 404, - }, -}; +const [ + SPAN_0_NAVIGATE, + SPAN_1_LINK, + SPAN_2_SCRIPT, + SPAN_3_FETCH, + SPAN_4_IMG, + SPAN_5_CSS, + SPAN_6_PUSH, + SPAN_7_FETCH_GET, + SPAN_8_FETCH_POST, +] = hydrateSpans(TestStubs.ReplayRecord(), [ + TestStubs.Replay.NavigationFrame({ + op: 'navigation.navigate', + description: 'http://localhost:3000/', + startTimestamp: new Date(1663131080.5554), + endTimestamp: new Date(1663131080.6947), + data: { + size: 1334, + }, + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.link', + description: 'http://localhost:3000/static/css/main.1856e8e3.chunk.css', + startTimestamp: new Date(1663131080.5767), + endTimestamp: new Date(1663131080.5951), + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.script', + description: 'http://localhost:3000/static/js/2.3b866bed.chunk.js', + startTimestamp: new Date(1663131080.5770998), + endTimestamp: new Date(1663131080.5979), + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon', + startTimestamp: new Date(1663131080.641), + endTimestamp: new Date(1663131080.65), + data: { + method: 'GET', + statusCode: 200, + }, + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.img', + description: 'http://localhost:3000/static/media/logo.ddd5084d.png', + startTimestamp: new Date(1663131080.6422), + endTimestamp: new Date(1663131080.6441), + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.css', + description: + 'http://localhost:3000/static/media/glyphicons-halflings-regular.448c34a5.woff2', + startTimestamp: new Date(1663131080.6447997), + endTimestamp: new Date(1663131080.6548998), + }), + TestStubs.Replay.NavigationPushFrame({ + op: 'navigation.push', + description: '/mypokemon', + startTimestamp: new Date(1663131082.346), + endTimestamp: new Date(1663131082.346), + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon/pikachu', + startTimestamp: new Date(1663131092.471), + endTimestamp: new Date(1663131092.48), + data: { + method: 'GET', + statusCode: 200, + }, + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon/mewtu', + startTimestamp: new Date(1663131120.198), + endTimestamp: new Date(1663131122.693), + data: { + method: 'POST', + statusCode: 404, + }, + }), +]); describe('useNetworkFilters', () => { - const networkSpans: NetworkSpan[] = [ + const networkFrames = [ SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT, @@ -171,7 +144,7 @@ describe('useNetworkFilters', () => { } as Location); const {result, rerender} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); result.current.setFilters([TYPE_OPTION]); @@ -249,7 +222,7 @@ describe('useNetworkFilters', () => { } as Location); const {result, rerender} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); result.current.setFilters([TYPE_OPTION]); @@ -294,7 +267,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(9); }); @@ -308,7 +281,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toStrictEqual([SPAN_8_FETCH_POST]); }); @@ -322,7 +295,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(8); }); @@ -336,7 +309,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(2); }); @@ -350,7 +323,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(3); }); @@ -364,7 +337,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(1); }); @@ -380,7 +353,7 @@ describe('useNetworkFilters', () => { } as Location); const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.items).toHaveLength(1); }); @@ -388,10 +361,10 @@ describe('useNetworkFilters', () => { describe('getMethodTypes', () => { it('should default to having GET in the list of method types', () => { - const networkSpans = []; + const networkFrames = []; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getMethodTypes()).toStrictEqual([ @@ -400,10 +373,10 @@ describe('getMethodTypes', () => { }); it('should return a sorted list of method types', () => { - const networkSpans = [SPAN_8_FETCH_POST, SPAN_7_FETCH_GET]; + const networkFrames = [SPAN_8_FETCH_POST, SPAN_7_FETCH_GET]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getMethodTypes()).toStrictEqual([ @@ -413,10 +386,10 @@ describe('getMethodTypes', () => { }); it('should deduplicate BreadcrumbType', () => { - const networkSpans = [SPAN_2_SCRIPT, SPAN_3_FETCH, SPAN_7_FETCH_GET]; + const networkFrames = [SPAN_2_SCRIPT, SPAN_3_FETCH, SPAN_7_FETCH_GET]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getMethodTypes()).toStrictEqual([ @@ -427,10 +400,10 @@ describe('getMethodTypes', () => { describe('getResourceTypes', () => { it('should default to having fetch in the list of span types', () => { - const networkSpans = []; + const networkFrames = []; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getResourceTypes()).toStrictEqual([ @@ -439,10 +412,10 @@ describe('getResourceTypes', () => { }); it('should return a sorted list of BreadcrumbType', () => { - const networkSpans = [SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT]; + const networkFrames = [SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getResourceTypes()).toStrictEqual([ @@ -454,7 +427,7 @@ describe('getResourceTypes', () => { }); it('should deduplicate BreadcrumbType', () => { - const networkSpans = [ + const networkFrames = [ SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT, @@ -463,7 +436,7 @@ describe('getResourceTypes', () => { ]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getResourceTypes()).toStrictEqual([ @@ -477,10 +450,15 @@ describe('getResourceTypes', () => { describe('getStatusTypes', () => { it('should return a sorted list of BreadcrumbType', () => { - const networkSpans = [SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT, SPAN_8_FETCH_POST]; + const networkFrames = [ + SPAN_0_NAVIGATE, + SPAN_1_LINK, + SPAN_2_SCRIPT, + SPAN_8_FETCH_POST, + ]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getStatusTypes()).toStrictEqual([ @@ -495,7 +473,7 @@ describe('getStatusTypes', () => { }); it('should deduplicate BreadcrumbType', () => { - const networkSpans = [ + const networkFrames = [ SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT, @@ -505,7 +483,7 @@ describe('getStatusTypes', () => { ]; const {result} = reactHooks.renderHook(useNetworkFilters, { - initialProps: {networkSpans}, + initialProps: {networkFrames}, }); expect(result.current.getStatusTypes()).toStrictEqual([ diff --git a/static/app/views/replays/detail/network/useNetworkFilters.tsx b/static/app/views/replays/detail/network/useNetworkFilters.tsx index fae57edc6decd6..c67d2a0b58a8e9 100644 --- a/static/app/views/replays/detail/network/useNetworkFilters.tsx +++ b/static/app/views/replays/detail/network/useNetworkFilters.tsx @@ -3,8 +3,9 @@ import {useCallback, useMemo} from 'react'; import type {SelectOption} from 'sentry/components/compactSelect'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import useFiltersInLocationQuery from 'sentry/utils/replays/hooks/useFiltersInLocationQuery'; +import {getFrameMethod, getFrameStatus} from 'sentry/utils/replays/resourceFrame'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import {filterItems, operationName} from 'sentry/views/replays/detail/utils'; -import type {NetworkSpan} from 'sentry/views/replays/types'; export interface NetworkSelectOption extends SelectOption { qs: 'f_n_method' | 'f_n_status' | 'f_n_type'; @@ -25,7 +26,7 @@ export type FilterFields = { }; type Options = { - networkSpans: NetworkSpan[]; + networkFrames: SpanFrame[]; }; const UNKNOWN_STATUS = 'unknown'; @@ -34,7 +35,7 @@ type Return = { getMethodTypes: () => NetworkSelectOption[]; getResourceTypes: () => NetworkSelectOption[]; getStatusTypes: () => NetworkSelectOption[]; - items: NetworkSpan[]; + items: SpanFrame[]; searchTerm: string; selectValue: string[]; setFilters: (val: NetworkSelectOption[]) => void; @@ -42,21 +43,21 @@ type Return = { }; const FILTERS = { - method: (item: NetworkSpan, method: string[]) => - method.length === 0 || method.includes(item.data.method || 'GET'), - status: (item: NetworkSpan, status: string[]) => + method: (item: SpanFrame, method: string[]) => + method.length === 0 || method.includes(String(getFrameMethod(item))), + status: (item: SpanFrame, status: string[]) => status.length === 0 || - status.includes(String(item.data.statusCode)) || - (status.includes(UNKNOWN_STATUS) && item.data.statusCode === undefined), + status.includes(String(getFrameStatus(item))) || + (status.includes(UNKNOWN_STATUS) && getFrameStatus(item) === undefined), - type: (item: NetworkSpan, types: string[]) => + type: (item: SpanFrame, types: string[]) => types.length === 0 || types.includes(item.op), - searchTerm: (item: NetworkSpan, searchTerm: string) => + searchTerm: (item: SpanFrame, searchTerm: string) => JSON.stringify(item.description).toLowerCase().includes(searchTerm), }; -function useNetworkFilters({networkSpans}: Options): Return { +function useNetworkFilters({networkFrames}: Options): Return { const {setFilter, query} = useFiltersInLocationQuery(); const method = decodeList(query.f_n_method); @@ -81,23 +82,16 @@ function useNetworkFilters({networkSpans}: Options): Return { const items = useMemo( () => filterItems({ - items: networkSpans, + items: networkFrames, filterFns: FILTERS, filterVals: {method, status, type, searchTerm}, }), - [networkSpans, method, status, type, searchTerm] + [networkFrames, method, status, type, searchTerm] ); const getMethodTypes = useCallback( () => - Array.from( - new Set( - networkSpans - .map(networkSpan => networkSpan.data.method) - .concat('GET') - .concat(method) - ) - ) + Array.from(new Set(networkFrames.map(getFrameMethod).concat('GET').concat(method))) .filter(Boolean) .sort() .map( @@ -107,12 +101,12 @@ function useNetworkFilters({networkSpans}: Options): Return { qs: 'f_n_method', }) ), - [networkSpans, method] + [networkFrames, method] ); const getResourceTypes = useCallback( () => - Array.from(new Set(networkSpans.map(networkSpan => networkSpan.op).concat(type))) + Array.from(new Set(networkFrames.map(frame => frame.op).concat(type))) .sort((a, b) => (operationName(a) < operationName(b) ? -1 : 1)) .map( (value): NetworkSelectOption => ({ @@ -121,15 +115,15 @@ function useNetworkFilters({networkSpans}: Options): Return { qs: 'f_n_type', }) ), - [networkSpans, type] + [networkFrames, type] ); const getStatusTypes = useCallback( () => Array.from( new Set( - networkSpans - .map(networkSpan => networkSpan.data.statusCode ?? UNKNOWN_STATUS) + networkFrames + .map(frame => String(getFrameStatus(frame) ?? UNKNOWN_STATUS)) .concat(status) .map(String) ) @@ -142,7 +136,7 @@ function useNetworkFilters({networkSpans}: Options): Return { qs: 'f_n_status', }) ), - [networkSpans, status] + [networkFrames, status] ); const setSearchTerm = useCallback( diff --git a/static/app/views/replays/detail/network/useSortNetwork.spec.tsx b/static/app/views/replays/detail/network/useSortNetwork.spec.tsx index 3396eaa382fe4c..6de5cc34723096 100644 --- a/static/app/views/replays/detail/network/useSortNetwork.spec.tsx +++ b/static/app/views/replays/detail/network/useSortNetwork.spec.tsx @@ -2,7 +2,7 @@ import {act} from 'react-test-renderer'; import {reactHooks} from 'sentry-test/reactTestingLibrary'; -import type {NetworkSpan} from 'sentry/views/replays/types'; +import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; import useSortNetwork from './useSortNetwork'; @@ -22,118 +22,91 @@ jest.mock('sentry/utils/useUrlParams', () => { }; }); -const SPAN_0_NAVIGATE = { - id: '0', - timestamp: 1663131080555.4, - op: 'navigation.navigate', - description: 'http://localhost:3000/', - startTimestamp: 1663131080.5554, - endTimestamp: 1663131080.6947, - data: { - size: 1334, - }, -}; - -const SPAN_1_LINK = { - id: '1', - timestamp: 1663131080576.7, - op: 'resource.link', - description: 'http://localhost:3000/static/css/main.1856e8e3.chunk.css', - startTimestamp: 1663131080.5767, - endTimestamp: 1663131080.5951, - data: { - size: 300, - }, -}; - -const SPAN_2_SCRIPT = { - id: '2', - timestamp: 1663131080577.0998, - op: 'resource.script', - description: 'http://localhost:3000/static/js/2.3b866bed.chunk.js', - startTimestamp: 1663131080.5770998, - endTimestamp: 1663131080.5979, - data: { - size: 300, - }, -}; - -const SPAN_3_FETCH = { - id: '3', - timestamp: 1663131080641, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon', - startTimestamp: 1663131080.641, - endTimestamp: 1663131080.65, - data: { - method: 'GET', - statusCode: 200, - }, -}; - -const SPAN_4_IMG = { - id: '4', - timestamp: 1663131080642.2, - op: 'resource.img', - description: 'http://localhost:3000/static/media/logo.ddd5084d.png', - startTimestamp: 1663131080.6422, - endTimestamp: 1663131080.6441, - data: { - size: 300, - }, -}; - -const SPAN_5_CSS = { - id: '5', - timestamp: 1663131080644.7997, - op: 'resource.css', - description: - 'http://localhost:3000/static/media/glyphicons-halflings-regular.448c34a5.woff2', - startTimestamp: 1663131080.6447997, - endTimestamp: 1663131080.6548998, - data: { - size: 300, - }, -}; - -const SPAN_6_PUSH = { - id: '6', - timestamp: 1663131082346, - op: 'navigation.push', - description: '/mypokemon', - startTimestamp: 1663131082.346, - endTimestamp: 1663131082.346, - data: {}, -}; - -const SPAN_7_FETCH_GET = { - id: '7', - timestamp: 1663131092471, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon/pikachu', - startTimestamp: 1663131092.471, - endTimestamp: 1663131092.48, - data: { - method: 'GET', - statusCode: 200, - }, -}; - -const SPAN_8_FETCH_POST = { - id: '8', - timestamp: 1663131120198, - op: 'resource.fetch', - description: 'https://pokeapi.co/api/v2/pokemon/mewtu', - startTimestamp: 1663131120.198, - endTimestamp: 1663131122.693, - data: { - method: 'POST', - statusCode: 404, - }, -}; +const [ + SPAN_0_NAVIGATE, + SPAN_1_LINK, + SPAN_2_SCRIPT, + SPAN_3_FETCH, + SPAN_4_IMG, + SPAN_5_CSS, + SPAN_6_PUSH, + SPAN_7_FETCH_GET, + SPAN_8_FETCH_POST, +] = hydrateSpans(TestStubs.ReplayRecord(), [ + TestStubs.Replay.NavigationFrame({ + op: 'navigation.navigate', + description: 'http://localhost:3000/', + startTimestamp: new Date(1663131080.5554), + endTimestamp: new Date(1663131080.6947), + data: { + size: 1334, + }, + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.link', + description: 'http://localhost:3000/static/css/main.1856e8e3.chunk.css', + startTimestamp: new Date(1663131080.5767), + endTimestamp: new Date(1663131080.5951), + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.script', + description: 'http://localhost:3000/static/js/2.3b866bed.chunk.js', + startTimestamp: new Date(1663131080.5770998), + endTimestamp: new Date(1663131080.5979), + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon', + startTimestamp: new Date(1663131080.641), + endTimestamp: new Date(1663131080.65), + data: { + method: 'GET', + statusCode: 200, + }, + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.img', + description: 'http://localhost:3000/static/media/logo.ddd5084d.png', + startTimestamp: new Date(1663131080.6422), + endTimestamp: new Date(1663131080.6441), + }), + TestStubs.Replay.ResourceFrame({ + op: 'resource.css', + description: + 'http://localhost:3000/static/media/glyphicons-halflings-regular.448c34a5.woff2', + startTimestamp: new Date(1663131080.6447997), + endTimestamp: new Date(1663131080.6548998), + }), + TestStubs.Replay.NavigationPushFrame({ + op: 'navigation.push', + description: '/mypokemon', + startTimestamp: new Date(1663131082.346), + endTimestamp: new Date(1663131082.346), + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon/pikachu', + startTimestamp: new Date(1663131092.471), + endTimestamp: new Date(1663131092.48), + data: { + method: 'GET', + statusCode: 200, + }, + }), + TestStubs.Replay.RequestFrame({ + op: 'resource.fetch', + description: 'https://pokeapi.co/api/v2/pokemon/mewtu', + startTimestamp: new Date(1663131120.198), + endTimestamp: new Date(1663131122.693), + data: { + method: 'POST', + statusCode: 404, + }, + }), +]); describe('useSortNetwork', () => { - const items: NetworkSpan[] = [ + const items = [ SPAN_0_NAVIGATE, SPAN_1_LINK, SPAN_2_SCRIPT, @@ -156,12 +129,12 @@ describe('useSortNetwork', () => { getValue: expect.any(Function), }); expect(result.current.items).toStrictEqual([ - SPAN_0_NAVIGATE, - SPAN_1_LINK, - SPAN_2_SCRIPT, - SPAN_3_FETCH, - SPAN_4_IMG, SPAN_5_CSS, + SPAN_4_IMG, + SPAN_3_FETCH, + SPAN_2_SCRIPT, + SPAN_1_LINK, + SPAN_0_NAVIGATE, SPAN_6_PUSH, SPAN_7_FETCH_GET, SPAN_8_FETCH_POST, diff --git a/static/app/views/replays/detail/network/useSortNetwork.tsx b/static/app/views/replays/detail/network/useSortNetwork.tsx index 4b7ed29ab58a40..9144305a4509f8 100644 --- a/static/app/views/replays/detail/network/useSortNetwork.tsx +++ b/static/app/views/replays/detail/network/useSortNetwork.tsx @@ -1,12 +1,12 @@ import {useCallback, useMemo} from 'react'; +import type {SpanFrame} from 'sentry/utils/replays/types'; import useUrlParams from 'sentry/utils/useUrlParams'; -import type {NetworkSpan} from 'sentry/views/replays/types'; interface SortConfig { asc: boolean; - by: keyof NetworkSpan | string; - getValue: (row: NetworkSpan) => any; + by: keyof SpanFrame | string; + getValue: (row: SpanFrame) => any; } const SortStrategies: Record any> = { @@ -22,7 +22,7 @@ const SortStrategies: Record any> = { const DEFAULT_ASC = 'true'; const DEFAULT_BY = 'startTimestamp'; -type Opts = {items: NetworkSpan[]}; +type Opts = {items: SpanFrame[]}; function useSortNetwork({items}: Opts) { const {getParamValue: getSortAsc, setParamValue: setSortAsc} = useUrlParams( @@ -70,7 +70,7 @@ function useSortNetwork({items}: Opts) { }; } -function sortNetwork(network: NetworkSpan[], sortConfig: SortConfig): NetworkSpan[] { +function sortNetwork(network: SpanFrame[], sortConfig: SortConfig): SpanFrame[] { return [...network].sort((a, b) => { let valueA = sortConfig.getValue(a); let valueB = sortConfig.getValue(b); diff --git a/static/app/views/replays/types.tsx b/static/app/views/replays/types.tsx index f595096767d0e1..779e121c96328e 100644 --- a/static/app/views/replays/types.tsx +++ b/static/app/views/replays/types.tsx @@ -211,8 +211,6 @@ export type MemorySpan = ReplaySpan<{ }; }>; -export type NetworkSpan = ReplaySpan; - type Overwrite = Pick> & U; export type ReplayCrumb = Overwrite;