Files
2024-02-15 17:01:50 +00:00

226 lines
7.2 KiB
TypeScript

import {useCallback, useMemo} from 'react';
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
import useFetchParallelPages from 'sentry/utils/api/useFetchParallelPages';
import useFetchSequentialPages from 'sentry/utils/api/useFetchSequentialPages';
import parseLinkHeader from 'sentry/utils/parseLinkHeader';
import type {ApiQueryKey} from 'sentry/utils/queryClient';
import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils';
import type RequestError from 'sentry/utils/requestError/requestError';
import useProjects from 'sentry/utils/useProjects';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
type Options = {
/**
* The organization slug
*/
orgSlug: string;
/**
* The replayId
*/
replayId: string;
/**
* Default: 50
* You can override this for testing
*/
errorsPerPage?: number;
/**
* Default: 100
* You can override this for testing
*/
segmentsPerPage?: number;
};
interface Result {
attachments: unknown[];
errors: ReplayError[];
fetchError: undefined | RequestError;
fetching: boolean;
onRetry: () => void;
projectSlug: string | null;
replayRecord: ReplayRecord | undefined;
}
/**
* A react hook to load core replay data over the network.
*
* Core replay data includes:
* 1. The root replay EventTransaction object
* - This includes `startTimestamp`, and `tags`
* 2. RRWeb, Breadcrumb, and Span attachment data
* - We make an API call to get a list of segments, each segment contains a
* list of attachments
* - There may be a few large segments, or many small segments. It depends!
* ie: If the replay has many events/errors then there will be many small segments,
* or if the page changes rapidly across each pageload, then there will be
* larger segments, but potentially fewer of them.
* 3. Related Event data
* - Event details are not part of the attachments payload, so we have to
* request them separately
*
* This function should stay focused on loading data over the network.
* Front-end processing, filtering and re-mixing of the different data streams
* must be delegated to the `ReplayReader` class.
*
* @param {orgSlug, replayId} Where to find the root replay event
* @returns An object representing a unified result of the network requests. Either a single `ReplayReader` data object or fetch errors.
*/
function useReplayData({
replayId,
orgSlug,
errorsPerPage = 50,
segmentsPerPage = 100,
}: Options): Result {
const projects = useProjects();
const queryClient = useQueryClient();
// Fetch every field of the replay. The TS type definition lists every field
// that's available. It's easier to ask for them all and not have to deal with
// partial types or nullable fields.
// We're overfetching for sure.
const {
data: replayData,
isFetching: isFetchingReplay,
error: fetchReplayError,
} = useApiQuery<{data: unknown}>([`/organizations/${orgSlug}/replays/${replayId}/`], {
staleTime: Infinity,
retry: false,
});
const replayRecord = useMemo(
() => (replayData?.data ? mapResponseToReplayRecord(replayData.data) : undefined),
[replayData?.data]
);
const projectSlug = useMemo(() => {
if (!replayRecord) {
return null;
}
return projects.projects.find(p => p.id === replayRecord.project_id)?.slug ?? null;
}, [replayRecord, projects.projects]);
const getAttachmentsQueryKey = useCallback(
({cursor, per_page}): ApiQueryKey => {
return [
`/projects/${orgSlug}/${projectSlug}/replays/${replayId}/recording-segments/`,
{
query: {
download: true,
per_page,
cursor,
},
},
];
},
[orgSlug, projectSlug, replayId]
);
const {pages: attachmentPages, isFetching: isFetchingAttachments} =
useFetchParallelPages({
enabled: !fetchReplayError && Boolean(projectSlug) && Boolean(replayRecord),
hits: replayRecord?.count_segments ?? 0,
getQueryKey: getAttachmentsQueryKey,
perPage: segmentsPerPage,
});
const getErrorsQueryKey = useCallback(
({cursor, per_page}): ApiQueryKey => {
// Clone the `finished_at` time and bump it up one second because finishedAt
// has the `ms` portion truncated, while replays-events-meta operates on
// timestamps with `ms` attached. So finishedAt could be at time `12:00:00.000Z`
// while the event is saved with `12:00:00.450Z`.
const finishedAtClone = new Date(replayRecord?.finished_at ?? '');
finishedAtClone.setSeconds(finishedAtClone.getSeconds() + 1);
return [
`/organizations/${orgSlug}/replays-events-meta/`,
{
query: {
start: replayRecord?.started_at.toISOString(),
end: finishedAtClone.toISOString(),
project: ALL_ACCESS_PROJECTS,
query: `replayId:[${replayRecord?.id}]`,
per_page,
cursor,
},
},
];
},
[orgSlug, replayRecord]
);
const {
pages: errorPages,
isFetching: isFetchingErrors,
getLastResponseHeader: lastErrorsResponseHeader,
} = useFetchParallelPages<{data: ReplayError[]}>({
enabled: !fetchReplayError && Boolean(projectSlug) && Boolean(replayRecord),
hits: replayRecord?.count_errors ?? 0,
getQueryKey: getErrorsQueryKey,
perPage: errorsPerPage,
});
const linkHeader = lastErrorsResponseHeader?.('Link') ?? null;
const links = parseLinkHeader(linkHeader);
const {pages: extraErrorPages, isFetching: isFetchingExtraErrors} =
useFetchSequentialPages<{data: ReplayError[]}>({
enabled: !fetchReplayError && !isFetchingErrors && Boolean(links.next?.results),
initialCursor: links.next?.cursor,
getQueryKey: getErrorsQueryKey,
perPage: errorsPerPage,
});
const clearQueryCache = useCallback(() => {
() => {
queryClient.invalidateQueries({
queryKey: [`/organizations/${orgSlug}/replays/${replayId}/`],
});
queryClient.invalidateQueries({
queryKey: [
`/projects/${orgSlug}/${projectSlug}/replays/${replayId}/recording-segments/`,
],
});
// The next one isn't optimized
// This statement will invalidate the cache of fetched error events for all replayIds
queryClient.invalidateQueries({
queryKey: [`/organizations/${orgSlug}/replays-events-meta/`],
});
};
}, [orgSlug, replayId, projectSlug, queryClient]);
return useMemo(() => {
const fetching =
isFetchingReplay ||
isFetchingAttachments ||
isFetchingErrors ||
isFetchingExtraErrors;
return {
attachments: attachmentPages.flat(2),
errors: errorPages.concat(extraErrorPages).flatMap(page => page.data),
fetchError: fetchReplayError ?? undefined,
fetching,
onRetry: clearQueryCache,
projectSlug,
replayRecord,
};
}, [
attachmentPages,
clearQueryCache,
errorPages,
extraErrorPages,
fetchReplayError,
isFetchingAttachments,
isFetchingErrors,
isFetchingExtraErrors,
isFetchingReplay,
projectSlug,
replayRecord,
]);
}
export default useReplayData;