events.tsx 9.1 KB

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