events.tsx 9.3 KB

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