From a5b7c79116596d5d751e9ecd0b4640fe2f99133f Mon Sep 17 00:00:00 2001 From: "Armen Zambrano G." <44410+armenzg@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:32:22 -0400 Subject: [PATCH] Revert "ci(api): Create a list of valid api urls (#100515)" This reverts commit 2d54fa1f0314576c7351432cca13d851153bd5e0. --- .github/file-filters.yml | 20 +- .github/workflows/frontend.yml | 2 +- .pre-commit-config.yaml | 6 - static/app/api/apiDefinition.ts | 3 + .../app/{utils => }/api/apiOptions.spec.tsx | 131 +-- static/app/{utils => }/api/apiOptions.ts | 17 +- static/app/api/getApiUrl.spec.ts | 88 ++ static/app/{utils => }/api/getApiUrl.ts | 21 +- static/app/components/waitingForEvents.tsx | 27 +- static/app/utils/api/getApiUrl.spec.ts | 110 --- static/app/utils/api/knownGetsentryApiUrls.ts | 76 -- .../utils/api/knownSentryApiUrls.generated.ts | 794 ------------------ static/app/utils/useRelease.tsx | 10 +- static/gsAdmin/views/billingPlans.tsx | 2 +- tests/tools/test_api_urls_to_typescript.py | 41 - tools/api_urls_to_typescript.py | 187 ----- 16 files changed, 213 insertions(+), 1322 deletions(-) create mode 100644 static/app/api/apiDefinition.ts rename static/app/{utils => }/api/apiOptions.spec.tsx (54%) rename static/app/{utils => }/api/apiOptions.ts (72%) create mode 100644 static/app/api/getApiUrl.spec.ts rename static/app/{utils => }/api/getApiUrl.ts (59%) delete mode 100644 static/app/utils/api/getApiUrl.spec.ts delete mode 100644 static/app/utils/api/knownGetsentryApiUrls.ts delete mode 100644 static/app/utils/api/knownSentryApiUrls.generated.ts delete mode 100644 tests/tools/test_api_urls_to_typescript.py delete mode 100644 tools/api_urls_to_typescript.py diff --git a/.github/file-filters.yml b/.github/file-filters.yml index 5a5bb5b1c8685f..b1724db399cb60 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -1,20 +1,21 @@ # This is used by the action https://github.com/dorny/paths-filter -sentry_frontend_workflow_file: &sentry_frontend_workflow_file +sentry_specific_workflow: &sentry_specific_workflow - 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 the list directly, instead we shard tests inside jest.config.js +# getsentry/sentry does not use this directly, instead we shard tests inside jest.config.js testable_modified: &testable_modified - - '!**/*.generated.ts' - added|modified: 'package.json' - - added|modified: 'static/**/*.{ts,tsx,js,jsx,mjs}' - - added|modified: 'tests/js/**/*' - - added|deleted|modified: 'fixtures/search-syntax/**/*' + - added|modified: 'static/**/*.[tj]{s,sx}' + - *sentry_specific_test_files # Trigger for when we must run full tests (jest) testable_rules_changed: &testable_rules_changed - - *sentry_frontend_workflow_file + - *sentry_specific_workflow - added|modified: '.github/file-filters.yml' - added|modified: 'jest.config.ts' @@ -22,14 +23,13 @@ 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_frontend_workflow_file + - *sentry_specific_workflow - added|modified: '.github/file-filters.yml' - added|deleted|modified: '**/tsconfig*.json' - - added|deleted|modified: 'static/**/*.{ts,tsx,js,jsx,mjs}' + - added|deleted|modified: 'static/**/*.[tj]{s,sx}' # Trigger to apply the 'Scope: Frontend' label to PRs frontend_all: &frontend_all - - *sentry_frontend_workflow_file - 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 f3d1db6bf9227f..453e596bad0015 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 08514004902478..04aac4c911b468 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -135,12 +135,6 @@ 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 new file mode 100644 index 00000000000000..61858ac079efa7 --- /dev/null +++ b/static/app/api/apiDefinition.ts @@ -0,0 +1,3 @@ +type KnownApiUrls = ['/projects/$orgSlug/$projectSlug/releases/$releaseVersion/']; + +export type MaybeApiPath = KnownApiUrls[number] | (string & {}); diff --git a/static/app/utils/api/apiOptions.spec.tsx b/static/app/api/apiOptions.spec.tsx similarity index 54% rename from static/app/utils/api/apiOptions.spec.tsx rename to static/app/api/apiOptions.spec.tsx index 215b566aa7513b..1bba2c13ea485f 100644 --- a/static/app/utils/api/apiOptions.spec.tsx +++ b/static/app/api/apiOptions.spec.tsx @@ -4,13 +4,14 @@ 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>; @@ -21,41 +22,44 @@ const wrapper = ({children}: {children?: React.ReactNode}) => ( ); describe('apiOptions', () => { - it('should encode path parameters correctly', () => { + afterEach(() => { + MockApiClient.clearMockResponses(); + }); + test('should encode path parameters correctly', () => { const options = apiOptions.as()( - '/organizations/$organizationIdOrSlug/releases/$version/', + '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', { staleTime: 0, path: { - organizationIdOrSlug: 'my-org', - version: 'v 1.0.0', + orgSlug: 'my-org', + projectSlug: 'my-project', + releaseVersion: 'v 1.0.0', }, } ); - expect(options.queryKey[0]).toBe('/organizations/my-org/releases/v%201.0.0/'); + expect(options.queryKey[0]).toBe('/projects/my-org/my-project/releases/v%201.0.0/'); }); - it('should not include empty options in queryKey', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + test('should not include empty options in queryKey', () => { + const options = apiOptions.as()('/projects/$id/', { staleTime: 0, - path: {tokenId: '123'}, + path: {id: '123'}, }); - expect(options.queryKey).toEqual(['/api-tokens/123/']); + expect(options.queryKey).toEqual(['/projects/123/']); }); - it('should stringify number path params', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + test('should stringify number path params', () => { + const options = apiOptions.as()('/items/$id/', { staleTime: 0, - path: {tokenId: 123}, + path: {id: 123}, }); - expect(options.queryKey[0]).toBe('/api-tokens/123/'); + expect(options.queryKey[0]).toBe('/items/123/'); }); - it('should not do accidental replacements', () => { - // @ts-expect-error Using a sample path, not a real one + test('should not do accidental replacements', () => { const options = apiOptions.as()('/projects/$id1/$id', { staleTime: 0, path: {id: '123', id1: '456'}, @@ -64,21 +68,21 @@ describe('apiOptions', () => { expect(options.queryKey).toEqual(['/projects/456/123']); }); - it('should allow skipToken as path', () => { - function getOptions(tokenId: string | null) { - return apiOptions.as()('/api-tokens/$tokenId/', { + test('should allow skipToken as path', () => { + function getOptions(id: string | null) { + return apiOptions.as()('/projects/$id/', { staleTime: 0, - path: tokenId ? {tokenId} : skipToken, + path: id ? {id} : skipToken, }); } expect(getOptions('123').queryFn).toEqual(expect.any(Function)); - expect(getOptions('123').queryKey).toEqual(['/api-tokens/123/']); + expect(getOptions('123').queryKey).toEqual(['/projects/123/']); expect(getOptions(null).queryFn).toEqual(skipToken); - expect(getOptions(null).queryKey).toEqual(['/api-tokens/$tokenId/']); + expect(getOptions(null).queryKey).toEqual(['/projects/$id/']); }); - it('should extract content data per default', async () => { + test('should extract content data per default', async () => { const options = apiOptions.as()('/projects/', { staleTime: 0, }); @@ -95,7 +99,7 @@ describe('apiOptions', () => { expect(result.current.data).toEqual(['Project 1', 'Project 2']); }); - it('should extract headers', async () => { + test('should extract headers', async () => { const options = apiOptions.as()('/projects/', { staleTime: 0, }); @@ -130,37 +134,47 @@ describe('apiOptions', () => { }); describe('types', () => { - it('should always require staleTime', () => { + test('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/', {}); }); - it('should not allow invalid/excess path parameters', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + 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/', { staleTime: 0, - // @ts-expect-error Missing required path parameter - path: {tokenId: 'my-org', invalidParam: 'invalid'}, + // @ts-expect-error Excess path parameter + path: {orgSlug: 'my-org', extraParam: 'extra'}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should require path params for paths with parameters', () => { + test('should require path params for paths with parameters', () => { expect(() => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + const options = apiOptions.as()('/projects/$orgSlug/', { staleTime: 0, // @ts-expect-error Missing required path parameter path: {}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - }).toThrow('Missing path param: tokenId'); + }).toThrow('Missing path param: orgSlug'); }); - it('should not allow empty path parameters for paths without parameters', () => { - const options = apiOptions.as()('/api-tokens/', { + test('should not allow empty path parameters for paths without parameters', () => { + const options = apiOptions.as()('/projects/', { staleTime: 0, // @ts-expect-error Empty path parameters not allowed path: {}, @@ -169,32 +183,31 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should not need path params for paths without parameters', () => { - const options = apiOptions.as()('/api-tokens/', { + test('should not need path params for paths without parameters', () => { + const options = apiOptions.as()('/projects/', { staleTime: 0, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should allow string or number path parameters', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + test('should allow string or number path parameters', () => { + const options = apiOptions.as()('/items/$id/', { staleTime: 0, - path: {tokenId: 123}, + path: {id: 123}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); - const options2 = apiOptions.as()('/api-tokens/$tokenId/', { + const options2 = apiOptions.as()('/items/$id/', { staleTime: 0, - path: {tokenId: 'abc'}, + path: {id: 'abc'}, }); expectTypeOf(options2.queryFn).returns.toEqualTypeOf>(); }); - it('should default to never for unknown API paths', () => { - // @ts-expect-error Unknown API path + test('should default to never for unknown API paths', () => { const options = apiOptions.as()('/unknown/$param/', { staleTime: 0, path: {param: 'value'}, @@ -203,17 +216,33 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should allow providing manual data type', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + test('should allow providing manual data type', () => { + const options = apiOptions.as()('/foo/$bar', { staleTime: 0, - path: {tokenId: 'abc'}, + path: {bar: 'baz'}, }); expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should disallow unknown path if there are no path params', () => { - const options = apiOptions.as()('/api-tokens/', { + 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, // @ts-expect-error Path is not allowed when there are no path params path: {bar: 'baz'}, @@ -222,10 +251,10 @@ describe('apiOptions', () => { expectTypeOf(options.queryFn).returns.toEqualTypeOf>(); }); - it('should have a default select that extracts content', () => { - const options = apiOptions.as()('/api-tokens/$tokenId/', { + test('should have a default select that extracts content', () => { + const options = apiOptions.as()('/items/$id/', { staleTime: 0, - path: {tokenId: 123}, + path: {id: 123}, }); expectTypeOf(options.select).returns.toEqualTypeOf(); diff --git a/static/app/utils/api/apiOptions.ts b/static/app/api/apiOptions.ts similarity index 72% rename from static/app/utils/api/apiOptions.ts rename to static/app/api/apiOptions.ts index 48cc9eb90fa5e5..ad7f5f815ad8fe 100644 --- a/static/app/utils/api/apiOptions.ts +++ b/static/app/api/apiOptions.ts @@ -1,15 +1,10 @@ -import {queryOptions, skipToken} from '@tanstack/react-query'; -import type {SkipToken} from '@tanstack/react-query'; +import {queryOptions, skipToken, type SkipToken} from '@tanstack/react-query'; import type {ApiResult} from 'sentry/api'; -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 {fetchDataQuery, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; -type KnownApiUrls = KnownGetsentryApiUrls | KnownSentryApiUrls; +import type {MaybeApiPath} from './apiDefinition'; +import {getApiUrl, type ExtractPathParams, type OptionalPathParams} from './getApiUrl'; type Options = QueryKeyEndpointOptions & {staleTime: number}; @@ -34,7 +29,7 @@ export const selectWithHeaders = function _apiOptions< TManualData = never, - TApiPath extends KnownApiUrls = KnownApiUrls, + TApiPath extends MaybeApiPath = MaybeApiPath, // todo: infer the actual data type from the ApiMapping TActualData = TManualData, >( @@ -73,7 +68,7 @@ function _apiOptions< export const apiOptions = { as: () => - ( + ( path: TApiPath, options: Options & PathParamOptions ) => diff --git a/static/app/api/getApiUrl.spec.ts b/static/app/api/getApiUrl.spec.ts new file mode 100644 index 00000000000000..87a1afdf8022b4 --- /dev/null +++ b/static/app/api/getApiUrl.spec.ts @@ -0,0 +1,88 @@ +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/utils/api/getApiUrl.ts b/static/app/api/getApiUrl.ts similarity index 59% rename from static/app/utils/api/getApiUrl.ts rename to static/app/api/getApiUrl.ts index 3fcb6523e36d52..b305e2efca997e 100644 --- a/static/app/utils/api/getApiUrl.ts +++ b/static/app/api/getApiUrl.ts @@ -1,19 +1,10 @@ -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; +import type {MaybeApiPath} from './apiDefinition'; export type ExtractPathParams = TApiPath extends `${string}$${infer Param}/${infer Rest}` - ? SplitColon | ExtractPathParams<`/${Rest}`> + ? Param | ExtractPathParams<`/${Rest}`> : TApiPath extends `${string}$${infer Param}` - ? SplitColon + ? Param : never; type PathParamOptions = @@ -22,13 +13,15 @@ type PathParamOptions = : {path: Record, string | number>}; export type OptionalPathParams = - ExtractPathParams extends never ? never[] : [PathParamOptions]; + ExtractPathParams extends never + ? [] // eslint-disable-line @typescript-eslint/no-restricted-types + : [PathParamOptions]; const paramRegex = /\$([a-zA-Z0-9_-]+)/g; type ApiUrl = string & {__apiUrl: true}; -export default function getApiUrl( +export function getApiUrl( path: TApiPath, ...[options]: OptionalPathParams ): ApiUrl { diff --git a/static/app/components/waitingForEvents.tsx b/static/app/components/waitingForEvents.tsx index c9a67fa7e81a7e..321c74899ed46b 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,20 +27,17 @@ type Props = { function WaitingForEvents({org, project, sampleIssueId: sampleIssueIdProp}: Props) { const {data, error, isPending} = useQuery( - apiOptions.as>()( - '/projects/$organizationIdOrSlug/$projectIdOrSlug/issues/', - { - staleTime: Infinity, - data: {limit: 1}, - path: - project && sampleIssueIdProp === undefined - ? { - organizationIdOrSlug: org.slug, - projectIdOrSlug: project.slug, - } - : skipToken, - } - ) + apiOptions.as>()('/projects/$orgSlug/$projectSlug/issues/', { + staleTime: Infinity, + data: {limit: 1}, + path: + project && sampleIssueIdProp === undefined + ? { + orgSlug: org.slug, + projectSlug: project.slug, + } + : skipToken, + }) ); const sampleIssueId = sampleIssueIdProp ?? data?.[0]?.id ?? ''; diff --git a/static/app/utils/api/getApiUrl.spec.ts b/static/app/utils/api/getApiUrl.spec.ts deleted file mode 100644 index cca0bba5573297..00000000000000 --- a/static/app/utils/api/getApiUrl.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -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/utils/api/knownGetsentryApiUrls.ts b/static/app/utils/api/knownGetsentryApiUrls.ts deleted file mode 100644 index c3721164805035..00000000000000 --- a/static/app/utils/api/knownGetsentryApiUrls.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * 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 deleted file mode 100644 index 2cdab59151b9ef..00000000000000 --- a/static/app/utils/api/knownSentryApiUrls.generated.ts +++ /dev/null @@ -1,794 +0,0 @@ -/** - * 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 fe075eeb4726fe..861eaaa9c8d35a 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/$organizationIdOrSlug/$projectIdOrSlug/releases/$version/', + '/projects/$orgSlug/$projectSlug/releases/$releaseVersion/', { path: { - organizationIdOrSlug: orgSlug, - projectIdOrSlug: projectSlug, - version: releaseVersion, + orgSlug, + projectSlug, + releaseVersion, }, staleTime: Infinity, } diff --git a/static/gsAdmin/views/billingPlans.tsx b/static/gsAdmin/views/billingPlans.tsx index 8d33386bef29ed..7393549cb38d5c 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 deleted file mode 100644 index dc5532d5ab210a..00000000000000 --- a/tests/tools/test_api_urls_to_typescript.py +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index cb47ba39b707fc..00000000000000 --- a/tools/api_urls_to_typescript.py +++ /dev/null @@ -1,187 +0,0 @@ -# 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", - ] - )