eventWaiter.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import {Component} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import {Client} from 'sentry/api';
  4. import {Group, Organization, Project} from 'sentry/types';
  5. import withApi from 'sentry/utils/withApi';
  6. const DEFAULT_POLL_INTERVAL = 5000;
  7. /**
  8. * When no event has been received this will be set to null or false.
  9. * Otherwise it will be the Group of the issue that was received.
  10. * Or in the case of transactions & replay the value will be set to true.
  11. * The `group.id` value is used to generate links directly into the event.
  12. */
  13. type FirstIssue = null | boolean | Group;
  14. export interface EventWaiterProps {
  15. api: Client;
  16. children: (props: {firstIssue: FirstIssue}) => React.ReactNode;
  17. eventType: 'error' | 'transaction' | 'replay' | 'profile';
  18. organization: Organization;
  19. project: Project;
  20. disabled?: boolean;
  21. onIssueReceived?: (props: {firstIssue: FirstIssue}) => void;
  22. onTransactionReceived?: (props: {firstIssue: FirstIssue}) => void;
  23. pollInterval?: number;
  24. }
  25. type EventWaiterState = {
  26. firstIssue: FirstIssue;
  27. };
  28. function getFirstEvent(eventType: EventWaiterProps['eventType'], resp: Project) {
  29. switch (eventType) {
  30. case 'error':
  31. return resp.firstEvent;
  32. case 'transaction':
  33. return resp.firstTransactionEvent;
  34. case 'replay':
  35. return resp.hasReplays;
  36. case 'profile':
  37. return resp.hasProfiles;
  38. default:
  39. return null;
  40. }
  41. }
  42. /**
  43. * This is a render prop component that can be used to wait for the first event
  44. * of a project to be received via polling.
  45. */
  46. class EventWaiter extends Component<EventWaiterProps, EventWaiterState> {
  47. state: EventWaiterState = {
  48. firstIssue: null,
  49. };
  50. componentDidMount() {
  51. this.pollHandler();
  52. this.startPolling();
  53. }
  54. componentDidUpdate() {
  55. this.stopPolling();
  56. this.startPolling();
  57. }
  58. componentWillUnmount() {
  59. this.stopPolling();
  60. }
  61. pollingInterval: number | null = null;
  62. pollHandler = async () => {
  63. const {api, organization, project, eventType, onIssueReceived} = this.props;
  64. let firstEvent: string | boolean | null = null;
  65. let firstIssue: Group | boolean | null = null;
  66. try {
  67. const resp = await api.requestPromise(
  68. `/projects/${organization.slug}/${project.slug}/`
  69. );
  70. firstEvent = getFirstEvent(eventType, resp);
  71. } catch (resp) {
  72. if (!resp) {
  73. return;
  74. }
  75. // This means org or project does not exist, we need to stop polling
  76. // Also stop polling on auth-related errors (403/401)
  77. if ([404, 403, 401, 0].includes(resp.status)) {
  78. // TODO: Add some UX around this... redirect? error message?
  79. this.stopPolling();
  80. return;
  81. }
  82. Sentry.setExtras({
  83. status: resp.status,
  84. detail: resp.responseJSON?.detail,
  85. });
  86. Sentry.captureException(new Error(`Error polling for first ${eventType} event`));
  87. }
  88. if (firstEvent === null || firstEvent === false) {
  89. return;
  90. }
  91. if (eventType === 'error') {
  92. // Locate the projects first issue group. The project.firstEvent field will
  93. // *not* include sample events, while just looking at the issues list will.
  94. // We will wait until the project.firstEvent is set and then locate the
  95. // event given that event datetime
  96. const issues: Group[] = await api.requestPromise(
  97. `/projects/${organization.slug}/${project.slug}/issues/`
  98. );
  99. // The event may have expired, default to true
  100. firstIssue = issues.find((issue: Group) => issue.firstSeen === firstEvent) || true;
  101. } else if (eventType === 'transaction') {
  102. firstIssue = Boolean(firstEvent);
  103. } else if (eventType === 'replay') {
  104. firstIssue = Boolean(firstEvent);
  105. }
  106. if (onIssueReceived) {
  107. onIssueReceived({firstIssue});
  108. }
  109. this.stopPolling();
  110. this.setState({firstIssue});
  111. };
  112. startPolling() {
  113. const {disabled, organization, project} = this.props;
  114. if (disabled || !organization || !project || this.state.firstIssue) {
  115. return;
  116. }
  117. // Proactively clear interval just in case stopPolling was not called
  118. if (this.pollingInterval) {
  119. window.clearInterval(this.pollingInterval);
  120. }
  121. this.pollingInterval = window.setInterval(
  122. this.pollHandler,
  123. this.props.pollInterval || DEFAULT_POLL_INTERVAL
  124. );
  125. }
  126. stopPolling() {
  127. if (this.pollingInterval) {
  128. clearInterval(this.pollingInterval);
  129. }
  130. }
  131. render() {
  132. return this.props.children({firstIssue: this.state.firstIssue});
  133. }
  134. }
  135. export default withApi(EventWaiter);