Refactor the Sentry plugin to use the proxy backend instead of a custom backend
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
---
|
||||
'@backstage/plugin-sentry': minor
|
||||
'@backstage/plugin-sentry-backend': minor
|
||||
---
|
||||
|
||||
The plugin uses the `proxy-backend` instead of a custom `sentry-backend`.
|
||||
It requires a proxy configuration:
|
||||
|
||||
`app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
'/sentry/api':
|
||||
target: https://sentry.io/api/
|
||||
allowedMethods: ['GET']
|
||||
headers:
|
||||
Authorization:
|
||||
$env: SENTRY_TOKEN # export SENTRY_TOKEN="Bearer <your-sentry-token>"
|
||||
```
|
||||
|
||||
The `MockApiBackend` is no longer configured by the `NODE_ENV` variable.
|
||||
Instead, the mock backend can be used with an api-override:
|
||||
|
||||
`packages/app/src/apis.ts`:
|
||||
|
||||
```ts
|
||||
import { createApiFactory } from '@backstage/core';
|
||||
import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry';
|
||||
|
||||
export const apis = [
|
||||
// ...
|
||||
|
||||
createApiFactory(sentryApiRef, new MockSentryApi()),
|
||||
];
|
||||
```
|
||||
|
||||
If you already use the Sentry backend, you must remove it from the backend:
|
||||
|
||||
Delete `packages/backend/src/plugins/sentry.ts`.
|
||||
|
||||
```diff
|
||||
# packages/backend/package.json
|
||||
|
||||
...
|
||||
"@backstage/plugin-scaffolder-backend": "^0.3.2",
|
||||
- "@backstage/plugin-sentry-backend": "^0.1.3",
|
||||
"@backstage/plugin-techdocs-backend": "^0.3.0",
|
||||
...
|
||||
```
|
||||
|
||||
```diff
|
||||
// packages/backend/src/index.html
|
||||
|
||||
const apiRouter = Router();
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
|
||||
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
|
||||
- apiRouter.use('/sentry', await sentry(sentryEnv));
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
apiRouter.use('/proxy', await proxy(proxyEnv));
|
||||
apiRouter.use('/graphql', await graphql(graphqlEnv));
|
||||
apiRouter.use(notFoundHandler());
|
||||
```
|
||||
@@ -52,6 +52,13 @@ proxy:
|
||||
Authorization:
|
||||
$env: BUILDKITE_TOKEN
|
||||
|
||||
'/sentry/api':
|
||||
target: https://sentry.io/api/
|
||||
allowedMethods: ['GET']
|
||||
headers:
|
||||
Authorization:
|
||||
$env: SENTRY_TOKEN
|
||||
|
||||
organization:
|
||||
name: My Company
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
"@backstage/plugin-proxy-backend": "^0.2.2",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.4",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.3.3",
|
||||
"@backstage/plugin-sentry-backend": "^0.1.3",
|
||||
"@backstage/plugin-techdocs-backend": "^0.3.1",
|
||||
"@gitbeaker/node": "^25.2.0",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
import Router from 'express-promise-router';
|
||||
import {
|
||||
createServiceBuilder,
|
||||
loadBackendConfig,
|
||||
getRootLogger,
|
||||
useHotMemoize,
|
||||
loadBackendConfig,
|
||||
notFoundHandler,
|
||||
SingleConnectionDatabaseManager,
|
||||
SingleHostDiscovery,
|
||||
UrlReaders,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import healthcheck from './plugins/healthcheck';
|
||||
@@ -40,7 +40,6 @@ import catalog from './plugins/catalog';
|
||||
import kubernetes from './plugins/kubernetes';
|
||||
import rollbar from './plugins/rollbar';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import sentry from './plugins/sentry';
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import graphql from './plugins/graphql';
|
||||
@@ -76,7 +75,6 @@ async function main() {
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
|
||||
const sentryEnv = useHotMemoize(module, () => createEnv('sentry'));
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
|
||||
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
|
||||
@@ -86,7 +84,6 @@ async function main() {
|
||||
apiRouter.use('/catalog', await catalog(catalogEnv));
|
||||
apiRouter.use('/rollbar', await rollbar(rollbarEnv));
|
||||
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
|
||||
apiRouter.use('/sentry', await sentry(sentryEnv));
|
||||
apiRouter.use('/auth', await auth(authEnv));
|
||||
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
|
||||
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# sentry-backend
|
||||
|
||||
Simple plugin forwarding requests to [Sentry](https://sentry.io) API.
|
||||
> DEPRECATED
|
||||
|
||||
Please use the [proxy-backend](../proxy-backend) instead. See [CHANGELOG.md](./CHANGELOG.md).
|
||||
|
||||
@@ -14,4 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
import { Router } from 'express';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const createRouter = async (_: Logger): Promise<Router> => Router();
|
||||
|
||||
throw new Error(
|
||||
'The sentry-backend has been deprecated and replaced by the proxy-backend. See the ' +
|
||||
'changelog on how to migrate to the proxy backend: https://github.com/backstage/backstage/blob/master/plugins/sentry/CHANGELOG.md.',
|
||||
);
|
||||
|
||||
@@ -1,42 +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 { Logger } from 'winston';
|
||||
import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { getSentryApiForwarder } from './sentry-api';
|
||||
|
||||
export async function createRouter(logger: Logger): Promise<express.Router> {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
const SENTRY_TOKEN = process.env.SENTRY_TOKEN;
|
||||
if (!SENTRY_TOKEN) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.',
|
||||
);
|
||||
}
|
||||
logger.warn(
|
||||
'Failed to initialize Sentry backend, set SENTRY_TOKEN environment variable to start the API.',
|
||||
);
|
||||
} else {
|
||||
const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger);
|
||||
|
||||
router.use(sentryForwarder);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,47 +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 express from 'express';
|
||||
import axios from 'axios';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export function getRequestHeaders(token: string) {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getSentryApiForwarder(token: string, logger: Logger) {
|
||||
return function forwardRequest(
|
||||
request: express.Request,
|
||||
response: express.Response,
|
||||
) {
|
||||
const sentryUrl = request.path;
|
||||
const effectiveUrl = `https://sentry.io/${sentryUrl}`;
|
||||
logger.info(`Calling Sentry REST API, ${effectiveUrl}`);
|
||||
axios
|
||||
.get(effectiveUrl, getRequestHeaders(token))
|
||||
.then(res => {
|
||||
response.send(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
return response.status(err.response.status).json({
|
||||
detail: err.response.statusText,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,42 +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 {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
} from '@backstage/backend-common';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export async function createStandaloneApplication(
|
||||
logger: Logger,
|
||||
): Promise<express.Application> {
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use('/', await createRouter(logger));
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -1,42 +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 { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { createStandaloneApplication } from './standaloneApplication';
|
||||
|
||||
export async function startStandaloneServer(
|
||||
parentLogger: Logger,
|
||||
): Promise<Server> {
|
||||
const logger = parentLogger.child({ service: 'scaffolder-backend' });
|
||||
logger.debug('Creating application...');
|
||||
|
||||
const app = await createStandaloneApplication(logger);
|
||||
|
||||
logger.debug('Starting application server...');
|
||||
const PORT = parseInt(process.env.PORT || '5001', 10);
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = app.listen(PORT, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Listening on port ${PORT}`);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
}
|
||||
+109
-10
@@ -1,17 +1,116 @@
|
||||
# sentry
|
||||
# Sentry Plugin
|
||||
|
||||
Welcome to the sentry plugin!
|
||||
The Sentry Plugin displays issues from [Sentry](https://sentry.io).
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||

|
||||
|
||||
## Getting started
|
||||
## Getting Started
|
||||
|
||||
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/sentry](http://localhost:3000/sentry).
|
||||
1. Install the Sentry Plugin:
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
```bash
|
||||
# packages/app
|
||||
|
||||
Needs SENTRY_TOKEN set in the environment for the backend to startup
|
||||
yarn add @backstage/plugin-sentry
|
||||
```
|
||||
|
||||
export SENTRY_TOKEN=<INSERT_SENTRY_TOKEN_HERE>
|
||||
2. Add plugin to the app:
|
||||
|
||||
```js
|
||||
// packages/app/src/plugins.ts
|
||||
|
||||
export { plugin as Sentry } from '@backstage/plugin-sentry';
|
||||
```
|
||||
|
||||
3. Add the `SentryIssuesWidget` to the EntityPage:
|
||||
|
||||
```jsx
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
|
||||
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
|
||||
|
||||
const OverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
// ...
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<SentryIssuesWidget entity={entity} />
|
||||
</Grid>
|
||||
// ...
|
||||
</Grid>
|
||||
);
|
||||
```
|
||||
|
||||
> You can also import a `Router` if you want to have a dedicated sentry page:
|
||||
>
|
||||
> ```tsx
|
||||
> // packages/app/src/components/catalog/EntityPage.tsx
|
||||
>
|
||||
> import { Router as SentryRouter } from '@backstage/plugin-sentry';
|
||||
>
|
||||
> const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
> <EntityPageLayout>
|
||||
> // ...
|
||||
> <EntityPageLayout.Content
|
||||
> path="/sentry"
|
||||
> title="Sentry"
|
||||
> element={<SentryRouter entity={entity} />}
|
||||
> />
|
||||
> // ...
|
||||
> </EntityPageLayout>
|
||||
> );
|
||||
> ```
|
||||
|
||||
4. Add the proxy config:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
proxy:
|
||||
'/sentry/api':
|
||||
target: https://sentry.io/api/
|
||||
allowedMethods: ['GET']
|
||||
headers:
|
||||
Authorization:
|
||||
# Content: 'Bearer <your-sentry-token>'
|
||||
$env: SENTRY_TOKEN
|
||||
|
||||
sentry:
|
||||
organization: <your-organization>
|
||||
```
|
||||
|
||||
5. Create a new internal integration with the permissions `Issues & Events: Read` (https://docs.sentry.io/product/integrations/integration-platform/) and provide it as `SENTRY_TOKEN` as env variable.
|
||||
|
||||
6. Add the `sentry.io/project-slug` annotation to your catalog-info.yaml file:
|
||||
|
||||
```yaml
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage
|
||||
description: |
|
||||
Backstage is an open-source developer portal that puts the developer experience first.
|
||||
annotations:
|
||||
sentry.io/project-slug: YOUR_PROJECT_SLUG
|
||||
spec:
|
||||
type: library
|
||||
owner: CNCF
|
||||
lifecycle: experimental
|
||||
```
|
||||
|
||||
### Demo Mode
|
||||
|
||||
The plugin provides a MockAPI that always returns dummy data instead of talking to the sentry backend.
|
||||
You can add it by overriding the `sentryApiRef`:
|
||||
|
||||
```ts
|
||||
// packages/app/src/apis.ts
|
||||
|
||||
import { createApiFactory } from '@backstage/core';
|
||||
import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry';
|
||||
|
||||
export const apis = [
|
||||
// ...
|
||||
|
||||
createApiFactory(sentryApiRef, new MockSentryApi()),
|
||||
];
|
||||
```
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
@@ -27,7 +27,6 @@
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
@@ -44,6 +43,7 @@
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
@@ -61,9 +61,15 @@
|
||||
"organization": {
|
||||
"type": "string",
|
||||
"visibility": "frontend"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"organization"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sentry"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRouter } from '@backstage/plugin-sentry-backend';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter(logger);
|
||||
}
|
||||
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';
|
||||
+1
-1
@@ -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 {
|
||||
+20
-24
@@ -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[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 { 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 }> = ({
|
||||
|
||||
@@ -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 || []} />;
|
||||
};
|
||||
+1
-4
@@ -13,8 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { SentryIssue } from './sentry-issue';
|
||||
|
||||
export interface SentryApi {
|
||||
fetchIssues(project: string, statsFor: string): Promise<SentryIssue[]>;
|
||||
}
|
||||
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 || []} />;
|
||||
};
|
||||
+1
-10
@@ -13,14 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { getRequestHeaders } from './sentry-api';
|
||||
|
||||
describe('SentryApiForwarder', () => {
|
||||
it('should generate headers based on token passed in constructor', () => {
|
||||
expect(getRequestHeaders('testtoken')).toEqual({
|
||||
headers: {
|
||||
Authorization: `Bearer testtoken`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
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] ?? '';
|
||||
};
|
||||
@@ -1,28 +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 { 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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user