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