123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import {useCallback, useEffect, useState} from 'react';
- import * as Sentry from '@sentry/react';
- import type {eventWithTime} from 'rrweb/typings/types';
- import {MemorySpanType} from 'sentry/components/events/interfaces/spans/types';
- import {IssueAttachment} from 'sentry/types';
- import {Entry, Event, EventTransaction} from 'sentry/types/event';
- import EventView from 'sentry/utils/discover/eventView';
- import {generateEventSlug} from 'sentry/utils/discover/urls';
- import ReplayReader from 'sentry/utils/replays/replayReader';
- import RequestError from 'sentry/utils/requestError/requestError';
- import useApi from 'sentry/utils/useApi';
- import createHighlightEvents from './createHighlightEvents';
- import mergeAndSortEvents from './mergeAndSortEvents';
- import mergeBreadcrumbsEntries from './mergeBreadcrumbsEntries';
- import mergeEventsWithSpans from './mergeEventsWithSpans';
- type State = {
- /**
- * List of breadcrumbs
- */
- breadcrumbEntry: undefined | Entry;
- /**
- * 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;
- memorySpans: undefined | MemorySpanType[];
- mergedReplayEvent: undefined | Event;
- /**
- * The list of related `sentry-replay-event` objects that were captured during this `sentry-replay`
- */
- replayEvents: undefined | Event[];
- /**
- * The flattened list of rrweb events. These are stored as multiple attachments on the root replay object: the `event` prop.
- */
- rrwebEvents: undefined | eventWithTime[];
- };
- type Options = {
- /**
- * When provided, fetches specified replay event by slug
- */
- eventSlug: string;
- /**
- *
- */
- location: any;
- /**
- *
- */
- orgId: string;
- };
- interface Result extends State {
- 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);
- }
- const INITIAL_STATE: State = Object.freeze({
- fetchError: undefined,
- fetching: true,
- breadcrumbEntry: undefined,
- event: undefined,
- replayEvents: undefined,
- rrwebEvents: undefined,
- mergedReplayEvent: undefined,
- memorySpans: undefined,
- });
- function useReplayEvent({eventSlug, location, orgId}: Options): Result {
- const [projectId, eventId] = eventSlug.split(':');
- const api = useApi();
- const [retry, setRetry] = useState(false);
- const [state, setState] = useState<State>(INITIAL_STATE);
- function fetchEvent() {
- return api.requestPromise(
- `/organizations/${orgId}/events/${eventSlug}/`
- ) as Promise<EventTransaction>;
- }
- async function fetchRRWebEvents() {
- 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`
- );
- return JSON.parse(response).events as eventWithTime;
- })
- );
- return attachments.flat();
- }
- async function fetchReplayEvents() {
- const replayEventsView = EventView.fromSavedQuery({
- id: '',
- name: '',
- version: 2,
- fields: ['timestamp', 'replayId'],
- orderby: 'timestamp',
- projects: [],
- range: '14d',
- query: `transaction:sentry-replay-event`,
- });
- replayEventsView.additionalConditions.addFilterValues('replayId', [eventId]);
- const replayEventsQuery = replayEventsView.getEventsAPIPayload(location);
- const replayEventList = await api.requestPromise(
- `/organizations/${orgId}/eventsv2/`,
- {
- query: replayEventsQuery,
- }
- );
- return Promise.all(
- replayEventList.data.map(
- event =>
- api.requestPromise(
- `/organizations/${orgId}/events/${generateEventSlug(event)}/`
- ) as Promise<Event>
- )
- );
- }
- async function loadEvents() {
- setRetry(false);
- setState({
- ...INITIAL_STATE,
- });
- try {
- const [event, rrwebEvents, replayEvents] = await Promise.all([
- fetchEvent(),
- fetchRRWebEvents(),
- fetchReplayEvents(),
- ]);
- const breadcrumbEntry = mergeBreadcrumbsEntries(replayEvents || [], event);
- const mergedReplayEvent = mergeEventsWithSpans(replayEvents || []);
- const memorySpans =
- mergedReplayEvent?.entries[0]?.data?.filter(datum => datum?.data?.memory) || [];
- if (mergedReplayEvent.entries[0]) {
- mergedReplayEvent.entries[0].data = mergedReplayEvent?.entries[0]?.data?.filter(
- datum => !datum?.data?.memory
- );
- }
- // Find LCP spans that have a valid replay node id, this will be used to
- const highlights = createHighlightEvents(mergedReplayEvent?.entries[0].data);
- // TODO(replays): ideally this would happen on SDK, but due
- // to how plugins work, we are unable to specify a timestamp for an event
- // (rrweb applies it), so it's possible actual LCP timestamp does not
- // match when the observer happens and we emit an rrweb event (will
- // look into this)
- const rrwebEventsWithHighlights = mergeAndSortEvents(rrwebEvents, highlights);
- setState({
- ...state,
- fetchError: undefined,
- fetching: false,
- event,
- mergedReplayEvent,
- replayEvents,
- rrwebEvents: rrwebEventsWithHighlights,
- breadcrumbEntry,
- memorySpans,
- });
- } catch (error) {
- Sentry.captureException(error);
- setState({
- ...INITIAL_STATE,
- fetchError: error,
- fetching: false,
- });
- }
- }
- useEffect(() => void loadEvents(), [orgId, eventSlug, retry]); // eslint-disable-line react-hooks/exhaustive-deps
- const onRetry = useCallback(() => {
- setRetry(true);
- }, []);
- return {
- fetchError: state.fetchError,
- fetching: state.fetching,
- onRetry,
- replay: ReplayReader.factory(state.event, state.rrwebEvents, state.replayEvents),
- breadcrumbEntry: state.breadcrumbEntry,
- event: state.event,
- replayEvents: state.replayEvents,
- rrwebEvents: state.rrwebEvents,
- mergedReplayEvent: state.mergedReplayEvent,
- memorySpans: state.memorySpans,
- };
- }
- export default useReplayEvent;
|