eventWaiter.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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' | 'profile';
  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. case 'profile':
  51. return resp.hasProfiles;
  52. default:
  53. return null;
  54. }
  55. }
  56. /**
  57. * This is a render prop component that can be used to wait for the first event
  58. * of a project to be received via polling.
  59. */
  60. class EventWaiter extends Component<EventWaiterProps, EventWaiterState> {
  61. state: EventWaiterState = {
  62. firstIssue: null,
  63. };
  64. componentDidMount() {
  65. this.pollHandler();
  66. this.startPolling();
  67. }
  68. componentDidUpdate() {
  69. this.stopPolling();
  70. this.startPolling();
  71. }
  72. componentWillUnmount() {
  73. this.stopPolling();
  74. }
  75. pollingInterval: number | null = null;
  76. pollHandler = async () => {
  77. const {api, organization, project, eventType, onIssueReceived} = this.props;
  78. let firstEvent: string | boolean | null = null;
  79. let firstIssue: Group | boolean | null = null;
  80. try {
  81. const resp = await api.requestPromise(
  82. `/projects/${organization.slug}/${project.slug}/`
  83. );
  84. firstEvent = getFirstEvent(eventType, resp);
  85. } catch (resp) {
  86. if (!resp) {
  87. return;
  88. }
  89. // This means org or project does not exist, we need to stop polling
  90. // Also stop polling on auth-related errors (403/401)
  91. if ([404, 403, 401, 0].includes(resp.status)) {
  92. // TODO: Add some UX around this... redirect? error message?
  93. this.stopPolling();
  94. return;
  95. }
  96. Sentry.setExtras({
  97. status: resp.status,
  98. detail: resp.responseJSON?.detail,
  99. });
  100. Sentry.captureException(new Error(`Error polling for first ${eventType} event`));
  101. }
  102. if (firstEvent === null || firstEvent === false) {
  103. return;
  104. }
  105. if (eventType === 'error') {
  106. // Locate the projects first issue group. The project.firstEvent field will
  107. // *not* include sample events, while just looking at the issues list will.
  108. // We will wait until the project.firstEvent is set and then locate the
  109. // event given that event datetime
  110. const issues: Group[] = await api.requestPromise(
  111. `/projects/${organization.slug}/${project.slug}/issues/`
  112. );
  113. // The event may have expired, default to true
  114. firstIssue = issues.find((issue: Group) => issue.firstSeen === firstEvent) || true;
  115. recordAnalyticsFirstEvent({
  116. key: 'first_event_recieved',
  117. organization,
  118. project,
  119. });
  120. } else if (eventType === 'transaction') {
  121. firstIssue = Boolean(firstEvent);
  122. recordAnalyticsFirstEvent({
  123. key: 'first_transaction_recieved',
  124. organization,
  125. project,
  126. });
  127. } else if (eventType === 'replay') {
  128. firstIssue = Boolean(firstEvent);
  129. recordAnalyticsFirstEvent({
  130. key: 'first_replay_recieved',
  131. organization,
  132. project,
  133. });
  134. }
  135. if (onIssueReceived) {
  136. onIssueReceived({firstIssue});
  137. }
  138. this.stopPolling();
  139. this.setState({firstIssue});
  140. };
  141. startPolling() {
  142. const {disabled, organization, project} = this.props;
  143. if (disabled || !organization || !project || this.state.firstIssue) {
  144. return;
  145. }
  146. // Proactively clear interval just in case stopPolling was not called
  147. if (this.pollingInterval) {
  148. window.clearInterval(this.pollingInterval);
  149. }
  150. this.pollingInterval = window.setInterval(
  151. this.pollHandler,
  152. this.props.pollInterval || DEFAULT_POLL_INTERVAL
  153. );
  154. }
  155. stopPolling() {
  156. if (this.pollingInterval) {
  157. clearInterval(this.pollingInterval);
  158. }
  159. }
  160. render() {
  161. return this.props.children({firstIssue: this.state.firstIssue});
  162. }
  163. }
  164. export default withApi(EventWaiter);