events.tsx 9.4 KB

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