Merge pull request #1497 from andrewthauer/rollbar-plugin

Add Rollbar plugin & backend
This commit is contained in:
Patrik Oldsberg
2020-07-09 10:05:57 +02:00
committed by GitHub
44 changed files with 1975 additions and 0 deletions
+1
View File
@@ -13,6 +13,7 @@
"@backstage/plugin-graphiql": "^0.1.1-alpha.13",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.13",
"@backstage/plugin-register-component": "^0.1.1-alpha.13",
"@backstage/plugin-rollbar": "^0.1.1-alpha.13",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.13",
"@backstage/plugin-sentry": "^0.1.1-alpha.13",
"@backstage/plugin-tech-radar": "^0.1.1-alpha.13",
+10
View File
@@ -57,6 +57,8 @@ import {
} from '@backstage/plugin-graphiql';
import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder';
import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
@@ -170,5 +172,13 @@ export const apis = (config: ConfigApi) => {
]),
);
builder.add(
rollbarApiRef,
new RollbarClient({
apiOrigin: backendUrl,
basePath: '/rollbar',
}),
);
return builder.build();
};
+1
View File
@@ -26,3 +26,4 @@ export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
export { plugin as TechDocs } from '@backstage/plugin-techdocs';
export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
+1
View File
@@ -25,6 +25,7 @@
"@backstage/plugin-auth-backend": "^0.1.1-alpha.13",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.13",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.13",
"@backstage/plugin-rollbar-backend": "^0.1.1-alpha.13",
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.13",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.13",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.13",
+7
View File
@@ -33,6 +33,7 @@ import knex from 'knex';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
import rollbar from './plugins/rollbar';
import scaffolder from './plugins/scaffolder';
import sentry from './plugins/sentry';
import techdocs from './plugins/techdocs';
@@ -69,6 +70,12 @@ async function main() {
const service = createServiceBuilder(module)
.loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter(
'/rollbar',
await rollbar(
getRootLogger().child({ type: 'plugin', plugin: 'rollbar' }),
),
)
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(
'/sentry',
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 { createRouter } from '@backstage/plugin-rollbar-backend';
import { Logger } from 'winston';
export default async function createPlugin(logger: Logger) {
return await createRouter({ logger });
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+12
View File
@@ -0,0 +1,12 @@
# Rollbar Backend
Simple plugin that proxies requests to the [Rollbar](https://rollbar.com) API.
## Setup
A `ROLLBAR_TOKEN` environment variable must be set to a read access account token.
## Links
- (Frontend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar]
- (The Backstage homepage)[https://backstage.io]
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@backstage/plugin-rollbar-backend",
"version": "0.1.1-alpha.13",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.13",
"@types/express": "^4.17.6",
"axios": "^0.19.2",
"camelcase-keys": "^6.2.2",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"helmet": "^3.22.0",
"lodash": "^4.17.15",
"morgan": "^1.10.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.13",
"@types/supertest": "^2.0.8",
"jest-fetch-mock": "^3.0.3",
"supertest": "^4.0.2"
},
"files": [
"dist"
]
}
@@ -0,0 +1,29 @@
/*
* 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 { getRequestHeaders } from './RollbarApi';
describe('RollbarApi', () => {
describe('getRequestHeaders', () => {
it('should generate headers based on token passed in constructor', () => {
expect(getRequestHeaders('testtoken')).toEqual({
headers: {
'X-Rollbar-Access-Token': `testtoken`,
},
});
});
});
});
@@ -0,0 +1,183 @@
/*
* 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 axios from 'axios';
import { Logger } from 'winston';
import camelcaseKeys from 'camelcase-keys';
import { buildQuery } from '../util';
import {
RollbarItemCount,
RollbarItemsResponse,
RollbarProject,
RollbarProjectAccessToken,
RollbarTopActiveItem,
} from './types';
const baseUrl = 'https://api.rollbar.com/api/1';
const buildUrl = (url: string) => `${baseUrl}${url}`;
export class RollbarApi {
private projectMap: ProjectMetadataMap | undefined;
constructor(
private readonly accessToken: string,
private readonly logger: Logger,
) {}
async getAllProjects() {
return this.get<RollbarProject[]>('/projects').then(projects =>
projects.filter(p => p.name),
);
}
async getProject(projectName: string) {
return this.getForProject<RollbarProject>(
`/project/:projectId`,
projectName,
false,
);
}
async getProjectItems(projectName: string) {
return this.getForProject<RollbarItemsResponse>(
`/items`,
projectName,
true,
);
}
async getTopActiveItems(
projectName: string,
options: { hours: number; environment: string } = {
hours: 24,
environment: 'production',
},
) {
return this.getForProject<RollbarTopActiveItem[]>(
`/reports/top_active_items?${buildQuery(options)}`,
projectName,
);
}
async getOccuranceCounts(
projectName: string,
options: { environment: string; item_id?: number } = {
environment: 'production',
},
) {
return this.getForProject<RollbarItemCount[]>(
`/reports/occurrence_counts?${buildQuery(options as any)}`,
projectName,
);
}
async getActivatedCounts(
projectName: string,
options: { environment: string; item_id?: number } = {
environment: 'production',
},
) {
return this.getForProject<RollbarItemCount[]>(
`/reports/activated_counts?${buildQuery(options as any)}`,
projectName,
);
}
private async getProjectAccessTokens(projectId: number) {
return this.get<RollbarProjectAccessToken[]>(
`/project/${projectId}/access_tokens`,
);
}
private async get<T>(url: string, accessToken?: string) {
const fullUrl = buildUrl(url);
if (this.logger) {
this.logger.info(`Calling Rollbar REST API, ${fullUrl}`);
}
return axios
.get(fullUrl, getRequestHeaders(accessToken || this.accessToken || ''))
.then(response =>
camelcaseKeys<T>(response?.data?.result, { deep: true }),
);
}
private async getForProject<T>(
url: string,
projectName: string,
useProjectToken = true,
) {
const project = await this.getProjectMetadata(projectName);
const resolvedUrl = url.replace(':projectId', project.id.toString());
return this.get<T>(resolvedUrl, useProjectToken ? project.accessToken : '');
}
private async getProjectMetadata(name: string) {
const projectMap = await this.getProjectMap();
const project = projectMap[name];
if (!project) {
throw Error(`Invalid project: '${name}'`);
}
if (!project.accessToken) {
const tokens = await this.getProjectAccessTokens(project.id);
const token = tokens.find(t => t.scopes.includes('read'));
project.accessToken = token ? token.accessToken : undefined;
}
if (!project.accessToken) {
throw Error(`Could not find project read access token for '${name}'`);
}
return project;
}
private async getProjectMap() {
if (this.projectMap) {
return this.projectMap;
}
const projects = await this.getAllProjects();
this.projectMap = projects.reduce((accum: ProjectMetadataMap, i) => {
accum[i.name] = { id: i.id, name: i.name };
return accum;
}, {});
return this.projectMap;
}
}
export function getRequestHeaders(token: string) {
return {
headers: {
'X-Rollbar-Access-Token': `${token}`,
},
};
}
type ProjectMetadata = {
name: string;
id: number;
accessToken?: string | undefined;
};
interface ProjectMetadataMap {
[name: string]: ProjectMetadata;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { RollbarApi, getRequestHeaders } from './RollbarApi';
+135
View File
@@ -0,0 +1,135 @@
/*
* 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.
*/
// TODO: Make this re-usable with backend
export type RollbarProjectAccessTokenScope = 'read' | 'write';
export type RollbarEnvironment = 'production' | string;
export enum RollbarLevel {
debug = 10,
info = 20,
warning = 30,
error = 40,
critical = 50,
}
export enum RollbarFrameworkId {
'unknown' = 0,
'rails' = 1,
'django' = 2,
'pyramid' = 3,
'node-js' = 4,
'pylons' = 5,
'php' = 6,
'browser-js' = 7,
'rollbar-system' = 8,
'android' = 9,
'ios' = 10,
'mailgun' = 11,
'logentries' = 12,
'python' = 13,
'ruby' = 14,
'sidekiq' = 15,
'flask' = 16,
'celery' = 17,
'rq' = 18,
}
export enum RollbarPlatformId {
'unknown' = 0,
'browser' = 1,
'flash' = 2,
'android' = 3,
'ios' = 4,
'heroku' = 5,
'google-app-engine' = 6,
'client' = 7,
}
export type RollbarProject = {
id: number;
name: string;
accountId: number;
status: 'enabled' | string;
};
export type RollbarProjectAccessToken = {
projectId: number;
name: string;
scopes: RollbarProjectAccessTokenScope[];
accessToken: string;
status: 'enabled' | string;
};
export type RollbarItem = {
publicItemId: number;
integrationsData: null;
levelLock: number;
controllingId: number;
lastActivatedTimestamp: number;
assignedUserId: number;
groupStatus: number;
hash: string;
id: number;
environment: RollbarEnvironment;
titleLock: number;
title: string;
lastOccurrenceId: number;
lastOccurrenceTimestamp: number;
platform: RollbarPlatformId;
firstOccurrenceTimestamp: number;
project_id: number;
resolvedInVersion: string;
status: 'enabled' | string;
uniqueOccurrences: number;
groupItemId: number;
framework: RollbarFrameworkId;
totalOccurrences: number;
level: RollbarLevel;
counter: number;
lastModifiedBy: number;
firstOccurrenceId: number;
activatingOccurrenceId: number;
lastResolvedTimestamp: number;
};
export type RollbarItemsResponse = {
items: RollbarItem[];
page: number;
totalCount: number;
};
export type RollbarItemCount = {
timestamp: number;
count: number;
};
export type RollbarTopActiveItem = {
item: {
id: number;
counter: number;
environment: RollbarEnvironment;
framework: RollbarFrameworkId;
lastOccurrenceTimestamp: number;
level: number;
occurrences: number;
projectId: number;
title: string;
uniqueOccurrences: number;
};
counts: number[];
};
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './api';
export * from './service/router';
+33
View File
@@ -0,0 +1,33 @@
/*
* 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 { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,122 @@
/*
* 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 { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { RollbarApi } from '../api';
import { createRouter } from './router';
import { RollbarProject, RollbarTopActiveItem } from '../api/types';
describe('createRouter', () => {
let rollbarApi: jest.Mocked<RollbarApi>;
let app: express.Express;
beforeAll(async () => {
rollbarApi = {
getAllProjects: jest.fn(),
getProject: jest.fn(),
getProjectItems: jest.fn(),
getTopActiveItems: jest.fn(),
getOccuranceCounts: jest.fn(),
getActivatedCounts: jest.fn(),
} as any;
const router = await createRouter({
rollbarApi,
logger: getVoidLogger(),
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /projects', () => {
it('lists projects', async () => {
const projects: RollbarProject[] = [
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
];
rollbarApi.getAllProjects.mockResolvedValueOnce(projects);
const response = await request(app).get('/projects');
expect(response.status).toEqual(200);
expect(response.body).toEqual(projects);
});
it('throws an error', async () => {
rollbarApi.getAllProjects.mockImplementationOnce(() => {
throw Error('error');
});
const response = await request(app).get('/projects');
expect(response.status).toEqual(500);
});
});
describe('GET /projects/:id', () => {
it('fetches a single project', async () => {
const project: RollbarProject = {
id: 123,
name: 'abc',
accountId: 1,
status: 'enabled',
};
rollbarApi.getProject.mockResolvedValueOnce(project);
const response = await request(app).get(`/projects/${123}`);
expect(response.status).toEqual(200);
expect(response.body).toEqual(project);
});
});
describe('GET /projects/:id/top_active_items', () => {
it('fetches a single project', async () => {
const items: RollbarTopActiveItem[] = [
{
item: {
id: 9898989,
counter: 1234,
environment: 'production',
framework: 2,
lastOccurrenceTimestamp: new Date().getTime() / 1000,
level: 50,
occurrences: 100,
projectId: 12345,
title: 'error occured',
uniqueOccurrences: 10,
},
counts: [10, 10, 10, 10, 10, 50],
},
];
rollbarApi.getTopActiveItems.mockResolvedValueOnce(items);
const response = await request(app).get(
`/projects/${123}/top_active_items`,
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(items);
});
});
});
@@ -0,0 +1,135 @@
/*
* 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 } from '@backstage/backend-common';
import { Logger } from 'winston';
import Router from 'express-promise-router';
import express from 'express';
import { RollbarApi } from '../api';
export interface RouterOptions {
rollbarApi?: RollbarApi;
logger: Logger;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const router = Router();
const logger = options.logger.child({ plugin: 'rollbar' });
const accessToken = !options.rollbarApi ? getRollbarToken(logger) : '';
if (options.rollbarApi || accessToken) {
const rollbarApi =
options.rollbarApi || new RollbarApi(accessToken, logger);
router.use(express.json());
const runAsync = createRunAsyncWrapper(logger);
router.get(
'/projects',
runAsync(async (_req, res) => {
const projects = await rollbarApi.getAllProjects();
res.status(200).header('').send(projects);
}),
);
router.get(
'/projects/:id',
runAsync(async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProject(id);
res.status(200).send(projects);
}),
);
router.get(
'/projects/:id/items',
runAsync(async (req, res) => {
const { id } = req.params;
const projects = await rollbarApi.getProjectItems(id);
res.status(200).send(projects);
}),
);
router.get(
'/projects/:id/top_active_items',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getTopActiveItems(id, query as any);
res.status(200).send(items);
}),
);
router.get(
'/projects/:id/occurance_counts',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getOccuranceCounts(id, query as any);
res.status(200).send(items);
}),
);
router.get(
'/projects/:id/activated_item_counts',
runAsync(async (req, res) => {
const { id } = req.params;
const query = req.query;
const items = await rollbarApi.getActivatedCounts(id, query as any);
res.status(200).send(items);
}),
);
}
router.use(errorHandler());
return router;
}
function createRunAsyncWrapper(logger: Logger) {
return function runAsyncWrapper(callback: express.RequestHandler) {
return function runAsync(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
return Promise.resolve(callback(req, res, next)).catch(error => {
logger.error(error);
next(error);
});
};
};
}
function getRollbarToken(logger: Logger) {
const token = process.env.ROLLBAR_TOKEN || '';
if (!token) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Rollbar token must be provided in ROLLBAR_TOKEN environment variable to start the API.',
);
}
logger.warn(
'Failed to initialize rollbar backend, set ROLLBAR_TOKEN environment variable to start the API.',
);
}
return token;
}
@@ -0,0 +1,44 @@
/*
* 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 { createServiceBuilder } from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'rollbar-backend' });
logger.debug('Creating application...');
const router = await createRouter({ logger });
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/catalog', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
+19
View File
@@ -0,0 +1,19 @@
/*
* 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.
*/
require('jest-fetch-mock').enableMocks();
export {};
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
interface PrimitiveMap {
[name: string]: number | string | boolean;
}
export const buildQuery = (obj: PrimitiveMap) => {
return Object.entries(obj)
.map(pair => pair.map(encodeURIComponent).join('='))
.join('&');
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+56
View File
@@ -0,0 +1,56 @@
# Rollbar Plugin
Website: [https://rollbar.com/](https://rollbar.com/)
## Setup
1. Configure the [rollbar backend plugin](https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend/README.md)
2. If you have standalone app (you didn't clone this repo), then do
```bash
yarn add @backstage/plugin-rollbar
```
3. Add plugin to the list of plugins:
```ts
// packages/app/src/plugins.ts
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
```
4. Add plugin API to your Backstage instance:
```ts
// packages/app/src/api.ts
import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar';
// ...
builder.add(
rollbarApiRef,
new RollbarClient({
apiOrigin: backendUrl,
basePath: '/rollbar',
}),
);
// Alternatively you can use the mock client
// builder.add(rollbarApiRef, new RollbarMockClient());
```
5. Run app with `yarn start` and navigate to `/rollbar`
## Features
- List rollbar projects
- View top active items for each project
## Limitations
- Rollbar has rate limits per token
## Links
- (Backend part of the plugin)[https://github.com/spotify/backstage/tree/master/plugins/rollbar-backend]
- (The Backstage homepage)[https://backstage.io]
+20
View File
@@ -0,0 +1,20 @@
/*
* 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 { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-rollbar",
"version": "0.1.1-alpha.13",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.13",
"@backstage/theme": "^0.1.1-alpha.13",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"lodash": "^4.17.15",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-sparklines": "^1.7.0",
"react-use": "^14.2.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.13",
"@backstage/dev-utils": "^0.1.1-alpha.13",
"@backstage/test-utils": "^0.1.1-alpha.13",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/react-hooks": "^3.3.0",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/node": "^12.0.0",
"@types/react": "^16.9",
"jest-fetch-mock": "^3.0.3"
},
"files": [
"dist"
]
}
+37
View File
@@ -0,0 +1,37 @@
/*
* 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 { createApiRef } from '@backstage/core';
import {
RollbarItemsResponse,
RollbarProject,
RollbarTopActiveItem,
} from './types';
export const rollbarApiRef = createApiRef<RollbarApi>({
id: 'plugin.rollbar.service',
description:
'Used by the Rollbar plugin to make requests to accompanying backend',
});
export interface RollbarApi {
getAllProjects(): Promise<RollbarProject[]>;
getTopActiveItems(
project: string,
hours?: number,
): Promise<RollbarTopActiveItem[]>;
getProjectItems(project: string): Promise<RollbarItemsResponse>;
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 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 { RollbarApi } from './RollbarApi';
import {
RollbarItemsResponse,
RollbarProject,
RollbarTopActiveItem,
} from './types';
export class RollbarClient implements RollbarApi {
private apiOrigin: string;
private basePath: string;
constructor({
apiOrigin,
basePath,
}: {
apiOrigin: string;
basePath: string;
}) {
this.apiOrigin = apiOrigin;
this.basePath = basePath;
}
async getAllProjects(): Promise<RollbarProject[]> {
const path = `/projects`;
return await this.get(path);
}
async getTopActiveItems(
project: string,
hours = 24,
environment = 'production',
): Promise<RollbarTopActiveItem[]> {
const path = `/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`;
return await this.get(path);
}
async getProjectItems(project: string): Promise<RollbarItemsResponse> {
const path = `/projects/${project}/items`;
return await this.get(path);
}
private async get(path: string): Promise<any> {
const url = `${this.apiOrigin}${this.basePath}${path}`;
const response = await fetch(url);
if (!response.ok) {
const payload = await response.text();
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
throw new Error(message);
}
return await response.json();
}
}
@@ -0,0 +1,70 @@
/*
* 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.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import { RollbarApi } from './RollbarApi';
import {
RollbarItemsResponse,
RollbarProject,
RollbarTopActiveItem,
} from './types';
export class RollbarMockClient implements RollbarApi {
async getAllProjects(): Promise<RollbarProject[]> {
return Promise.resolve([
{ id: 123, name: 'project-a', accountId: 1, status: 'enabled' },
{ id: 356, name: 'project-b', accountId: 1, status: 'enabled' },
{ id: 789, name: 'project-c', accountId: 1, status: 'enabled' },
]);
}
async getTopActiveItems(
_project: string,
_hours = 24,
_environment = 'production',
): Promise<RollbarTopActiveItem[]> {
const createItem = (id: number): RollbarTopActiveItem => ({
item: {
id,
counter: id,
environment: 'production',
framework: 2,
lastOccurrenceTimestamp: new Date().getTime() / 1000,
level: 50,
occurrences: 100,
projectId: 12345,
title: `Some error occurred in service - ${id}`,
uniqueOccurrences: 10,
},
counts: Array.from({ length: 168 }, () =>
Math.floor(Math.random() * 100),
),
});
const items = Array.from({ length: 10 }, (_, i) => createItem(i));
return Promise.resolve(items);
}
async getProjectItems(_project: string): Promise<RollbarItemsResponse> {
return Promise.resolve({
items: [],
page: 0,
totalCount: 0,
});
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* 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.
*/
// TODO: Make this shared/dry with backend
export type RollbarProjectAccessTokenScope = 'read' | 'write';
export type RollbarEnvironment = 'production' | string;
export enum RollbarLevel {
debug = 10,
info = 20,
warning = 30,
error = 40,
critical = 50,
}
export enum RollbarFrameworkId {
'unknown' = 0,
'rails' = 1,
'django' = 2,
'pyramid' = 3,
'node-js' = 4,
'pylons' = 5,
'php' = 6,
'browser-js' = 7,
'rollbar-system' = 8,
'android' = 9,
'ios' = 10,
'mailgun' = 11,
'logentries' = 12,
'python' = 13,
'ruby' = 14,
'sidekiq' = 15,
'flask' = 16,
'celery' = 17,
'rq' = 18,
}
export enum RollbarPlatformId {
'unknown' = 0,
'browser' = 1,
'flash' = 2,
'android' = 3,
'ios' = 4,
'heroku' = 5,
'google-app-engine' = 6,
'client' = 7,
}
export type RollbarProject = {
id: number;
name: string;
accountId: number;
status: 'enabled' | string;
};
export type RollbarProjectAccessToken = {
projectId: number;
name: string;
scopes: RollbarProjectAccessTokenScope[];
accessToken: string;
status: 'enabled' | string;
};
export type RollbarItem = {
publicItemId: number;
integrationsData: null;
levelLock: number;
controllingId: number;
lastActivatedTimestamp: number;
assignedUserId: number;
groupStatus: number;
hash: string;
id: number;
environment: RollbarEnvironment;
titleLock: number;
title: string;
lastOccurrenceId: number;
lastOccurrenceTimestamp: number;
platform: RollbarPlatformId;
firstOccurrenceTimestamp: number;
project_id: number;
resolvedInVersion: string;
status: 'enabled' | string;
uniqueOccurrences: number;
groupItemId: number;
framework: RollbarFrameworkId;
totalOccurrences: number;
level: RollbarLevel;
counter: number;
lastModifiedBy: number;
firstOccurrenceId: number;
activatingOccurrenceId: number;
lastResolvedTimestamp: number;
};
export type RollbarItemsResponse = {
items: RollbarItem[];
page: number;
totalCount: number;
};
export type RollbarItemCount = {
timestamp: number;
count: number;
};
export type RollbarTopActiveItem = {
item: {
id: number;
counter: number;
environment: RollbarEnvironment;
framework: RollbarFrameworkId;
lastOccurrenceTimestamp: number;
level: number;
occurrences: number;
projectId: number;
title: string;
uniqueOccurrences: number;
};
counts: number[];
};
@@ -0,0 +1,52 @@
/*
* 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, { ReactNode } from 'react';
import {
Header,
Page,
pageTheme,
Content,
ContentHeader,
SupportButton,
} from '@backstage/core';
import { Grid } from '@material-ui/core';
type Props = {
title?: string;
children: ReactNode;
};
export const RollbarLayout = ({ title = 'Dashboard', children }: Props) => {
return (
<Page theme={pageTheme.tool}>
<Header
title="Rollbar"
subtitle="Real-time error tracking & debugging tools for developers"
/>
<Content>
<ContentHeader title={title}>
<SupportButton>
Rollbar plugin allows you to preview issues and navigate to rollbar.
</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="column">
{children}
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,47 @@
/*
* 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 * as React from 'react';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
import { RollbarProject } from '../../api/types';
import { RollbarPage } from './RollbarPage';
describe('RollbarPage component', () => {
const projects: RollbarProject[] = [
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
];
const rollbarApi: Partial<RollbarApi> = {
getAllProjects: () => Promise.resolve(projects),
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
{children}
</ApiProvider>,
),
);
it('should render rollbar landing page', async () => {
const rendered = renderWrapped(<RollbarPage />);
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
});
});
@@ -0,0 +1,33 @@
/*
* 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 { useAsync } from 'react-use';
import { useApi } from '@backstage/core';
import { rollbarApiRef } from '../../api/RollbarApi';
import { RollbarLayout } from '../RollbarLayout/RollbarLayout';
import { RollbarProjectTable } from './RollbarProjectTable';
export const RollbarPage = () => {
const rollbarApi = useApi(rollbarApiRef);
const { value, loading } = useAsync(() => rollbarApi.getAllProjects());
return (
<RollbarLayout>
<RollbarProjectTable loading={loading} projects={value || []} />
</RollbarLayout>
);
};
@@ -0,0 +1,72 @@
/*
* 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 { Link as RouterLink } from 'react-router-dom';
import { Link } from '@material-ui/core';
import { Table, TableColumn } from '@backstage/core';
import { RollbarProject } from '../../api/types';
const columns: TableColumn[] = [
{
title: 'ID',
field: 'id',
type: 'numeric',
align: 'left',
width: '100px',
},
{
title: 'Name',
field: 'name',
type: 'string',
align: 'left',
highlight: true,
render: (row: Partial<RollbarProject>) => (
<Link component={RouterLink} to={`/rollbar/${row.name}`}>
{row.name}
</Link>
),
},
{
title: 'Status',
field: 'status',
type: 'string',
align: 'left',
},
];
type Props = {
projects: RollbarProject[];
loading: boolean;
};
export const RollbarProjectTable = ({ projects, loading }: Props) => {
return (
<Table
isLoading={loading}
columns={columns}
options={{
padding: 'dense',
paging: true,
search: true,
pageSize: 10,
showEmptyDataSourceMessage: !loading,
}}
title="Projects"
data={projects}
/>
);
};
@@ -0,0 +1,60 @@
/*
* 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 * as React from 'react';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
import { RollbarTopActiveItem } from '../../api/types';
import { RollbarProjectPage } from './RollbarProjectPage';
describe('RollbarProjectPage component', () => {
const items: RollbarTopActiveItem[] = [
{
item: {
id: 9898989,
counter: 1234,
environment: 'production',
framework: 2,
lastOccurrenceTimestamp: new Date().getTime() / 1000,
level: 50,
occurrences: 100,
projectId: 12345,
title: 'error occured',
uniqueOccurrences: 10,
},
counts: [10, 10, 10, 10, 10, 50],
},
];
const rollbarApi: Partial<RollbarApi> = {
getTopActiveItems: () => Promise.resolve(items),
};
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<ApiProvider apis={ApiRegistry.from([[rollbarApiRef, rollbarApi]])}>
{children}
</ApiProvider>,
),
);
it('should render rollbar project page', async () => {
const rendered = renderWrapped(<RollbarProjectPage />);
expect(rendered.getByText(/Top Active Items/)).toBeInTheDocument();
});
});
@@ -0,0 +1,43 @@
/*
* 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 { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core';
import { rollbarApiRef } from '../../api/RollbarApi';
import { RollbarLayout } from '../RollbarLayout/RollbarLayout';
import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable';
export const RollbarProjectPage = () => {
const rollbarApi = useApi(rollbarApiRef);
const { componentId } = useParams() as {
componentId: string;
};
const { value, loading } = useAsync(() =>
rollbarApi
.getTopActiveItems(componentId, 168)
.then(data =>
data.sort((a, b) => b.item.occurrences - a.item.occurrences),
),
);
return (
<RollbarLayout>
<RollbarTopItemsTable loading={loading} items={value || []} />
</RollbarLayout>
);
};
@@ -0,0 +1,57 @@
/*
* 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 { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import * as React from 'react';
import { RollbarTopItemsTable } from './RollbarTopItemsTable';
import { RollbarTopActiveItem } from '../../api/types';
const items: RollbarTopActiveItem[] = [
{
item: {
id: 89898989,
counter: 1234,
environment: 'production',
framework: 0,
lastOccurrenceTimestamp: new Date().getTime() / 1000,
level: 50,
occurrences: 150,
title: 'Error in foo',
uniqueOccurrences: 40,
projectId: 12345,
},
counts: [10, 20, 30, 40, 50],
},
];
describe('RollbarTopItemsTable component', () => {
it('should render empty data message when loaded and no data', async () => {
const rendered = render(
wrapInTestApp(<RollbarTopItemsTable items={[]} 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} />),
);
expect(rendered.getByText(/1234/)).toBeInTheDocument();
expect(rendered.getByText(/Error in foo/)).toBeInTheDocument();
expect(rendered.getByText(/critical/)).toBeInTheDocument();
});
});
@@ -0,0 +1,102 @@
/*
* 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 { Table, TableColumn } from '@backstage/core';
import {
RollbarFrameworkId,
RollbarLevel,
RollbarTopActiveItem,
} from '../../api/types';
import { RollbarTrendGraph } from '../RollbarTrendGraph/RollbarTrendGraph';
const columns: TableColumn[] = [
{
title: 'ID',
field: 'item.counter',
type: 'string',
align: 'left',
width: '70px',
},
{
title: 'Title',
field: 'item.title',
type: 'string',
align: 'left',
},
{
title: 'Trend',
sorting: false,
render: data => (
<RollbarTrendGraph counts={(data as RollbarTopActiveItem).counts} />
),
},
{
title: 'Occurrences',
field: 'item.occurrences',
type: 'numeric',
align: 'right',
},
{
title: 'Environment',
field: 'item.environment',
type: 'string',
},
{
title: 'Level',
field: 'item.level',
type: 'string',
render: data => RollbarLevel[(data as RollbarTopActiveItem).item.level],
},
{
title: 'Framework',
field: 'item.framework',
type: 'string',
render: data =>
RollbarFrameworkId[(data as RollbarTopActiveItem).item.framework],
},
{
title: 'Last Occurrence',
field: 'item.lastOccurrenceTimestamp',
type: 'datetime',
render: data =>
new Date(
(data as RollbarTopActiveItem).item.lastOccurrenceTimestamp * 1000,
).toLocaleDateString(),
},
];
type Props = {
items: RollbarTopActiveItem[];
loading: boolean;
};
export const RollbarTopItemsTable = ({ items, loading }: Props) => {
return (
<Table
isLoading={loading}
columns={columns}
options={{
padding: 'dense',
paging: false,
search: true,
showEmptyDataSourceMessage: !loading,
}}
title="Top Active Items"
data={items}
/>
);
};
@@ -0,0 +1,32 @@
/*
* 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 { wrapInTestApp } from '@backstage/test-utils';
import { RollbarTrendGraph } from './RollbarTrendGraph';
describe('RollbarTrendGraph component', () => {
it('should render a trend graph sparkline', async () => {
const mockCounts = [1, 2, 3, 4];
const rendered = render(
wrapInTestApp(
<RollbarTrendGraph counts={mockCounts} data-testid="graph" />,
),
);
expect(rendered).toBeTruthy();
});
});
@@ -0,0 +1,30 @@
/*
* 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 { Sparklines, SparklinesBars } from 'react-sparklines';
type Props = {
counts: number[];
};
export const RollbarTrendGraph = ({ counts }: Props) => {
return (
<Sparklines data={counts} svgHeight={48} min={0} margin={4}>
<SparklinesBars barWidth={2} />
</Sparklines>
);
};
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin } from './plugin';
export * from './api/RollbarApi';
export { RollbarClient } from './api/RollbarClient';
export { RollbarMockClient } from './api/RollbarMockClient';
+23
View File
@@ -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 { plugin } from './plugin';
describe('rollbar', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+28
View File
@@ -0,0 +1,28 @@
/*
* 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 { createPlugin } from '@backstage/core';
import { RollbarPage } from './components/RollbarPage/RollbarPage';
import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
import { rootRoute, rootProjectRoute } from './routes';
export const plugin = createPlugin({
id: 'rollbar',
register({ router }) {
router.addRoute(rootRoute, RollbarPage);
router.addRoute(rootProjectRoute, RollbarProjectPage);
},
});
+27
View File
@@ -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 { createRouteRef } from '@backstage/core';
export const rootRoute = createRouteRef({
path: '/rollbar',
title: 'Rollbar Home',
});
export const rootProjectRoute = createRouteRef({
path: '/rollbar/:componentId/*',
title: 'Rollbar',
});
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();
+19
View File
@@ -5839,6 +5839,15 @@ camelcase-keys@^4.0.0:
map-obj "^2.0.0"
quick-lru "^1.0.0"
camelcase-keys@^6.2.2:
version "6.2.2"
resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
dependencies:
camelcase "^5.3.1"
map-obj "^4.0.0"
quick-lru "^4.0.1"
camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@@ -12499,6 +12508,11 @@ map-obj@^2.0.0:
resolved "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
map-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5"
integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==
map-or-similar@^1.5.0:
version "1.5.0"
resolved "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
@@ -15240,6 +15254,11 @@ quick-lru@^1.0.0:
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"