diff --git a/.github/file-filters.yml b/.github/file-filters.yml index b1724db399cb60..4fc34b2143d4e7 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -1,21 +1,20 @@ # This is used by the action https://github.com/dorny/paths-filter -sentry_specific_workflow: &sentry_specific_workflow +sentry_frontend_workflow_file: &sentry_frontend_workflow_file - added|modified: '.github/workflows/frontend.yml' -sentry_specific_test_files: &sentry_specific_test_files - - added|modified: 'tests/js/**/*' - - added|deleted|modified: 'fixtures/search-syntax/**/*' # Provides list of changed files to test (jest) -# getsentry/sentry does not use this directly, instead we shard tests inside jest.config.js +# getsentry/sentry does not use the list directly, instead we shard tests inside jest.config.js testable_modified: &testable_modified + - '!**/*.generated.ts' - added|modified: 'package.json' - - added|modified: 'static/**/*.[tj]{s,sx}' - - *sentry_specific_test_files + - added|modified: 'static/**/*.{ts,tsx,js,jsx,mjs}' + - added|modified: 'tests/js/**/*' + - added|deleted|modified: 'fixtures/search-syntax/**/*' # Trigger for when we must run full tests (jest) testable_rules_changed: &testable_rules_changed - - *sentry_specific_workflow + - *sentry_frontend_workflow_file - added|modified: '.github/file-filters.yml' - added|modified: 'jest.config.ts' @@ -23,13 +22,15 @@ testable_rules_changed: &testable_rules_changed # There's no "rules_changed" for this, because we run it for all files always # getsentry/sentry does not use this directly, instead frontend_all is a superset to trigger tsc typecheckable_rules_changed: &typecheckable_rules_changed - - *sentry_specific_workflow + - *sentry_frontend_workflow_file - added|modified: '.github/file-filters.yml' - added|deleted|modified: '**/tsconfig*.json' - - added|deleted|modified: 'static/**/*.[tj]{s,sx}' + - added|deleted|modified: 'static/**/*.{ts,tsx,js,jsx,mjs}' # Trigger to apply the 'Scope: Frontend' label to PRs frontend_all: &frontend_all + - *sentry_frontend_workflow_file + - '!**/*.generated.ts' - added|modified: '**/*.{ts,tsx,js,jsx,mjs}' - added|modified: 'static/**/*.{less,json,yml,md,mdx}' - added|modified: '{.volta,vercel,tsconfig,biome,package}.json' diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 56cf260827be8c..c6da515c34bb16 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -25,8 +25,8 @@ jobs: # Map a step output to a job output outputs: testable_modified: ${{ steps.changes.outputs.testable_modified }} - testable_modified_files: ${{ steps.changes.outputs.testable_modified_files }} testable_rules_changed: ${{ steps.changes.outputs.testable_rules_changed }} + typecheckable_rules_changed: ${{ steps.changes.outputs.typecheckable_rules_changed }} frontend_all: ${{ steps.changes.outputs.frontend_all }} steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04aac4c911b468..08514004902478 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -135,6 +135,12 @@ repos: stages: [pre-push] entry: bash -c 'if [ -n "${SENTRY_KNIP_PRE_PUSH:-}" ]; then exec ./node_modules/.bin/knip; fi' -- + - id: gen-ts-api-urls + name: gen-ts-api-urls + language: system + files: /urls\.py$ + entry: python3 -m tools.api_urls_to_typescript + - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts deleted file mode 100644 index 61858ac079efa7..00000000000000 --- a/static/app/api/apiDefinition.ts +++ /dev/null @@ -1,3 +0,0 @@ -type KnownApiUrls = ['/projects/$orgSlug/$projectSlug/releases/$releaseVersion/']; - -export type MaybeApiPath = KnownApiUrls[number] | (string & {}); diff --git a/static/app/api/getApiUrl.spec.ts b/static/app/api/getApiUrl.spec.ts deleted file mode 100644 index 87a1afdf8022b4..00000000000000 --- a/static/app/api/getApiUrl.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {expectTypeOf} from 'expect-type'; - -import {getApiUrl} from './getApiUrl'; - -describe('getApiUrl', () => { - test('should replace path parameters with their values', () => { - const url = getApiUrl('/projects/$orgSlug/$projectSlug/', { - path: { - orgSlug: 'my-org', - projectSlug: 'my-project', - }, - }); - - expect(url).toBe('/projects/my-org/my-project/'); - }); - - test('should not require path parameters if none are present', () => { - const url = getApiUrl('/projects/'); - - expect(url).toBe('/projects/'); - }); - - test('should encode path parameters correctly', () => { - const url = getApiUrl('/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', { - path: { - orgSlug: 'my-org', - projectSlug: 'my-project', - releaseVersion: 'v 1.0.0', - }, - }); - - expect(url).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); - }); - - test('should stringify number path params', () => { - const url = getApiUrl('/items/$id/', { - path: {id: 123}, - }); - - expect(url).toBe('/items/123/'); - }); - - test('should not do accidental replacements', () => { - const url = getApiUrl('/projects/$id1/$id', { - path: {id: '123', id1: '456'}, - }); - - expect(url).toBe('/projects/456/123'); - }); - - describe('types', () => { - test('should return branded string type', () => { - const url = getApiUrl('/projects/$orgSlug/', { - path: {orgSlug: 'my-org'}, - }); - - expectTypeOf(url).toEqualTypeOf(); - }); - test('should not allow invalid path parameters', () => { - getApiUrl('/projects/$orgSlug/', { - // @ts-expect-error Invalid path parameter - path: {orgSlug: 'my-org', invalidParam: 'invalid'}, - }); - }); - - test('should not allow excess path parameters', () => { - getApiUrl('/projects/$orgSlug/', { - staleTime: 0, - // @ts-expect-error Excess path parameter - path: {orgSlug: 'my-org', extraParam: 'extra'}, - }); - }); - - test('should require path params for paths with parameters', () => { - expect(() => { - getApiUrl('/projects/$orgSlug/', { - // @ts-expect-error Missing required path parameter - path: {}, - }); - }).toThrow('Missing path param: orgSlug'); - }); - - test('should not allow empty path parameters for paths without parameters', () => { - // @ts-expect-error Expected 1 argument, but got 2 - getApiUrl('/projects/', {path: {}}); - }); - }); -}); diff --git a/static/app/components/waitingForEvents.tsx b/static/app/components/waitingForEvents.tsx index 321c74899ed46b..c9a67fa7e81a7e 100644 --- a/static/app/components/waitingForEvents.tsx +++ b/static/app/components/waitingForEvents.tsx @@ -3,13 +3,13 @@ import {skipToken, useQuery} from '@tanstack/react-query'; import waitingForEventImg from 'sentry-images/spot/waiting-for-event.svg'; -import {apiOptions} from 'sentry/api/apiOptions'; import {LinkButton} from 'sentry/components/core/button/linkButton'; import {Link} from 'sentry/components/core/link'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton'; import {makeProjectsPathname} from 'sentry/views/projects/pathname'; @@ -27,17 +27,20 @@ type Props = { function WaitingForEvents({org, project, sampleIssueId: sampleIssueIdProp}: Props) { const {data, error, isPending} = useQuery( - apiOptions.as>()('/projects/$orgSlug/$projectSlug/issues/', { - staleTime: Infinity, - data: {limit: 1}, - path: - project && sampleIssueIdProp === undefined - ? { - orgSlug: org.slug, - projectSlug: project.slug, - } - : skipToken, - }) + apiOptions.as>()( + '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/', + { + staleTime: Infinity, + data: {limit: 1}, + path: + project && sampleIssueIdProp === undefined + ? { + organizationIdOrSlug: org.slug, + projectIdOrSlug: project.slug, + } + : skipToken, + } + ) ); const sampleIssueId = sampleIssueIdProp ?? data?.[0]?.id ?? ''; diff --git a/static/app/api/apiOptions.spec.tsx b/static/app/utils/api/apiOptions.spec.tsx similarity index 54% rename from static/app/api/apiOptions.spec.tsx rename to static/app/utils/api/apiOptions.spec.tsx index 1bba2c13ea485f..215b566aa7513b 100644 --- a/static/app/api/apiOptions.spec.tsx +++ b/static/app/utils/api/apiOptions.spec.tsx @@ -4,14 +4,13 @@ import {expectTypeOf} from 'expect-type'; import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary'; import type {ApiResult} from 'sentry/api'; +import {apiOptions, selectWithHeaders} from 'sentry/utils/api/apiOptions'; import { DEFAULT_QUERY_CLIENT_CONFIG, QueryClient, QueryClientProvider, } from 'sentry/utils/queryClient'; -import {apiOptions, selectWithHeaders} from './apiOptions'; - type Promisable = T | Promise; type QueryFunctionResult = Promisable>; @@ -22,44 +21,41 @@ const wrapper = ({children}: {children?: React.ReactNode}) => ( ); describe('apiOptions', () => { - afterEach(() => { - MockApiClient.clearMockResponses(); - }); - test('should encode path parameters correctly', () => { + it('should encode path parameters correctly', () => { const options = apiOptions.as()( - '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + '/organizations/$organizationIdOrSlug/releases/$version/', { staleTime: 0, path: { - orgSlug: 'my-org', - projectSlug: 'my-project', - releaseVersion: 'v 1.0.0', + organizationIdOrSlug: 'my-org', + version: 'v 1.0.0', }, } ); - expect(options.queryKey[0]).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); + expect(options.queryKey[0]).toBe('/organizations/my-org/releases/v%201.0.0/'); }); - test('should not include empty options in queryKey', () => { - const options = apiOptions.as()('/projects/$id/', { + it('should not include empty options in queryKey', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {id: '123'}, + path: {tokenId: '123'}, }); - expect(options.queryKey).toEqual(['/projects/123/']); + expect(options.queryKey).toEqual(['/api-tokens/123/']); }); - test('should stringify number path params', () => { - const options = apiOptions.as()('/items/$id/', { + it('should stringify number path params', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {id: 123}, + path: {tokenId: 123}, }); - expect(options.queryKey[0]).toBe('/items/123/'); + expect(options.queryKey[0]).toBe('/api-tokens/123/'); }); - test('should not do accidental replacements', () => { + it('should not do accidental replacements', () => { + // @ts-expect-error Using a sample path, not a real one const options = apiOptions.as()('/projects/$id1/$id', { staleTime: 0, path: {id: '123', id1: '456'}, @@ -68,21 +64,21 @@ describe('apiOptions', () => { expect(options.queryKey).toEqual(['/projects/456/123']); }); - test('should allow skipToken as path', () => { - function getOptions(id: string | null) { - return apiOptions.as()('/projects/$id/', { + it('should allow skipToken as path', () => { + function getOptions(tokenId: string | null) { + return apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: id ? {id} : skipToken, + path: tokenId ? {tokenId} : skipToken, }); } expect(getOptions('123').queryFn).toEqual(expect.any(Function)); - expect(getOptions('123').queryKey).toEqual(['/projects/123/']); + expect(getOptions('123').queryKey).toEqual(['/api-tokens/123/']); expect(getOptions(null).queryFn).toEqual(skipToken); - expect(getOptions(null).queryKey).toEqual(['/projects/$id/']); + expect(getOptions(null).queryKey).toEqual(['/api-tokens/$tokenId/']); }); - test('should extract content data per default', async () => { + it('should extract content data per default', async () => { const options = apiOptions.as()('/projects/', { staleTime: 0, }); @@ -99,7 +95,7 @@ describe('apiOptions', () => { expect(result.current.data).toEqual(['Project 1', 'Project 2']); }); - test('should extract headers', async () => { + it('should extract headers', async () => { const options = apiOptions.as()('/projects/', { staleTime: 0, }); @@ -134,47 +130,37 @@ describe('apiOptions', () => { }); describe('types', () => { - test('should always require staleTime', () => { + it('should always require staleTime', () => { // @ts-expect-error staleTime is required apiOptions.as()('/projects/$orgSlug/', {path: {orgSlug: 'my-org'}}); // @ts-expect-error staleTime is required apiOptions.as()('/projects/', {}); }); - test('should not allow invalid path parameters', () => { - const options = apiOptions.as()('/projects/$orgSlug/', { - staleTime: 0, - // @ts-expect-error Invalid path parameter - path: {orgSlug: 'my-org', invalidParam: 'invalid'}, - }); - - expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - }); - - test('should not allow excess path parameters', () => { - const options = apiOptions.as()('/projects/$orgSlug/', { + it('should not allow invalid/excess path parameters', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - // @ts-expect-error Excess path parameter - path: {orgSlug: 'my-org', extraParam: 'extra'}, + // @ts-expect-error Missing required path parameter + path: {tokenId: 'my-org', invalidParam: 'invalid'}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('should require path params for paths with parameters', () => { + it('should require path params for paths with parameters', () => { expect(() => { - const options = apiOptions.as()('/projects/$orgSlug/', { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, // @ts-expect-error Missing required path parameter path: {}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - }).toThrow('Missing path param: orgSlug'); + }).toThrow('Missing path param: tokenId'); }); - test('should not allow empty path parameters for paths without parameters', () => { - const options = apiOptions.as()('/projects/', { + it('should not allow empty path parameters for paths without parameters', () => { + const options = apiOptions.as()('/api-tokens/', { staleTime: 0, // @ts-expect-error Empty path parameters not allowed path: {}, @@ -183,31 +169,32 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('should not need path params for paths without parameters', () => { - const options = apiOptions.as()('/projects/', { + it('should not need path params for paths without parameters', () => { + const options = apiOptions.as()('/api-tokens/', { staleTime: 0, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('should allow string or number path parameters', () => { - const options = apiOptions.as()('/items/$id/', { + it('should allow string or number path parameters', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {id: 123}, + path: {tokenId: 123}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - const options2 = apiOptions.as()('/items/$id/', { + const options2 = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {id: 'abc'}, + path: {tokenId: 'abc'}, }); expectTypeOf(options2.queryFn).returns.toEqualTypeOf>(); }); - test('should default to never for unknown API paths', () => { + it('should default to never for unknown API paths', () => { + // @ts-expect-error Unknown API path const options = apiOptions.as()('/unknown/$param/', { staleTime: 0, path: {param: 'value'}, @@ -216,33 +203,17 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('should allow providing manual data type', () => { - const options = apiOptions.as()('/foo/$bar', { + it('should allow providing manual data type', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {bar: 'baz'}, + path: {tokenId: 'abc'}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('manual data type should override even for known api urls', () => { - const options = apiOptions.as()( - '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', - { - staleTime: 0, - path: { - orgSlug: 'my-org', - projectSlug: 'my-project', - releaseVersion: 'v1.0.0', - }, - } - ); - - expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - }); - - test('should disallow path if there are no path params', () => { - const options = apiOptions.as()('/foo', { + it('should disallow unknown path if there are no path params', () => { + const options = apiOptions.as()('/api-tokens/', { staleTime: 0, // @ts-expect-error Path is not allowed when there are no path params path: {bar: 'baz'}, @@ -251,10 +222,10 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - test('should have a default select that extracts content', () => { - const options = apiOptions.as()('/items/$id/', { + it('should have a default select that extracts content', () => { + const options = apiOptions.as()('/api-tokens/$tokenId/', { staleTime: 0, - path: {id: 123}, + path: {tokenId: 123}, }); expectTypeOf(options.select).returns.toEqualTypeOf(); diff --git a/static/app/api/apiOptions.ts b/static/app/utils/api/apiOptions.ts similarity index 72% rename from static/app/api/apiOptions.ts rename to static/app/utils/api/apiOptions.ts index ad7f5f815ad8fe..48cc9eb90fa5e5 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/utils/api/apiOptions.ts @@ -1,10 +1,15 @@ -import {queryOptions, skipToken, type SkipToken} from '@tanstack/react-query'; +import {queryOptions, skipToken} from '@tanstack/react-query'; +import type {SkipToken} from '@tanstack/react-query'; import type {ApiResult} from 'sentry/api'; -import {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; +import getApiUrl from 'sentry/utils/api/getApiUrl'; +import type {ExtractPathParams, OptionalPathParams} from 'sentry/utils/api/getApiUrl'; +import type {KnownGetsentryApiUrls} from 'sentry/utils/api/knownGetsentryApiUrls'; +import type {KnownSentryApiUrls} from 'sentry/utils/api/knownSentryApiUrls.generated'; +import type {QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; +import {fetchDataQuery} from 'sentry/utils/queryClient'; -import type {MaybeApiPath} from './apiDefinition'; -import {getApiUrl, type ExtractPathParams, type OptionalPathParams} from './getApiUrl'; +type KnownApiUrls = KnownGetsentryApiUrls | KnownSentryApiUrls; type Options = QueryKeyEndpointOptions & {staleTime: number}; @@ -29,7 +34,7 @@ export const selectWithHeaders = function _apiOptions< TManualData = never, - TApiPath extends MaybeApiPath = MaybeApiPath, + TApiPath extends KnownApiUrls = KnownApiUrls, // todo: infer the actual data type from the ApiMapping TActualData = TManualData, >( @@ -68,7 +73,7 @@ function _apiOptions< export const apiOptions = { as: () => - ( + ( path: TApiPath, options: Options & PathParamOptions ) => diff --git a/static/app/utils/api/getApiUrl.spec.ts b/static/app/utils/api/getApiUrl.spec.ts new file mode 100644 index 00000000000000..cca0bba5573297 --- /dev/null +++ b/static/app/utils/api/getApiUrl.spec.ts @@ -0,0 +1,110 @@ +import {expectTypeOf} from 'expect-type'; + +import getApiUrl from 'sentry/utils/api/getApiUrl'; + +describe('getApiUrl', () => { + it('should replace path parameters with their values', () => { + // @ts-expect-error Using a sample path, not a real one + const url = getApiUrl('/projects/$orgSlug/$projectSlug/', { + path: { + orgSlug: 'my-org', + projectSlug: 'my-project', + }, + }); + + expect(url).toBe('/projects/my-org/my-project/'); + }); + + it('should not require path parameters if none are present', () => { + const url = getApiUrl('/api-tokens/'); + + expect(url).toBe('/api-tokens/'); + }); + + it('should encode path parameters correctly', () => { + const url = getApiUrl('/organizations/$organizationIdOrSlug/releases/$version/', { + path: { + organizationIdOrSlug: 'my-org', + version: 'v 1.0.0', + }, + }); + + expect(url).toBe('/organizations/my-org/releases/v%201.0.0/'); + }); + + it('should stringify number path params', () => { + const url = getApiUrl('/api-tokens/$tokenId/', { + path: {tokenId: 123}, + }); + + expect(url).toBe('/api-tokens/123/'); + }); + + it('should not do accidental replacements', () => { + // @ts-expect-error Using a sample path, not a real one + const url = getApiUrl('/projects/$id1/$id', { + path: {id: '123', id1: '456'}, + }); + + expect(url).toBe('/projects/456/123'); + }); + + it('should replace segments with : in the middle', () => { + const url = getApiUrl( + '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/', + { + path: { + organizationIdOrSlug: 'org-slug', + projectIdOrSlug: 'abc', + eventId: '123', + }, + } + ); + + expect(url).toBe('/organizations/org-slug/events/abc:123/'); + }); + + it('should allow string or number path parameters', () => { + const url1 = getApiUrl('/api-tokens/$tokenId/', { + path: {tokenId: 123}, + }); + + expect(url1).toBe('/api-tokens/123/'); + + const url2 = getApiUrl('/api-tokens/$tokenId/', { + path: {tokenId: 'abc'}, + }); + + expect(url2).toBe('/api-tokens/abc/'); + }); + + describe('types', () => { + it('should return branded string type', () => { + const url = getApiUrl('/api-tokens/$tokenId/', { + path: {tokenId: 'my-token'}, + }); + + expectTypeOf(url).toEqualTypeOf(); + }); + it('should not allow invalid/excess path parameters', () => { + getApiUrl('/api-tokens/$tokenId/', { + // @ts-expect-error Missing required path parameter + path: {tokenId: 'my-org', invalidParam: 'invalid'}, + }); + }); + + it('should require path params for paths with parameters', () => { + expect(() => { + getApiUrl('/api-tokens/$tokenId/', { + // @ts-expect-error Missing required path parameter + path: {}, + }); + }).toThrow('Missing path param: tokenId'); + }); + + it('should not allow empty path parameters for paths without parameters', () => { + // @ts-expect-error Expected 1 argument, but got 2 + getApiUrl('/api-tokens/', {path: {}}); + }); + }); +}); diff --git a/static/app/api/getApiUrl.ts b/static/app/utils/api/getApiUrl.ts similarity index 59% rename from static/app/api/getApiUrl.ts rename to static/app/utils/api/getApiUrl.ts index b305e2efca997e..3fcb6523e36d52 100644 --- a/static/app/api/getApiUrl.ts +++ b/static/app/utils/api/getApiUrl.ts @@ -1,10 +1,19 @@ -import type {MaybeApiPath} from './apiDefinition'; +import type {KnownGetsentryApiUrls} from 'sentry/utils/api/knownGetsentryApiUrls'; +import type {KnownSentryApiUrls} from 'sentry/utils/api/knownSentryApiUrls.generated'; + +type KnownApiUrls = KnownGetsentryApiUrls | KnownSentryApiUrls; + +type StripDollar = T extends `$${infer Name}` ? Name : T; + +type SplitColon = T extends `${infer A}:${infer B}` + ? StripDollar | SplitColon + : StripDollar; export type ExtractPathParams = TApiPath extends `${string}$${infer Param}/${infer Rest}` - ? Param | ExtractPathParams<`/${Rest}`> + ? SplitColon | ExtractPathParams<`/${Rest}`> : TApiPath extends `${string}$${infer Param}` - ? Param + ? SplitColon : never; type PathParamOptions = @@ -13,15 +22,13 @@ type PathParamOptions = : {path: Record, string | number>}; export type OptionalPathParams = - ExtractPathParams extends never - ? [] // eslint-disable-line @typescript-eslint/no-restricted-types - : [PathParamOptions]; + ExtractPathParams extends never ? never[] : [PathParamOptions]; const paramRegex = /\$([a-zA-Z0-9_-]+)/g; type ApiUrl = string & {__apiUrl: true}; -export function getApiUrl( +export default function getApiUrl( path: TApiPath, ...[options]: OptionalPathParams ): ApiUrl { diff --git a/static/app/utils/api/knownGetsentryApiUrls.ts b/static/app/utils/api/knownGetsentryApiUrls.ts new file mode 100644 index 00000000000000..c3721164805035 --- /dev/null +++ b/static/app/utils/api/knownGetsentryApiUrls.ts @@ -0,0 +1,76 @@ +/** + * MANUALLY UPDATED FILE. + * Keep this file in sync with getsentry/conf/urls/app.py in getsentry/getsentry + * + * This file is the sibling to knownSentryApiUrls.generated.ts. + */ + +export type KnownGetsentryApiUrls = + | '/audit-logs/' + | '/beacons/' + | '/beacons/$beaconId/' + | '/beacons/$beaconId/checkins/' + | '/beacons/$beaconId/related-beacons/' + | '/billing-plans/' + | '/billingadmins/' + | '/broadcasts/' + | '/broadcasts/$broadcastId/' + | '/copilot/' + | '/copilot/webhook/' + | '/customers/' + | '/customers/$organizationIdOrSlug/' + | '/customers/$organizationIdOrSlug/balancechanges/' + | '/customers/$organizationIdOrSlug/billing-config/' + | '/customers/$organizationIdOrSlug/billing-details/' + | '/customers/$organizationIdOrSlug/charges/' + | '/customers/$organizationIdOrSlug/charges/$chargeId/' + | '/customers/$organizationIdOrSlug/history/' + | '/customers/$organizationIdOrSlug/history/$historyId/' + | '/customers/$organizationIdOrSlug/invoices/' + | '/customers/$organizationIdOrSlug/invoices/$invoiceId/' + | '/customers/$organizationIdOrSlug/invoices/$invoiceId/close/' + | '/customers/$organizationIdOrSlug/invoices/$invoiceId/effective-at/' + | '/customers/$organizationIdOrSlug/invoices/$invoiceId/retry-payment/' + | '/customers/$organizationIdOrSlug/members/' + | '/customers/$organizationIdOrSlug/migrate-google-domain/' + | '/customers/$organizationIdOrSlug/ondemand-budgets/' + | '/customers/$organizationIdOrSlug/plan-migrations/' + | '/customers/$organizationIdOrSlug/policies/' + | '/customers/$organizationIdOrSlug/product-trial/' + | '/customers/$organizationIdOrSlug/projects/$projectIdOrSlug/stats/' + | '/customers/$organizationIdOrSlug/provision-subscription/' + | '/customers/$organizationIdOrSlug/recurring-credits/' + | '/customers/$organizationIdOrSlug/redeem-promo/' + | '/customers/$organizationIdOrSlug/send-weekly-email/' + | '/customers/$organizationIdOrSlug/spend-notifications/' + | '/customers/$organizationIdOrSlug/stats/' + | '/customers/$organizationIdOrSlug/subscription/' + | '/customers/$organizationIdOrSlug/subscription/preview/' + | '/customers/$organizationIdOrSlug/subscription/usage-logs/' + | '/customers/$organizationIdOrSlug/usage/' + | '/internal-stats/$organizationIdOrSlug/integrations/' + | '/internal-stats/$organizationIdOrSlug/onboarding-tasks/' + | '/internal-stats/$organizationIdOrSlug/platforms/' + | '/invoices/' + | '/invoices/$invoiceId/' + | '/invoices/$invoiceId/close/' + | '/invoices/$invoiceId/effective-at/' + | '/invoices/$invoiceId/retry-payment/' + | '/organizations/$organizationIdOrSlug/codecov-jwt/' + | '/organizations/$organizationIdOrSlug/issues/force-auto-assignment/' + | '/organizations/$organizationIdOrSlug/monitor-count/' + | '/organizations/$organizationIdOrSlug/partnership-agreements/' + | '/organizations/$organizationIdOrSlug/promotions/$promoSlug/claim/' + | '/organizations/$organizationIdOrSlug/promotions/$promoSlug/decline/' + | '/organizations/$organizationIdOrSlug/promotions/trigger-check/' + | '/policies/' + | '/policies/$policySlug/' + | '/policies/$policySlug/revisions/' + | '/policies/$policySlug/revisions/$version/' + | '/promocodes-external/$code' + | '/promocodes/' + | '/promocodes/$code/' + | '/promocodes/$code/claimants/' + | '/signup/' + | '/users/$userId/customers/' + | '/users/$userId/merge-accounts/'; diff --git a/static/app/utils/api/knownSentryApiUrls.generated.ts b/static/app/utils/api/knownSentryApiUrls.generated.ts new file mode 100644 index 00000000000000..2cdab59151b9ef --- /dev/null +++ b/static/app/utils/api/knownSentryApiUrls.generated.ts @@ -0,0 +1,794 @@ +/** + * GENERATED FILE. Do not edit manually. + * To update it run `python3 -m tools.api_urls_to_typescript` + * + * This file is the sibling to knownGetsentryApiUrls.ts. + */ + +export type KnownSentryApiUrls = + | '/' + | '/accept-invite/$memberId/$token/' + | '/accept-invite/$organizationIdOrSlug/$memberId/$token/' + | '/accept-transfer/' + | '/api-applications/' + | '/api-applications/$appId/' + | '/api-applications/$appId/rotate-secret/' + | '/api-authorizations/' + | '/api-tokens/' + | '/api-tokens/$tokenId/' + | '/assistant/' + | '/auth-v2/csrf/' + | '/auth-v2/flag/' + | '/auth-v2/login/' + | '/auth-v2/merge-accounts/' + | '/auth-v2/user-merge-verification-codes/' + | '/auth/' + | '/auth/config/' + | '/auth/login/' + | '/auth/validate/' + | '/authenticators/' + | '/broadcasts/' + | '/broadcasts/$broadcastId/' + | '/builtin-symbol-sources/' + | '/data-export/notifications/google-cloud/' + | '/doc-integrations/' + | '/doc-integrations/$docIntegrationIdOrSlug/' + | '/doc-integrations/$docIntegrationIdOrSlug/avatar/' + | '/grouping-configs/' + | '/groups/$issueId/' + | '/groups/$issueId/activities/' + | '/groups/$issueId/attachments/' + | '/groups/$issueId/autofix/' + | '/groups/$issueId/autofix/setup/' + | '/groups/$issueId/autofix/update/' + | '/groups/$issueId/comments/' + | '/groups/$issueId/comments/$noteId/' + | '/groups/$issueId/current-release/' + | '/groups/$issueId/events/' + | '/groups/$issueId/events/$eventId/' + | '/groups/$issueId/external-issues/' + | '/groups/$issueId/external-issues/$externalIssueId/' + | '/groups/$issueId/first-last-release/' + | '/groups/$issueId/hashes/' + | '/groups/$issueId/integrations/' + | '/groups/$issueId/integrations/$integrationId/' + | '/groups/$issueId/notes/' + | '/groups/$issueId/notes/$noteId/' + | '/groups/$issueId/plugins/asana/autocomplete' + | '/groups/$issueId/plugins/asana/create/' + | '/groups/$issueId/plugins/asana/link/' + | '/groups/$issueId/plugins/asana/unlink/' + | '/groups/$issueId/plugins/bitbucket/autocomplete' + | '/groups/$issueId/plugins/bitbucket/create/' + | '/groups/$issueId/plugins/bitbucket/link/' + | '/groups/$issueId/plugins/bitbucket/unlink/' + | '/groups/$issueId/plugins/github/autocomplete' + | '/groups/$issueId/plugins/github/create/' + | '/groups/$issueId/plugins/github/link/' + | '/groups/$issueId/plugins/github/unlink/' + | '/groups/$issueId/plugins/gitlab/create/' + | '/groups/$issueId/plugins/gitlab/link/' + | '/groups/$issueId/plugins/gitlab/unlink/' + | '/groups/$issueId/plugins/jira/autocomplete' + | '/groups/$issueId/plugins/jira/create/' + | '/groups/$issueId/plugins/jira/link/' + | '/groups/$issueId/plugins/jira/unlink/' + | '/groups/$issueId/plugins/pivotal/autocomplete' + | '/groups/$issueId/plugins/pivotal/create/' + | '/groups/$issueId/plugins/pivotal/link/' + | '/groups/$issueId/plugins/pivotal/unlink/' + | '/groups/$issueId/plugins/trello/autocomplete' + | '/groups/$issueId/plugins/trello/create/' + | '/groups/$issueId/plugins/trello/link/' + | '/groups/$issueId/plugins/trello/options' + | '/groups/$issueId/plugins/trello/unlink/' + | '/groups/$issueId/reprocessing/' + | '/groups/$issueId/similar-issues-embeddings/' + | '/groups/$issueId/similar/' + | '/groups/$issueId/stats/' + | '/groups/$issueId/summarize/' + | '/groups/$issueId/suspect/flags/' + | '/groups/$issueId/suspect/tags/' + | '/groups/$issueId/tags/' + | '/groups/$issueId/tags/$key/' + | '/groups/$issueId/tags/$key/values/' + | '/groups/$issueId/user-feedback/' + | '/groups/$issueId/user-reports/' + | '/integration-features/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/assemble-generic/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/size/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/size/$identifier/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/update/' + | '/internal/beacon/' + | '/internal/check-am2-compatibility/' + | '/internal/demo/email-capture/' + | '/internal/environment/' + | '/internal/feature-flags/' + | '/internal/feature-flags/ea-feature-flags' + | '/internal/frontend-version/' + | '/internal/health/' + | '/internal/integration-proxy/' + | '/internal/mail/' + | '/internal/notifications/registered-templates/' + | '/internal/options/' + | '/internal/packages/' + | '/internal/preprod-artifact/$headArtifactId/info/' + | '/internal/preprod-artifact/batch-delete/' + | '/internal/preprod-artifact/rerun-analysis/' + | '/internal/prevent/pr-review/configs/resolved/' + | '/internal/prevent/pr-review/github/sentry-org/' + | '/internal/project-config/' + | '/internal/queue/tasks/' + | '/internal/rpc/$serviceName/$methodName/' + | '/internal/seer-rpc/$methodName/' + | '/internal/stats/' + | '/internal/warnings/' + | '/issues/$issueId/' + | '/issues/$issueId/activities/' + | '/issues/$issueId/attachments/' + | '/issues/$issueId/autofix/' + | '/issues/$issueId/autofix/setup/' + | '/issues/$issueId/autofix/update/' + | '/issues/$issueId/comments/' + | '/issues/$issueId/comments/$noteId/' + | '/issues/$issueId/current-release/' + | '/issues/$issueId/events/' + | '/issues/$issueId/events/$eventId/' + | '/issues/$issueId/external-issues/' + | '/issues/$issueId/external-issues/$externalIssueId/' + | '/issues/$issueId/first-last-release/' + | '/issues/$issueId/hashes/' + | '/issues/$issueId/integrations/' + | '/issues/$issueId/integrations/$integrationId/' + | '/issues/$issueId/notes/' + | '/issues/$issueId/notes/$noteId/' + | '/issues/$issueId/plugins/asana/autocomplete' + | '/issues/$issueId/plugins/asana/create/' + | '/issues/$issueId/plugins/asana/link/' + | '/issues/$issueId/plugins/asana/unlink/' + | '/issues/$issueId/plugins/bitbucket/autocomplete' + | '/issues/$issueId/plugins/bitbucket/create/' + | '/issues/$issueId/plugins/bitbucket/link/' + | '/issues/$issueId/plugins/bitbucket/unlink/' + | '/issues/$issueId/plugins/github/autocomplete' + | '/issues/$issueId/plugins/github/create/' + | '/issues/$issueId/plugins/github/link/' + | '/issues/$issueId/plugins/github/unlink/' + | '/issues/$issueId/plugins/gitlab/create/' + | '/issues/$issueId/plugins/gitlab/link/' + | '/issues/$issueId/plugins/gitlab/unlink/' + | '/issues/$issueId/plugins/jira/autocomplete' + | '/issues/$issueId/plugins/jira/create/' + | '/issues/$issueId/plugins/jira/link/' + | '/issues/$issueId/plugins/jira/unlink/' + | '/issues/$issueId/plugins/pivotal/autocomplete' + | '/issues/$issueId/plugins/pivotal/create/' + | '/issues/$issueId/plugins/pivotal/link/' + | '/issues/$issueId/plugins/pivotal/unlink/' + | '/issues/$issueId/plugins/trello/autocomplete' + | '/issues/$issueId/plugins/trello/create/' + | '/issues/$issueId/plugins/trello/link/' + | '/issues/$issueId/plugins/trello/options' + | '/issues/$issueId/plugins/trello/unlink/' + | '/issues/$issueId/related-issues/' + | '/issues/$issueId/reprocessing/' + | '/issues/$issueId/similar-issues-embeddings/' + | '/issues/$issueId/similar/' + | '/issues/$issueId/stats/' + | '/issues/$issueId/summarize/' + | '/issues/$issueId/suspect/flags/' + | '/issues/$issueId/suspect/tags/' + | '/issues/$issueId/tags/' + | '/issues/$issueId/tags/$key/' + | '/issues/$issueId/tags/$key/values/' + | '/issues/$issueId/user-feedback/' + | '/issues/$issueId/user-reports/' + | '/notification-defaults/' + | '/organizations/' + | '/organizations/$organizationIdOrSlug/' + | '/organizations/$organizationIdOrSlug/access-requests/' + | '/organizations/$organizationIdOrSlug/access-requests/$requestId/' + | '/organizations/$organizationIdOrSlug/alert-rules/' + | '/organizations/$organizationIdOrSlug/alert-rules/$alertRuleId/' + | '/organizations/$organizationIdOrSlug/alert-rules/available-actions/' + | '/organizations/$organizationIdOrSlug/api-keys/' + | '/organizations/$organizationIdOrSlug/api-keys/$apiKeyId/' + | '/organizations/$organizationIdOrSlug/artifactbundle/assemble/' + | '/organizations/$organizationIdOrSlug/audit-logs/' + | '/organizations/$organizationIdOrSlug/auth-provider/' + | '/organizations/$organizationIdOrSlug/auth-providers/' + | '/organizations/$organizationIdOrSlug/available-actions/' + | '/organizations/$organizationIdOrSlug/avatar/' + | '/organizations/$organizationIdOrSlug/broadcasts/' + | '/organizations/$organizationIdOrSlug/builtin-symbol-sources/' + | '/organizations/$organizationIdOrSlug/chunk-upload/' + | '/organizations/$organizationIdOrSlug/code-mappings/' + | '/organizations/$organizationIdOrSlug/code-mappings/$configId/' + | '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/' + | '/organizations/$organizationIdOrSlug/codeowners-associations/' + | '/organizations/$organizationIdOrSlug/combined-rules/' + | '/organizations/$organizationIdOrSlug/config/integrations/' + | '/organizations/$organizationIdOrSlug/config/repos/' + | '/organizations/$organizationIdOrSlug/dashboards/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/favorite/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/visit/' + | '/organizations/$organizationIdOrSlug/dashboards/starred/' + | '/organizations/$organizationIdOrSlug/dashboards/starred/order/' + | '/organizations/$organizationIdOrSlug/dashboards/widgets/' + | '/organizations/$organizationIdOrSlug/data-conditions/' + | '/organizations/$organizationIdOrSlug/data-export/' + | '/organizations/$organizationIdOrSlug/data-export/$dataExportId/' + | '/organizations/$organizationIdOrSlug/data-scrubbing-selector-suggestions/' + | '/organizations/$organizationIdOrSlug/derive-code-mappings/' + | '/organizations/$organizationIdOrSlug/detector-types/' + | '/organizations/$organizationIdOrSlug/detector-workflow/' + | '/organizations/$organizationIdOrSlug/detector-workflow/$detectorWorkflowId/' + | '/organizations/$organizationIdOrSlug/detectors/' + | '/organizations/$organizationIdOrSlug/detectors/$detectorId/' + | '/organizations/$organizationIdOrSlug/detectors/count/' + | '/organizations/$organizationIdOrSlug/discover/homepage/' + | '/organizations/$organizationIdOrSlug/discover/saved/' + | '/organizations/$organizationIdOrSlug/discover/saved/$queryId/' + | '/organizations/$organizationIdOrSlug/discover/saved/$queryId/visit/' + | '/organizations/$organizationIdOrSlug/dynamic-sampling/custom-rules/' + | '/organizations/$organizationIdOrSlug/environments/' + | '/organizations/$organizationIdOrSlug/eventids/$eventId/' + | '/organizations/$organizationIdOrSlug/events-facets-performance-histogram/' + | '/organizations/$organizationIdOrSlug/events-facets-performance/' + | '/organizations/$organizationIdOrSlug/events-facets/' + | '/organizations/$organizationIdOrSlug/events-has-measurements/' + | '/organizations/$organizationIdOrSlug/events-histogram/' + | '/organizations/$organizationIdOrSlug/events-meta/' + | '/organizations/$organizationIdOrSlug/events-root-cause-analysis/' + | '/organizations/$organizationIdOrSlug/events-span-ops/' + | '/organizations/$organizationIdOrSlug/events-spans-histogram/' + | '/organizations/$organizationIdOrSlug/events-spans-performance/' + | '/organizations/$organizationIdOrSlug/events-spans-stats/' + | '/organizations/$organizationIdOrSlug/events-spans/' + | '/organizations/$organizationIdOrSlug/events-stats/' + | '/organizations/$organizationIdOrSlug/events-timeseries/' + | '/organizations/$organizationIdOrSlug/events-trace-light/$traceId/' + | '/organizations/$organizationIdOrSlug/events-trace-meta/$traceId/' + | '/organizations/$organizationIdOrSlug/events-trace/$traceId/' + | '/organizations/$organizationIdOrSlug/events-trends-stats/' + | '/organizations/$organizationIdOrSlug/events-trends-statsv2/' + | '/organizations/$organizationIdOrSlug/events-trends/' + | '/organizations/$organizationIdOrSlug/events-vitals/' + | '/organizations/$organizationIdOrSlug/events/' + | '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/' + | '/organizations/$organizationIdOrSlug/events/anomalies/' + | '/organizations/$organizationIdOrSlug/experimental/projects/' + | '/organizations/$organizationIdOrSlug/explore/saved/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/starred/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/visit/' + | '/organizations/$organizationIdOrSlug/explore/saved/starred/order/' + | '/organizations/$organizationIdOrSlug/external-users/' + | '/organizations/$organizationIdOrSlug/external-users/$externalUserId/' + | '/organizations/$organizationIdOrSlug/feedback-categories/' + | '/organizations/$organizationIdOrSlug/feedback-summary/' + | '/organizations/$organizationIdOrSlug/flags/hooks/provider/$provider/' + | '/organizations/$organizationIdOrSlug/flags/logs/' + | '/organizations/$organizationIdOrSlug/flags/logs/$flagLogId/' + | '/organizations/$organizationIdOrSlug/flags/signing-secrets/' + | '/organizations/$organizationIdOrSlug/flags/signing-secrets/$signingSecretId/' + | '/organizations/$organizationIdOrSlug/fork/' + | '/organizations/$organizationIdOrSlug/group-search-views/' + | '/organizations/$organizationIdOrSlug/group-search-views/$viewId/' + | '/organizations/$organizationIdOrSlug/group-search-views/$viewId/starred/' + | '/organizations/$organizationIdOrSlug/group-search-views/$viewId/visit/' + | '/organizations/$organizationIdOrSlug/group-search-views/starred/' + | '/organizations/$organizationIdOrSlug/group-search-views/starred/order/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/activities/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/attachments/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/autofix/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/autofix/setup/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/autofix/update/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/comments/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/comments/$noteId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/current-release/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/events/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/events/$eventId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/external-issues/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/external-issues/$externalIssueId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/first-last-release/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/hashes/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/integrations/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/integrations/$integrationId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/notes/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/notes/$noteId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/asana/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/asana/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/asana/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/asana/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/bitbucket/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/bitbucket/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/bitbucket/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/bitbucket/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/github/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/github/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/github/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/github/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/gitlab/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/gitlab/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/gitlab/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/jira/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/jira/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/jira/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/jira/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/pivotal/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/pivotal/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/pivotal/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/pivotal/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/trello/autocomplete' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/trello/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/trello/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/trello/options' + | '/organizations/$organizationIdOrSlug/groups/$issueId/plugins/trello/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/reprocessing/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/similar-issues-embeddings/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/similar/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/stats/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/summarize/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/suspect/flags/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/suspect/tags/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/tags/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/tags/$key/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/user-feedback/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/user-reports/' + | '/organizations/$organizationIdOrSlug/incidents/' + | '/organizations/$organizationIdOrSlug/incidents/$incidentIdentifier/' + | '/organizations/$organizationIdOrSlug/insights/starred-segments/' + | '/organizations/$organizationIdOrSlug/insights/tree/' + | '/organizations/$organizationIdOrSlug/integration-requests/' + | '/organizations/$organizationIdOrSlug/integrations/' + | '/organizations/$organizationIdOrSlug/integrations/$integrationId/' + | '/organizations/$organizationIdOrSlug/integrations/$integrationId/issues/' + | '/organizations/$organizationIdOrSlug/integrations/$integrationId/migrate-opsgenie/' + | '/organizations/$organizationIdOrSlug/integrations/$integrationId/repos/' + | '/organizations/$organizationIdOrSlug/integrations/$integrationId/serverless-functions/' + | '/organizations/$organizationIdOrSlug/integrations/coding-agents/' + | '/organizations/$organizationIdOrSlug/invite-requests/' + | '/organizations/$organizationIdOrSlug/invite-requests/$memberId/' + | '/organizations/$organizationIdOrSlug/invited-members/' + | '/organizations/$organizationIdOrSlug/invited-members/$memberInviteId/' + | '/organizations/$organizationIdOrSlug/invited-members/$memberInviteId/reinvite/' + | '/organizations/$organizationIdOrSlug/issues-count/' + | '/organizations/$organizationIdOrSlug/issues-metrics/' + | '/organizations/$organizationIdOrSlug/issues-stats/' + | '/organizations/$organizationIdOrSlug/issues/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/activities/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/attachments/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/autofix/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/autofix/setup/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/autofix/update/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/comments/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/comments/$noteId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/current-release/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/events/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/events/$eventId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/external-issues/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/external-issues/$externalIssueId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/first-last-release/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/hashes/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/integrations/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/integrations/$integrationId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/notes/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/notes/$noteId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/asana/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/asana/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/asana/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/asana/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/bitbucket/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/bitbucket/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/bitbucket/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/bitbucket/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/github/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/github/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/github/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/github/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/gitlab/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/gitlab/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/gitlab/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/jira/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/jira/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/jira/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/jira/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/pivotal/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/pivotal/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/pivotal/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/pivotal/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/trello/autocomplete' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/trello/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/trello/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/trello/options' + | '/organizations/$organizationIdOrSlug/issues/$issueId/plugins/trello/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/reprocessing/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/similar-issues-embeddings/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/similar/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/stats/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/summarize/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/suspect/flags/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/suspect/tags/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/tags/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/tags/$key/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/user-feedback/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/user-reports/' + | '/organizations/$organizationIdOrSlug/join-request/' + | '/organizations/$organizationIdOrSlug/key-transactions-list/' + | '/organizations/$organizationIdOrSlug/key-transactions/' + | '/organizations/$organizationIdOrSlug/measurements-meta/' + | '/organizations/$organizationIdOrSlug/members/' + | '/organizations/$organizationIdOrSlug/members/$memberId/' + | '/organizations/$organizationIdOrSlug/members/$memberId/teams/$teamIdOrSlug/' + | '/organizations/$organizationIdOrSlug/metrics-compatibility-sums/' + | '/organizations/$organizationIdOrSlug/metrics-compatibility/' + | '/organizations/$organizationIdOrSlug/metrics-estimation-stats/' + | '/organizations/$organizationIdOrSlug/metrics/data/' + | '/organizations/$organizationIdOrSlug/missing-members/' + | '/organizations/$organizationIdOrSlug/monitors-count/' + | '/organizations/$organizationIdOrSlug/monitors-schedule-data/' + | '/organizations/$organizationIdOrSlug/monitors-stats/' + | '/organizations/$organizationIdOrSlug/monitors/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/checkins/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/environments/$environment' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/stats/' + | '/organizations/$organizationIdOrSlug/notifications/actions/' + | '/organizations/$organizationIdOrSlug/notifications/actions/$actionId/' + | '/organizations/$organizationIdOrSlug/notifications/available-actions/' + | '/organizations/$organizationIdOrSlug/onboarding-continuation-email/' + | '/organizations/$organizationIdOrSlug/onboarding-tasks/' + | '/organizations/$organizationIdOrSlug/ondemand-rules-stats/' + | '/organizations/$organizationIdOrSlug/open-periods/' + | '/organizations/$organizationIdOrSlug/org-auth-tokens/' + | '/organizations/$organizationIdOrSlug/org-auth-tokens/$tokenId/' + | '/organizations/$organizationIdOrSlug/pinned-searches/' + | '/organizations/$organizationIdOrSlug/plugins/' + | '/organizations/$organizationIdOrSlug/plugins/$pluginSlug/deprecation-info/' + | '/organizations/$organizationIdOrSlug/plugins/configs/' + | '/organizations/$organizationIdOrSlug/prevent/github/repos/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/sync/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/tokens/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/branches/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/test-results-aggregates/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/test-results/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/test-suites/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repository/$repository/token/regenerate/' + | '/organizations/$organizationIdOrSlug/processing-errors/' + | '/organizations/$organizationIdOrSlug/profiling/chunks/' + | '/organizations/$organizationIdOrSlug/profiling/flamegraph/' + | '/organizations/$organizationIdOrSlug/profiling/function-trends/' + | '/organizations/$organizationIdOrSlug/profiling/has-chunks/' + | '/organizations/$organizationIdOrSlug/project-templates/' + | '/organizations/$organizationIdOrSlug/project-templates/$templateId/' + | '/organizations/$organizationIdOrSlug/project-transaction-threshold-override/' + | '/organizations/$organizationIdOrSlug/projects-count/' + | '/organizations/$organizationIdOrSlug/projects/' + | '/organizations/$organizationIdOrSlug/prompts-activity/' + | '/organizations/$organizationIdOrSlug/recent-searches/' + | '/organizations/$organizationIdOrSlug/region/' + | '/organizations/$organizationIdOrSlug/related-issues/' + | '/organizations/$organizationIdOrSlug/relay_usage/' + | '/organizations/$organizationIdOrSlug/release-threshold-statuses/' + | '/organizations/$organizationIdOrSlug/release-thresholds/' + | '/organizations/$organizationIdOrSlug/releases/' + | '/organizations/$organizationIdOrSlug/releases/$version/' + | '/organizations/$organizationIdOrSlug/releases/$version/assemble/' + | '/organizations/$organizationIdOrSlug/releases/$version/commitfiles/' + | '/organizations/$organizationIdOrSlug/releases/$version/commits/' + | '/organizations/$organizationIdOrSlug/releases/$version/deploys/' + | '/organizations/$organizationIdOrSlug/releases/$version/files/' + | '/organizations/$organizationIdOrSlug/releases/$version/files/$fileId/' + | '/organizations/$organizationIdOrSlug/releases/$version/meta/' + | '/organizations/$organizationIdOrSlug/releases/$version/previous-with-commits/' + | '/organizations/$organizationIdOrSlug/releases/$version/resolved/' + | '/organizations/$organizationIdOrSlug/releases/stats/' + | '/organizations/$organizationIdOrSlug/replay-count/' + | '/organizations/$organizationIdOrSlug/replay-selectors/' + | '/organizations/$organizationIdOrSlug/replays-events-meta/' + | '/organizations/$organizationIdOrSlug/replays/' + | '/organizations/$organizationIdOrSlug/replays/$replayId/' + | '/organizations/$organizationIdOrSlug/repos/' + | '/organizations/$organizationIdOrSlug/repos/$repoId/' + | '/organizations/$organizationIdOrSlug/repos/$repoId/commits/' + | '/organizations/$organizationIdOrSlug/request-project-creation/' + | '/organizations/$organizationIdOrSlug/sampling/admin-metrics/' + | '/organizations/$organizationIdOrSlug/sampling/effective-sample-rate/' + | '/organizations/$organizationIdOrSlug/sampling/project-rates/' + | '/organizations/$organizationIdOrSlug/sampling/project-root-counts/' + | '/organizations/$organizationIdOrSlug/scim/v2/Groups' + | '/organizations/$organizationIdOrSlug/scim/v2/Groups/$teamIdOrSlug' + | '/organizations/$organizationIdOrSlug/scim/v2/Schemas' + | '/organizations/$organizationIdOrSlug/scim/v2/Users' + | '/organizations/$organizationIdOrSlug/scim/v2/Users/$memberId' + | '/organizations/$organizationIdOrSlug/sdk-deprecations/' + | '/organizations/$organizationIdOrSlug/sdk-updates/' + | '/organizations/$organizationIdOrSlug/sdks/' + | '/organizations/$organizationIdOrSlug/searches/' + | '/organizations/$organizationIdOrSlug/searches/$searchId/' + | '/organizations/$organizationIdOrSlug/seer/explorer-chat/$runId' + | '/organizations/$organizationIdOrSlug/seer/setup-check/' + | '/organizations/$organizationIdOrSlug/sent-first-event/' + | '/organizations/$organizationIdOrSlug/sentry-app-components/' + | '/organizations/$organizationIdOrSlug/sentry-app-installations/' + | '/organizations/$organizationIdOrSlug/sentry-apps/' + | '/organizations/$organizationIdOrSlug/sessions/' + | '/organizations/$organizationIdOrSlug/shared/groups/$shareId/' + | '/organizations/$organizationIdOrSlug/shared/issues/$shareId/' + | '/organizations/$organizationIdOrSlug/shortids/$issueId/' + | '/organizations/$organizationIdOrSlug/spans-samples/' + | '/organizations/$organizationIdOrSlug/spans/fields/' + | '/organizations/$organizationIdOrSlug/spans/fields/$key/values/' + | '/organizations/$organizationIdOrSlug/spans/fields/stats/' + | '/organizations/$organizationIdOrSlug/stats-summary/' + | '/organizations/$organizationIdOrSlug/stats/' + | '/organizations/$organizationIdOrSlug/stats_v2/' + | '/organizations/$organizationIdOrSlug/tags/' + | '/organizations/$organizationIdOrSlug/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/teams/' + | '/organizations/$organizationIdOrSlug/test-fire-actions/' + | '/organizations/$organizationIdOrSlug/trace-explorer-ai/query/' + | '/organizations/$organizationIdOrSlug/trace-explorer-ai/setup/' + | '/organizations/$organizationIdOrSlug/trace-items/attributes/' + | '/organizations/$organizationIdOrSlug/trace-items/attributes/$key/values/' + | '/organizations/$organizationIdOrSlug/trace-items/attributes/ranked/' + | '/organizations/$organizationIdOrSlug/trace-logs/' + | '/organizations/$organizationIdOrSlug/trace-meta/$traceId/' + | '/organizations/$organizationIdOrSlug/trace-summary/' + | '/organizations/$organizationIdOrSlug/trace/$traceId/' + | '/organizations/$organizationIdOrSlug/traces/' + | '/organizations/$organizationIdOrSlug/unsubscribe/issue/$id/' + | '/organizations/$organizationIdOrSlug/unsubscribe/project/$id/' + | '/organizations/$organizationIdOrSlug/uptime-count/' + | '/organizations/$organizationIdOrSlug/uptime-stats/' + | '/organizations/$organizationIdOrSlug/uptime-summary/' + | '/organizations/$organizationIdOrSlug/uptime/' + | '/organizations/$organizationIdOrSlug/user-feedback/' + | '/organizations/$organizationIdOrSlug/user-teams/' + | '/organizations/$organizationIdOrSlug/users/' + | '/organizations/$organizationIdOrSlug/users/$userId/' + | '/organizations/$organizationIdOrSlug/workflows/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/group-history/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/stats/' + | '/projects/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rule-task/$taskUuid/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/$alertRuleId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/$ruleId/snooze/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-bundles/$bundleId/files/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-bundles/$bundleId/files/$fileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-lookup/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/cluster-transaction-names/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/codeowners/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/codeowners/$codeownersId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/commits/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/create-sample-transaction/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/create-sample/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/environments/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/environments/$environment/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/actionable-items/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/apple-crash-report' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/attachments/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/attachments/$attachmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/committers/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/grouping-info/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/json/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/owners/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/reprocessable/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/source-map-debug-blue-thunder-edition/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/source-map-debug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/artifact-bundles/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/difs/assemble/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/dsyms/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/dsyms/associate/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/dsyms/unknown/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/installablepreprodartifact/$urlPath/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$headArtifactId/size-analysis/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/assemble/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/proguard-artifact-releases' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/files/source-maps/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/filters/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/filters/$filterId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/grouping-configs/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/groups/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/groups/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/$hookId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/$hookId/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/$keyId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/$keyId/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/members/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/monitors/$monitorIdOrSlug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/monitors/$monitorIdOrSlug/checkins/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/monitors/$monitorIdOrSlug/environments/$environment' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/monitors/$monitorIdOrSlug/processing-errors/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/monitors/$monitorIdOrSlug/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/overview/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/ownership/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/performance-issues/configure/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/performance/configure/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/plugins/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/plugins/$pluginId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$headArtifactId/build-details/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$headArtifactId/delete/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$headArtifactId/install-details/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/check-for-updates/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/list-builds/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/size-analysis/compare/$headArtifactId/$baseArtifactId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/size-analysis/compare/$headSizeMetricId/$baseSizeMetricId/download/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/$uuid/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/profiling/profiles/$profileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/profiling/raw_chunks/$profilerId/$chunkId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/profiling/raw_profiles/$profileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/release-thresholds/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/release-thresholds/$releaseThreshold/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/commits/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/files/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/files/$fileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/repositories/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/resolved/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/completion/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/token/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/clicks/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/recording-segments/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/recording-segments/$segmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/summarize/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/videos/$segmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/jobs/delete/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/jobs/delete/$jobId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/repo-path-parsing/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/reprocessing/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rule-actions/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rule-task/$taskUuid/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/$ruleId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/$ruleId/enable/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/$ruleId/group-history/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/$ruleId/snooze/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/$ruleId/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/configuration/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/preview/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/seer/preferences/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/stacktrace-coverage/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/stacktrace-link/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/statistical-detector/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/symbol-sources/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tags/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tags/$key/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tags/$key/values/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/teams/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/teams/$teamIdOrSlug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tempest-credentials/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tempest-credentials/$tempestCredentialsId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tombstones/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tombstones/$tombstoneId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/trace-items/$itemId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/transaction-threshold/configure/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/transfer/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/$uptimeDetectorId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/$uptimeDetectorId/checks/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-feedback/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-issue/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-reports/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/users/' + | '/projects/$organizationIdOrSlug/pr-comments/$repoName/$prNumber/' + | '/projects/$organizationIdOrSlug/pullrequest-details/$repoName/$prNumber/' + | '/prompts-activity/' + | '/publickeys/relocations/' + | '/relays/' + | '/relays/$relayId/' + | '/relays/live/' + | '/relays/projectconfigs/' + | '/relays/projectids/' + | '/relays/publickeys/' + | '/relays/register/challenge/' + | '/relays/register/response/' + | '/relocations/' + | '/relocations/$relocationUuid/' + | '/relocations/$relocationUuid/abort/' + | '/relocations/$relocationUuid/artifacts/' + | '/relocations/$relocationUuid/artifacts/$artifactKind/$fileName' + | '/relocations/$relocationUuid/cancel/' + | '/relocations/$relocationUuid/pause/' + | '/relocations/$relocationUuid/recover/' + | '/relocations/$relocationUuid/retry/' + | '/relocations/$relocationUuid/unpause/' + | '/reporting-api-experiment/' + | '/secret-scanning/github/' + | '/sentry-app-installations/$uuid/' + | '/sentry-app-installations/$uuid/authorizations/' + | '/sentry-app-installations/$uuid/external-issue-actions/' + | '/sentry-app-installations/$uuid/external-issues/' + | '/sentry-app-installations/$uuid/external-issues/$externalIssueId/' + | '/sentry-app-installations/$uuid/external-requests/' + | '/sentry-app-installations/$uuid/service-hook-projects/' + | '/sentry-apps-stats/' + | '/sentry-apps/' + | '/sentry-apps/$sentryAppIdOrSlug/' + | '/sentry-apps/$sentryAppIdOrSlug/api-tokens/' + | '/sentry-apps/$sentryAppIdOrSlug/api-tokens/$apiTokenId/' + | '/sentry-apps/$sentryAppIdOrSlug/avatar/' + | '/sentry-apps/$sentryAppIdOrSlug/components/' + | '/sentry-apps/$sentryAppIdOrSlug/features/' + | '/sentry-apps/$sentryAppIdOrSlug/interaction/' + | '/sentry-apps/$sentryAppIdOrSlug/publish-request/' + | '/sentry-apps/$sentryAppIdOrSlug/requests/' + | '/sentry-apps/$sentryAppIdOrSlug/rotate-secret/' + | '/sentry-apps/$sentryAppIdOrSlug/stats/' + | '/sentry-apps/$sentryAppIdOrSlug/webhook-requests/' + | '/shared/groups/$shareId/' + | '/shared/issues/$shareId/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/alerts-triggered-index/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/alerts-triggered/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/all-unresolved-issues/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/external-teams/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/external-teams/$externalTeamId/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/issue-breakdown/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/issues/old/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/members/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/projects/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/release-count/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/stats/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/time-to-resolution/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/unresolved-issue-age/' + | '/tempest-ips/' + | '/uptime-ips/' + | '/userroles/' + | '/userroles/$roleName/' + | '/users/' + | '/users/$userId/' + | '/users/$userId/authenticators/' + | '/users/$userId/authenticators/$authId/' + | '/users/$userId/authenticators/$authId/$interfaceDeviceId/' + | '/users/$userId/authenticators/$interfaceId/enroll/' + | '/users/$userId/avatar/' + | '/users/$userId/emails/' + | '/users/$userId/emails/confirm/' + | '/users/$userId/identities/' + | '/users/$userId/identities/$identityId/' + | '/users/$userId/ips/' + | '/users/$userId/notification-options/' + | '/users/$userId/notification-options/$notificationOptionId/' + | '/users/$userId/notification-providers/' + | '/users/$userId/notifications/' + | '/users/$userId/notifications/email/' + | '/users/$userId/organization-integrations/' + | '/users/$userId/organizations/' + | '/users/$userId/password/' + | '/users/$userId/permissions/' + | '/users/$userId/permissions/$permissionName/' + | '/users/$userId/permissions/config/' + | '/users/$userId/regions/' + | '/users/$userId/roles/' + | '/users/$userId/roles/$roleName/' + | '/users/$userId/subscriptions/' + | '/users/$userId/user-identities/' + | '/users/$userId/user-identities/$category/$identityId/' + | '/wizard/' + | '/wizard/$wizardHash/'; diff --git a/static/app/utils/useRelease.tsx b/static/app/utils/useRelease.tsx index 861eaaa9c8d35a..fe075eeb4726fe 100644 --- a/static/app/utils/useRelease.tsx +++ b/static/app/utils/useRelease.tsx @@ -1,7 +1,7 @@ import {useQuery} from '@tanstack/react-query'; -import {apiOptions} from 'sentry/api/apiOptions'; import type {Release} from 'sentry/types/release'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; export function useRelease({ orgSlug, @@ -16,12 +16,12 @@ export function useRelease({ }) { return useQuery({ ...apiOptions.as()( - '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/', { path: { - orgSlug, - projectSlug, - releaseVersion, + organizationIdOrSlug: orgSlug, + projectIdOrSlug: projectSlug, + version: releaseVersion, }, staleTime: Infinity, } diff --git a/static/gsAdmin/views/billingPlans.tsx b/static/gsAdmin/views/billingPlans.tsx index 7393549cb38d5c..8d33386bef29ed 100644 --- a/static/gsAdmin/views/billingPlans.tsx +++ b/static/gsAdmin/views/billingPlans.tsx @@ -1,13 +1,13 @@ import styled from '@emotion/styled'; import {useQuery} from '@tanstack/react-query'; -import {apiOptions} from 'sentry/api/apiOptions'; import {Badge} from 'sentry/components/core/badge'; import {Button} from 'sentry/components/core/button'; import Panel from 'sentry/components/panels/panel'; import {IconDownload} from 'sentry/icons'; import {space} from 'sentry/styles/space'; import type {DataCategory} from 'sentry/types/core'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import ResultTable from 'admin/components/resultTable'; import formatCurrency from 'getsentry/utils/formatCurrency'; diff --git a/tests/tools/test_api_urls_to_typescript.py b/tests/tools/test_api_urls_to_typescript.py new file mode 100644 index 00000000000000..dc5532d5ab210a --- /dev/null +++ b/tests/tools/test_api_urls_to_typescript.py @@ -0,0 +1,41 @@ +import pytest + +from tools.api_urls_to_typescript import regexp_to_routes + + +@pytest.mark.parametrize( + ("input_regexp", "expected"), + ( + # Basic named group + (r"^(?P[^/]+)/plugins?/$", ["$issueId/plugins/"]), + # Multiple named groups + ( + r"^api/v1/(?P[^/]+)/(?P[^/]+)/$", + ["api/v1/$orgSlug/$projectId/"], + ), + # Alternates + (r"/(?:issues|groups)/", ["/issues/", "/groups/"]), + # Complex alternate with named groups + ( + r"^api/(?P[^/]+)/(?:issues|groups)/(?P[^/]+)/$", + ["api/$version/issues/$id/", "api/$version/groups/$id/"], + ), + # Simple pattern without named groups + (r"^api/health/$", ["api/health/"]), + # Pattern with optional parts + (r"^api/v1/(?P[^/]+)/?$", ["api/v1/$orgSlug/"]), + # Complex pattern with nested groups and character classes + ( + r"^(?P[^/]+)/(?P[^/]+)/events/(?P(?:\d+|[A-Fa-f0-9]{32}))/$", + ["$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/"], + ), + # Multiple named groups per url segment + ( + r"^(?P[^/]+)/events/(?P[^/]+):(?P(?:\d+|[A-Fa-f0-9-]{32,36}))/$", + ["$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/"], + ), + ), +) +def test_regexp_to_route(input_regexp, expected) -> None: + result = regexp_to_routes(input_regexp) + assert result == expected diff --git a/tools/api_urls_to_typescript.py b/tools/api_urls_to_typescript.py new file mode 100644 index 00000000000000..cb47ba39b707fc --- /dev/null +++ b/tools/api_urls_to_typescript.py @@ -0,0 +1,187 @@ +# flake8: noqa: S002 + +import re +from typing import Any + +from django.urls import URLPattern, URLResolver + + +def snake_to_camel_case(value: str) -> str: + """ + Converts a string from snake_case to camelCase + """ + words = value.strip("_").split("_") + return words[0].lower() + "".join(word.capitalize() for word in words[1:]) + + +def urls_to_routes(prefix: str, urlpatterns: list[Any]) -> list[str]: + routes = [] + for urlpattern in urlpatterns: + if isinstance(urlpattern, URLResolver): + children = regexp_to_routes(urlpattern.pattern.regex.pattern) + for child in children: + routes += urls_to_routes(prefix + child, urlpattern.url_patterns) + elif isinstance(urlpattern, URLPattern): + variants = regexp_to_routes(urlpattern.pattern.regex.pattern) + routes += [prefix + variant for variant in variants] + else: + raise ValueError(f"Unknown pattern type: {type(urlpattern)}") + return routes + + +def regexp_to_routes(regexp_string: str) -> list[str]: + """ + Convert a regexp string to a route-style string. + + Handles: + - Stripping ^ and $ from start/end + - Converting named groups like (?Ppattern) to :name + - Breaking out alternates like (?:a|b) into separate routes + + Args: + regexp_string: The regexp pattern to convert + + Returns: + Either a single route string or a list of route strings for alternates + + Examples: + >>> regexp_to_route(r'^(?P[^/]+)/plugins?/$') + ':issue_id/plugins/' + >>> regexp_to_route(r'/(?:issues|groups)/') + ['/issues/', '/groups/'] + >>> regexp_to_route(r'^api/v1/(?P[^/]+)/$') + 'api/v1/:org_slug/' + """ + # Strip ^ and $ from start and end + pattern = regexp_string.strip("^$") + + # Check for alternates (non-capturing groups with |) that are NOT inside named groups + alternate_pattern = r"\((?:\?\:([^)]+))\)" + + # Find all matches and check if they're inside named groups + matches = list(re.finditer(alternate_pattern, pattern)) + + # Filter out matches that are inside named groups + valid_alternates = [] + for match in matches: + # Check if this match is inside a named group by looking backwards + start_pos = match.start() + in_named_group = False + + # Look backwards for the most recent unclosed (?P< + i = start_pos - 1 + paren_count = 0 + while i >= 0: + if pattern[i] == ")": + paren_count += 1 + elif pattern[i] == "(": + if paren_count == 0: + # Check if this is a named group by looking for (?P< + if i + 2 < len(pattern) and pattern[i : i + 3] == "(?P": + # Look ahead to see if there's a < after the P + if i + 3 < len(pattern) and pattern[i + 3] == "<": + in_named_group = True + break + # Only decrement paren_count if we didn't find a named group + if not in_named_group: + paren_count -= 1 + i -= 1 + + if not in_named_group: + valid_alternates.append(match) + + if valid_alternates: + # Use the first valid alternate + alternate_match = valid_alternates[0] + # Extract the alternatives + alternatives = alternate_match.group(1).split("|") + + # Process each alternative + routes = [] + for alt in alternatives: + # Clean up the alternative + alt = alt.strip() + + # Create the full pattern by replacing the alternate group with this alternative + full_pattern = pattern.replace(alternate_match.group(0), alt) + route = _process_single_pattern(full_pattern) + routes.append(route) + return routes + return [_process_single_pattern(pattern)] + + +def _process_single_pattern(pattern: str) -> str: + """Process a single regexp pattern into a route string.""" + # Convert named groups (?Ppattern) to :name + # This regex matches (?P...) and captures the name, handling nested groups + named_group_pattern = r"\(\?P<([^>]+)>([^)]*(?:\([^)]*\)[^)]*)*)\)" + + def replace_named_group(match: re.Match[str]) -> str: + group_name = snake_to_camel_case(match.group(1)) + return f"${group_name}" + + # First, clean up nested patterns inside named groups + # Remove non-capturing groups that aren't alternates (already handled) + # This handles patterns like (?:\d+|[A-Fa-f0-9]{32}) + pattern = re.sub(r"\(\?\:[^)]+\)", "", pattern) + + # Remove quantifiers like {32} + pattern = re.sub(r"\{[^}]+\}", "", pattern) + + # Remove character classes like [^/]+ and replace with just the placeholder + pattern = re.sub(r"\[[^\]]+\]\+?", "", pattern) + + # Replace all named groups (including those with nested patterns) + route = re.sub(named_group_pattern, replace_named_group, pattern) + + # Remove optional groups markers (but not the ? after characters) + route = re.sub(r"\(\?[^)]*\)", "", route) + + # Handle optional characters (like s? -> s) + route = re.sub(r"([^/])\?", r"\1", route) + + # Handle optional trailing slash + route = re.sub(r"/\?$", "/", route) + + # Clean up any double slashes that might have been created + route = re.sub(r"/+", "/", route) + + # Remove trailing slashes from the end if they were added by cleaning + if route.endswith("/") and not route.endswith("//"): + pass # Keep single trailing slash + elif route.endswith("//"): + route = route.rstrip("/") + "/" + + return route + + +if __name__ == "__main__": + import sys + + from sentry.runner import configure + + configure() + + from sentry.api.urls import urlpatterns + + route_patterns = sorted(set(urls_to_routes("/", urlpatterns))) + + command = len(sys.argv) > 1 and sys.argv[1] + if command == "list": + for route_pattern in route_patterns: + print(route_pattern) + else: + with open("static/app/utils/api/knownSentryApiUrls.generated.ts", "w") as f: + f.writelines( + [ + "/**\n", + " * GENERATED FILE. Do not edit manually.\n", + " * To update it run `python3 -m tools.api_urls_to_typescript`\n", + " *\n", + " * This file is the sibling to knownGetsentryApiUrls.ts.\n", + " */\n", + "\n", + "export type KnownSentryApiUrls =\n", + "\n".join([f" | '{r}'" for r in route_patterns]) + ";\n", + ] + )