eventWaiter.tsx 5.1 KB

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