createSampleEventButton.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {Component} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. clearIndicators,
  7. } from 'sentry/actionCreators/indicator';
  8. import type {Client} from 'sentry/api';
  9. import type {ButtonProps} from 'sentry/components/button';
  10. import {Button} from 'sentry/components/button';
  11. import {t} from 'sentry/locale';
  12. import type {Organization} from 'sentry/types/organization';
  13. import type {Project} from 'sentry/types/project';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {browserHistory} from 'sentry/utils/browserHistory';
  16. import withApi from 'sentry/utils/withApi';
  17. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  18. import withOrganization from 'sentry/utils/withOrganization';
  19. type CreateSampleEventButtonProps = ButtonProps & {
  20. api: Client;
  21. organization: Organization;
  22. source: string;
  23. onClick?: () => void;
  24. onCreateSampleGroup?: () => void;
  25. project?: Project;
  26. };
  27. type State = {
  28. creating: boolean;
  29. };
  30. const EVENT_POLL_RETRIES = 30;
  31. const EVENT_POLL_INTERVAL = 1000;
  32. async function latestEventAvailable(
  33. api: Client,
  34. groupID: string
  35. ): Promise<{eventCreated: boolean; retries: number}> {
  36. let retries = 0;
  37. // eslint-disable-next-line no-constant-condition
  38. while (true) {
  39. if (retries > EVENT_POLL_RETRIES) {
  40. return {eventCreated: false, retries: retries - 1};
  41. }
  42. await new Promise(resolve => window.setTimeout(resolve, EVENT_POLL_INTERVAL));
  43. try {
  44. await api.requestPromise(`/issues/${groupID}/events/latest/`);
  45. return {eventCreated: true, retries};
  46. } catch {
  47. ++retries;
  48. }
  49. }
  50. }
  51. class CreateSampleEventButton extends Component<CreateSampleEventButtonProps, State> {
  52. state: State = {
  53. creating: false,
  54. };
  55. componentDidMount() {
  56. const {organization, project, source} = this.props;
  57. if (!project) {
  58. return;
  59. }
  60. trackAnalytics('sample_event.button_viewed', {
  61. organization,
  62. project_id: project.id,
  63. source,
  64. });
  65. }
  66. componentWillUnmount() {
  67. this._isMounted = false;
  68. }
  69. private _isMounted = true;
  70. recordAnalytics({eventCreated, retries, duration}) {
  71. const {organization, project, source} = this.props;
  72. if (!project) {
  73. return;
  74. }
  75. const eventKey = `sample_event.${eventCreated ? 'created' : 'failed'}` as const;
  76. trackAnalytics(eventKey, {
  77. organization,
  78. project_id: project.id,
  79. platform: project.platform || '',
  80. interval: EVENT_POLL_INTERVAL,
  81. retries,
  82. duration,
  83. source,
  84. });
  85. }
  86. createSampleGroup = async () => {
  87. // TODO(dena): swap out for action creator
  88. const {api, organization, project, onCreateSampleGroup} = this.props;
  89. let eventData;
  90. if (!project) {
  91. return;
  92. }
  93. if (onCreateSampleGroup) {
  94. onCreateSampleGroup();
  95. } else {
  96. trackAnalytics('growth.onboarding_view_sample_event', {
  97. platform: project.platform,
  98. organization,
  99. });
  100. }
  101. addLoadingMessage(t('Processing sample event...'), {
  102. duration: EVENT_POLL_RETRIES * EVENT_POLL_INTERVAL,
  103. });
  104. this.setState({creating: true});
  105. try {
  106. const url = `/projects/${organization.slug}/${project.slug}/create-sample/`;
  107. eventData = await api.requestPromise(url, {method: 'POST'});
  108. } catch (error) {
  109. Sentry.withScope(scope => {
  110. scope.setExtra('error', error);
  111. Sentry.captureException(new Error('Failed to create sample event'));
  112. });
  113. this.setState({creating: false});
  114. clearIndicators();
  115. addErrorMessage(t('Failed to create a new sample event'));
  116. return;
  117. }
  118. // Wait for the event to be fully processed and available on the group
  119. // before redirecting.
  120. const t0 = performance.now();
  121. const {eventCreated, retries} = await latestEventAvailable(api, eventData.groupID);
  122. // Navigated away before event was created - skip analytics and error messages
  123. // latestEventAvailable will succeed even if the request was cancelled
  124. if (!this._isMounted) {
  125. return;
  126. }
  127. const t1 = performance.now();
  128. clearIndicators();
  129. this.setState({creating: false});
  130. const duration = Math.ceil(t1 - t0);
  131. this.recordAnalytics({eventCreated, retries, duration});
  132. if (!eventCreated) {
  133. addErrorMessage(t('Failed to load sample event'));
  134. Sentry.withScope(scope => {
  135. scope.setTag('groupID', eventData.groupID);
  136. scope.setTag('platform', project.platform || '');
  137. scope.setTag('interval', EVENT_POLL_INTERVAL.toString());
  138. scope.setTag('retries', retries.toString());
  139. scope.setTag('duration', duration.toString());
  140. scope.setLevel('warning');
  141. Sentry.captureMessage('Failed to load sample event');
  142. });
  143. return;
  144. }
  145. this.props.onClick?.();
  146. browserHistory.push(
  147. normalizeUrl(
  148. `/organizations/${organization.slug}/issues/${eventData.groupID}/?project=${project.id}&referrer=sample-error`
  149. )
  150. );
  151. };
  152. render() {
  153. const {
  154. api: _api,
  155. organization: _organization,
  156. project: _project,
  157. source: _source,
  158. ...props
  159. } = this.props;
  160. const {creating} = this.state;
  161. return (
  162. <Button
  163. {...props}
  164. disabled={props.disabled || creating}
  165. onClick={this.createSampleGroup}
  166. />
  167. );
  168. }
  169. }
  170. export default withApi(withOrganization(CreateSampleEventButton));