events.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import {LocationDescriptor} from 'history';
  2. import pick from 'lodash/pick';
  3. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  4. import {Client, ResponseMeta} from 'sentry/api';
  5. import {canIncludePreviousPeriod} from 'sentry/components/charts/utils';
  6. import {t} from 'sentry/locale';
  7. import {
  8. DateString,
  9. EventsStats,
  10. IssueAttachment,
  11. MultiSeriesEventsStats,
  12. OrganizationSummary,
  13. } from 'sentry/types';
  14. import {LocationQuery} from 'sentry/utils/discover/eventView';
  15. import {getPeriod} from 'sentry/utils/getPeriod';
  16. import {PERFORMANCE_URL_PARAM} from 'sentry/utils/performance/constants';
  17. import {QueryBatching} from 'sentry/utils/performance/contexts/genericQueryBatcher';
  18. import {
  19. ApiQueryKey,
  20. useApiQuery,
  21. UseApiQueryOptions,
  22. useMutation,
  23. UseMutationOptions,
  24. useQueryClient,
  25. } from 'sentry/utils/queryClient';
  26. import RequestError from 'sentry/utils/requestError/requestError';
  27. import useApi from 'sentry/utils/useApi';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. type Options = {
  30. organization: OrganizationSummary;
  31. partial: boolean;
  32. comparisonDelta?: number;
  33. end?: DateString;
  34. environment?: Readonly<string[]>;
  35. excludeOther?: boolean;
  36. field?: string[];
  37. generatePathname?: (org: OrganizationSummary) => string;
  38. includePrevious?: boolean;
  39. interval?: string;
  40. limit?: number;
  41. orderby?: string;
  42. period?: string | null;
  43. project?: Readonly<number[]>;
  44. query?: string;
  45. queryBatching?: QueryBatching;
  46. queryExtras?: Record<string, string>;
  47. referrer?: string;
  48. start?: DateString;
  49. team?: Readonly<string | string[]>;
  50. topEvents?: number;
  51. withoutZerofill?: boolean;
  52. yAxis?: string | string[];
  53. };
  54. /**
  55. * Make requests to `events-stats` endpoint
  56. *
  57. * @param {Object} api API client instance
  58. * @param {Object} options Request parameters
  59. * @param {Object} options.organization Organization object
  60. * @param {Number[]} options.project List of project ids
  61. * @param {String[]} options.environment List of environments to query for
  62. * @param {Boolean} options.excludeOther Exclude the "Other" series when making a topEvents query
  63. * @param {String[]} options.team List of teams to query for
  64. * @param {String} options.period Time period to query for, in the format: <integer><units> where units are "d" or "h"
  65. * @param {String} options.interval Time interval to group results in, in the format: <integer><units> where units are "d", "h", "m", "s"
  66. * @param {Number} options.comparisonDelta Comparison delta for change alert event stats to include comparison stats
  67. * @param {Boolean} options.includePrevious Should request also return reqsults for previous period?
  68. * @param {Number} options.limit The number of rows to return
  69. * @param {String} options.query Search query
  70. * @param {QueryBatching} options.queryBatching A container for batching functions from a provider
  71. * @param {Record<string, string>} options.queryExtras A list of extra query parameters
  72. * @param {(org: OrganizationSummary) => string} options.generatePathname A function that returns an override for the pathname
  73. */
  74. export const doEventsRequest = <IncludeAllArgsType extends boolean = false>(
  75. api: Client,
  76. {
  77. organization,
  78. project,
  79. environment,
  80. team,
  81. period,
  82. start,
  83. end,
  84. interval,
  85. comparisonDelta,
  86. includePrevious,
  87. query,
  88. yAxis,
  89. field,
  90. topEvents,
  91. orderby,
  92. partial,
  93. withoutZerofill,
  94. referrer,
  95. queryBatching,
  96. generatePathname,
  97. queryExtras,
  98. excludeOther,
  99. includeAllArgs,
  100. }: {includeAllArgs?: IncludeAllArgsType} & Options
  101. ): IncludeAllArgsType extends true
  102. ? Promise<
  103. [EventsStats | MultiSeriesEventsStats, string | undefined, ResponseMeta | undefined]
  104. >
  105. : Promise<EventsStats | MultiSeriesEventsStats> => {
  106. const pathname =
  107. generatePathname?.(organization) ??
  108. `/organizations/${organization.slug}/events-stats/`;
  109. const shouldDoublePeriod = canIncludePreviousPeriod(includePrevious, period);
  110. const urlQuery = Object.fromEntries(
  111. Object.entries({
  112. interval,
  113. comparisonDelta,
  114. project,
  115. environment,
  116. team,
  117. query,
  118. yAxis,
  119. field,
  120. topEvents,
  121. orderby,
  122. partial: partial ? '1' : undefined,
  123. withoutZerofill: withoutZerofill ? '1' : undefined,
  124. referrer: referrer ? referrer : 'api.organization-event-stats',
  125. excludeOther: excludeOther ? '1' : undefined,
  126. }).filter(([, value]) => typeof value !== 'undefined')
  127. );
  128. // Doubling period for absolute dates is not accurate unless starting and
  129. // ending times are the same (at least for daily intervals). This is
  130. // the tradeoff for now.
  131. const periodObj = getPeriod({period, start, end}, {shouldDoublePeriod});
  132. const queryObject = {
  133. includeAllArgs,
  134. query: {
  135. ...urlQuery,
  136. ...periodObj,
  137. ...queryExtras,
  138. },
  139. };
  140. if (queryBatching?.batchRequest) {
  141. return queryBatching.batchRequest(api, pathname, queryObject);
  142. }
  143. return api.requestPromise<IncludeAllArgsType>(pathname, queryObject);
  144. };
  145. export type EventQuery = {
  146. field: string[];
  147. query: string;
  148. environment?: string[];
  149. equation?: string[];
  150. noPagination?: boolean;
  151. per_page?: number;
  152. project?: string | string[];
  153. referrer?: string;
  154. sort?: string | string[];
  155. team?: string | string[];
  156. };
  157. export type TagSegment = {
  158. count: number;
  159. name: string;
  160. url: LocationDescriptor;
  161. value: string;
  162. isOther?: boolean;
  163. key?: string;
  164. };
  165. export type Tag = {
  166. key: string;
  167. topValues: Array<TagSegment>;
  168. };
  169. /**
  170. * Fetches tag facets for a query
  171. */
  172. export function fetchTagFacets(
  173. api: Client,
  174. orgSlug: string,
  175. query: EventQuery
  176. ): Promise<Tag[]> {
  177. const urlParams = pick(query, Object.values(PERFORMANCE_URL_PARAM));
  178. const queryOption = {...urlParams, query: query.query};
  179. return api.requestPromise(`/organizations/${orgSlug}/events-facets/`, {
  180. query: queryOption,
  181. });
  182. }
  183. /**
  184. * Fetches total count of events for a given query
  185. */
  186. export function fetchTotalCount(
  187. api: Client,
  188. orgSlug: string,
  189. query: EventQuery & LocationQuery
  190. ): Promise<number> {
  191. const urlParams = pick(query, Object.values(PERFORMANCE_URL_PARAM));
  192. const queryOption = {...urlParams, query: query.query};
  193. type Response = {
  194. count: number;
  195. };
  196. return api
  197. .requestPromise(`/organizations/${orgSlug}/events-meta/`, {
  198. query: queryOption,
  199. })
  200. .then((res: Response) => res.count);
  201. }
  202. type FetchEventAttachmentParameters = {
  203. eventId: string;
  204. orgSlug: string;
  205. projectSlug: string;
  206. };
  207. type FetchEventAttachmentResponse = IssueAttachment[];
  208. export const makeFetchEventAttachmentsQueryKey = ({
  209. orgSlug,
  210. projectSlug,
  211. eventId,
  212. }: FetchEventAttachmentParameters): ApiQueryKey => [
  213. `/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/`,
  214. ];
  215. export const useFetchEventAttachments = (
  216. {orgSlug, projectSlug, eventId}: FetchEventAttachmentParameters,
  217. options: Partial<UseApiQueryOptions<FetchEventAttachmentResponse>> = {}
  218. ) => {
  219. const organization = useOrganization();
  220. return useApiQuery<FetchEventAttachmentResponse>(
  221. [`/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/`],
  222. {
  223. staleTime: Infinity,
  224. ...options,
  225. enabled:
  226. (organization.features?.includes('event-attachments') ?? false) &&
  227. options.enabled !== false,
  228. }
  229. );
  230. };
  231. type DeleteEventAttachmentVariables = {
  232. attachmentId: string;
  233. eventId: string;
  234. orgSlug: string;
  235. projectSlug: string;
  236. };
  237. type DeleteEventAttachmentResponse = unknown;
  238. type DeleteEventAttachmentContext = {
  239. previous?: IssueAttachment[];
  240. };
  241. type DeleteEventAttachmentOptions = UseMutationOptions<
  242. DeleteEventAttachmentResponse,
  243. RequestError,
  244. DeleteEventAttachmentVariables,
  245. DeleteEventAttachmentContext
  246. >;
  247. export const useDeleteEventAttachmentOptimistic = (
  248. incomingOptions: Partial<DeleteEventAttachmentOptions> = {}
  249. ) => {
  250. const api = useApi({persistInFlight: true});
  251. const queryClient = useQueryClient();
  252. const options: DeleteEventAttachmentOptions = {
  253. ...incomingOptions,
  254. mutationFn: ({orgSlug, projectSlug, eventId, attachmentId}) => {
  255. return api.requestPromise(
  256. `/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/${attachmentId}/`,
  257. {method: 'DELETE'}
  258. );
  259. },
  260. onMutate: async variables => {
  261. await queryClient.cancelQueries(makeFetchEventAttachmentsQueryKey(variables));
  262. const previous = queryClient.getQueryData<FetchEventAttachmentResponse>(
  263. makeFetchEventAttachmentsQueryKey(variables)
  264. );
  265. queryClient.setQueryData<FetchEventAttachmentResponse>(
  266. makeFetchEventAttachmentsQueryKey(variables),
  267. oldData => {
  268. if (!Array.isArray(oldData)) {
  269. return oldData;
  270. }
  271. return oldData.filter(attachment => attachment?.id !== variables.attachmentId);
  272. }
  273. );
  274. incomingOptions.onMutate?.(variables);
  275. return {previous};
  276. },
  277. onError: (error, variables, context) => {
  278. addErrorMessage(t('An error occurred while deleting the attachment'));
  279. if (context) {
  280. queryClient.setQueryData(
  281. makeFetchEventAttachmentsQueryKey(variables),
  282. context.previous
  283. );
  284. }
  285. incomingOptions.onError?.(error, variables, context);
  286. },
  287. };
  288. return useMutation(options);
  289. };