123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- import {useCallback, useEffect, useMemo, useState} from 'react';
- import * as Sentry from '@sentry/react';
- import {inflate} from 'pako';
- import {IssueAttachment} from 'sentry/types';
- import {EventTransaction} from 'sentry/types/event';
- import ReplayReader from 'sentry/utils/replays/replayReader';
- import RequestError from 'sentry/utils/requestError/requestError';
- import useApi from 'sentry/utils/useApi';
- import type {
- RecordingEvent,
- ReplayCrumb,
- ReplayError,
- ReplaySpan,
- } from 'sentry/views/replays/types';
- import flattenListOfObjects from '../flattenListOfObjects';
- import useReplayErrors from './useReplayErrors';
- type State = {
- breadcrumbs: undefined | ReplayCrumb[];
- /**
- * List of errors that occurred during replay
- */
- errors: undefined | ReplayError[];
- /**
- * The root replay event
- */
- event: undefined | EventTransaction;
- /**
- * If any request returned an error then nothing is being returned
- */
- fetchError: undefined | RequestError;
- /**
- * If a fetch is underway for the requested root reply.
- * This includes fetched all the sub-resources like attachments and `sentry-replay-event`
- */
- fetching: boolean;
- /**
- * Are errors currently being fetched
- */
- isErrorsFetching: boolean;
- /**
- * The flattened list of rrweb events. These are stored as multiple attachments on the root replay object: the `event` prop.
- */
- rrwebEvents: undefined | RecordingEvent[];
- spans: undefined | ReplaySpan[];
- };
- type Options = {
- /**
- * The projectSlug and eventId concatenated together
- */
- eventSlug: string;
- /**
- * The organization slug
- */
- orgId: string;
- };
- // Errors if it is an interface
- // See https://github.com/microsoft/TypeScript/issues/15300
- type ReplayAttachment = {
- breadcrumbs: ReplayCrumb[];
- recording: RecordingEvent[];
- replaySpans: ReplaySpan[];
- };
- interface Result extends Pick<State, 'fetchError' | 'fetching'> {
- onRetry: () => void;
- replay: ReplayReader | null;
- }
- const IS_RRWEB_ATTACHMENT_FILENAME = /rrweb-[0-9]{13}.json/;
- function isRRWebEventAttachment(attachment: IssueAttachment) {
- return IS_RRWEB_ATTACHMENT_FILENAME.test(attachment.name);
- }
- export function mapRRWebAttachments(unsortedReplayAttachments): ReplayAttachment {
- const replayAttachments: ReplayAttachment = {
- breadcrumbs: [],
- replaySpans: [],
- recording: [],
- };
- unsortedReplayAttachments.forEach(attachment => {
- if (attachment.data?.tag === 'performanceSpan') {
- replayAttachments.replaySpans.push(attachment.data.payload);
- } else if (attachment?.data?.tag === 'breadcrumb') {
- replayAttachments.breadcrumbs.push(attachment.data.payload);
- } else {
- replayAttachments.recording.push(attachment);
- }
- });
- return replayAttachments;
- }
- const INITIAL_STATE: State = Object.freeze({
- errors: undefined,
- event: undefined,
- fetchError: undefined,
- fetching: true,
- isErrorsFetching: true,
- rrwebEvents: undefined,
- spans: undefined,
- breadcrumbs: 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` data
- * 2. Breadcrumb and Span data from all the related Event objects
- * - Data is merged for consumption
- * 3. RRWeb payloads for the replayer video stream
- * - TODO(replay): incrementally load the stream to speedup pageload
- *
- * 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 {orgId, eventSlug} Where to find the root replay event
- * @returns An object representing a unified result of the network reqeusts. Either a single `ReplayReader` data object or fetch errors.
- */
- function useReplayData({eventSlug, orgId}: Options): Result {
- const [projectId, eventId] = eventSlug.split(':');
- const api = useApi();
- const [state, setState] = useState<State>(INITIAL_STATE);
- const fetchEvent = useCallback(() => {
- return api.requestPromise(
- `/organizations/${orgId}/events/${eventSlug}/`
- ) as Promise<EventTransaction>;
- }, [api, orgId, eventSlug]);
- const fetchRRWebEvents = useCallback(async () => {
- const attachmentIds = (await api.requestPromise(
- `/projects/${orgId}/${projectId}/events/${eventId}/attachments/`
- )) as IssueAttachment[];
- const rrwebAttachmentIds = attachmentIds.filter(isRRWebEventAttachment);
- const attachments = await Promise.all(
- rrwebAttachmentIds.map(async attachment => {
- const response = await api.requestPromise(
- `/api/0/projects/${orgId}/${projectId}/events/${eventId}/attachments/${attachment.id}/?download`,
- {
- includeAllArgs: true,
- }
- );
- // for non-compressed events, parse and return
- try {
- return JSON.parse(response[0]) as ReplayAttachment;
- } catch (error) {
- // swallow exception.. if we can't parse it, it's going to be compressed
- }
- // for non-compressed events, parse and return
- try {
- // for compressed events, inflate the blob and map the events
- const responseBlob = await response[2]?.rawResponse.blob();
- const responseArray = (await responseBlob?.arrayBuffer()) as Uint8Array;
- const parsedPayload = JSON.parse(inflate(responseArray, {to: 'string'}));
- const replayAttachments = mapRRWebAttachments(parsedPayload);
- return replayAttachments;
- } catch (error) {
- return {};
- }
- })
- );
- // ReplayAttachment[] => ReplayAttachment (merge each key of ReplayAttachment)
- return flattenListOfObjects(attachments);
- }, [api, eventId, orgId, projectId]);
- const {isLoading: isErrorsFetching, data: errors} = useReplayErrors({
- replayId: eventId,
- });
- useEffect(() => {
- if (!isErrorsFetching) {
- setState(prevState => ({
- ...prevState,
- fetching: prevState.fetching || isErrorsFetching,
- isErrorsFetching,
- errors,
- }));
- }
- }, [isErrorsFetching, errors]);
- const loadEvents = useCallback(async () => {
- setState(INITIAL_STATE);
- try {
- const [event, attachments] = await Promise.all([fetchEvent(), fetchRRWebEvents()]);
- setState(prev => ({
- ...prev,
- event,
- fetchError: undefined,
- fetching: prev.isErrorsFetching || false,
- rrwebEvents: attachments.recording,
- spans: attachments.replaySpans,
- breadcrumbs: attachments.breadcrumbs,
- }));
- } catch (error) {
- Sentry.captureException(error);
- setState({
- ...INITIAL_STATE,
- fetchError: error,
- fetching: false,
- });
- }
- }, [fetchEvent, fetchRRWebEvents]);
- useEffect(() => {
- loadEvents();
- }, [loadEvents]);
- const replay = useMemo(() => {
- return ReplayReader.factory({
- event: state.event,
- errors: state.errors,
- rrwebEvents: state.rrwebEvents,
- breadcrumbs: state.breadcrumbs,
- spans: state.spans,
- });
- }, [state.event, state.rrwebEvents, state.breadcrumbs, state.spans, state.errors]);
- return {
- fetchError: state.fetchError,
- fetching: state.fetching,
- onRetry: loadEvents,
- replay,
- };
- }
- export default useReplayData;
|