Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions static/app/components/feedback/useMutateFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {Actor} from 'sentry/types/core';
import type {GroupStatus} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import {parseQueryKey} from 'sentry/utils/api/apiQueryKey';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {fetchMutation} from 'sentry/utils/queryClient';

type TFeedbackIds = 'all' | string[];
Expand Down Expand Up @@ -35,8 +36,12 @@ export function useMutateFeedback({feedbackIds, organization, projectIds}: Props
mutationFn: ([ids, payload]) => {
const isSingleId = ids !== 'all' && ids.length === 1;
const url = isSingleId
? `/organizations/${organization.slug}/issues/${ids[0]}/`
: `/organizations/${organization.slug}/issues/`;
? getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {organizationIdOrSlug: organization.slug, issueId: String(ids[0])},
})
: getApiUrl('/organizations/$organizationIdOrSlug/issues/', {
path: {organizationIdOrSlug: organization.slug},
});

// TODO: it would be excellent if `PUT /issues/` could return the same data
// as `GET /issues/` when query params are set. IE: it should expand inbox & owners
Expand Down
8 changes: 7 additions & 1 deletion static/app/components/onboarding/createSampleEventButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {t} from 'sentry/locale';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {fetchMutation} from 'sentry/utils/queryClient';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useNavigate} from 'sentry/utils/useNavigate';
Expand Down Expand Up @@ -57,7 +58,12 @@ export function CreateSampleEventButton({

const {mutate: createSampleGroup, isPending} = useMutation({
mutationFn: () => {
const url = `/projects/${organization.slug}/${project!.slug}/create-sample/`;
const url = getApiUrl(
'/projects/$organizationIdOrSlug/$projectIdOrSlug/create-sample/',
{
path: {organizationIdOrSlug: organization.slug, projectIdOrSlug: project!.slug},
}
);
return fetchMutation<{groupID: string}>({method: 'POST', url});
},
onMutate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {IconCheckmark} from 'sentry/icons';
import {t, tn} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import type {ListCheckboxQueryKeyRef} from 'sentry/utils/list/useListItemCheckboxState';
import {fetchMutation} from 'sentry/utils/queryClient';
import {replayListApiOptions} from 'sentry/utils/replays/replayListApiOptions';
Expand Down Expand Up @@ -40,7 +41,16 @@ export function ReplayBulkViewedActions({

const results = await Promise.allSettled(
selectedRows.map(replay => {
const url = `/projects/${organization.slug}/${replay.project_id}/replays/${replay.id}/viewed-by/`;
const url = getApiUrl(
'/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/',
{
path: {
organizationIdOrSlug: organization.slug,
projectIdOrSlug: String(replay.project_id),
replayId: replay.id,
},
}
);

return fetchMutation({method: 'POST', url}).then(() => replay.id);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useSearchQueryBuilderAI,
} from 'sentry/components/searchQueryBuilder/context';
import * as analytics from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {fetchMutation} from 'sentry/utils/queryClient';
import {GlobalFeedbackForm} from 'sentry/utils/useFeedbackForm';
import {
Expand All @@ -36,7 +37,9 @@ const askSeerMutationOptions = mutationOptions({
status: string;
unsupported_reason: string | null;
}>({
url: '/organizations/org-slug/trace-explorer-ai/query/',
url: getApiUrl('/organizations/$organizationIdOrSlug/trace-explorer-ai/query/', {
path: {organizationIdOrSlug: 'org-slug'},
}),
method: 'POST',
data: {},
});
Expand Down
8 changes: 7 additions & 1 deletion static/app/components/searchQueryBuilder/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import {InvalidReason, WildcardOperators} from 'sentry/components/searchSyntax/parser';
import {SavedSearchType, type TagCollection} from 'sentry/types/group';
import * as analytics from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {
FieldKey,
FieldKind,
Expand Down Expand Up @@ -7409,7 +7410,12 @@ describe('SearchQueryBuilder', () => {
status: string;
unsupported_reason: string | null;
}>({
url: '/organizations/org-slug/trace-explorer-ai/query/',
url: getApiUrl(
'/organizations/$organizationIdOrSlug/trace-explorer-ai/query/',
{
path: {organizationIdOrSlug: 'org-slug'},
}
),
method: 'POST',
data: {},
});
Expand Down
2 changes: 2 additions & 0 deletions static/app/utils/api/knownGetsentryApiUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type KnownGetsentryApiUrls =
| '/_admin/cells/$region/invoice-comparison/'
| '/_admin/cells/$region/queue-spike-projection-batch/'
| '/_admin/customers/$organizationIdOrSlug/balance-changes/'
| '/_admin/customers/$organizationIdOrSlug/billing-platform-migration/'
| '/_admin/customers/$organizationIdOrSlug/queue-spike-projection/'
| '/_admin/instance-level-oauth/'
| '/_admin/users/$userId/suspend/'
Expand All @@ -33,6 +34,7 @@ export type KnownGetsentryApiUrls =
| '/customers/$organizationIdOrSlug/billing-details/'
| '/customers/$organizationIdOrSlug/billing-seats/current/'
| '/customers/$organizationIdOrSlug/charges/'
| '/customers/$organizationIdOrSlug/delete-billing-metric-history/'
| '/customers/$organizationIdOrSlug/history/'
| '/customers/$organizationIdOrSlug/history/current/'
| '/customers/$organizationIdOrSlug/invoices/'
Expand Down
17 changes: 0 additions & 17 deletions static/app/utils/integrationUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,23 +325,6 @@ export const getAlertText = (integrations?: Integration[]): string | undefined =
}
};

/**
* Uses the mapping and baseEndpoint to derive the details for the mappings request.
* @param baseEndpoint Must have a trailing slash, since the id is appended for PUT requests!
* @param mapping The mapping or suggestion being sent to the endpoint
* @returns An object containing the request method (apiMethod), and final endpoint (apiEndpoint)
*/
export const getExternalActorEndpointDetails = (
baseEndpoint: string,
mapping?: ExternalActorMappingOrSuggestion
): {apiEndpoint: string; apiMethod: 'POST' | 'PUT'} => {
const isValidMapping = mapping && isExternalActorMapping(mapping);
return {
apiMethod: isValidMapping ? 'PUT' : 'POST',
apiEndpoint: isValidMapping ? `${baseEndpoint}${mapping.id}/` : baseEndpoint,
};
};

export function getIntegrationStatus(integration: Integration) {
// there are multiple status fields for an integration we consider
const statusList = [integration.organizationIntegrationStatus, integration.status];
Expand Down
3 changes: 2 additions & 1 deletion static/app/utils/queryClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {apiFetch} from 'sentry/utils/api/apiFetch';
import {selectJson} from 'sentry/utils/api/apiOptions';
import {normalizeQueryKey} from 'sentry/utils/api/apiQueryKey';
import type {ApiQueryKey, QueryKeyEndpointOptions} from 'sentry/utils/api/apiQueryKey';
import type {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {RequestError} from 'sentry/utils/requestError/requestError';

const nonRetryCodes = new Set<number | undefined>([400, 401, 402, 403, 404]);
Expand Down Expand Up @@ -155,7 +156,7 @@ export function setApiQueryData<TResponseData>(

type ApiMutationVariables = {
method: 'PUT' | 'POST' | 'PATCH' | 'DELETE';
url: string;
url: ReturnType<typeof getApiUrl>;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where the type is enforced going forward!

data?: Record<string, unknown>;
options?: Pick<
QueryKeyEndpointOptions,
Expand Down
23 changes: 21 additions & 2 deletions static/app/utils/replays/hooks/useMarkReplayViewed.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useMutation, useQueryClient} from '@tanstack/react-query';

import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {fetchMutation} from 'sentry/utils/queryClient';
import {useOrganization} from 'sentry/utils/useOrganization';

Expand All @@ -13,11 +14,29 @@ export function useMarkReplayViewed() {

return useMutation<TData, TError, TVariables>({
mutationFn: ({projectSlug, replayId}) => {
const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
const url = getApiUrl(
'/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/',
{
path: {
organizationIdOrSlug: organization.slug,
projectIdOrSlug: projectSlug,
replayId,
},
}
);
return fetchMutation({method: 'POST', url});
},
onSuccess(_data, {projectSlug, replayId}) {
const url = `/projects/${organization.slug}/${projectSlug}/replays/${replayId}/viewed-by/`;
const url = getApiUrl(
'/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/',
{
path: {
organizationIdOrSlug: organization.slug,
projectIdOrSlug: projectSlug,
replayId,
},
}
);
queryClient.refetchQueries({queryKey: [url]});
},
retry: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {addLoadingMessage, clearIndicators} from 'sentry/actionCreators/indicato
import {t} from 'sentry/locale';
import {GroupStore} from 'sentry/stores/groupStore';
import {IssueListCacheStore} from 'sentry/stores/IssueListCacheStore';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {uniqueId} from 'sentry/utils/guid';
import {fetchMutation} from 'sentry/utils/queryClient';
import type {useNavigate} from 'sentry/utils/useNavigate';
Expand All @@ -23,7 +24,12 @@ export function discardIssueMutationOptions({
mutationFn: (variables: DiscardIssueVariables) =>
fetchMutation({
method: 'PUT',
url: `/issues/${variables.groupId}/`,
url: getApiUrl('/organizations/$organizationIdOrSlug/issues/$issueId/', {
path: {
organizationIdOrSlug: variables.orgSlug,
issueId: String(variables.groupId),
},
}),
data: {discard: true},
}),
onMutate: variables => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export default function BuildComparison() {
RequestError
>({
mutationFn: () => {
return fetchMutation({url: `${compareUrl}?rerun=true`, method: 'POST'});
return fetchMutation({
url: compareUrl,
method: 'POST',
options: {query: {rerun: 'true'}},
});
},
onSuccess: response => {
if (response?.status === 'exists') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
import {API_ACCESS_SCOPES} from 'sentry/constants';
import {t} from 'sentry/locale';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {fetchMutation} from 'sentry/utils/queryClient';
import {useNavigate} from 'sentry/utils/useNavigate';
import {useOrganization} from 'sentry/utils/useOrganization';
Expand Down Expand Up @@ -82,7 +83,9 @@ function OrganizationApiKeyForm({
const mutation = useMutation({
mutationFn: (data: ApiKeyFormValues) =>
fetchMutation<DeprecatedApiKey>({
url: `/organizations/${organizationSlug}/api-keys/${apiKey.id}/`,
url: getApiUrl('/organizations/$organizationIdOrSlug/api-keys/$apiKeyId/', {
path: {organizationIdOrSlug: organizationSlug, apiKeyId: apiKey.id},
}),
method: 'PUT',
data,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@ import {
ModalFooter,
} from '@sentry/scraps/modal';

import {getApiUrl} from 'sentry/utils/api/getApiUrl';

import {IntegrationExternalMappingForm} from './integrationExternalMappingForm';

describe('IntegrationExternalMappingForm', () => {
const membersEndpoint = '/organizations/org-slug/members/';
const membersEndpoint = getApiUrl('/organizations/$organizationIdOrSlug/members/', {
path: {organizationIdOrSlug: 'org-slug'},
});
const teamsEndpoint = '/organizations/org-slug/teams/';
const memberEndpoint = (memberId: string) =>
getApiUrl('/organizations/$organizationIdOrSlug/members/$memberId/', {
path: {organizationIdOrSlug: 'org-slug', memberId},
});
const baseProps = {
integration: GitHubIntegrationFixture(),
getBaseFormEndpoint: jest.fn(_mapping => membersEndpoint),
// Callers own the whole url, so an existing mapping resolves to its own resource.
getBaseFormEndpoint: jest.fn(mapping =>
mapping && 'id' in mapping ? memberEndpoint(mapping.id) : membersEndpoint
),
} satisfies Partial<React.ComponentProps<typeof IntegrationExternalMappingForm>>;

const closeModal = jest.fn();
Expand Down Expand Up @@ -74,7 +85,7 @@ describe('IntegrationExternalMappingForm', () => {
body: {},
});
putResponse = MockApiClient.addMockResponse({
url: `${membersEndpoint}1/`,
url: memberEndpoint('1'),
method: 'PUT',
body: {},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ import type {
} from 'sentry/types/integrations';
import type {Member, Team} from 'sentry/types/organization';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {
getExternalActorEndpointDetails,
isExternalActorMapping,
} from 'sentry/utils/integrationUtil';
import type {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {isExternalActorMapping} from 'sentry/utils/integrationUtil';
import {fetchMutation} from 'sentry/utils/queryClient';
import {RequestError} from 'sentry/utils/requestError/requestError';
import {requestErrorToFieldErrors} from 'sentry/utils/requestError/requestErrorToFieldErrors';
Expand All @@ -34,7 +32,9 @@ import {useOrganization} from 'sentry/utils/useOrganization';
type SentrySelection = {id: string; name: string};

type BaseProps = {
getBaseFormEndpoint: (mapping?: ExternalActorMappingOrSuggestion) => string;
getBaseFormEndpoint: (
mapping?: ExternalActorMappingOrSuggestion
) => ReturnType<typeof getApiUrl>;
integration: Integration;
type: 'user' | 'team';
defaultOptions?: Array<{label: React.ReactNode; value: SentrySelection}>;
Expand Down Expand Up @@ -182,14 +182,11 @@ function InlineMappingForm({
initialValue={initialValue}
mutationOptions={{
mutationFn: ({sentryId}: {sentryId: SentrySelection}) => {
const isValidMapping = Object.hasOwn(mapping || {}, 'id');
const fullData = buildMutationData(mapping, integration, type, sentryId);
const {apiEndpoint, apiMethod} = getExternalActorEndpointDetails(
getBaseFormEndpoint(fullData as ExternalActorMappingOrSuggestion),
fullData as ExternalActorMappingOrSuggestion
);
return fetchMutation<ExternalActorMapping>({
url: apiEndpoint,
method: apiMethod,
url: getBaseFormEndpoint(fullData as ExternalActorMappingOrSuggestion),
method: isValidMapping ? 'PUT' : 'POST',
data: fullData,
});
Comment on lines -186 to 194

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the place with the 'most' change in the PR.

Before we were accepting mapping and getBaseFormEndpoint() and calling them ourselves. Then we'd call getExternalActorEndpointDetails() which could potentially append an id to the end of the url.

All that gets streamlined. Now it's the job of each getBaseFormEndpoint() function to return a correct url, with or without the id appended. Therefore we don't need getExternalActorEndpointDetails anymore either.

there's a only two real files that implement getBaseFormEndpoint. They each accept the mapping parameter and append an id if it's passed in.

},
Expand Down Expand Up @@ -264,20 +261,17 @@ function ModalMappingForm({
externalName: string;
sentryId: SentrySelection;
}) => {
const isValidMapping = mapping && Object.hasOwn(mapping || {}, 'id');
const fullData = buildMutationData(
mapping,
integration,
type,
sentryId,
externalName
);
const {apiEndpoint, apiMethod} = getExternalActorEndpointDetails(
getBaseFormEndpoint(fullData as ExternalActorMappingOrSuggestion),
fullData as ExternalActorMappingOrSuggestion
);
return fetchMutation<ExternalActorMapping>({
url: apiEndpoint,
method: apiMethod,
url: getBaseFormEndpoint(fullData as ExternalActorMappingOrSuggestion),
method: isValidMapping ? 'PUT' : 'POST',
data: fullData,
});
},
Expand Down
Loading
Loading