diff --git a/.changeset/chatty-pens-bathe.md b/.changeset/chatty-pens-bathe.md new file mode 100644 index 0000000000..4ca3378b2e --- /dev/null +++ b/.changeset/chatty-pens-bathe.md @@ -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 " +``` + +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()); +``` diff --git a/app-config.yaml b/app-config.yaml index 54d564de7d..7c26176943 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -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 diff --git a/packages/backend/package.json b/packages/backend/package.json index 5937d6a6f3..8f328db285 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b74954ecc5..68cc170901 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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)); diff --git a/plugins/sentry-backend/README.md b/plugins/sentry-backend/README.md index efd4c1b472..7a559276be 100644 --- a/plugins/sentry-backend/README.md +++ b/plugins/sentry-backend/README.md @@ -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). diff --git a/plugins/sentry-backend/src/index.ts b/plugins/sentry-backend/src/index.ts index 7612c392a2..6e1d12359e 100644 --- a/plugins/sentry-backend/src/index.ts +++ b/plugins/sentry-backend/src/index.ts @@ -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(); + +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.', +); diff --git a/plugins/sentry-backend/src/service/router.ts b/plugins/sentry-backend/src/service/router.ts deleted file mode 100644 index 153a8325b5..0000000000 --- a/plugins/sentry-backend/src/service/router.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/plugins/sentry-backend/src/service/sentry-api.ts b/plugins/sentry-backend/src/service/sentry-api.ts deleted file mode 100644 index 8d35038840..0000000000 --- a/plugins/sentry-backend/src/service/sentry-api.ts +++ /dev/null @@ -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, - }); - }); - }; -} diff --git a/plugins/sentry-backend/src/service/standaloneApplication.ts b/plugins/sentry-backend/src/service/standaloneApplication.ts deleted file mode 100644 index fdaad8bb2e..0000000000 --- a/plugins/sentry-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/plugins/sentry-backend/src/service/standaloneServer.ts b/plugins/sentry-backend/src/service/standaloneServer.ts deleted file mode 100644 index 37b87c5c40..0000000000 --- a/plugins/sentry-backend/src/service/standaloneServer.ts +++ /dev/null @@ -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 { - 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); - }); - }); -} diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index 73cca29d34..5d280de249 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -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_ +![Sentry Card](./docs/sentry-card.png) -## 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= +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 }) => ( + + // ... + + + + // ... + +); +``` + +> 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 }) => ( +> +> // ... +> path="/sentry" +> title="Sentry" +> element={} +> /> +> // ... +> +> ); +> ``` + +4. Add the proxy config: + +```yaml +# app-config.yaml + +proxy: + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: + # Content: 'Bearer ' + $env: SENTRY_TOKEN + +sentry: + 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()), +]; +``` diff --git a/plugins/sentry/docs/sentry-card.png b/plugins/sentry/docs/sentry-card.png new file mode 100644 index 0000000000..30aad2a56e Binary files /dev/null and b/plugins/sentry/docs/sentry-card.png differ diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index b952a81d51..c9708ee875 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -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" + ] } } diff --git a/packages/backend/src/plugins/sentry.ts b/plugins/sentry/src/api/index.ts similarity index 71% rename from packages/backend/src/plugins/sentry.ts rename to plugins/sentry/src/api/index.ts index 5cd0e55761..4cccfdb7a1 100644 --- a/packages/backend/src/plugins/sentry.ts +++ b/plugins/sentry/src/api/index.ts @@ -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'; diff --git a/plugins/sentry/src/components/SentryPluginPage/index.ts b/plugins/sentry/src/api/mock/index.ts similarity index 92% rename from plugins/sentry/src/components/SentryPluginPage/index.ts rename to plugins/sentry/src/api/mock/index.ts index 67b34db517..b65fb7a919 100644 --- a/plugins/sentry/src/components/SentryPluginPage/index.ts +++ b/plugins/sentry/src/api/mock/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './SentryPluginPage'; +export { MockSentryApi } from './mock-api'; diff --git a/plugins/sentry/src/data/mock-api.ts b/plugins/sentry/src/api/mock/mock-api.ts similarity index 93% rename from plugins/sentry/src/data/mock-api.ts rename to plugins/sentry/src/api/mock/mock-api.ts index 8026fec4ee..6743cee79f 100644 --- a/plugins/sentry/src/data/mock-api.ts +++ b/plugins/sentry/src/api/mock/mock-api.ts @@ -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 { diff --git a/plugins/sentry/src/data/sentry-issue-mock.json b/plugins/sentry/src/api/mock/sentry-issue-mock.json similarity index 100% rename from plugins/sentry/src/data/sentry-issue-mock.json rename to plugins/sentry/src/api/mock/sentry-issue-mock.json diff --git a/plugins/sentry/src/data/production-api.ts b/plugins/sentry/src/api/production-api.ts similarity index 53% rename from plugins/sentry/src/data/production-api.ts rename to plugins/sentry/src/api/production-api.ts index 5d21e80604..bf6fa98bce 100644 --- a/plugins/sentry/src/data/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -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 { - 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[]; } } diff --git a/plugins/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts new file mode 100644 index 0000000000..1edb550ec4 --- /dev/null +++ b/plugins/sentry/src/api/sentry-api.ts @@ -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({ + id: 'plugin.sentry.service', + description: 'Used by the Sentry plugin to make requests', +}); + +export interface SentryApi { + fetchIssues(project: string, statsFor: string): Promise; +} diff --git a/plugins/sentry/src/data/sentry-issue.ts b/plugins/sentry/src/api/sentry-issue.ts similarity index 99% rename from plugins/sentry/src/data/sentry-issue.ts rename to plugins/sentry/src/api/sentry-issue.ts index 14621bf629..017396229d 100644 --- a/plugins/sentry/src/data/sentry-issue.ts +++ b/plugins/sentry/src/api/sentry-issue.ts @@ -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[]; diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx index 74f1501225..b02c2860ad 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx @@ -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'; diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 617bdb3ce9..022453a7a1 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -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'; diff --git a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx index 7643c5ac6e..c4226780e4 100644 --- a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx +++ b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx @@ -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 }> = ({ diff --git a/plugins/sentry/src/components/Router.tsx b/plugins/sentry/src/components/Router.tsx index 6d15268629..7bed3700ab 100644 --- a/plugins/sentry/src/components/Router.tsx +++ b/plugins/sentry/src/components/Router.tsx @@ -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 ; - } - return ( - } + element={} /> ) diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx index 6926fdfc2a..441104c0ee 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx @@ -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'; diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index aae8009d98..72081bf2da 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -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'; diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx new file mode 100644 index 0000000000..c52b76c4bc --- /dev/null +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -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(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 ( + + {loading && } + + {!loading && !projectId && ( + + )} + + {!loading && error && ( + + )} + + ); + } + + return ; +}; diff --git a/plugins/sentry/src/data/sentry-api.ts b/plugins/sentry/src/components/SentryIssuesWidget/index.ts similarity index 79% rename from plugins/sentry/src/data/sentry-api.ts rename to plugins/sentry/src/components/SentryIssuesWidget/index.ts index 538900b738..fddc1374f2 100644 --- a/plugins/sentry/src/data/sentry-api.ts +++ b/plugins/sentry/src/components/SentryIssuesWidget/index.ts @@ -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; -} +export { SentryIssuesWidget } from './SentryIssuesWidget'; diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx deleted file mode 100644 index 21c21ce385..0000000000 --- a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx +++ /dev/null @@ -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( - - - - - , - ); - expect(rendered.getByText('Sentry issues')).toBeInTheDocument(); - expect(rendered.getByText('24H')).toBeInTheDocument(); - expect(rendered.getByText('12H')).toBeInTheDocument(); - }); -}); diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx deleted file mode 100644 index 763a8d753f..0000000000 --- a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx +++ /dev/null @@ -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 ( - -
- - - - - 24H - - - 12H - - - - Sentry plugin allows you to preview issues and navigate to sentry. - - - - - - - - - - ); -}; - -export default SentryPluginPage; diff --git a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx deleted file mode 100644 index 8d522df4a7..0000000000 --- a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx +++ /dev/null @@ -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(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 ( - - - - ); - } - - return ; -}; diff --git a/plugins/sentry-backend/src/service/sentry-api.test.ts b/plugins/sentry/src/components/index.ts similarity index 66% rename from plugins/sentry-backend/src/service/sentry-api.test.ts rename to plugins/sentry/src/components/index.ts index f15692861f..b1588954c9 100644 --- a/plugins/sentry-backend/src/service/sentry-api.test.ts +++ b/plugins/sentry/src/components/index.ts @@ -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'; diff --git a/plugins/sentry/src/components/useProjectSlug.ts b/plugins/sentry/src/components/useProjectSlug.ts new file mode 100644 index 0000000000..072d517b05 --- /dev/null +++ b/plugins/sentry/src/components/useProjectSlug.ts @@ -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] ?? ''; +}; diff --git a/plugins/sentry/src/data/api-factory.ts b/plugins/sentry/src/data/api-factory.ts deleted file mode 100644 index 88e1148b72..0000000000 --- a/plugins/sentry/src/data/api-factory.ts +++ /dev/null @@ -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(); -} diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts index a0d3cab1be..2b9e2186ec 100644 --- a/plugins/sentry/src/index.ts +++ b/plugins/sentry/src/index.ts @@ -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'; diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts index fc4b1af5e0..6a4fe4e5a4 100644 --- a/plugins/sentry/src/plugin.ts +++ b/plugins/sentry/src/plugin.ts @@ -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'), + ), + }), + ], });