Add ability to alter the api base url for the sentry scaffolder

Signed-off-by: InsidersByte <6938575+InsidersByte@users.noreply.github.com>
This commit is contained in:
InsidersByte
2025-12-30 10:03:54 +00:00
parent 33d584fa54
commit ab606b23b3
11 changed files with 306 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-sentry': minor
---
Add ability to configure the API Base URL
@@ -36,6 +36,14 @@ scaffolder:
token: ${SENTRY_TOKEN}
```
You can optional override the default Sentry API Base URL (https://sentry.io/api/0) in your `app-config.yaml`:
```yaml
scaffolder:
sentry:
apiBaseUrl: API-BASE-URL
```
After that you can use the action in your template:
```yaml
@@ -18,6 +18,7 @@ export function createSentryCreateProjectAction(options: {
slug?: string | undefined;
platform?: string | undefined;
authToken?: string | undefined;
apiBaseUrl?: string | undefined;
},
{
[x: string]: any;
@@ -33,6 +34,7 @@ export function createSentryFetchDSNAction(options: {
organizationSlug: string;
projectSlug: string;
authToken?: string | undefined;
apiBaseUrl?: string | undefined;
},
{
dsn?: string | undefined;
@@ -45,6 +45,7 @@ describe('sentry:project:create action', () => {
slug?: string;
platform?: string;
authToken?: string;
apiBaseUrl?: string;
}> =>
createMockActionContext({
workspacePath: './dev/proj',
@@ -279,4 +280,47 @@ describe('sentry:project:create action', () => {
},
});
});
it(`should ${examples[3].description}`, async () => {
expect.assertions(3);
let input;
try {
input = yaml.parse(examples[3].example).steps[0].input;
} catch (error) {
console.error('Failed to parse YAML:', error);
}
const action = createSentryCreateProjectAction(createScaffolderConfig());
const actionContext = getActionContext();
worker.use(
http.post(
`${input.apiBaseUrl || 'https://sentry.io/api/0'}/teams/${
input.organizationSlug
}/${input.teamSlug}/projects/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer d16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf95`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
await expect(request.json()).resolves.toEqual({
name: 'Scaffolded project A',
});
return new HttpResponse(JSON.stringify({ id: 'mock-id' }), {
status: 201,
headers: { 'content-type': 'application/json' },
});
},
),
);
await action.handler({
...actionContext,
input: {
...actionContext.input,
...input,
},
});
});
});
@@ -100,4 +100,24 @@ export const examples: TemplateExample[] = [
],
}),
},
{
description: 'Creates a Sentry project with a custom API base URL.',
example: yaml.stringify({
steps: [
{
id: 'create-sentry-project',
action: 'sentry:project:create',
name: 'Create a Sentry project with custom API base URL.',
input: {
organizationSlug: 'my-org',
teamSlug: 'team-a',
name: 'Scaffolded project A',
apiBaseUrl: 'https://custom.sentry.io/api/0',
authToken:
'd16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf95',
},
},
],
}),
},
];
@@ -44,6 +44,7 @@ describe('sentry:project:create action', () => {
slug?: string;
platform?: string;
authToken?: string;
apiBaseUrl?: string;
}> =>
createMockActionContext({
workspacePath: './dev/proj',
@@ -245,4 +246,64 @@ describe('sentry:project:create action', () => {
new InputError(`Sentry Response was: OUCH`),
);
});
it('should create a Sentry project with custom apiBaseUrl.', async () => {
expect.assertions(3);
const action = createSentryCreateProjectAction(createScaffolderConfig());
const actionContext = getActionContext();
actionContext.input = {
...actionContext.input,
apiBaseUrl: 'https://custom.sentry.io/api/0',
};
worker.use(
http.post(
`https://custom.sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer ${actionContext.input.authToken}`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
await expect(request.json()).resolves.toEqual({
name: actionContext.input.name,
});
return HttpResponse.json({ id: 'mock-id' }, { status: 201 });
},
),
);
await action.handler(actionContext);
});
it('should create a Sentry project with apiBaseUrl from config.', async () => {
expect.assertions(3);
const action = createSentryCreateProjectAction(
createScaffolderConfig({
sentry: {
apiBaseUrl: 'https://config.sentry.io/api/0',
},
}),
);
const actionContext = getActionContext();
worker.use(
http.post(
`https://config.sentry.io/api/0/teams/${actionContext.input.organizationSlug}/${actionContext.input.teamSlug}/projects/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer ${actionContext.input.authToken}`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
await expect(request.json()).resolves.toEqual({
name: actionContext.input.name,
});
return HttpResponse.json({ id: 'mock-id' }, { status: 201 });
},
),
);
await action.handler(actionContext);
});
});
@@ -67,11 +67,25 @@ export function createSentryCreateProjectAction(options: { config: Config }) {
'authenticate via bearer auth token. Requires scope: project:write',
})
.optional(),
apiBaseUrl: z =>
z
.string({
description:
'Optional base URL for the Sentry API. e.g. https://sentry.io/api/0',
})
.optional(),
},
},
async handler(ctx) {
const { organizationSlug, teamSlug, name, slug, platform, authToken } =
ctx.input;
const {
organizationSlug,
teamSlug,
name,
slug,
platform,
authToken,
apiBaseUrl,
} = ctx.input;
const body: any = {
name: name,
@@ -93,11 +107,16 @@ export function createSentryCreateProjectAction(options: { config: Config }) {
throw new InputError(`No valid sentry token given`);
}
const baseUrl =
apiBaseUrl ||
config.getOptionalString('scaffolder.sentry.apiBaseUrl') ||
'https://sentry.io/api/0';
const { result } = await ctx.checkpoint({
key: `create.project.${organizationSlug}.${teamSlug}`,
fn: async () => {
const response = await fetch(
`https://sentry.io/api/0/teams/${organizationSlug}/${teamSlug}/projects/`,
`${baseUrl}/teams/${organizationSlug}/${teamSlug}/projects/`,
{
method: 'POST',
headers: {
@@ -42,6 +42,7 @@ describe('sentry:fetch:dsn action', () => {
organizationSlug: string;
projectSlug: string;
authToken?: string;
apiBaseUrl?: string;
}> =>
createMockActionContext({
workspacePath: './dev/proj',
@@ -147,4 +148,50 @@ describe('sentry:fetch:dsn action', () => {
);
},
);
it(`should ${examples[2].description}`, async () => {
expect.assertions(3);
let input;
try {
input = yaml.parse(examples[2].example).steps[0].input;
} catch (error) {
console.error('Failed to parse YAML:', error);
}
const action = createSentryFetchDSNAction(createScaffolderConfig());
const actionContext = getActionContext(null);
worker.use(
http.get(
`${input.apiBaseUrl || 'https://sentry.io/api/0'}/projects/${
input.organizationSlug
}/${input.projectSlug}/keys/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer b14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df97`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
return HttpResponse.json(
[{ dsn: { public: 'https://abcdef1234567890@sentry.io/67890' } }],
{
status: 200,
},
);
},
),
);
await action.handler({
...actionContext,
input: {
...actionContext.input,
...input,
},
});
expect(actionContext.output).toHaveBeenCalledWith(
'dsn',
'https://abcdef1234567890@sentry.io/67890',
);
});
});
@@ -54,4 +54,23 @@ export const examples: TemplateExample[] = [
],
}),
},
{
description: 'Fetch the DSN for a Sentry project with custom API base URL.',
example: yaml.stringify({
steps: [
{
id: 'fetch-sentry-dsn',
action: 'sentry:fetch:dsn',
name: 'Fetch DSN with custom API base URL',
input: {
organizationSlug: 'my-org',
projectSlug: 'my-project',
apiBaseUrl: 'https://sentry.io/api/0/custom',
authToken:
'b14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df97',
},
},
],
}),
},
];
@@ -41,6 +41,7 @@ describe('sentry:fetch:dsn action', () => {
organizationSlug: string;
projectSlug: string;
authToken?: string;
apiBaseUrl?: string;
}> =>
createMockActionContext({
workspacePath: './dev/proj',
@@ -196,4 +197,66 @@ describe('sentry:fetch:dsn action', () => {
new InputError('No public DSN found in project keys'),
);
});
it('should fetch DSN with custom apiBaseUrl.', async () => {
expect.assertions(3);
const action = createSentryFetchDSNAction(createScaffolderConfig());
const actionContext = getActionContext();
actionContext.input = {
...actionContext.input,
apiBaseUrl: 'https://custom.sentry.io/api/0',
};
const mockDSN = 'https://test@sentry.io/123';
worker.use(
http.get(
`https://custom.sentry.io/api/0/projects/${actionContext.input.organizationSlug}/${actionContext.input.projectSlug}/keys/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer ${actionContext.input.authToken}`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
return HttpResponse.json([{ dsn: { public: mockDSN } }], {
status: 200,
});
},
),
);
await action.handler(actionContext);
expect(actionContext.output).toHaveBeenCalledWith('dsn', mockDSN);
});
it('should fetch DSN with apiBaseUrl from config.', async () => {
expect.assertions(3);
const action = createSentryFetchDSNAction(
createScaffolderConfig({
sentry: {
apiBaseUrl: 'https://config.sentry.io/api/0',
},
}),
);
const actionContext = getActionContext();
const mockDSN = 'https://test@sentry.io/123';
worker.use(
http.get(
`https://config.sentry.io/api/0/projects/${actionContext.input.organizationSlug}/${actionContext.input.projectSlug}/keys/`,
async ({ request }) => {
expect(request.headers.get('Authorization')).toBe(
`Bearer ${actionContext.input.authToken}`,
);
expect(request.headers.get('Content-Type')).toBe(`application/json`);
return HttpResponse.json([{ dsn: { public: mockDSN } }], {
status: 200,
});
},
),
);
await action.handler(actionContext);
expect(actionContext.output).toHaveBeenCalledWith('dsn', mockDSN);
});
});
@@ -51,6 +51,13 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
'authenticate via bearer auth token. Requires one of the following scopes: project:admin, project:read, project:write',
})
.optional(),
apiBaseUrl: z =>
z
.string({
description:
'Optional base URL for the Sentry API. e.g. https://sentry.io/api/0',
})
.optional(),
},
output: {
dsn: z =>
@@ -62,7 +69,8 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
},
},
async handler(ctx) {
const { organizationSlug, projectSlug, authToken } = ctx.input;
const { organizationSlug, projectSlug, authToken, apiBaseUrl } =
ctx.input;
const token = authToken
? authToken
@@ -72,8 +80,13 @@ export function createSentryFetchDSNAction(options: { config: Config }) {
throw new InputError(`No valid sentry token given`);
}
const baseUrl =
apiBaseUrl ||
config.getOptionalString('scaffolder.sentry.apiBaseUrl') ||
'https://sentry.io/api/0';
const response = await fetch(
`https://sentry.io/api/0/projects/${organizationSlug}/${projectSlug}/keys/`,
`${baseUrl}/projects/${organizationSlug}/${projectSlug}/keys/`,
{
method: 'GET',
headers: {