Merge pull request #1877 from andrewthauer/rollbar-updates

Add rollbar organization links, etc.
This commit is contained in:
Fredrik Adelöw
2020-08-12 22:22:05 +02:00
committed by GitHub
15 changed files with 207 additions and 38 deletions
+7 -1
View File
@@ -35,6 +35,12 @@ techdocs:
sentry:
organization: spotify
rollbar:
organization: spotify
accountToken:
$secret:
env: ROLLBAR_ACCOUNT_TOKEN
newrelic:
api:
baseUrl: 'https://api.newrelic.com/v2'
@@ -62,7 +68,7 @@ auth:
clientSecret:
$secret:
env: AUTH_GITHUB_CLIENT_SECRET
enterpriseInstanceUrl:
enterpriseInstanceUrl:
$secret:
env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
gitlab:
+5 -2
View File
@@ -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 });
}
+12 -1
View File
@@ -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
+1
View File
@@ -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",
@@ -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);
});
+16 -8
View File
@@ -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<express.Router> {
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.',
);
}
@@ -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<Server> {
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' })
@@ -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) => {
<Header
title="Rollbar"
subtitle="Real-time error tracking & debugging tools for developers"
/>
>
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ContentHeader title={title}>
<SupportButton>
@@ -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<RollbarApi> = {
getAllProjects: () => Promise.resolve(projects),
};
const config: Partial<ConfigApi> = {
getString: () => 'foo',
getOptionalString: () => 'foo',
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
<ApiProvider
apis={ApiRegistry.from([
[rollbarApiRef, rollbarApi],
[configApiRef, config],
])}
>
{children}
</ApiProvider>,
),
@@ -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 (
<RollbarLayout>
<RollbarProjectTable loading={loading} projects={value || []} />
<RollbarProjectTable
projects={value || []}
organization={org}
loading={loading}
error={error}
/>
</RollbarLayout>
);
};
@@ -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<RollbarProject>) => (
<Link component={RouterLink} to={`/rollbar/${row.name}`}>
@@ -44,29 +48,58 @@ const columns: TableColumn[] = [
title: 'Status',
field: 'status',
type: 'string',
align: 'left',
},
{
title: 'Open',
width: '10%',
render: (row: any) => (
<Link
href={projectUrl(row.organization, row.id)}
target="_blank"
rel="noreferrer"
>
<OpenInNewIcon />
</Link>
),
},
];
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 (
<div>
<Alert severity="error">
Error encountered while fetching rollbar projects. {error.toString()}
</Alert>
</div>
);
}
return (
<Table
isLoading={loading}
columns={columns}
options={{
padding: 'dense',
paging: true,
search: true,
paging: true,
pageSize: 10,
showEmptyDataSourceMessage: !loading,
}}
title="Projects"
data={projects}
data={projects.map(p => ({ organization, ...p }))}
/>
);
};
@@ -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<RollbarApi> = {
getTopActiveItems: () => Promise.resolve(items),
};
const config: Partial<ConfigApi> = {
getString: () => 'foo',
getOptionalString: () => 'foo',
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
<ApiProvider
apis={ApiRegistry.from([
[rollbarApiRef, rollbarApi],
[configApiRef, config],
])}
>
{children}
</ApiProvider>,
),
@@ -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 (
<RollbarLayout>
<RollbarTopItemsTable loading={loading} items={value || []} />
<RollbarTopItemsTable
items={value || []}
organization={org}
project={componentId}
loading={loading}
error={error}
/>
</RollbarLayout>
);
};
@@ -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(<RollbarTopItemsTable items={[]} loading={false} />),
wrapInTestApp(
<RollbarTopItemsTable
items={[]}
organization="foo"
project="bar"
loading={false}
/>,
),
);
expect(rendered.getByText(/No records to display/)).toBeInTheDocument();
});
it('should display item attributes when loading has finished', async () => {
const rendered = render(
wrapInTestApp(<RollbarTopItemsTable items={items} loading={false} />),
wrapInTestApp(
<RollbarTopItemsTable
items={items}
organization="foo"
project="bar"
loading={false}
/>,
),
);
expect(rendered.getByText(/1234/)).toBeInTheDocument();
expect(rendered.getByText(/Error in foo/)).toBeInTheDocument();
@@ -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) => (
<Link
href={itemUrl(data.org, data.project, data.item.counter)}
target="_blank"
rel="noreferrer"
>
{data.item.counter}
</Link>
),
},
{
title: 'Title',
@@ -40,9 +54,7 @@ const columns: TableColumn[] = [
{
title: 'Trend',
sorting: false,
render: data => (
<RollbarTrendGraph counts={(data as RollbarTopActiveItem).counts} />
),
render: (data: any) => <RollbarTrendGraph counts={data.counts} />,
},
{
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 (
<div>
<Alert severity="error">
Error encountered while fetching rollbar top items. {error.toString()}
</Alert>
</div>
);
}
return (
<Table
isLoading={loading}
columns={columns}
options={{
padding: 'dense',
paging: false,
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title="Top Active Items"
data={items}
data={items.map(i => ({ org: organization, project, ...i }))}
/>
);
};