From c46d51e5f42c0843185c3cc62b39a364060a3315 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 1 Aug 2025 14:18:19 +0200 Subject: [PATCH 01/15] feat: apiOptions --- static/app/api/apiDefinition.ts | 6 + static/app/api/apiOptions.spec.tsx | 259 +++++++++++++++++++++++++++++ static/app/api/apiOptions.ts | 67 ++++++++ static/app/api/getApiUrl.ts | 6 +- static/app/utils/useRelease.tsx | 24 +-- 5 files changed, 347 insertions(+), 15 deletions(-) create mode 100644 static/app/api/apiDefinition.ts create mode 100644 static/app/api/apiOptions.spec.tsx create mode 100644 static/app/api/apiOptions.ts diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts new file mode 100644 index 00000000000000..432389336cd5af --- /dev/null +++ b/static/app/api/apiDefinition.ts @@ -0,0 +1,6 @@ +type KnownApiUrls = ['/projects/$orgSlug/$projectSlug/releases/$releaseVersion/']; + +export type ApiMapping = Record; +export type ApiPath = keyof ApiMapping; +// adding a union with string & {} enables auto-completion +export type MaybeApiPath = ApiPath | (string & {}); diff --git a/static/app/api/apiOptions.spec.tsx b/static/app/api/apiOptions.spec.tsx new file mode 100644 index 00000000000000..d0d2300f2ca822 --- /dev/null +++ b/static/app/api/apiOptions.spec.tsx @@ -0,0 +1,259 @@ +import {useQuery} from '@tanstack/react-query'; +import {expectTypeOf} from 'expect-type'; + +import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary'; + +import type {ApiResult} from 'sentry/api'; +import { + DEFAULT_QUERY_CLIENT_CONFIG, + QueryClient, + QueryClientProvider, +} from 'sentry/utils/queryClient'; + +import {apiOptions, selectWithHeaders} from './apiOptions'; + +type Promisable = T | Promise; +type QueryFunctionResult = Promisable>; + +const wrapper = ({children}: {children?: React.ReactNode}) => ( + + {children} + +); + +describe('apiOptions', () => { + afterEach(() => { + MockApiClient.clearMockResponses(); + }); + test('should encode path parameters correctly', () => { + const options = apiOptions( + '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + { + staleTime: 0, + path: { + orgSlug: 'my-org', + projectSlug: 'my-project', + releaseVersion: 'v 1.0.0', + }, + } + ); + + expect(options.queryKey[0]).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); + }); + + test('should not modify already encoded parameters', () => { + const options = apiOptions('/search/$query/', { + staleTime: 0, + path: {query: 'test%20query'}, + }); + + expect(options.queryKey[0]).toBe('/search/test%20query/'); + }); + + test('should not include empty options in queryKey', () => { + const options = apiOptions('/projects/$id/', { + staleTime: 0, + path: {id: '123'}, + }); + + expect(options.queryKey).toEqual(['/projects/123/']); + }); + + test('should stringify number path params', () => { + const options = apiOptions('/items/$id/', { + staleTime: 0, + path: {id: 123}, + }); + + expect(options.queryKey[0]).toBe('/items/123/'); + }); + + test('should not do accidental replacements', () => { + const options = apiOptions('/projects/$id1/$id', { + staleTime: 0, + path: {id: '123', id1: '456'}, + }); + + expect(options.queryKey).toEqual(['/projects/456/123']); + }); + + test('should extract content data per default', async () => { + const options = apiOptions.as()('/projects/', { + staleTime: 0, + }); + + MockApiClient.addMockResponse({ + url: '/projects/', + body: ['Project 1', 'Project 2'], + }); + + const {result} = renderHook(() => useQuery(options), {wrapper}); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toEqual(['Project 1', 'Project 2']); + }); + + test('should extract headers', async () => { + const options = apiOptions.as()('/projects/', { + staleTime: 0, + }); + + MockApiClient.addMockResponse({ + url: '/projects/', + body: ['Project 1', 'Project 2'], + headers: { + Link: 'my-link', + 'X-Hits': 'some-hits', + }, + }); + + const {result} = renderHook( + () => + useQuery({...options, select: selectWithHeaders(['Link', 'X-Hits'] as const)}), + {wrapper} + ); + + await waitFor(() => result.current.isSuccess); + + expect(result.current.data).toEqual({ + content: ['Project 1', 'Project 2'], + headers: {Link: 'my-link', 'X-Hits': 'some-hits'}, + }); + + // headers should be narrowly typed + expectTypeOf(result.current.data!.headers).toEqualTypeOf<{ + Link: string | undefined; + 'X-Hits': string | undefined; + }>(); + }); + + describe('types', () => { + test('should infer types of known API paths', () => { + const options = apiOptions( + '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + { + staleTime: 0, + path: { + orgSlug: 'my-org', + projectSlug: 'my-project', + releaseVersion: 'v1.0.0', + }, + } + ); + + // todo: Replace `never` with the actual type of apiDefinition + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should always require staleTime', () => { + // @ts-expect-error staleTime is required + apiOptions('/projects/$orgSlug/', {path: {orgSlug: 'my-org'}}); + // @ts-expect-error staleTime is required + apiOptions('/projects/', {}); + }); + + test('should not allow invalid path parameters', () => { + const options = apiOptions('/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('/projects/$orgSlug/', { + staleTime: 0, + // @ts-expect-error Excess path parameter + path: {orgSlug: 'my-org', extraParam: 'extra'}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should require path params for paths with parameters', () => { + expect(() => { + const options = apiOptions('/projects/$orgSlug/', { + staleTime: 0, + // @ts-expect-error Missing required path parameter + path: {}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }).toThrow('Missing path param: orgSlug'); + }); + + test('should not allow empty path parameters for paths without parameters', () => { + const options = apiOptions('/projects/', { + staleTime: 0, + // @ts-expect-error Empty path parameters not allowed + path: {}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should not need path params for paths without parameters', () => { + const options = apiOptions('/projects/', { + staleTime: 0, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should allow string or number path parameters', () => { + const options = apiOptions('/items/$id/', { + staleTime: 0, + path: {id: 123}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + + const options2 = apiOptions('/items/$id/', { + staleTime: 0, + path: {id: 'abc'}, + }); + + expectTypeOf(options2.queryFn).returns.toEqualTypeOf>(); + }); + + test('should default to never for unknown API paths', () => { + const options = apiOptions('/unknown/$param/', { + staleTime: 0, + path: {param: 'value'}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should allow providing manual data type', () => { + const options = apiOptions.as()('/foo/$bar', { + staleTime: 0, + path: {bar: 'baz'}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should disallow path if there are no path params', () => { + const options = apiOptions.as()('/foo', { + staleTime: 0, + // @ts-expect-error Path is not allowed when there are no path params + path: {bar: 'baz'}, + }); + + expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); + }); + + test('should have a default select that extracts content', () => { + const options = apiOptions.as()('/items/$id/', { + staleTime: 0, + path: {id: 123}, + }); + + expectTypeOf(options.select).returns.toEqualTypeOf(); + }); + }); +}); diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts new file mode 100644 index 00000000000000..a1183fc5fa60d6 --- /dev/null +++ b/static/app/api/apiOptions.ts @@ -0,0 +1,67 @@ +import {queryOptions} from '@tanstack/react-query'; + +import type {ApiResult} from 'sentry/api'; +import {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; + +import type {ApiMapping, ApiPath, MaybeApiPath} from './apiDefinition'; +import { + type ExtractPathParams, + getApiUrl, + type OptionalPathParams, + type PathParamOptions, +} from './getApiUrl'; + +type Options = QueryKeyEndpointOptions & {staleTime: number}; + +const selectContent = (data: ApiResult) => data[0]; +export const selectWithHeaders = + (headers: THeaders) => + (data: ApiResult) => ({ + content: data[0], + headers: Object.fromEntries( + headers.flatMap(header => { + const value = data[2]?.getResponseHeader(header); + return value ? [[header, value]] : []; + }) + ) as Record, + }); + +export function apiOptions< + TManualData = never, + TApiPath extends MaybeApiPath = MaybeApiPath, + TActualData = [TApiPath] extends [ApiPath] ? ApiMapping[TApiPath] : TManualData, +>( + path: TApiPath, + ...[ + {staleTime, path: pathParams, ...options}, + ]: ExtractPathParams extends never + ? [Options & {path?: never}] + : [Options & PathParamOptions] +) { + const url = getApiUrl( + path, + ...((path + ? [ + { + path: pathParams, + }, + ] + : []) as OptionalPathParams) + ); + + return queryOptions({ + queryKey: + Object.keys(options).length > 0 ? ([url, options] as const) : ([url] as const), + queryFn: fetchDataQuery, + staleTime, + select: selectContent, + }); +} + +apiOptions.as = + () => + ( + path: TApiPath, + options: Options & PathParamOptions + ) => + apiOptions(path, options as never); diff --git a/static/app/api/getApiUrl.ts b/static/app/api/getApiUrl.ts index afb24a25547d4c..de52add1080eab 100644 --- a/static/app/api/getApiUrl.ts +++ b/static/app/api/getApiUrl.ts @@ -1,16 +1,16 @@ -type ExtractPathParams = +export type ExtractPathParams = TApiPath extends `${string}$${infer Param}/${infer Rest}` ? Param | ExtractPathParams<`/${Rest}`> : TApiPath extends `${string}$${infer Param}` ? Param : never; -type PathParamOptions = +export type PathParamOptions = ExtractPathParams extends never ? {path?: never} : {path: Record, string | number>}; -type OptionalPathParams = +export type OptionalPathParams = ExtractPathParams extends never ? [] // eslint-disable-line @typescript-eslint/no-restricted-types : [PathParamOptions]; diff --git a/static/app/utils/useRelease.tsx b/static/app/utils/useRelease.tsx index 7f8bc8889b3a53..861eaaa9c8d35a 100644 --- a/static/app/utils/useRelease.tsx +++ b/static/app/utils/useRelease.tsx @@ -1,6 +1,7 @@ -import {getApiUrl} from 'sentry/api/getApiUrl'; +import {useQuery} from '@tanstack/react-query'; + +import {apiOptions} from 'sentry/api/apiOptions'; import type {Release} from 'sentry/types/release'; -import {useApiQuery} from 'sentry/utils/queryClient'; export function useRelease({ orgSlug, @@ -13,19 +14,18 @@ export function useRelease({ releaseVersion: string; enabled?: boolean; }) { - return useApiQuery( - [ - getApiUrl('/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', { + return useQuery({ + ...apiOptions.as()( + '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + { path: { orgSlug, projectSlug, releaseVersion, }, - }), - ], - { - enabled, - staleTime: Infinity, - } - ); + staleTime: Infinity, + } + ), + enabled, + }); } From 7487078d94af938f58e1251e8a954e1f611060da Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 1 Aug 2025 14:19:52 +0200 Subject: [PATCH 02/15] fix: give url definition hints in getApiUrl --- static/app/api/getApiUrl.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/api/getApiUrl.ts b/static/app/api/getApiUrl.ts index de52add1080eab..90e9cba3d67fd6 100644 --- a/static/app/api/getApiUrl.ts +++ b/static/app/api/getApiUrl.ts @@ -1,3 +1,5 @@ +import type {MaybeApiPath} from './apiDefinition'; + export type ExtractPathParams = TApiPath extends `${string}$${infer Param}/${infer Rest}` ? Param | ExtractPathParams<`/${Rest}`> @@ -19,7 +21,7 @@ const paramRegex = /\$([a-zA-Z0-9_-]+)/g; type ApiUrl = string & {__apiUrl: true}; -export function getApiUrl( +export function getApiUrl( path: TApiPath, ...[options]: OptionalPathParams ): ApiUrl { From c8f8a85577ee6f34a9526e05c807239608548cc3 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 1 Aug 2025 15:05:32 +0200 Subject: [PATCH 03/15] fix: manual type annoation with .as<>() should be able to override known api url types --- static/app/api/apiDefinition.ts | 4 ++-- static/app/api/apiOptions.spec.tsx | 16 ++++++++++++++++ static/app/api/apiOptions.ts | 5 +++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts index 432389336cd5af..bac70481292950 100644 --- a/static/app/api/apiDefinition.ts +++ b/static/app/api/apiDefinition.ts @@ -1,6 +1,6 @@ type KnownApiUrls = ['/projects/$orgSlug/$projectSlug/releases/$releaseVersion/']; -export type ApiMapping = Record; -export type ApiPath = keyof ApiMapping; +type ApiMapping = Record; +type ApiPath = keyof ApiMapping; // adding a union with string & {} enables auto-completion export type MaybeApiPath = ApiPath | (string & {}); diff --git a/static/app/api/apiOptions.spec.tsx b/static/app/api/apiOptions.spec.tsx index d0d2300f2ca822..d8da45908ea99f 100644 --- a/static/app/api/apiOptions.spec.tsx +++ b/static/app/api/apiOptions.spec.tsx @@ -237,6 +237,22 @@ describe('apiOptions', () => { 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', { staleTime: 0, diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts index a1183fc5fa60d6..97fd7380be4b51 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -3,7 +3,7 @@ import {queryOptions} from '@tanstack/react-query'; import type {ApiResult} from 'sentry/api'; import {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; -import type {ApiMapping, ApiPath, MaybeApiPath} from './apiDefinition'; +import type {MaybeApiPath} from './apiDefinition'; import { type ExtractPathParams, getApiUrl, @@ -29,7 +29,8 @@ export const selectWithHeaders = export function apiOptions< TManualData = never, TApiPath extends MaybeApiPath = MaybeApiPath, - TActualData = [TApiPath] extends [ApiPath] ? ApiMapping[TApiPath] : TManualData, + // todo: infer the actual data type from the ApiMapping + TActualData = TManualData, >( path: TApiPath, ...[ From 02b88219ed1471024be1676e7739a437416c497d Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 1 Aug 2025 15:24:18 +0200 Subject: [PATCH 04/15] ref: we removed this "feature" --- static/app/api/apiOptions.spec.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/static/app/api/apiOptions.spec.tsx b/static/app/api/apiOptions.spec.tsx index d8da45908ea99f..83200d5c3169be 100644 --- a/static/app/api/apiOptions.spec.tsx +++ b/static/app/api/apiOptions.spec.tsx @@ -41,15 +41,6 @@ describe('apiOptions', () => { expect(options.queryKey[0]).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); }); - test('should not modify already encoded parameters', () => { - const options = apiOptions('/search/$query/', { - staleTime: 0, - path: {query: 'test%20query'}, - }); - - expect(options.queryKey[0]).toBe('/search/test%20query/'); - }); - test('should not include empty options in queryKey', () => { const options = apiOptions('/projects/$id/', { staleTime: 0, From e55d95a02e4b68592fc1b221e8d7e88ec3ec35c7 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Fri, 1 Aug 2025 15:48:29 +0200 Subject: [PATCH 05/15] ref: remove unnecessary path check the params are checked inside `getApiUrl`, so it's fine to pass `path:undefined` here --- static/app/api/apiOptions.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts index 97fd7380be4b51..3684e9c09a8a51 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -41,13 +41,11 @@ export function apiOptions< ) { const url = getApiUrl( path, - ...((path - ? [ - { - path: pathParams, - }, - ] - : []) as OptionalPathParams) + ...([ + { + path: pathParams, + }, + ] as OptionalPathParams) ); return queryOptions({ From 462c1ab90c9a7bad8f6dd588a805b32636d29f69 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Mon, 4 Aug 2025 11:39:51 +0200 Subject: [PATCH 06/15] api url generation --- .../commands/generate_ts_api_routes.py | 133 +++ static/app/api/apiDefinition.ts | 7 +- static/app/api/apiOptions.ts | 6 +- static/app/api/getApiUrl.ts | 4 +- static/app/api/knownUrls.ts | 883 ++++++++++++++++++ static/app/utils/useRelease.tsx | 20 +- 6 files changed, 1033 insertions(+), 20 deletions(-) create mode 100644 src/sentry/management/commands/generate_ts_api_routes.py create mode 100644 static/app/api/knownUrls.ts diff --git a/src/sentry/management/commands/generate_ts_api_routes.py b/src/sentry/management/commands/generate_ts_api_routes.py new file mode 100644 index 00000000000000..1343a8ef16f969 --- /dev/null +++ b/src/sentry/management/commands/generate_ts_api_routes.py @@ -0,0 +1,133 @@ +import re +import itertools +from importlib import import_module + +from django.core.management.base import BaseCommand +from django.urls import URLPattern, URLResolver +from django.conf import settings + + +# --- UTILS --- + +def snake_to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + ''.join(p.capitalize() for p in parts[1:]) + + +def final_cleanup(path: str) -> str: + path = path.replace(r"\/", "/") + path = re.sub(r"\([^)]*\)", "", path) # remove leftover groups + path = path.replace(".*", "") + path = re.sub(r"//+", "/", path) + path = "/" + path.strip("/") + + if not path.endswith("/"): + path += "/" + + return path + + +def convert_django_regex_to_ts_all(regex: str) -> list[str]: + regex = regex.strip("^$") + + # Replace named capturing groups (?P...) with $camelCaseName + def replace_named_group(match): + return f"${snake_to_camel(match.group(1))}" + + regex = re.sub(r"\(\?P<(\w+)>((?:[^()]+|\([^()]*\))*)\)", replace_named_group, regex) + + # Handle non-capturing groups (e.g. (?:x|y) → x and y) + pattern = r"\(\?:([^)]+)\)" + matches = list(re.finditer(pattern, regex)) + + # Also detect optional static segments like 'plugins?' + optional_pattern = r"([a-zA-Z0-9_-]+)\?" + matches += list(re.finditer(optional_pattern, regex)) + + if not matches: + return [final_cleanup(regex)] + + # Build route combinations + parts = [] + last_end = 0 + + for match in matches: + start, end = match.span() + parts.append([regex[last_end:start]]) + + if match.re.pattern == pattern: + parts.append(match.group(1).split("|")) + else: + token = match.group(1) + parts.append([token, ""]) # with or without the optional char + + last_end = end + + parts.append([regex[last_end:]]) + + combos = itertools.product(*parts) + return [final_cleanup("".join(c)) for c in combos] + + +def normalize_regex_part(regex: str) -> str: + return regex.strip("^$") + + +def extract_ts_routes(urlpatterns, prefix="") -> list[str]: + routes = [] + + for pattern in urlpatterns: + if isinstance(pattern, URLPattern): + raw_regex = normalize_regex_part(pattern.pattern.regex.pattern) + full_path = prefix + raw_regex + ts_paths = convert_django_regex_to_ts_all(full_path) + for path in ts_paths: + routes.append(f"'{path}'") + + elif isinstance(pattern, URLResolver): + new_prefix = prefix + normalize_regex_part(pattern.pattern.regex.pattern) + routes.extend(extract_ts_routes(pattern.url_patterns, new_prefix)) + + return routes + + +# --- COMMAND --- + +class Command(BaseCommand): + help = "Generate TypeScript route types from Django urlpatterns" + + def add_arguments(self, parser): + parser.add_argument( + "--output", + type=str, + default="routes.ts", + help="Output file for TypeScript route types", + ) + parser.add_argument( + "--urls", + type=str, + default=settings.ROOT_URLCONF, + help="Python path to root URLconf (default: settings.ROOT_URLCONF)", + ) + + def handle(self, *args, **options): + urls_module_path = options["urls"] + output_file = options["output"] + + self.stdout.write(f"🔍 Loading URLconf: {urls_module_path}") + urlconf = import_module(urls_module_path) + urlpatterns = getattr(urlconf, "urlpatterns", []) + + ts_routes = extract_ts_routes(urlpatterns) + ts_routes = sorted(set(ts_routes)) + + with open(output_file, "w") as f: + f.write("/* prettier-ignore */\n") + f.write("// Auto-generated TypeScript route types\n") + f.write("// To update it run `sentry django generate_ts_api_routes --urls sentry.api.urls --output=path/to/thisfile.ts`\n") + f.write("export type KnownApiUrls =\n") + for route in ts_routes: + f.write(f" | {route}\n") + f.write(";\n") + + self.stdout.write(self.style.SUCCESS(f"✅ Wrote {len(ts_routes)} routes to {output_file}")) diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts index bac70481292950..13e3620ac75450 100644 --- a/static/app/api/apiDefinition.ts +++ b/static/app/api/apiDefinition.ts @@ -1,6 +1,3 @@ -type KnownApiUrls = ['/projects/$orgSlug/$projectSlug/releases/$releaseVersion/']; +import type {KnownApiUrls} from './knownUrls'; -type ApiMapping = Record; -type ApiPath = keyof ApiMapping; -// adding a union with string & {} enables auto-completion -export type MaybeApiPath = ApiPath | (string & {}); +export type ApiPath = KnownApiUrls; diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts index 3684e9c09a8a51..48c9d526aad2e5 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -3,7 +3,7 @@ import {queryOptions} from '@tanstack/react-query'; import type {ApiResult} from 'sentry/api'; import {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; -import type {MaybeApiPath} from './apiDefinition'; +import type {ApiPath} from './apiDefinition'; import { type ExtractPathParams, getApiUrl, @@ -28,7 +28,7 @@ export const selectWithHeaders = export function apiOptions< TManualData = never, - TApiPath extends MaybeApiPath = MaybeApiPath, + TApiPath extends ApiPath = ApiPath, // todo: infer the actual data type from the ApiMapping TActualData = TManualData, >( @@ -59,7 +59,7 @@ export function apiOptions< apiOptions.as = () => - ( + ( path: TApiPath, options: Options & PathParamOptions ) => diff --git a/static/app/api/getApiUrl.ts b/static/app/api/getApiUrl.ts index 90e9cba3d67fd6..3b52b0a61664d5 100644 --- a/static/app/api/getApiUrl.ts +++ b/static/app/api/getApiUrl.ts @@ -1,4 +1,4 @@ -import type {MaybeApiPath} from './apiDefinition'; +import type {ApiPath} from './apiDefinition'; export type ExtractPathParams = TApiPath extends `${string}$${infer Param}/${infer Rest}` @@ -21,7 +21,7 @@ const paramRegex = /\$([a-zA-Z0-9_-]+)/g; type ApiUrl = string & {__apiUrl: true}; -export function getApiUrl( +export function getApiUrl( path: TApiPath, ...[options]: OptionalPathParams ): ApiUrl { diff --git a/static/app/api/knownUrls.ts b/static/app/api/knownUrls.ts new file mode 100644 index 00000000000000..3441108ea60bb5 --- /dev/null +++ b/static/app/api/knownUrls.ts @@ -0,0 +1,883 @@ +/* prettier-ignore */ +// Auto-generated TypeScript route types +// To update it run `sentry django generate_ts_api_routes --urls sentry.api.urls --output=path/to/thisfile.ts` +export type KnownApiUrls = + | '/' + | '/accept-invite/$memberId/$token/' + | '/accept-invite/$organizationIdOrSlug/$memberId/$token/' + | '/accept-transfer/' + | '/api-applications/$appId/' + | '/api-applications/$appId/rotate-secret/' + | '/api-applications/' + | '/api-authorizations/' + | '/api-tokens/$tokenId/' + | '/api-tokens/' + | '/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/$broadcastId/' + | '/broadcasts/' + | '/builtin-symbol-sources/' + | '/doc-integrations/$docIntegrationIdOrSlug/' + | '/doc-integrations/$docIntegrationIdOrSlug/avatar/' + | '/doc-integrations/' + | '/grouping-configs/' + | '/groups/$issueId/' + | '/groups/$issueId/activities/' + | '/groups/$issueId/asana/autocomplete/' + | '/groups/$issueId/asana/create/' + | '/groups/$issueId/asana/link/' + | '/groups/$issueId/asana/unlink/' + | '/groups/$issueId/attachments/' + | '/groups/$issueId/autofix/' + | '/groups/$issueId/autofix/setup/' + | '/groups/$issueId/autofix/update/' + | '/groups/$issueId/bitbucket/autocomplete/' + | '/groups/$issueId/bitbucket/create/' + | '/groups/$issueId/bitbucket/link/' + | '/groups/$issueId/bitbucket/unlink/' + | '/groups/$issueId/comments/$noteId/' + | '/groups/$issueId/comments/' + | '/groups/$issueId/current-release/' + | '/groups/$issueId/events/$eventId/' + | '/groups/$issueId/events/' + | '/groups/$issueId/external-issues/$externalIssueId/' + | '/groups/$issueId/external-issues/' + | '/groups/$issueId/first-last-release/' + | '/groups/$issueId/github/autocomplete/' + | '/groups/$issueId/github/create/' + | '/groups/$issueId/github/link/' + | '/groups/$issueId/github/unlink/' + | '/groups/$issueId/gitlab/create/' + | '/groups/$issueId/gitlab/link/' + | '/groups/$issueId/gitlab/unlink/' + | '/groups/$issueId/hashes/' + | '/groups/$issueId/integrations/$integrationId/' + | '/groups/$issueId/integrations/' + | '/groups/$issueId/jira/autocomplete/' + | '/groups/$issueId/jira/create/' + | '/groups/$issueId/jira/link/' + | '/groups/$issueId/jira/unlink/' + | '/groups/$issueId/notes/$noteId/' + | '/groups/$issueId/notes/' + | '/groups/$issueId/open-periods/' + | '/groups/$issueId/pivotal/autocomplete/' + | '/groups/$issueId/pivotal/create/' + | '/groups/$issueId/pivotal/link/' + | '/groups/$issueId/pivotal/unlink/' + | '/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/$key/' + | '/groups/$issueId/tags/$key/values/' + | '/groups/$issueId/tags/' + | '/groups/$issueId/trello/autocomplete/' + | '/groups/$issueId/trello/create/' + | '/groups/$issueId/trello/link/' + | '/groups/$issueId/trello/options/' + | '/groups/$issueId/trello/unlink/' + | '/groups/$issueId/user-feedback/' + | '/groups/$issueId/user-reports/' + | '/integration-features/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$artifactId/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$artifactId/assemble-generic/' + | '/internal/$organizationIdOrSlug/$projectIdOrSlug/files/preprodartifacts/$artifactId/update/' + | '/internal/beacon/' + | '/internal/check-am2-compatibility/' + | '/internal/demo/email-capture/' + | '/internal/environment/' + | '/internal/feature-flags/' + | '/internal/feature-flags/ea-feature-flags/' + | '/internal/health/' + | '/internal/integration-proxy/' + | '/internal/mail/' + | '/internal/options/' + | '/internal/packages/' + | '/internal/project-config/' + | '/internal/queue/tasks/' + | '/internal/quotas/' + | '/internal/rpc/$serviceName/$methodName/' + | '/internal/seer-rpc/$methodName/' + | '/internal/stats/' + | '/internal/warnings/' + | '/issues/$issueId/' + | '/issues/$issueId/activities/' + | '/issues/$issueId/asana/autocomplete/' + | '/issues/$issueId/asana/create/' + | '/issues/$issueId/asana/link/' + | '/issues/$issueId/asana/unlink/' + | '/issues/$issueId/attachments/' + | '/issues/$issueId/autofix/' + | '/issues/$issueId/autofix/setup/' + | '/issues/$issueId/autofix/update/' + | '/issues/$issueId/bitbucket/autocomplete/' + | '/issues/$issueId/bitbucket/create/' + | '/issues/$issueId/bitbucket/link/' + | '/issues/$issueId/bitbucket/unlink/' + | '/issues/$issueId/comments/$noteId/' + | '/issues/$issueId/comments/' + | '/issues/$issueId/current-release/' + | '/issues/$issueId/events/$eventId/' + | '/issues/$issueId/events/' + | '/issues/$issueId/external-issues/$externalIssueId/' + | '/issues/$issueId/external-issues/' + | '/issues/$issueId/first-last-release/' + | '/issues/$issueId/github/autocomplete/' + | '/issues/$issueId/github/create/' + | '/issues/$issueId/github/link/' + | '/issues/$issueId/github/unlink/' + | '/issues/$issueId/gitlab/create/' + | '/issues/$issueId/gitlab/link/' + | '/issues/$issueId/gitlab/unlink/' + | '/issues/$issueId/hashes/' + | '/issues/$issueId/integrations/$integrationId/' + | '/issues/$issueId/integrations/' + | '/issues/$issueId/jira/autocomplete/' + | '/issues/$issueId/jira/create/' + | '/issues/$issueId/jira/link/' + | '/issues/$issueId/jira/unlink/' + | '/issues/$issueId/notes/$noteId/' + | '/issues/$issueId/notes/' + | '/issues/$issueId/open-periods/' + | '/issues/$issueId/pivotal/autocomplete/' + | '/issues/$issueId/pivotal/create/' + | '/issues/$issueId/pivotal/link/' + | '/issues/$issueId/pivotal/unlink/' + | '/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/$key/' + | '/issues/$issueId/tags/$key/values/' + | '/issues/$issueId/tags/' + | '/issues/$issueId/trello/autocomplete/' + | '/issues/$issueId/trello/create/' + | '/issues/$issueId/trello/link/' + | '/issues/$issueId/trello/options/' + | '/issues/$issueId/trello/unlink/' + | '/issues/$issueId/user-feedback/' + | '/issues/$issueId/user-reports/' + | '/notification-defaults/' + | '/organizations/$organizationIdOrSlug/' + | '/organizations/$organizationIdOrSlug/access-requests/$requestId/' + | '/organizations/$organizationIdOrSlug/access-requests/' + | '/organizations/$organizationIdOrSlug/alert-rules/$alertRuleId/' + | '/organizations/$organizationIdOrSlug/alert-rules/' + | '/organizations/$organizationIdOrSlug/alert-rules/available-actions/' + | '/organizations/$organizationIdOrSlug/api-keys/$apiKeyId/' + | '/organizations/$organizationIdOrSlug/api-keys/' + | '/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/$configId/' + | '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/' + | '/organizations/$organizationIdOrSlug/code-mappings/' + | '/organizations/$organizationIdOrSlug/codeowners-associations/' + | '/organizations/$organizationIdOrSlug/combined-rules/' + | '/organizations/$organizationIdOrSlug/config/integrations/' + | '/organizations/$organizationIdOrSlug/config/repos/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/favorite/' + | '/organizations/$organizationIdOrSlug/dashboards/$dashboardId/visit/' + | '/organizations/$organizationIdOrSlug/dashboards/' + | '/organizations/$organizationIdOrSlug/dashboards/starred/' + | '/organizations/$organizationIdOrSlug/dashboards/starred/order/' + | '/organizations/$organizationIdOrSlug/dashboards/widgets/' + | '/organizations/$organizationIdOrSlug/data-conditions/' + | '/organizations/$organizationIdOrSlug/data-export/$dataExportId/' + | '/organizations/$organizationIdOrSlug/data-export/' + | '/organizations/$organizationIdOrSlug/data-scrubbing-selector-suggestions/' + | '/organizations/$organizationIdOrSlug/derive-code-mappings/' + | '/organizations/$organizationIdOrSlug/detector-types/' + | '/organizations/$organizationIdOrSlug/detector-workflow/$detectorWorkflowId/' + | '/organizations/$organizationIdOrSlug/detector-workflow/' + | '/organizations/$organizationIdOrSlug/detectors/$detectorId/' + | '/organizations/$organizationIdOrSlug/detectors/' + | '/organizations/$organizationIdOrSlug/discover/homepage/' + | '/organizations/$organizationIdOrSlug/discover/saved/$queryId/' + | '/organizations/$organizationIdOrSlug/discover/saved/$queryId/visit/' + | '/organizations/$organizationIdOrSlug/discover/saved/' + | '/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/$projectIdOrSlug:$eventId/' + | '/organizations/$organizationIdOrSlug/events/' + | '/organizations/$organizationIdOrSlug/events/anomalies/' + | '/organizations/$organizationIdOrSlug/experimental/projects/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/starred/' + | '/organizations/$organizationIdOrSlug/explore/saved/$id/visit/' + | '/organizations/$organizationIdOrSlug/explore/saved/' + | '/organizations/$organizationIdOrSlug/explore/saved/starred/order/' + | '/organizations/$organizationIdOrSlug/external-users/$externalUserId/' + | '/organizations/$organizationIdOrSlug/external-users/' + | '/organizations/$organizationIdOrSlug/feedback-summary/' + | '/organizations/$organizationIdOrSlug/flags/hooks/provider/$provider/' + | '/organizations/$organizationIdOrSlug/flags/logs/$flagLogId/' + | '/organizations/$organizationIdOrSlug/flags/logs/' + | '/organizations/$organizationIdOrSlug/flags/signing-secrets/$signingSecretId/' + | '/organizations/$organizationIdOrSlug/flags/signing-secrets/' + | '/organizations/$organizationIdOrSlug/fork/' + | '/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/' + | '/organizations/$organizationIdOrSlug/group-search-views/starred/' + | '/organizations/$organizationIdOrSlug/group-search-views/starred/order/' + | '/organizations/$organizationIdOrSlug/grouping-configs/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/activities/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/asana/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/asana/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/asana/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/asana/unlink/' + | '/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/bitbucket/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/bitbucket/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/bitbucket/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/bitbucket/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/comments/$noteId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/comments/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/current-release/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/events/$eventId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/events/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/external-issues/$externalIssueId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/external-issues/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/first-last-release/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/github/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/github/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/github/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/github/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/gitlab/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/gitlab/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/gitlab/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/hashes/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/integrations/$integrationId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/integrations/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/jira/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/jira/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/jira/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/jira/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/notes/$noteId/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/notes/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/open-periods/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/pivotal/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/pivotal/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/pivotal/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/pivotal/unlink/' + | '/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/$key/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/tags/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/trello/autocomplete/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/trello/create/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/trello/link/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/trello/options/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/trello/unlink/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/user-feedback/' + | '/organizations/$organizationIdOrSlug/groups/$issueId/user-reports/' + | '/organizations/$organizationIdOrSlug/incidents/$incidentIdentifier/' + | '/organizations/$organizationIdOrSlug/incidents/' + | '/organizations/$organizationIdOrSlug/insights/starred-segments/' + | '/organizations/$organizationIdOrSlug/insights/tree/' + | '/organizations/$organizationIdOrSlug/integration-requests/' + | '/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/' + | '/organizations/$organizationIdOrSlug/invite-requests/$memberId/' + | '/organizations/$organizationIdOrSlug/invite-requests/' + | '/organizations/$organizationIdOrSlug/invited-members/$memberInviteId/' + | '/organizations/$organizationIdOrSlug/invited-members/' + | '/organizations/$organizationIdOrSlug/issues-count/' + | '/organizations/$organizationIdOrSlug/issues-metrics/' + | '/organizations/$organizationIdOrSlug/issues-stats/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/activities/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/asana/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/asana/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/asana/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/asana/unlink/' + | '/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/bitbucket/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/bitbucket/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/bitbucket/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/bitbucket/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/comments/$noteId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/comments/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/current-release/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/events/$eventId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/events/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/external-issues/$externalIssueId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/external-issues/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/first-last-release/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/github/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/github/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/github/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/github/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/gitlab/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/gitlab/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/gitlab/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/hashes/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/integrations/$integrationId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/integrations/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/jira/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/jira/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/jira/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/jira/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/notes/$noteId/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/notes/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/open-periods/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/pivotal/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/pivotal/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/pivotal/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/pivotal/unlink/' + | '/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/$key/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/tags/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/trello/autocomplete/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/trello/create/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/trello/link/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/trello/options/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/trello/unlink/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/user-feedback/' + | '/organizations/$organizationIdOrSlug/issues/$issueId/user-reports/' + | '/organizations/$organizationIdOrSlug/issues/' + | '/organizations/$organizationIdOrSlug/join-request/' + | '/organizations/$organizationIdOrSlug/key-transactions-list/' + | '/organizations/$organizationIdOrSlug/key-transactions/' + | '/organizations/$organizationIdOrSlug/measurements-meta/' + | '/organizations/$organizationIdOrSlug/members/$memberId/' + | '/organizations/$organizationIdOrSlug/members/$memberId/teams/$teamIdOrSlug/' + | '/organizations/$organizationIdOrSlug/members/' + | '/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/$monitorIdOrSlug/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/checkins/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/environments/$environment/' + | '/organizations/$organizationIdOrSlug/monitors/$monitorIdOrSlug/stats/' + | '/organizations/$organizationIdOrSlug/monitors/' + | '/organizations/$organizationIdOrSlug/notifications/actions/$actionId/' + | '/organizations/$organizationIdOrSlug/notifications/actions/' + | '/organizations/$organizationIdOrSlug/notifications/available-actions/' + | '/organizations/$organizationIdOrSlug/onboarding-continuation-email/' + | '/organizations/$organizationIdOrSlug/onboarding-tasks/' + | '/organizations/$organizationIdOrSlug/ondemand-rules-stats/' + | '/organizations/$organizationIdOrSlug/org-auth-tokens/$tokenId/' + | '/organizations/$organizationIdOrSlug/org-auth-tokens/' + | '/organizations/$organizationIdOrSlug/page-web-vitals-summary/' + | '/organizations/$organizationIdOrSlug/pinned-searches/' + | '/organizations/$organizationIdOrSlug/plugins/' + | '/organizations/$organizationIdOrSlug/plugins/configs/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/' + | '/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/$templateId/' + | '/organizations/$organizationIdOrSlug/project-templates/' + | '/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/$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/$fileId/' + | '/organizations/$organizationIdOrSlug/releases/$version/files/' + | '/organizations/$organizationIdOrSlug/releases/$version/meta/' + | '/organizations/$organizationIdOrSlug/releases/$version/previous-with-commits/' + | '/organizations/$organizationIdOrSlug/releases/$version/resolved/' + | '/organizations/$organizationIdOrSlug/releases/' + | '/organizations/$organizationIdOrSlug/releases/stats/' + | '/organizations/$organizationIdOrSlug/replay-count/' + | '/organizations/$organizationIdOrSlug/replay-selectors/' + | '/organizations/$organizationIdOrSlug/replays-events-meta/' + | '/organizations/$organizationIdOrSlug/replays/$replayId/' + | '/organizations/$organizationIdOrSlug/replays/' + | '/organizations/$organizationIdOrSlug/repos/$repoId/' + | '/organizations/$organizationIdOrSlug/repos/$repoId/commits/' + | '/organizations/$organizationIdOrSlug/repos/' + | '/organizations/$organizationIdOrSlug/request-project-creation/' + | '/organizations/$organizationIdOrSlug/sampling/admin-metrics/' + | '/organizations/$organizationIdOrSlug/sampling/project-rates/' + | '/organizations/$organizationIdOrSlug/sampling/project-root-counts/' + | '/organizations/$organizationIdOrSlug/scim/v2/Groups/$teamIdOrSlug/' + | '/organizations/$organizationIdOrSlug/scim/v2/Groups/' + | '/organizations/$organizationIdOrSlug/scim/v2/Schemas/' + | '/organizations/$organizationIdOrSlug/scim/v2/Users/$memberId/' + | '/organizations/$organizationIdOrSlug/scim/v2/Users/' + | '/organizations/$organizationIdOrSlug/sdk-deprecations/' + | '/organizations/$organizationIdOrSlug/sdk-updates/' + | '/organizations/$organizationIdOrSlug/sdks/' + | '/organizations/$organizationIdOrSlug/searches/$searchId/' + | '/organizations/$organizationIdOrSlug/searches/' + | '/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/$key/values/' + | '/organizations/$organizationIdOrSlug/spans/fields/' + | '/organizations/$organizationIdOrSlug/spans/fields/stats/' + | '/organizations/$organizationIdOrSlug/stats-summary/' + | '/organizations/$organizationIdOrSlug/stats/' + | '/organizations/$organizationIdOrSlug/stats_v2/' + | '/organizations/$organizationIdOrSlug/tags/$key/values/' + | '/organizations/$organizationIdOrSlug/tags/' + | '/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/$key/values/' + | '/organizations/$organizationIdOrSlug/trace-items/attributes/' + | '/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/trace/$traceId/spans/' + | '/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/$userId/' + | '/organizations/$organizationIdOrSlug/users/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/group-history/' + | '/organizations/$organizationIdOrSlug/workflows/$workflowId/stats/' + | '/organizations/$organizationIdOrSlug/workflows/' + | '/organizations/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rule-task/$taskUuid/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/$alertRuleId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/$ruleId/snooze/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/alert-rules/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-bundles/$bundleId/files/$fileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-bundles/$bundleId/files/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/artifact-lookup/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/cluster-transaction-names/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/codeowners/$codeownersId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/codeowners/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/commits/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/create-sample-transaction/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/create-sample/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/environments/$environment/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/environments/' + | '/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/$attachmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/events/$eventId/attachments/' + | '/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/events/' + | '/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/$artifactId/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/$filterId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/filters/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/grouping-configs/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/groups/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/groups/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/$hookId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/$hookId/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/hooks/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/$keyId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/$keyId/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/keys/' + | '/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/$pluginId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/plugins/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$artifactId/build-details/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$artifactId/install-details/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/$uuid/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/' + | '/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/$releaseThreshold/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/release-thresholds/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/commits/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/files/$fileId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/files/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/repositories/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/resolved/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/' + | '/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/$segmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/recording-segments/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/summarize/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/summarize/breadcrumbs/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/videos/$segmentId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/jobs/delete/$jobId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/jobs/delete/' + | '/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/$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/' + | '/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/$key/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tags/$key/values/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tags/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/teams/$teamIdOrSlug/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/teams/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tempest-credentials/$tempestCredentialsId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tempest-credentials/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tombstones/$tombstoneId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/tombstones/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/trace-items/$itemId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/transaction-threshold/configure/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/transfer/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/$uptimeProjectSubscriptionId/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/$uptimeProjectSubscriptionId/checks/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/uptime/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-feedback/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-reports/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/user-stats/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/users/' + | '/projects/' + | '/prompts-activity/' + | '/publickeys/relocations/' + | '/relays/$relayId/' + | '/relays/' + | '/relays/live/' + | '/relays/projectconfigs/' + | '/relays/projectids/' + | '/relays/publickeys/' + | '/relays/register/challenge/' + | '/relays/register/response/' + | '/relocations/$relocationUuid/' + | '/relocations/$relocationUuid/abort/' + | '/relocations/$relocationUuid/artifacts/$artifactKind/$fileName/' + | '/relocations/$relocationUuid/artifacts/' + | '/relocations/$relocationUuid/cancel/' + | '/relocations/$relocationUuid/pause/' + | '/relocations/$relocationUuid/recover/' + | '/relocations/$relocationUuid/retry/' + | '/relocations/$relocationUuid/unpause/' + | '/relocations/' + | '/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/$externalIssueId/' + | '/sentry-app-installations/$uuid/external-issues/' + | '/sentry-app-installations/$uuid/external-requests/' + | '/sentry-app-installations/$uuid/service-hook-projects/' + | '/sentry-apps-stats/' + | '/sentry-apps/$sentryAppIdOrSlug/' + | '/sentry-apps/$sentryAppIdOrSlug/api-tokens/$apiTokenId/' + | '/sentry-apps/$sentryAppIdOrSlug/api-tokens/' + | '/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/' + | '/sentry-apps/' + | '/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/$externalTeamId/' + | '/teams/$organizationIdOrSlug/$teamIdOrSlug/external-teams/' + | '/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/$roleName/' + | '/userroles/' + | '/users/$userId/' + | '/users/$userId/authenticators/$authId/$interfaceDeviceId/' + | '/users/$userId/authenticators/$authId/' + | '/users/$userId/authenticators/$interfaceId/enroll/' + | '/users/$userId/authenticators/' + | '/users/$userId/avatar/' + | '/users/$userId/emails/' + | '/users/$userId/emails/confirm/' + | '/users/$userId/identities/$identityId/' + | '/users/$userId/identities/' + | '/users/$userId/ips/' + | '/users/$userId/notification-options/$notificationOptionId/' + | '/users/$userId/notification-options/' + | '/users/$userId/notification-providers/' + | '/users/$userId/notifications/' + | '/users/$userId/notifications/email/' + | '/users/$userId/organization-integrations/' + | '/users/$userId/organizations/' + | '/users/$userId/password/' + | '/users/$userId/permissions/$permissionName/' + | '/users/$userId/permissions/' + | '/users/$userId/permissions/config/' + | '/users/$userId/regions/' + | '/users/$userId/roles/$roleName/' + | '/users/$userId/roles/' + | '/users/$userId/subscriptions/' + | '/users/$userId/user-identities/$category/$identityId/' + | '/users/$userId/user-identities/' + | '/users/' + | '/wizard/$wizardHash/' + | '/wizard/' +; diff --git a/static/app/utils/useRelease.tsx b/static/app/utils/useRelease.tsx index 861eaaa9c8d35a..ab0048bc256443 100644 --- a/static/app/utils/useRelease.tsx +++ b/static/app/utils/useRelease.tsx @@ -4,24 +4,24 @@ import {apiOptions} from 'sentry/api/apiOptions'; import type {Release} from 'sentry/types/release'; export function useRelease({ - orgSlug, - projectSlug, - releaseVersion, + organizationIdOrSlug, + projectIdOrSlug, + version, enabled, }: { - orgSlug: string; - projectSlug: string; - releaseVersion: string; + organizationIdOrSlug: string; + projectIdOrSlug: string; + version: string; enabled?: boolean; }) { return useQuery({ ...apiOptions.as()( - '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', + '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/', { path: { - orgSlug, - projectSlug, - releaseVersion, + organizationIdOrSlug, + projectIdOrSlug, + version, }, staleTime: Infinity, } From 38763e67354e2397847bdfa344dd3ce287cadfcc Mon Sep 17 00:00:00 2001 From: TkDodo Date: Mon, 4 Aug 2025 20:46:34 +0200 Subject: [PATCH 07/15] fix: merge conflicts --- static/app/api/apiOptions.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts index 939fe6f4d7db8b..6d992fbbe3e5cd 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -57,10 +57,12 @@ function _apiOptions< }); } -apiOptions.as = - () => - ( - path: TApiPath, - options: Options & PathParamOptions - ) => - apiOptions(path, options as never); +export const apiOptions = { + as: + () => + ( + path: TApiPath, + options: Options & PathParamOptions + ) => + _apiOptions(path, options as never), +}; From 8711a2b3296d61d46ea0e9148357fa32b3ab3237 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 5 Aug 2025 09:47:33 +0200 Subject: [PATCH 08/15] ref: update known urls --- static/app/api/knownUrls.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/static/app/api/knownUrls.ts b/static/app/api/knownUrls.ts index 3441108ea60bb5..a0a8dda1ef5317 100644 --- a/static/app/api/knownUrls.ts +++ b/static/app/api/knownUrls.ts @@ -323,7 +323,6 @@ export type KnownApiUrls = | '/organizations/$organizationIdOrSlug/group-search-views/' | '/organizations/$organizationIdOrSlug/group-search-views/starred/' | '/organizations/$organizationIdOrSlug/group-search-views/starred/order/' - | '/organizations/$organizationIdOrSlug/grouping-configs/' | '/organizations/$organizationIdOrSlug/groups/$issueId/' | '/organizations/$organizationIdOrSlug/groups/$issueId/activities/' | '/organizations/$organizationIdOrSlug/groups/$issueId/asana/autocomplete/' @@ -552,6 +551,7 @@ export type KnownApiUrls = | '/organizations/$organizationIdOrSlug/plugins/' | '/organizations/$organizationIdOrSlug/plugins/configs/' | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/' + | '/organizations/$organizationIdOrSlug/prevent/owner/$owner/repositories/tokens/' | '/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/' @@ -638,7 +638,6 @@ export type KnownApiUrls = | '/organizations/$organizationIdOrSlug/trace-meta/$traceId/' | '/organizations/$organizationIdOrSlug/trace-summary/' | '/organizations/$organizationIdOrSlug/trace/$traceId/' - | '/organizations/$organizationIdOrSlug/trace/$traceId/spans/' | '/organizations/$organizationIdOrSlug/traces/' | '/organizations/$organizationIdOrSlug/unsubscribe/issue/$id/' | '/organizations/$organizationIdOrSlug/unsubscribe/project/$id/' @@ -721,6 +720,7 @@ export type KnownApiUrls = | '/projects/$organizationIdOrSlug/$projectIdOrSlug/plugins/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$artifactId/build-details/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/$artifactId/install-details/' + | '/projects/$organizationIdOrSlug/$projectIdOrSlug/preprodartifacts/check-for-updates/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/$uuid/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/processing-errors/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/profiling/profiles/$profileId/' @@ -743,7 +743,6 @@ export type KnownApiUrls = | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/recording-segments/$segmentId/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/recording-segments/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/summarize/' - | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/summarize/breadcrumbs/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/videos/$segmentId/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/$replayId/viewed-by/' | '/projects/$organizationIdOrSlug/$projectIdOrSlug/replays/jobs/delete/$jobId/' From ce921c34ae007ffc9c115c01daad66a24c598b32 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 5 Aug 2025 09:49:00 +0200 Subject: [PATCH 09/15] fix: allow other urls than KnownApiUrls --- static/app/api/apiDefinition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts index 13e3620ac75450..a1437368b7ed51 100644 --- a/static/app/api/apiDefinition.ts +++ b/static/app/api/apiDefinition.ts @@ -1,3 +1,3 @@ import type {KnownApiUrls} from './knownUrls'; -export type ApiPath = KnownApiUrls; +export type ApiPath = KnownApiUrls | (string & {}); From 329f5b4781a3d24e486a4f351d848d87c389207a Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:51:04 +0000 Subject: [PATCH 10/15] :hammer_and_wrench: apply pre-commit fixes --- .../management/commands/generate_ts_api_routes.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sentry/management/commands/generate_ts_api_routes.py b/src/sentry/management/commands/generate_ts_api_routes.py index 1343a8ef16f969..0507987da27da2 100644 --- a/src/sentry/management/commands/generate_ts_api_routes.py +++ b/src/sentry/management/commands/generate_ts_api_routes.py @@ -1,17 +1,17 @@ -import re import itertools +import re from importlib import import_module +from django.conf import settings from django.core.management.base import BaseCommand from django.urls import URLPattern, URLResolver -from django.conf import settings - # --- UTILS --- + def snake_to_camel(s: str) -> str: parts = s.split("_") - return parts[0] + ''.join(p.capitalize() for p in parts[1:]) + return parts[0] + "".join(p.capitalize() for p in parts[1:]) def final_cleanup(path: str) -> str: @@ -93,6 +93,7 @@ def extract_ts_routes(urlpatterns, prefix="") -> list[str]: # --- COMMAND --- + class Command(BaseCommand): help = "Generate TypeScript route types from Django urlpatterns" @@ -124,7 +125,9 @@ def handle(self, *args, **options): with open(output_file, "w") as f: f.write("/* prettier-ignore */\n") f.write("// Auto-generated TypeScript route types\n") - f.write("// To update it run `sentry django generate_ts_api_routes --urls sentry.api.urls --output=path/to/thisfile.ts`\n") + f.write( + "// To update it run `sentry django generate_ts_api_routes --urls sentry.api.urls --output=path/to/thisfile.ts`\n" + ) f.write("export type KnownApiUrls =\n") for route in ts_routes: f.write(f" | {route}\n") From 094a70f472c9c154ccccbde40333cfc881f1bb9e Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 5 Aug 2025 11:26:08 +0200 Subject: [PATCH 11/15] fix: avoid Inefficient regular expression --- .../commands/generate_ts_api_routes.py | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/sentry/management/commands/generate_ts_api_routes.py b/src/sentry/management/commands/generate_ts_api_routes.py index 0507987da27da2..6342174e94eed5 100644 --- a/src/sentry/management/commands/generate_ts_api_routes.py +++ b/src/sentry/management/commands/generate_ts_api_routes.py @@ -19,6 +19,8 @@ def final_cleanup(path: str) -> str: path = re.sub(r"\([^)]*\)", "", path) # remove leftover groups path = path.replace(".*", "") path = re.sub(r"//+", "/", path) + + # Ensure leading and trailing slash path = "/" + path.strip("/") if not path.endswith("/"): @@ -27,14 +29,47 @@ def final_cleanup(path: str) -> str: return path +def replace_named_groups(regex: str) -> str: + result = [] + last_end = 0 + + # Match all `(?P...)` groups, even with nested `(...)` inside + pattern = re.compile(r"\(\?P<(\w+)>") + + for match in pattern.finditer(regex): + start = match.start() + name = match.group(1) + + # Find the matching closing parenthesis for this group + depth = 1 + i = match.end() + while i < len(regex): + if regex[i] == "(": + depth += 1 + elif regex[i] == ")": + depth -= 1 + if depth == 0: + break + i += 1 + + # If we couldn't find the end, skip the group entirely (fail-safe) + if depth != 0: + continue + + # Append text before the match and the replacement + result.append(regex[last_end:start]) + result.append(f"${snake_to_camel(name)}") + last_end = i + 1 # skip the closing ')' + + # Append the rest + result.append(regex[last_end:]) + return "".join(result) + + def convert_django_regex_to_ts_all(regex: str) -> list[str]: regex = regex.strip("^$") - # Replace named capturing groups (?P...) with $camelCaseName - def replace_named_group(match): - return f"${snake_to_camel(match.group(1))}" - - regex = re.sub(r"\(\?P<(\w+)>((?:[^()]+|\([^()]*\))*)\)", replace_named_group, regex) + regex = replace_named_groups(regex) # Handle non-capturing groups (e.g. (?:x|y) → x and y) pattern = r"\(\?:([^)]+)\)" From 9ea54a27cfc753509504cde58dc5d06727403629 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 5 Aug 2025 11:35:03 +0200 Subject: [PATCH 12/15] fix: account for $param1:$param2 url patterns --- static/app/api/getApiUrl.spec.ts | 15 +++++++++++++++ static/app/api/getApiUrl.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/static/app/api/getApiUrl.spec.ts b/static/app/api/getApiUrl.spec.ts index 87a1afdf8022b4..ed5e792e04e6ca 100644 --- a/static/app/api/getApiUrl.spec.ts +++ b/static/app/api/getApiUrl.spec.ts @@ -32,6 +32,21 @@ describe('getApiUrl', () => { expect(url).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); }); + test('advanced path params case', () => { + const url = getApiUrl( + '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/', + { + path: { + organizationIdOrSlug: 'my-org', + projectIdOrSlug: 'my-project', + eventId: '12345', + }, + } + ); + + expect(url).toBe('/organizations/my-org/events/my-project:12345/'); + }); + test('should stringify number path params', () => { const url = getApiUrl('/items/$id/', { path: {id: 123}, diff --git a/static/app/api/getApiUrl.ts b/static/app/api/getApiUrl.ts index 3b52b0a61664d5..5af0f87c3d1ef8 100644 --- a/static/app/api/getApiUrl.ts +++ b/static/app/api/getApiUrl.ts @@ -1,10 +1,16 @@ import type {ApiPath} from './apiDefinition'; +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; export type PathParamOptions = From 94856fc5eca00134e3819ef8a6d62c2b188ccd0c Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 5 Aug 2025 11:42:17 +0200 Subject: [PATCH 13/15] fix: useRelease api --- static/app/utils/useRelease.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/static/app/utils/useRelease.tsx b/static/app/utils/useRelease.tsx index ab0048bc256443..23be4cf3566c63 100644 --- a/static/app/utils/useRelease.tsx +++ b/static/app/utils/useRelease.tsx @@ -4,14 +4,14 @@ import {apiOptions} from 'sentry/api/apiOptions'; import type {Release} from 'sentry/types/release'; export function useRelease({ - organizationIdOrSlug, - projectIdOrSlug, - version, + orgSlug, + projectSlug, + releaseVersion, enabled, }: { - organizationIdOrSlug: string; - projectIdOrSlug: string; - version: string; + orgSlug: string; + projectSlug: string; + releaseVersion: string; enabled?: boolean; }) { return useQuery({ @@ -19,9 +19,9 @@ export function useRelease({ '/projects/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/', { path: { - organizationIdOrSlug, - projectIdOrSlug, - version, + organizationIdOrSlug: orgSlug, + projectIdOrSlug: projectSlug, + version: releaseVersion, }, staleTime: Infinity, } From 8005be5fc4b9c6027bfc47c5baa64eabab4b4a87 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 12 Aug 2025 14:13:31 +0200 Subject: [PATCH 14/15] feat: type transformation for openAPI generated urls --- static/app/api/apiDefinition.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/static/app/api/apiDefinition.ts b/static/app/api/apiDefinition.ts index a1437368b7ed51..b4dae36abf5702 100644 --- a/static/app/api/apiDefinition.ts +++ b/static/app/api/apiDefinition.ts @@ -1,3 +1,29 @@ import type {KnownApiUrls} from './knownUrls'; +import type {paths} from './openapi'; -export type ApiPath = KnownApiUrls | (string & {}); +type SnakeToCamel = S extends `${infer Head}_${infer Tail}` + ? `${Head}${Capitalize>}` + : S; + +// replace {param_name} → $paramName +type ParamToDollarCamel = S extends `{${infer Param}}` + ? `$${SnakeToCamel}` + : S; + +// Recursive: split by '/', transform params, join back +type TransformSegments = S extends `/${infer Rest}` + ? `/${TransformSegments}` + : S extends `${infer Segment}/${infer Tail}` + ? `${ParamToDollarCamel}/${TransformSegments}` + : ParamToDollarCamel; + +// filter GET paths, strip `/api/0`, and transform params +type GetPaths = { + [P in keyof paths]: 'get' extends keyof paths[P] + ? P extends `/api/0${infer Rest}` + ? TransformSegments + : never + : never; +}[keyof paths]; + +export type ApiPath = GetPaths | (string & {}); From e5cb47cb0e740174a0d266f9f09edd4f94fd4a81 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 12 Aug 2025 14:43:15 +0200 Subject: [PATCH 15/15] chore: imports --- static/app/api/apiOptions.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/app/api/apiOptions.ts b/static/app/api/apiOptions.ts index 269e3e13893840..a9e53c6935f136 100644 --- a/static/app/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -5,13 +5,11 @@ import {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryCl import type {ApiPath} from './apiDefinition'; import { - type ExtractPathParams, getApiUrl, + type ExtractPathParams, type OptionalPathParams, type PathParamOptions, } from './getApiUrl'; -import type {MaybeApiPath} from './apiDefinition'; -import {getApiUrl, type ExtractPathParams, type OptionalPathParams} from './getApiUrl'; type Options = QueryKeyEndpointOptions & {staleTime: number};