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;