events.tsx 9.8 KB

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