mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
521 lines
12 KiB
TypeScript
521 lines
12 KiB
TypeScript
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 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?.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}},
|
|
];
|
|
|
|
export const useFetchIssueTags = (
|
|
parameters: FetchIssueTagsParameters,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: Partial<UseApiQueryOptions<GroupTagsResponse | Tag[]>> = {}
|
|
) => {
|
|
return useApiQuery<GroupTagsResponse | Tag[]>(makeFetchIssueTagsQueryKey(parameters), {
|
|
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,
|
|
});
|
|
}
|