eventWaiter.tsx 4.2 KB

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