eventWaiter.tsx 4.6 KB

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