diff --git a/.changeset/purple-cherries-fix.md b/.changeset/purple-cherries-fix.md new file mode 100644 index 0000000000..d29a107cf3 --- /dev/null +++ b/.changeset/purple-cherries-fix.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-sentry': minor +--- + +Added the possibility to specify organization per component, now the annotation `sentry.io/project-slug` can have the format of `[organization]/[project-slug]` or just `[project-slug]`. + +**BREAKING**: The method `fetchIssue` changed the signature: + +```diff +export interface SentryApi { + fetchIssues( +- project: string, +- statsFor: string, +- query?: string, ++ entity: Entity, ++ options: { ++ statsFor: string; ++ query?: string; ++ }, + ): Promise; +} +``` diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 24464dd700..46f28dcb01 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -242,13 +242,14 @@ that entity if the periskop plugin is installed. # Example: metadata: annotations: - sentry.io/project-slug: pump-station + sentry.io/project-slug: backstage/pump-station ``` The value of this annotation is the so-called slug (or alternatively, the ID) of -a [Sentry](https://sentry.io) project within your organization. The organization -slug is currently not configurable on a per-entity basis, but is assumed to be -the same for all entities in the catalog. +a [Sentry](https://sentry.io) project within your organization. The value can +be the format of `[organization]/[project-slug]` or just `[project-slug]`. When +the organization slug is omitted the `app-config.yaml` will be used as a +fallback (`sentry.organization`). Specifying this annotation may enable Sentry related features in Backstage for that entity. diff --git a/plugins/sentry/api-report.md b/plugins/sentry/api-report.md index 043fa7a1fd..625db39959 100644 --- a/plugins/sentry/api-report.md +++ b/plugins/sentry/api-report.md @@ -53,9 +53,11 @@ export class ProductionSentryApi implements SentryApi { ); // (undocumented) fetchIssues( - project: string, - statsFor: string, - query?: string, + entity: Entity, + options: { + statsFor: string; + query?: string; + }, ): Promise; } @@ -70,9 +72,11 @@ export const Router: ({ entity }: { entity: Entity }) => JSX.Element; export interface SentryApi { // (undocumented) fetchIssues( - project: string, - statsFor: string, - query?: string, + entity: Entity, + options: { + statsFor: string; + query?: string; + }, ): Promise; } diff --git a/plugins/sentry/dev/index.tsx b/plugins/sentry/dev/index.tsx index 346cb7563a..667f965354 100644 --- a/plugins/sentry/dev/index.tsx +++ b/plugins/sentry/dev/index.tsx @@ -26,7 +26,10 @@ import { SentryApi, sentryApiRef, } from '../src'; -import { SENTRY_PROJECT_SLUG_ANNOTATION } from '../src/components/useProjectSlug'; +import { + SENTRY_PROJECT_SLUG_ANNOTATION, + getProjectSlug, +} from '../src/api/annotations'; import { Content, Header, Page } from '@backstage/core-components'; const entity = (name?: string) => @@ -47,8 +50,8 @@ createDevApp() deps: {}, factory: () => ({ - fetchIssues: async (project: string) => { - switch (project) { + fetchIssues: async (e: Entity) => { + switch (getProjectSlug(e)) { case 'error': throw new Error('Error!'); diff --git a/plugins/sentry/src/api/annotations.test.ts b/plugins/sentry/src/api/annotations.test.ts new file mode 100644 index 0000000000..633966dc10 --- /dev/null +++ b/plugins/sentry/src/api/annotations.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity } from '@backstage/catalog-model'; +import { + getOrganization, + getProjectSlug, + SENTRY_PROJECT_SLUG_ANNOTATION, +} from './annotations'; + +const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [SENTRY_PROJECT_SLUG_ANNOTATION]: name, + }, + name: 'something', + }, + } as Entity); + +describe('sentry annotations', () => { + it('getOrganization should works', () => { + const data = [ + [entity('orgs/some'), 'orgs'], + [entity('spotify/some'), 'spotify'], + [entity('slug'), ''], + ]; + + data.map(([input, expected]) => + expect(getOrganization(input as Entity)).toBe(expected), + ); + }); + it('getProjectSlug should works', () => { + const data = [ + [entity('orgs/some'), 'some'], + [entity('spotify/some'), 'some'], + [entity('slug'), 'slug'], + [entity(''), ''], + ]; + + data.map(([input, expected]) => + expect(getProjectSlug(input as Entity)).toBe(expected), + ); + }); +}); diff --git a/plugins/sentry/src/api/annotations.ts b/plugins/sentry/src/api/annotations.ts new file mode 100644 index 0000000000..169dc59ca0 --- /dev/null +++ b/plugins/sentry/src/api/annotations.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +export const SENTRY_PROJECT_SLUG_ANNOTATION = 'sentry.io/project-slug'; + +// The value can be the format of `[organization]/[project-slug]` or just `[project-slug] +const parseAnnotation = (entity: Entity) => { + return (entity?.metadata.annotations?.[SENTRY_PROJECT_SLUG_ANNOTATION] ?? '') + .split('/') + .reverse(); +}; + +export const getProjectSlug = (entity: Entity) => { + const [projectSlug, _] = parseAnnotation(entity); + return projectSlug ?? ''; +}; + +export const getOrganization = (entity: Entity) => { + const [_, organization] = parseAnnotation(entity); + return organization ?? ''; +}; diff --git a/plugins/sentry/src/api/production-api.ts b/plugins/sentry/src/api/production-api.ts index 5062f67380..da77f8a47d 100644 --- a/plugins/sentry/src/api/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -17,6 +17,8 @@ import { SentryIssue } from './sentry-issue'; import { SentryApi } from './sentry-api'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { getProjectSlug, getOrganization } from './annotations'; export class ProductionSentryApi implements SentryApi { constructor( @@ -26,22 +28,29 @@ export class ProductionSentryApi implements SentryApi { ) {} async fetchIssues( - project: string, - statsFor: string, - query?: string, + entity: Entity, + options: { + statsFor: string; + query?: string; + }, ): Promise { + const { query, statsFor } = options; + + const project = getProjectSlug(entity); + const organization = getOrganization(entity) || this.organization; + if (!project) { return []; } const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`; - const options = await this.authOptions(); + const authOpts = await this.authOptions(); const queryPart = query ? `&query=${query}` : ''; const response = await fetch( - `${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`, - options, + `${apiUrl}/0/projects/${organization}/${project}/issues/?statsPeriod=${statsFor}${queryPart}`, + authOpts, ); if (response.status >= 400 && response.status < 600) { diff --git a/plugins/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts index 0c5b709508..507803804d 100644 --- a/plugins/sentry/src/api/sentry-api.ts +++ b/plugins/sentry/src/api/sentry-api.ts @@ -16,6 +16,7 @@ import { SentryIssue } from './sentry-issue'; import { createApiRef } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; export const sentryApiRef = createApiRef({ id: 'plugin.sentry.service', @@ -23,8 +24,10 @@ export const sentryApiRef = createApiRef({ export interface SentryApi { fetchIssues( - project: string, - statsFor: string, - query?: string, + entity: Entity, + options: { + statsFor: string; + query?: string; + }, ): Promise; } diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx index d9e481b067..089ba0f1d6 100644 --- a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -19,10 +19,7 @@ import React, { useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { sentryApiRef } from '../../api'; import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; -import { - SENTRY_PROJECT_SLUG_ANNOTATION, - useProjectSlug, -} from '../useProjectSlug'; +import { SENTRY_PROJECT_SLUG_ANNOTATION, useProjectSlug } from '../hooks'; import { EmptyState, @@ -54,7 +51,7 @@ export const SentryIssuesWidget = ({ const projectId = useProjectSlug(entity); const { loading, value, error } = useAsync( - () => sentryApi.fetchIssues(projectId, statsFor, query), + () => sentryApi.fetchIssues(entity, { statsFor, query }), [sentryApi, statsFor, projectId, query], ); diff --git a/plugins/sentry/src/components/useProjectSlug.ts b/plugins/sentry/src/components/hooks.ts similarity index 70% rename from plugins/sentry/src/components/useProjectSlug.ts rename to plugins/sentry/src/components/hooks.ts index 0796e9e8df..0f1cb08526 100644 --- a/plugins/sentry/src/components/useProjectSlug.ts +++ b/plugins/sentry/src/components/hooks.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { + getProjectSlug, + getOrganization, + SENTRY_PROJECT_SLUG_ANNOTATION, +} from '../api/annotations'; -export const SENTRY_PROJECT_SLUG_ANNOTATION = 'sentry.io/project-slug'; - -export const useProjectSlug = (entity: Entity) => { - return entity?.metadata.annotations?.[SENTRY_PROJECT_SLUG_ANNOTATION] ?? ''; -}; +export const useProjectSlug = getProjectSlug; +export const useOrganization = getOrganization; +export { SENTRY_PROJECT_SLUG_ANNOTATION }; diff --git a/plugins/sentry/src/components/isSentryAvailable.ts b/plugins/sentry/src/components/isSentryAvailable.ts index 1a7d26f97c..a0803f6b45 100644 --- a/plugins/sentry/src/components/isSentryAvailable.ts +++ b/plugins/sentry/src/components/isSentryAvailable.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { SENTRY_PROJECT_SLUG_ANNOTATION } from './useProjectSlug'; +import { SENTRY_PROJECT_SLUG_ANNOTATION } from './hooks'; /** * @public