Refactor the Sentry plugin to use the proxy backend instead of a custom backend

This commit is contained in:
Dominik Henneke
2020-12-07 17:02:54 +01:00
parent 4266967979
commit 075d3dc5ae
36 changed files with 406 additions and 482 deletions
@@ -13,16 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SentryApi } from './sentry-api';
import { MockSentryApi } from './mock-api';
import { ProductionSentryApi } from './production-api';
export function sentryApiFactory(
organization: string,
backendBaseUrl: string,
): SentryApi {
if (process.env.NODE_ENV === 'production') {
return new ProductionSentryApi(organization, backendBaseUrl);
}
return new MockSentryApi();
}
export * from './mock';
export type { SentryApi } from './sentry-api';
export { sentryApiRef } from './sentry-api';
export type { SentryIssue } from './sentry-issue';
export { ProductionSentryApi } from './production-api';
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './SentryPluginPage';
export { MockSentryApi } from './mock-api';
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SentryIssue } from './sentry-issue';
import { SentryApi } from './sentry-api';
import { SentryIssue } from '../sentry-issue';
import { SentryApi } from '../sentry-api';
import mockData from './sentry-issue-mock.json';
function getMockIssue(): SentryIssue {
@@ -13,36 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SentryIssue } from './sentry-issue';
import { SentryApi } from './sentry-api';
import { DiscoveryApi } from '@backstage/core';
export class ProductionSentryApi implements SentryApi {
private organization: string;
private backendBaseUrl: string;
constructor(organization: string, backendBaseUrl: string) {
this.organization = organization;
this.backendBaseUrl = backendBaseUrl;
}
constructor(
private readonly discoveryApi: DiscoveryApi,
private readonly organization: string,
) {}
async fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]> {
try {
const apiBaseUrl = `${this.backendBaseUrl}/sentry/api/0/projects/`;
const response = await fetch(
`${apiBaseUrl}/${this.organization}/${project}/issues/?statsFor=${statsFor}`,
);
if (response.status >= 400 && response.status < 600) {
throw new Error('Failed fetching Sentry issues');
}
return (await response.json()) as SentryIssue[];
} catch (exception) {
if (exception.detail) {
return exception;
}
throw new Error('Unknown error');
if (!project) {
return [];
}
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`;
const response = await fetch(
`${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsFor=${statsFor}`,
);
if (response.status >= 400 && response.status < 600) {
throw new Error('Failed fetching Sentry issues');
}
return (await response.json()) as SentryIssue[];
}
}
@@ -13,7 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SentryIssue } from './sentry-issue';
import { createApiRef } from '@backstage/core';
export const sentryApiRef = createApiRef<SentryApi>({
id: 'plugin.sentry.service',
description: 'Used by the Sentry plugin to make requests',
});
export interface SentryApi {
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
type SentryPlatform = 'javascript' | 'javascript-react' | string;
type EventPoint = number[];
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ErrorCell } from './ErrorCell';
import React from 'react';
import { render } from '@testing-library/react';
import mockIssue from '../../data/sentry-issue-mock.json';
import mockIssue from '../../api/mock/sentry-issue-mock.json';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { SentryIssue } from '../../data/sentry-issue';
import { SentryIssue } from '../../api';
import { Link, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { SentryIssue } from '../../data/sentry-issue';
import { SentryIssue } from '../../api';
import { Sparklines, SparklinesBars } from 'react-sparklines';
export const ErrorGraph: FC<{ sentryIssue: SentryIssue }> = ({
+4 -14
View File
@@ -13,28 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Routes, Route } from 'react-router';
import { MissingAnnotationEmptyState } from '@backstage/core';
import { SentryPluginWidget } from './SentryPluginWidget/SentryPluginWidget';
const SENTRY_ANNOTATION = 'sentry.io/project-slug';
import { Route, Routes } from 'react-router';
import { SentryIssuesWidget } from './SentryIssuesWidget';
export const Router = ({ entity }: { entity: Entity }) => {
const projectId = entity.metadata.annotations?.[SENTRY_ANNOTATION];
if (!projectId) {
return <MissingAnnotationEmptyState annotation={SENTRY_ANNOTATION} />;
}
return (
<Routes>
<Route
path="/"
element={
<SentryPluginWidget sentryProjectId={projectId} statsFor="24h" />
}
element={<SentryIssuesWidget entity={entity} statsFor="24h" />}
/>
)
</Routes>
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import SentryIssuesTable from './SentryIssuesTable';
import { SentryIssue } from '../../data/sentry-issue';
import mockIssue from '../../data/sentry-issue-mock.json';
import { SentryIssue } from '../../api';
import mockIssue from '../../api/mock/sentry-issue-mock.json';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
@@ -16,7 +16,7 @@
import React from 'react';
import { Table, TableColumn } from '@backstage/core';
import { SentryIssue } from '../../data/sentry-issue';
import { SentryIssue } from '../../api';
import { format } from 'timeago.js';
import { ErrorCell } from '../ErrorCell/ErrorCell';
import { ErrorGraph } from '../ErrorGraph/ErrorGraph';
@@ -0,0 +1,84 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React, { useEffect } from 'react';
import {
EmptyState,
ErrorApi,
errorApiRef,
InfoCard,
MissingAnnotationEmptyState,
Progress,
useApi,
} from '@backstage/core';
import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable';
import { useAsync } from 'react-use';
import { sentryApiRef } from '../../api';
import {
SENTRY_PROJECT_SLUG_ANNOTATION,
useProjectSlug,
} from '../useProjectSlug';
import { Entity } from '@backstage/catalog-model';
export const SentryIssuesWidget = ({
entity,
statsFor = '24h',
variant = 'gridItem',
}: {
entity: Entity;
statsFor?: '24h' | '12h';
variant?: string;
}) => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const sentryApi = useApi(sentryApiRef);
const projectId = useProjectSlug(entity);
const { loading, value, error } = useAsync(
() => sentryApi.fetchIssues(projectId, statsFor),
[sentryApi, statsFor, projectId],
);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error, errorApi]);
if (loading || !projectId || error) {
return (
<InfoCard title="Sentry issues" variant={variant}>
{loading && <Progress />}
{!loading && !projectId && (
<MissingAnnotationEmptyState
annotation={SENTRY_PROJECT_SLUG_ANNOTATION}
/>
)}
{!loading && error && (
<EmptyState
missing="info"
title="No information to display"
description={`There is no Sentry project with id '${projectId}'.`}
/>
)}
</InfoCard>
);
}
return <SentryIssuesTable sentryIssues={value || []} />;
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export { SentryIssuesWidget } from './SentryIssuesWidget';
@@ -1,58 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React from 'react';
import { render } from '@testing-library/react';
import SentryPluginPage from './SentryPluginPage';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { msw } from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import {
ApiProvider,
ApiRegistry,
errorApiRef,
configApiRef,
} from '@backstage/core';
const errorApi = { post: () => {} };
const ConfigApi = { getString: () => 'test' };
describe('SentryPluginPage', () => {
const server = setupServer();
msw.setupDefaultHandlers(server);
it('should render header and time switched', () => {
server.use(rest.get('/', (_req, res, ctx) => res(ctx.json({}))));
const rendered = render(
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[configApiRef, ConfigApi],
])}
>
<ThemeProvider theme={lightTheme}>
<SentryPluginPage />
</ThemeProvider>
</ApiProvider>,
);
expect(rendered.getByText('Sentry issues')).toBeInTheDocument();
expect(rendered.getByText('24H')).toBeInTheDocument();
expect(rendered.getByText('12H')).toBeInTheDocument();
});
});
@@ -1,69 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React, { FC, useState } from 'react';
import { Grid } from '@material-ui/core';
import {
Header,
Page,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core';
import { SentryPluginWidget } from '../SentryPluginWidget/SentryPluginWidget';
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
const SentryPluginPage: FC<{}> = () => {
const [statsFor, setStatsFor] = useState<'12h' | '24h'>('12h');
const toggleStatsFor = () => setStatsFor(statsFor === '12h' ? '12h' : '24h');
const sentryProjectId = 'sample-sentry-project-id';
return (
<Page themeId="tool">
<Header title="Sentry" />
<Content>
<ContentHeader title="Issue on Sentry">
<ToggleButtonGroup
value={statsFor}
exclusive
onChange={toggleStatsFor}
aria-label="text alignment"
>
<ToggleButton value="24h" aria-label="left aligned">
24H
</ToggleButton>
<ToggleButton value="12h" aria-label="left aligned">
12H
</ToggleButton>
</ToggleButtonGroup>
<SupportButton>
Sentry plugin allows you to preview issues and navigate to sentry.
</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
<Grid item>
<SentryPluginWidget
sentryProjectId={sentryProjectId}
statsFor={statsFor}
/>
</Grid>
</Grid>
</Content>
</Page>
);
};
export default SentryPluginPage;
@@ -1,60 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 React, { FC, useEffect } from 'react';
import {
ErrorApi,
errorApiRef,
InfoCard,
Progress,
useApi,
configApiRef,
} from '@backstage/core';
import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable';
import { useAsync } from 'react-use';
import { sentryApiFactory } from '../../data/api-factory';
export const SentryPluginWidget: FC<{
sentryProjectId: string;
statsFor: '24h' | '12h';
}> = ({ sentryProjectId, statsFor }) => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const configApi = useApi(configApiRef);
const org = configApi.getString('sentry.organization');
const backendBaseUrl = configApi.getString('backend.baseUrl');
const api = sentryApiFactory(org, backendBaseUrl);
const { loading, value, error } = useAsync(
() => api.fetchIssues(sentryProjectId, statsFor),
[statsFor, sentryProjectId],
);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error, errorApi]);
if (loading) {
return (
<InfoCard title="Sentry issues">
<Progress />
</InfoCard>
);
}
return <SentryIssuesTable sentryIssues={value || []} />;
};
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export * from './SentryIssuesWidget';
@@ -0,0 +1,23 @@
/*
* Copyright 2020 Spotify AB
*
* 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';
export const useProjectSlug = (entity: Entity) => {
return entity?.metadata.annotations?.[SENTRY_PROJECT_SLUG_ANNOTATION] ?? '';
};
+2 -1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './api';
export * from './components';
export { plugin } from './plugin';
export { Router } from './components/Router';
export { SentryPluginWidget as SentryIssuesWidget } from './components/SentryPluginWidget/SentryPluginWidget';
+19 -5
View File
@@ -14,8 +14,14 @@
* limitations under the License.
*/
import { createPlugin, createRouteRef } from '@backstage/core';
import SentryPluginPage from './components/SentryPluginPage';
import {
configApiRef,
createApiFactory,
createPlugin,
createRouteRef,
discoveryApiRef,
} from '@backstage/core';
import { ProductionSentryApi, sentryApiRef } from './api';
export const rootRouteRef = createRouteRef({
path: '/sentry',
@@ -24,7 +30,15 @@ export const rootRouteRef = createRouteRef({
export const plugin = createPlugin({
id: 'sentry',
register({ router }) {
router.addRoute(rootRouteRef, SentryPluginPage);
},
apis: [
createApiFactory({
api: sentryApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new ProductionSentryApi(
discoveryApi,
configApi.getString('sentry.organization'),
),
}),
],
});