123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- import * as Sentry from '@sentry/react';
- import type {Tag} from 'sentry/actionCreators/events';
- import type {RequestCallbacks, RequestOptions} from 'sentry/api';
- import {Client} from 'sentry/api';
- import {getSampleEventQuery} from 'sentry/components/events/eventStatisticalDetector/eventComparison/eventDisplay';
- import GroupStore from 'sentry/stores/groupStore';
- import type {
- Actor,
- Group,
- Member,
- Note,
- Tag as GroupTag,
- TagValue,
- User,
- } from 'sentry/types';
- import {buildTeamId, buildUserId, defined} from 'sentry/utils';
- import {uniqueId} from 'sentry/utils/guid';
- import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient';
- import {useApiQuery} from 'sentry/utils/queryClient';
- type AssignedBy = 'suggested_assignee' | 'assignee_selector';
- type AssignToUserParams = {
- assignedBy: AssignedBy;
- /**
- * Issue id
- */
- id: string;
- orgSlug: string;
- user: User | Actor;
- member?: Member;
- };
- export function assignToUser(params: AssignToUserParams) {
- const api = new Client();
- const endpoint = `/organizations/${params.orgSlug}/issues/${params.id}/`;
- const id = uniqueId();
- GroupStore.onAssignTo(id, params.id, {
- email: (params.member && params.member.email) || '',
- });
- const request = api.requestPromise(endpoint, {
- method: 'PUT',
- // Sending an empty value to assignedTo is the same as "clear",
- // so if no member exists, that implies that we want to clear the
- // current assignee.
- data: {
- assignedTo: params.user ? buildUserId(params.user.id) : '',
- assignedBy: params.assignedBy,
- },
- });
- request
- .then(data => {
- GroupStore.onAssignToSuccess(id, params.id, data);
- })
- .catch(data => {
- GroupStore.onAssignToError(id, params.id, data);
- });
- return request;
- }
- export function clearAssignment(
- groupId: string,
- orgSlug: string,
- assignedBy: AssignedBy
- ) {
- const api = new Client();
- const endpoint = `/organizations/${orgSlug}/issues/${groupId}/`;
- const id = uniqueId();
- GroupStore.onAssignTo(id, groupId, {
- email: '',
- });
- const request = api.requestPromise(endpoint, {
- method: 'PUT',
- // Sending an empty value to assignedTo is the same as "clear"
- data: {
- assignedTo: '',
- assignedBy,
- },
- });
- request
- .then(data => {
- GroupStore.onAssignToSuccess(id, groupId, data);
- })
- .catch(data => {
- GroupStore.onAssignToError(id, groupId, data);
- });
- return request;
- }
- type AssignToActorParams = {
- actor: Pick<Actor, 'id' | 'type'>;
- assignedBy: AssignedBy;
- /**
- * Issue id
- */
- id: string;
- orgSlug: string;
- };
- export function assignToActor({id, actor, assignedBy, orgSlug}: AssignToActorParams) {
- const api = new Client();
- const endpoint = `/organizations/${orgSlug}/issues/${id}/`;
- const guid = uniqueId();
- let actorId = '';
- GroupStore.onAssignTo(guid, id, {email: ''});
- switch (actor.type) {
- case 'user':
- actorId = buildUserId(actor.id);
- break;
- case 'team':
- actorId = buildTeamId(actor.id);
- break;
- default:
- Sentry.withScope(scope => {
- scope.setExtra('actor', actor);
- Sentry.captureException('Unknown assignee type');
- });
- }
- return api
- .requestPromise(endpoint, {
- method: 'PUT',
- data: {assignedTo: actorId, assignedBy},
- })
- .then(data => {
- GroupStore.onAssignToSuccess(guid, id, data);
- })
- .catch(data => {
- GroupStore.onAssignToSuccess(guid, id, data);
- });
- }
- export function deleteNote(
- api: Client,
- orgSlug: string,
- group: Group,
- id: string,
- _oldText: string
- ) {
- const restore = group.activity.find(activity => activity.id === id);
- const index = GroupStore.removeActivity(group.id, id);
- if (index === -1 || restore === undefined) {
- // I dunno, the id wasn't found in the GroupStore
- return Promise.reject(new Error('Group was not found in store'));
- }
- const promise = api.requestPromise(
- `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
- {
- method: 'DELETE',
- }
- );
- promise.catch(() => GroupStore.addActivity(group.id, restore, index));
- return promise;
- }
- export function createNote(api: Client, orgSlug: string, group: Group, note: Note) {
- const promise = api.requestPromise(
- `/organizations/${orgSlug}/issues/${group.id}/comments/`,
- {
- method: 'POST',
- data: note,
- }
- );
- promise.then(data => GroupStore.addActivity(group.id, data));
- return promise;
- }
- export function updateNote(
- api: Client,
- orgSlug: string,
- group: Group,
- note: Note,
- id: string,
- oldText: string
- ) {
- GroupStore.updateActivity(group.id, id, {text: note.text});
- const promise = api.requestPromise(
- `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
- {
- method: 'PUT',
- data: note,
- }
- );
- promise.catch(() => GroupStore.updateActivity(group.id, id, {text: oldText}));
- return promise;
- }
- type ParamsType = {
- environment?: string | string[] | null;
- itemIds?: string[];
- project?: number[] | null;
- query?: string;
- };
- type UpdateParams = ParamsType & {
- orgId: string;
- projectId?: string;
- };
- type QueryArgs =
- | {
- query: string;
- environment?: string | Array<string>;
- project?: Array<number>;
- }
- | {
- id: Array<number> | Array<string>;
- environment?: string | Array<string>;
- project?: Array<number>;
- }
- | {
- environment?: string | Array<string>;
- project?: Array<number>;
- };
- /**
- * Converts input parameters to API-compatible query arguments
- */
- export function paramsToQueryArgs(params: ParamsType): QueryArgs {
- const p: QueryArgs = params.itemIds
- ? {id: params.itemIds} // items matching array of itemids
- : params.query
- ? {query: params.query} // items matching search query
- : {}; // all items
- // only include environment if it is not null/undefined
- if (params.query && params.environment !== null && params.environment !== undefined) {
- p.environment = params.environment;
- }
- // only include projects if it is not null/undefined/an empty array
- if (params.project?.length) {
- p.project = params.project;
- }
- // only include date filters if they are not null/undefined
- if (params.query) {
- ['start', 'end', 'period', 'utc'].forEach(prop => {
- if (params[prop] !== null && params[prop] !== undefined) {
- p[prop === 'period' ? 'statsPeriod' : prop] = params[prop];
- }
- });
- }
- return p;
- }
- function getUpdateUrl({projectId, orgId}: UpdateParams) {
- return projectId
- ? `/projects/${orgId}/${projectId}/issues/`
- : `/organizations/${orgId}/issues/`;
- }
- function chainUtil<Args extends any[]>(
- ...funcs: Array<((...args: Args) => any) | undefined>
- ) {
- const filteredFuncs = funcs.filter(
- (f): f is (...args: Args) => any => typeof f === 'function'
- );
- return (...args: Args): void => {
- filteredFuncs.forEach(func => {
- func.apply(funcs, args);
- });
- };
- }
- function wrapRequest(
- api: Client,
- path: string,
- options: RequestOptions,
- extraParams: RequestCallbacks = {}
- ) {
- options.success = chainUtil(options.success, extraParams.success);
- options.error = chainUtil(options.error, extraParams.error);
- options.complete = chainUtil(options.complete, extraParams.complete);
- return api.request(path, options);
- }
- type BulkDeleteParams = UpdateParams;
- export function bulkDelete(
- api: Client,
- params: BulkDeleteParams,
- options: RequestCallbacks
- ) {
- const {itemIds} = params;
- const path = getUpdateUrl(params);
- const query: QueryArgs = paramsToQueryArgs(params);
- const id = uniqueId();
- GroupStore.onDelete(id, itemIds);
- return wrapRequest(
- api,
- path,
- {
- query,
- method: 'DELETE',
- success: response => {
- GroupStore.onDeleteSuccess(id, itemIds, response);
- },
- error: error => {
- GroupStore.onDeleteError(id, itemIds, error);
- },
- },
- options
- );
- }
- type BulkUpdateParams = UpdateParams & {
- data?: any;
- failSilently?: boolean;
- };
- export function bulkUpdate(
- api: Client,
- params: BulkUpdateParams,
- options: RequestCallbacks
- ) {
- const {itemIds, failSilently, data} = params;
- const path = getUpdateUrl(params);
- const query: QueryArgs = paramsToQueryArgs(params);
- const id = uniqueId();
- GroupStore.onUpdate(id, itemIds, data);
- return wrapRequest(
- api,
- path,
- {
- query,
- method: 'PUT',
- data,
- success: response => {
- GroupStore.onUpdateSuccess(id, itemIds, response);
- },
- error: () => {
- GroupStore.onUpdateError(id, itemIds, !!failSilently);
- },
- },
- options
- );
- }
- type MergeGroupsParams = UpdateParams;
- export function mergeGroups(
- api: Client,
- params: MergeGroupsParams,
- options: RequestCallbacks
- ) {
- const {itemIds} = params;
- const path = getUpdateUrl(params);
- const query: QueryArgs = paramsToQueryArgs(params);
- const id = uniqueId();
- GroupStore.onMerge(id, itemIds);
- return wrapRequest(
- api,
- path,
- {
- query,
- method: 'PUT',
- data: {merge: 1},
- success: response => {
- GroupStore.onMergeSuccess(id, itemIds, response);
- },
- error: error => {
- GroupStore.onMergeError(id, itemIds, error);
- },
- },
- options
- );
- }
- export type GroupTagResponseItem = {
- key: string;
- name: string;
- topValues: Array<{
- count: number;
- firstSeen: string;
- lastSeen: string;
- name: string;
- value: string;
- readable?: boolean;
- }>;
- totalValues: number;
- };
- export type GroupTagsResponse = GroupTagResponseItem[];
- type FetchIssueTagsParameters = {
- environment: string[];
- orgSlug: string;
- groupId?: string;
- isStatisticalDetector?: boolean;
- limit?: number;
- readable?: boolean;
- statisticalDetectorParameters?: {
- durationBaseline: number;
- end: string;
- start: string;
- transaction: string;
- };
- };
- export const makeFetchIssueTagsQueryKey = ({
- groupId,
- orgSlug,
- environment,
- readable,
- limit,
- }: FetchIssueTagsParameters): ApiQueryKey => [
- `/organizations/${orgSlug}/issues/${groupId}/tags/`,
- {query: {environment, readable, limit}},
- ];
- const makeFetchStatisticalDetectorTagsQueryKey = ({
- orgSlug,
- environment,
- statisticalDetectorParameters,
- }: FetchIssueTagsParameters): ApiQueryKey => {
- const {transaction, durationBaseline, start, end} = statisticalDetectorParameters ?? {
- transaction: '',
- durationBaseline: 0,
- start: undefined,
- end: undefined,
- };
- return [
- `/organizations/${orgSlug}/events-facets/`,
- {
- query: {
- environment,
- transaction,
- includeAll: true,
- query: getSampleEventQuery({transaction, durationBaseline, addUpperBound: false}),
- start,
- end,
- },
- },
- ];
- };
- export const useFetchIssueTags = (
- parameters: FetchIssueTagsParameters,
- {
- enabled = true,
- ...options
- }: Partial<UseApiQueryOptions<GroupTagsResponse | Tag[]>> = {}
- ) => {
- let queryKey = makeFetchIssueTagsQueryKey(parameters);
- if (parameters.isStatisticalDetector) {
- // Statistical detector issues need to use a Discover query for tags
- queryKey = makeFetchStatisticalDetectorTagsQueryKey(parameters);
- }
- return useApiQuery<GroupTagsResponse | Tag[]>(queryKey, {
- staleTime: 30000,
- enabled: defined(parameters.groupId) && enabled,
- ...options,
- });
- };
- type FetchIssueTagValuesParameters = {
- groupId: string;
- orgSlug: string;
- tagKey: string;
- cursor?: string;
- environment?: string[];
- sort?: string | string[];
- };
- export const makeFetchIssueTagValuesQueryKey = ({
- orgSlug,
- groupId,
- tagKey,
- environment,
- sort,
- cursor,
- }: FetchIssueTagValuesParameters): ApiQueryKey => [
- `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/values/`,
- {query: {environment, sort, cursor}},
- ];
- export function useFetchIssueTagValues(
- parameters: FetchIssueTagValuesParameters,
- options: Partial<UseApiQueryOptions<TagValue[]>> = {}
- ) {
- return useApiQuery<TagValue[]>(makeFetchIssueTagValuesQueryKey(parameters), {
- staleTime: 0,
- retry: false,
- ...options,
- });
- }
- type FetchIssueTagParameters = {
- groupId: string;
- orgSlug: string;
- tagKey: string;
- };
- export const makeFetchIssueTagQueryKey = ({
- orgSlug,
- groupId,
- tagKey,
- environment,
- sort,
- }: FetchIssueTagValuesParameters): ApiQueryKey => [
- `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/`,
- {query: {environment, sort}},
- ];
- export function useFetchIssueTag(
- parameters: FetchIssueTagParameters,
- options: Partial<UseApiQueryOptions<GroupTag>> = {}
- ) {
- return useApiQuery<GroupTag>(makeFetchIssueTagQueryKey(parameters), {
- staleTime: 0,
- retry: false,
- ...options,
- });
- }
|