rrwebIntegration.tsx 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import styled from '@emotion/styled';
  2. import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
  3. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  4. import LazyLoad from 'sentry/components/lazyLoad';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {IssueAttachment, Organization, Project} from 'sentry/types';
  8. import {Event} from 'sentry/types/event';
  9. import useOrganization from 'sentry/utils/useOrganization';
  10. type Props = {
  11. event: Event;
  12. orgId: Organization['id'];
  13. projectSlug: Project['slug'];
  14. } & DeprecatedAsyncComponent['props'];
  15. type State = {
  16. attachmentList: Array<IssueAttachment> | null;
  17. } & DeprecatedAsyncComponent['state'];
  18. class EventRRWebIntegrationContent extends DeprecatedAsyncComponent<Props, State> {
  19. getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
  20. const {orgId, projectSlug, event} = this.props;
  21. return [
  22. [
  23. 'attachmentList',
  24. `/projects/${orgId}/${projectSlug}/events/${event.id}/attachments/`,
  25. // This was changed from `rrweb.json`, so that we can instead
  26. // support incremental rrweb events as attachments. This is to avoid
  27. // having clients uploading a single, large sized replay.
  28. //
  29. // Note: This will include all attachments that contain `rrweb`
  30. // anywhere its name. We need to maintain compatibility with existing
  31. // rrweb plugin users (single replay), but also support incremental
  32. // replays as well. I can't think of a reason why someone would have
  33. // a non-rrweb replay containing the string `rrweb`, but people have
  34. // surprised me before.
  35. {query: {query: 'rrweb'}},
  36. ],
  37. ];
  38. }
  39. renderLoading() {
  40. // hide loading indicator
  41. return null;
  42. }
  43. renderBody() {
  44. const {attachmentList} = this.state;
  45. if (!attachmentList?.length) {
  46. return null;
  47. }
  48. const {orgId, projectSlug, event} = this.props;
  49. function createAttachmentUrl(attachment: IssueAttachment) {
  50. return `/api/0/projects/${orgId}/${projectSlug}/events/${event.id}/attachments/${attachment.id}/?download`;
  51. }
  52. return (
  53. <StyledReplayEventDataSection type="context-replay" title={t('Replay')}>
  54. <LazyLoad
  55. component={() => import('./rrwebReplayer')}
  56. urls={attachmentList.map(createAttachmentUrl)}
  57. />
  58. </StyledReplayEventDataSection>
  59. );
  60. }
  61. }
  62. export function EventRRWebIntegration(props: Props) {
  63. const organization = useOrganization();
  64. const hasReplay = Boolean(
  65. props.event?.tags?.find(({key}) => key === 'replayId')?.value
  66. );
  67. const hasEventAttachmentsFeature = organization.features.includes('event-attachments');
  68. if (hasReplay || !hasEventAttachmentsFeature) {
  69. return null;
  70. }
  71. return <EventRRWebIntegrationContent {...props} />;
  72. }
  73. const StyledReplayEventDataSection = styled(EventDataSection)`
  74. overflow: hidden;
  75. margin-bottom: ${space(3)};
  76. `;