Files
2024-02-15 17:01:50 +00:00

340 lines
9.5 KiB
TypeScript

import type {LocationDescriptor} from 'history';
import pick from 'lodash/pick';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
import type {ApiResult, Client, ResponseMeta} from 'sentry/api';
import {canIncludePreviousPeriod} from 'sentry/components/charts/utils';
import {t} from 'sentry/locale';
import type {
DateString,
EventsStats,
IssueAttachment,
MultiSeriesEventsStats,
OrganizationSummary,
} from 'sentry/types';
import type {LocationQuery} from 'sentry/utils/discover/eventView';
import type {DiscoverDatasets} from 'sentry/utils/discover/types';
import {getPeriod} from 'sentry/utils/getPeriod';
import {PERFORMANCE_URL_PARAM} from 'sentry/utils/performance/constants';
import type {QueryBatching} from 'sentry/utils/performance/contexts/genericQueryBatcher';
import type {
ApiQueryKey,
UseApiQueryOptions,
UseMutationOptions,
} from 'sentry/utils/queryClient';
import {
getApiQueryData,
setApiQueryData,
useApiQuery,
useMutation,
useQueryClient,
} from 'sentry/utils/queryClient';
import type RequestError from 'sentry/utils/requestError/requestError';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
type Options = {
organization: OrganizationSummary;
partial: boolean;
comparisonDelta?: number;
dataset?: DiscoverDatasets;
end?: DateString;
environment?: Readonly<string[]>;
excludeOther?: boolean;
field?: string[];
generatePathname?: (org: OrganizationSummary) => string;
includePrevious?: boolean;
interval?: string;
limit?: number;
orderby?: string;
period?: string | null;
project?: Readonly<number[]>;
query?: string;
queryBatching?: QueryBatching;
queryExtras?: Record<string, string>;
referrer?: string;
start?: DateString;
team?: Readonly<string | string[]>;
topEvents?: number;
withoutZerofill?: boolean;
yAxis?: string | string[];
};
/**
* Make requests to `events-stats` endpoint
*
* @param {Object} api API client instance
* @param {Object} options Request parameters
* @param {Object} options.organization Organization object
* @param {Number[]} options.project List of project ids
* @param {String[]} options.environment List of environments to query for
* @param {Boolean} options.excludeOther Exclude the "Other" series when making a topEvents query
* @param {String[]} options.team List of teams to query for
* @param {String} options.period Time period to query for, in the format: <integer><units> where units are "d" or "h"
* @param {String} options.interval Time interval to group results in, in the format: <integer><units> where units are "d", "h", "m", "s"
* @param {Number} options.comparisonDelta Comparison delta for change alert event stats to include comparison stats
* @param {Boolean} options.includePrevious Should request also return reqsults for previous period?
* @param {Number} options.limit The number of rows to return
* @param {String} options.query Search query
* @param {QueryBatching} options.queryBatching A container for batching functions from a provider
* @param {Record<string, string>} options.queryExtras A list of extra query parameters
* @param {(org: OrganizationSummary) => string} options.generatePathname A function that returns an override for the pathname
*/
export const doEventsRequest = <IncludeAllArgsType extends boolean = false>(
api: Client,
{
organization,
project,
environment,
team,
period,
start,
end,
interval,
comparisonDelta,
includePrevious,
query,
yAxis,
field,
topEvents,
orderby,
partial,
withoutZerofill,
referrer,
queryBatching,
generatePathname,
queryExtras,
excludeOther,
includeAllArgs,
dataset,
}: {includeAllArgs?: IncludeAllArgsType} & Options
): IncludeAllArgsType extends true
? Promise<
[EventsStats | MultiSeriesEventsStats, string | undefined, ResponseMeta | undefined]
>
: Promise<EventsStats | MultiSeriesEventsStats> => {
const pathname =
generatePathname?.(organization) ??
`/organizations/${organization.slug}/events-stats/`;
const shouldDoublePeriod = canIncludePreviousPeriod(includePrevious, period);
const urlQuery = Object.fromEntries(
Object.entries({
interval,
comparisonDelta,
project,
environment,
team,
query,
yAxis,
field,
topEvents,
orderby,
partial: partial ? '1' : undefined,
withoutZerofill: withoutZerofill ? '1' : undefined,
referrer: referrer ? referrer : 'api.organization-event-stats',
excludeOther: excludeOther ? '1' : undefined,
dataset,
}).filter(([, value]) => typeof value !== 'undefined')
);
// Doubling period for absolute dates is not accurate unless starting and
// ending times are the same (at least for daily intervals). This is
// the tradeoff for now.
const periodObj = getPeriod({period, start, end}, {shouldDoublePeriod});
const queryObject = {
includeAllArgs,
query: {
...urlQuery,
...periodObj,
...queryExtras,
},
};
if (queryBatching?.batchRequest) {
return queryBatching.batchRequest(api, pathname, queryObject);
}
return api.requestPromise<IncludeAllArgsType>(pathname, queryObject);
};
export type EventQuery = {
field: string[];
query: string;
cursor?: string;
dataset?: DiscoverDatasets;
environment?: string[];
equation?: string[];
noPagination?: boolean;
per_page?: number;
project?: string | string[];
referrer?: string;
sort?: string | string[];
team?: string | string[];
};
export type TagSegment = {
count: number;
name: string;
url: LocationDescriptor;
value: string;
isOther?: boolean;
key?: string;
};
export type Tag = {
key: string;
topValues: Array<TagSegment>;
};
/**
* Fetches tag facets for a query
*/
export function fetchTagFacets(
api: Client,
orgSlug: string,
query: EventQuery
): Promise<ApiResult<Tag[]>> {
const urlParams = pick(query, [...Object.values(PERFORMANCE_URL_PARAM), 'cursor']);
const queryOption = {...urlParams, query: query.query};
return api.requestPromise(`/organizations/${orgSlug}/events-facets/`, {
query: queryOption,
includeAllArgs: true,
});
}
/**
* Fetches total count of events for a given query
*/
export function fetchTotalCount(
api: Client,
orgSlug: string,
query: EventQuery & LocationQuery
): Promise<number> {
const urlParams = pick(query, Object.values(PERFORMANCE_URL_PARAM));
const queryOption = {...urlParams, query: query.query};
type Response = {
count: number;
};
return api
.requestPromise(`/organizations/${orgSlug}/events-meta/`, {
query: queryOption,
})
.then((res: Response) => res.count);
}
type FetchEventAttachmentParameters = {
eventId: string;
orgSlug: string;
projectSlug: string;
};
type FetchEventAttachmentResponse = IssueAttachment[];
export const makeFetchEventAttachmentsQueryKey = ({
orgSlug,
projectSlug,
eventId,
}: FetchEventAttachmentParameters): ApiQueryKey => [
`/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/`,
];
export const useFetchEventAttachments = (
{orgSlug, projectSlug, eventId}: FetchEventAttachmentParameters,
options: Partial<UseApiQueryOptions<FetchEventAttachmentResponse>> = {}
) => {
const organization = useOrganization();
return useApiQuery<FetchEventAttachmentResponse>(
[`/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/`],
{
staleTime: Infinity,
...options,
enabled:
(organization.features?.includes('event-attachments') ?? false) &&
options.enabled !== false,
}
);
};
type DeleteEventAttachmentVariables = {
attachmentId: string;
eventId: string;
orgSlug: string;
projectSlug: string;
};
type DeleteEventAttachmentResponse = unknown;
type DeleteEventAttachmentContext = {
previous?: IssueAttachment[];
};
type DeleteEventAttachmentOptions = UseMutationOptions<
DeleteEventAttachmentResponse,
RequestError,
DeleteEventAttachmentVariables,
DeleteEventAttachmentContext
>;
export const useDeleteEventAttachmentOptimistic = (
incomingOptions: Partial<DeleteEventAttachmentOptions> = {}
) => {
const api = useApi({persistInFlight: true});
const queryClient = useQueryClient();
const options: DeleteEventAttachmentOptions = {
...incomingOptions,
mutationFn: ({orgSlug, projectSlug, eventId, attachmentId}) => {
return api.requestPromise(
`/projects/${orgSlug}/${projectSlug}/events/${eventId}/attachments/${attachmentId}/`,
{method: 'DELETE'}
);
},
onMutate: async variables => {
await queryClient.cancelQueries(makeFetchEventAttachmentsQueryKey(variables));
const previous = getApiQueryData<FetchEventAttachmentResponse>(
queryClient,
makeFetchEventAttachmentsQueryKey(variables)
);
setApiQueryData<FetchEventAttachmentResponse>(
queryClient,
makeFetchEventAttachmentsQueryKey(variables),
oldData => {
if (!Array.isArray(oldData)) {
return oldData;
}
return oldData.filter(attachment => attachment?.id !== variables.attachmentId);
}
);
incomingOptions.onMutate?.(variables);
return {previous};
},
onError: (error, variables, context) => {
addErrorMessage(t('An error occurred while deleting the attachment'));
if (context) {
setApiQueryData(
queryClient,
makeFetchEventAttachmentsQueryKey(variables),
context.previous
);
}
incomingOptions.onError?.(error, variables, context);
},
};
return useMutation(options);
};