feat(sentry): added possibility to specify organization per component

Signed-off-by: Antonio Musolino <antoniomusolino007@gmail.com>
This commit is contained in:
Antonio Musolino
2022-07-06 12:36:02 +02:00
parent e0a993834c
commit 1b7c691a3b
11 changed files with 170 additions and 34 deletions
+22
View File
@@ -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<SentryIssue[]>;
}
```
@@ -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.
+10 -6
View File
@@ -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<SentryIssue[]>;
}
@@ -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<SentryIssue[]>;
}
+6 -3
View File
@@ -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!');
@@ -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),
);
});
});
+36
View File
@@ -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 ?? '';
};
+15 -6
View File
@@ -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<SentryIssue[]> {
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) {
+6 -3
View File
@@ -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<SentryApi>({
id: 'plugin.sentry.service',
@@ -23,8 +24,10 @@ export const sentryApiRef = createApiRef<SentryApi>({
export interface SentryApi {
fetchIssues(
project: string,
statsFor: string,
query?: string,
entity: Entity,
options: {
statsFor: string;
query?: string;
},
): Promise<SentryIssue[]>;
}
@@ -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],
);
@@ -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 };
@@ -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