events.tsx 9.7 KB

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