diff --git a/app-config.yaml b/app-config.yaml index 5a5fdc1cab..90dd40158b 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -26,6 +26,12 @@ techdocs: sentry: organization: spotify +rollbar: + organization: spotify + accountToken: + $secret: + env: ROLLBAR_ACCOUNT_TOKEN + newrelic: api: baseUrl: 'https://api.newrelic.com/v2' @@ -53,7 +59,7 @@ auth: clientSecret: $secret: env: AUTH_GITHUB_CLIENT_SECRET - enterpriseInstanceUrl: + enterpriseInstanceUrl: $secret: env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: diff --git a/packages/backend/src/plugins/rollbar.ts b/packages/backend/src/plugins/rollbar.ts index 8899251db2..c6a2c27e9b 100644 --- a/packages/backend/src/plugins/rollbar.ts +++ b/packages/backend/src/plugins/rollbar.ts @@ -17,6 +17,9 @@ import { createRouter } from '@backstage/plugin-rollbar-backend'; import type { PluginEnvironment } from '../types'; -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter({ logger }); +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); } diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md index aacd0e9b6d..8cd08abed3 100644 --- a/plugins/rollbar-backend/README.md +++ b/plugins/rollbar-backend/README.md @@ -4,7 +4,18 @@ Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API. ## Setup -A `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read access account token. +The following values are read from the configuration file. + +```yaml +rollbar: + organization: spotify + accountToken: + $secret: + env: ROLLBAR_ACCOUNT_TOKEN +``` + +_NOTE: The `ROLLBAR_ACCOUNT_TOKEN` environment variable must be set to a read +access account token._ ## Links diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 102e233756..4b4a45b9dd 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.18", + "@backstage/config": "^0.1.1-alpha.18", "@types/express": "^4.17.6", "axios": "^0.19.2", "camelcase-keys": "^6.2.2", diff --git a/plugins/rollbar-backend/src/service/router.test.ts b/plugins/rollbar-backend/src/service/router.test.ts index 9ec0dbc7b5..f5dc2f1c3d 100644 --- a/plugins/rollbar-backend/src/service/router.test.ts +++ b/plugins/rollbar-backend/src/service/router.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; import { RollbarApi } from '../api'; @@ -37,6 +38,9 @@ describe('createRouter', () => { const router = await createRouter({ rollbarApi, logger: getVoidLogger(), + config: ConfigReader.fromConfigs([ + { context: 'abc', data: { rollbar: { accountToken: 'foo' } } }, + ]), }); app = express().use(router); }); diff --git a/plugins/rollbar-backend/src/service/router.ts b/plugins/rollbar-backend/src/service/router.ts index 09491b0d00..0a8c58a9d7 100644 --- a/plugins/rollbar-backend/src/service/router.ts +++ b/plugins/rollbar-backend/src/service/router.ts @@ -14,15 +14,17 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; -import { Logger } from 'winston'; -import Router from 'express-promise-router'; import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { errorHandler } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; import { RollbarApi } from '../api'; export interface RouterOptions { rollbarApi?: RollbarApi; logger: Logger; + config: Config; } export async function createRouter( @@ -30,7 +32,10 @@ export async function createRouter( ): Promise { const router = Router(); const logger = options.logger.child({ plugin: 'rollbar' }); - const accessToken = !options.rollbarApi ? getRollbarAccountToken(logger) : ''; + const config = options.config.getConfig('rollbar'); + const accessToken = !options.rollbarApi + ? getRollbarAccountToken(config, logger) + : ''; if (options.rollbarApi || accessToken) { const rollbarApi = @@ -82,17 +87,20 @@ export async function createRouter( return router; } -function getRollbarAccountToken(logger: Logger) { - const token = process.env.ROLLBAR_ACCOUNT_TOKEN || ''; +function getRollbarAccountToken(config: Config, logger: Logger) { + const token = + config.getOptionalString('accountToken') || + process.env.ROLLBAR_ACCOUNT_TOKEN || + ''; if (!token) { if (process.env.NODE_ENV !== 'development') { throw new Error( - 'Rollbar token must be provided in ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.', + 'The rollbar.accountToken must be provided in config to start the API.', ); } logger.warn( - 'Failed to initialize rollbar backend, set ROLLBAR_ACCOUNT_TOKEN environment variable to start the API.', + 'Failed to initialize rollbar backend, set rollbar.accountToken in config to start the API.', ); } diff --git a/plugins/rollbar-backend/src/service/standaloneServer.ts b/plugins/rollbar-backend/src/service/standaloneServer.ts index ecd5c072fb..a96d30d836 100644 --- a/plugins/rollbar-backend/src/service/standaloneServer.ts +++ b/plugins/rollbar-backend/src/service/standaloneServer.ts @@ -14,9 +14,13 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; +import { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { createRouter } from './router'; export interface ServerOptions { @@ -24,14 +28,16 @@ export interface ServerOptions { enableCors: boolean; logger: Logger; } + export async function startStandaloneServer( options: ServerOptions, ): Promise { const logger = options.logger.child({ service: 'rollbar-backend' }); + const config = ConfigReader.fromConfigs(await loadBackendConfig()); logger.debug('Creating application...'); - const router = await createRouter({ logger }); + const router = await createRouter({ logger, config }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) diff --git a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx index 7bda63ea5f..c0524fabc5 100644 --- a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx +++ b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx @@ -17,6 +17,7 @@ import React, { ReactNode } from 'react'; import { Header, + HeaderLabel, Page, pageTheme, Content, @@ -36,7 +37,10 @@ export const RollbarLayout = ({ title = 'Dashboard', children }: Props) => {
+ > + + +
diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx index 2b0268500d..11263ed820 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx @@ -15,7 +15,12 @@ */ import * as React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + ConfigApi, + configApiRef, +} from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; @@ -30,11 +35,20 @@ describe('RollbarPage component', () => { const rollbarApi: Partial = { getAllProjects: () => Promise.resolve(projects), }; + const config: Partial = { + getString: () => 'foo', + getOptionalString: () => 'foo', + }; const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - + {children} , ), diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx index ec07c0fa48..0460b4a308 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx @@ -16,18 +16,27 @@ import React from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { configApiRef, useApi } from '@backstage/core'; import { rollbarApiRef } from '../../api/RollbarApi'; import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; import { RollbarProjectTable } from './RollbarProjectTable'; export const RollbarPage = () => { + const configApi = useApi(configApiRef); const rollbarApi = useApi(rollbarApiRef); - const { value, loading } = useAsync(() => rollbarApi.getAllProjects()); + const org = + configApi.getOptionalString('rollbar.organization') ?? + configApi.getString('organization.name'); + const { value, loading, error } = useAsync(() => rollbarApi.getAllProjects()); return ( - + ); }; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx index bb8299b57e..fdf90c468e 100644 --- a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx +++ b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx @@ -17,9 +17,14 @@ import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import { Table, TableColumn } from '@backstage/core'; import { RollbarProject } from '../../api/types'; +const projectUrl = (org: string, id: number) => + `https://rollbar.com/${org}/all/items/?projects=${id}`; + const columns: TableColumn[] = [ { title: 'ID', @@ -32,7 +37,6 @@ const columns: TableColumn[] = [ title: 'Name', field: 'name', type: 'string', - align: 'left', highlight: true, render: (row: Partial) => ( @@ -44,29 +48,58 @@ const columns: TableColumn[] = [ title: 'Status', field: 'status', type: 'string', - align: 'left', + }, + { + title: 'Open', + width: '10%', + render: (row: any) => ( + + + + ), }, ]; type Props = { projects: RollbarProject[]; loading: boolean; + organization: string; + error?: any; }; -export const RollbarProjectTable = ({ projects, loading }: Props) => { +export const RollbarProjectTable = ({ + projects, + organization, + loading, + error, +}: Props) => { + if (error) { + return ( +
+ + Error encountered while fetching rollbar projects. {error.toString()} + +
+ ); + } + return ( ({ organization, ...p }))} /> ); }; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx index 135075bf82..e4919a68bd 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -15,7 +15,12 @@ */ import * as React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + ApiProvider, + ApiRegistry, + ConfigApi, + configApiRef, +} from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; @@ -43,11 +48,20 @@ describe('RollbarProjectPage component', () => { const rollbarApi: Partial = { getTopActiveItems: () => Promise.resolve(items), }; + const config: Partial = { + getString: () => 'foo', + getOptionalString: () => 'foo', + }; const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - + {children} , ), diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx index d5641e58ce..6635a72086 100644 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx @@ -17,17 +17,21 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { configApiRef, useApi } from '@backstage/core'; import { rollbarApiRef } from '../../api/RollbarApi'; import { RollbarLayout } from '../RollbarLayout/RollbarLayout'; import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable'; export const RollbarProjectPage = () => { + const configApi = useApi(configApiRef); const rollbarApi = useApi(rollbarApiRef); + const org = + configApi.getOptionalString('rollbar.organization') ?? + configApi.getString('organization.name'); const { componentId } = useParams() as { componentId: string; }; - const { value, loading } = useAsync(() => + const { value, loading, error } = useAsync(() => rollbarApi .getTopActiveItems(componentId, 168) .then(data => @@ -37,7 +41,13 @@ export const RollbarProjectPage = () => { return ( - + ); }; diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx index 724426ac8f..dc177265cd 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx @@ -41,14 +41,28 @@ const items: RollbarTopActiveItem[] = [ describe('RollbarTopItemsTable component', () => { it('should render empty data message when loaded and no data', async () => { const rendered = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); expect(rendered.getByText(/No records to display/)).toBeInTheDocument(); }); it('should display item attributes when loading has finished', async () => { const rendered = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); expect(rendered.getByText(/1234/)).toBeInTheDocument(); expect(rendered.getByText(/Error in foo/)).toBeInTheDocument(); diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx index 44dd5ed3f0..913ecca28a 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx @@ -16,6 +16,8 @@ import React from 'react'; import { Table, TableColumn } from '@backstage/core'; +import { Link } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import { RollbarFrameworkId, RollbarLevel, @@ -23,6 +25,9 @@ import { } from '../../api/types'; import { RollbarTrendGraph } from '../RollbarTrendGraph/RollbarTrendGraph'; +const itemUrl = (org: string, project: string, id: number) => + `https://rollbar.com/${org}/${project}/items/${id}`; + const columns: TableColumn[] = [ { title: 'ID', @@ -30,6 +35,15 @@ const columns: TableColumn[] = [ type: 'string', align: 'left', width: '70px', + render: (data: any) => ( + + {data.item.counter} + + ), }, { title: 'Title', @@ -40,9 +54,7 @@ const columns: TableColumn[] = [ { title: 'Trend', sorting: false, - render: data => ( - - ), + render: (data: any) => , }, { title: 'Occurrences', @@ -81,22 +93,42 @@ const columns: TableColumn[] = [ type Props = { items: RollbarTopActiveItem[]; + organization: string; + project: string; loading: boolean; + error?: any; }; -export const RollbarTopItemsTable = ({ items, loading }: Props) => { +export const RollbarTopItemsTable = ({ + items, + organization, + project, + loading, + error, +}: Props) => { + if (error) { + return ( +
+ + Error encountered while fetching rollbar top items. {error.toString()} + +
+ ); + } + return (
({ org: organization, project, ...i }))} /> ); };