From 8885ebee89271b82534b0fa57c1fb18394cfe48c Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Wed, 8 Jul 2020 21:43:53 -0400 Subject: [PATCH] adding rollbar plugin & backend This is the first commit of the rollbar plugin. It is fully functional but will require some additional features to make it more usable and feature complete. It currently includes a backend wraps the rollbar REST API and provides data for the frontend plugin. --- packages/app/package.json | 1 + packages/app/src/apis.ts | 10 + packages/app/src/plugins.ts | 1 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 7 + packages/backend/src/plugins/rollbar.ts | 22 +++ plugins/rollbar-backend/.eslintrc.js | 3 + plugins/rollbar-backend/README.md | 12 ++ plugins/rollbar-backend/package.json | 47 +++++ .../src/api/RollbarApi.test.ts | 29 +++ plugins/rollbar-backend/src/api/RollbarApi.ts | 183 ++++++++++++++++++ plugins/rollbar-backend/src/api/index.ts | 17 ++ plugins/rollbar-backend/src/api/types.ts | 135 +++++++++++++ plugins/rollbar-backend/src/index.ts | 18 ++ plugins/rollbar-backend/src/run.ts | 33 ++++ .../src/service/router.test.ts | 122 ++++++++++++ plugins/rollbar-backend/src/service/router.ts | 135 +++++++++++++ .../src/service/standaloneServer.ts | 44 +++++ plugins/rollbar-backend/src/setupTests.ts | 19 ++ plugins/rollbar-backend/src/util/index.ts | 25 +++ plugins/rollbar/.eslintrc.js | 3 + plugins/rollbar/README.md | 56 ++++++ plugins/rollbar/dev/index.tsx | 20 ++ plugins/rollbar/package.json | 53 +++++ plugins/rollbar/src/api/RollbarApi.ts | 37 ++++ plugins/rollbar/src/api/RollbarClient.ts | 73 +++++++ plugins/rollbar/src/api/RollbarMockClient.ts | 70 +++++++ plugins/rollbar/src/api/types.ts | 135 +++++++++++++ .../RollbarLayout/RollbarLayout.tsx | 52 +++++ .../RollbarPage/RollbarPage.test.tsx | 47 +++++ .../components/RollbarPage/RollbarPage.tsx | 33 ++++ .../RollbarPage/RollbarProjectTable.tsx | 72 +++++++ .../RollbarProjectPage.test.tsx | 60 ++++++ .../RollbarProjectPage/RollbarProjectPage.tsx | 43 ++++ .../RollbarTopItemsTable.test.tsx | 57 ++++++ .../RollbarTopItemsTable.tsx | 102 ++++++++++ .../RollbarTrendGraph.test.tsx | 32 +++ .../RollbarTrendGraph/RollbarTrendGraph.tsx | 30 +++ plugins/rollbar/src/index.ts | 20 ++ plugins/rollbar/src/plugin.test.ts | 23 +++ plugins/rollbar/src/plugin.ts | 28 +++ plugins/rollbar/src/routes.ts | 27 +++ plugins/rollbar/src/setupTests.ts | 19 ++ yarn.lock | 19 ++ 44 files changed, 1975 insertions(+) create mode 100644 packages/backend/src/plugins/rollbar.ts create mode 100644 plugins/rollbar-backend/.eslintrc.js create mode 100644 plugins/rollbar-backend/README.md create mode 100644 plugins/rollbar-backend/package.json create mode 100644 plugins/rollbar-backend/src/api/RollbarApi.test.ts create mode 100644 plugins/rollbar-backend/src/api/RollbarApi.ts create mode 100644 plugins/rollbar-backend/src/api/index.ts create mode 100644 plugins/rollbar-backend/src/api/types.ts create mode 100644 plugins/rollbar-backend/src/index.ts create mode 100644 plugins/rollbar-backend/src/run.ts create mode 100644 plugins/rollbar-backend/src/service/router.test.ts create mode 100644 plugins/rollbar-backend/src/service/router.ts create mode 100644 plugins/rollbar-backend/src/service/standaloneServer.ts create mode 100644 plugins/rollbar-backend/src/setupTests.ts create mode 100644 plugins/rollbar-backend/src/util/index.ts create mode 100644 plugins/rollbar/.eslintrc.js create mode 100644 plugins/rollbar/README.md create mode 100644 plugins/rollbar/dev/index.tsx create mode 100644 plugins/rollbar/package.json create mode 100644 plugins/rollbar/src/api/RollbarApi.ts create mode 100644 plugins/rollbar/src/api/RollbarClient.ts create mode 100644 plugins/rollbar/src/api/RollbarMockClient.ts create mode 100644 plugins/rollbar/src/api/types.ts create mode 100644 plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx create mode 100644 plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx create mode 100644 plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx create mode 100644 plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx create mode 100644 plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx create mode 100644 plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx create mode 100644 plugins/rollbar/src/index.ts create mode 100644 plugins/rollbar/src/plugin.test.ts create mode 100644 plugins/rollbar/src/plugin.ts create mode 100644 plugins/rollbar/src/routes.ts create mode 100644 plugins/rollbar/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 35e78a8793..ed256e6c2d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index bc7170c93a..abc490b658 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -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(); }; diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index a5461c1bea..46c8f23ef0 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 2e57282a07..2e9cd1ff49 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6841758be8..5562ffa242 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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', diff --git a/packages/backend/src/plugins/rollbar.ts b/packages/backend/src/plugins/rollbar.ts new file mode 100644 index 0000000000..4c8ae9bfeb --- /dev/null +++ b/packages/backend/src/plugins/rollbar.ts @@ -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 }); +} diff --git a/plugins/rollbar-backend/.eslintrc.js b/plugins/rollbar-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/rollbar-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/rollbar-backend/README.md b/plugins/rollbar-backend/README.md new file mode 100644 index 0000000000..39a3d93526 --- /dev/null +++ b/plugins/rollbar-backend/README.md @@ -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] diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json new file mode 100644 index 0000000000..5033bdbdea --- /dev/null +++ b/plugins/rollbar-backend/package.json @@ -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" + ] +} diff --git a/plugins/rollbar-backend/src/api/RollbarApi.test.ts b/plugins/rollbar-backend/src/api/RollbarApi.test.ts new file mode 100644 index 0000000000..b79d8d1ec7 --- /dev/null +++ b/plugins/rollbar-backend/src/api/RollbarApi.test.ts @@ -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`, + }, + }); + }); + }); +}); diff --git a/plugins/rollbar-backend/src/api/RollbarApi.ts b/plugins/rollbar-backend/src/api/RollbarApi.ts new file mode 100644 index 0000000000..acf0583cf0 --- /dev/null +++ b/plugins/rollbar-backend/src/api/RollbarApi.ts @@ -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('/projects').then(projects => + projects.filter(p => p.name), + ); + } + + async getProject(projectName: string) { + return this.getForProject( + `/project/:projectId`, + projectName, + false, + ); + } + + async getProjectItems(projectName: string) { + return this.getForProject( + `/items`, + projectName, + true, + ); + } + + async getTopActiveItems( + projectName: string, + options: { hours: number; environment: string } = { + hours: 24, + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/top_active_items?${buildQuery(options)}`, + projectName, + ); + } + + async getOccuranceCounts( + projectName: string, + options: { environment: string; item_id?: number } = { + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/occurrence_counts?${buildQuery(options as any)}`, + projectName, + ); + } + + async getActivatedCounts( + projectName: string, + options: { environment: string; item_id?: number } = { + environment: 'production', + }, + ) { + return this.getForProject( + `/reports/activated_counts?${buildQuery(options as any)}`, + projectName, + ); + } + + private async getProjectAccessTokens(projectId: number) { + return this.get( + `/project/${projectId}/access_tokens`, + ); + } + + private async get(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(response?.data?.result, { deep: true }), + ); + } + + private async getForProject( + url: string, + projectName: string, + useProjectToken = true, + ) { + const project = await this.getProjectMetadata(projectName); + const resolvedUrl = url.replace(':projectId', project.id.toString()); + return this.get(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; +} diff --git a/plugins/rollbar-backend/src/api/index.ts b/plugins/rollbar-backend/src/api/index.ts new file mode 100644 index 0000000000..706fa92b1c --- /dev/null +++ b/plugins/rollbar-backend/src/api/index.ts @@ -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'; diff --git a/plugins/rollbar-backend/src/api/types.ts b/plugins/rollbar-backend/src/api/types.ts new file mode 100644 index 0000000000..8a673593d3 --- /dev/null +++ b/plugins/rollbar-backend/src/api/types.ts @@ -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[]; +}; diff --git a/plugins/rollbar-backend/src/index.ts b/plugins/rollbar-backend/src/index.ts new file mode 100644 index 0000000000..e5023ebea4 --- /dev/null +++ b/plugins/rollbar-backend/src/index.ts @@ -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'; diff --git a/plugins/rollbar-backend/src/run.ts b/plugins/rollbar-backend/src/run.ts new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/plugins/rollbar-backend/src/run.ts @@ -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); +}); diff --git a/plugins/rollbar-backend/src/service/router.test.ts b/plugins/rollbar-backend/src/service/router.test.ts new file mode 100644 index 0000000000..9ec0dbc7b5 --- /dev/null +++ b/plugins/rollbar-backend/src/service/router.test.ts @@ -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; + 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); + }); + }); +}); diff --git a/plugins/rollbar-backend/src/service/router.ts b/plugins/rollbar-backend/src/service/router.ts new file mode 100644 index 0000000000..cee3297c2a --- /dev/null +++ b/plugins/rollbar-backend/src/service/router.ts @@ -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 { + 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; +} diff --git a/plugins/rollbar-backend/src/service/standaloneServer.ts b/plugins/rollbar-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..ecd5c072fb --- /dev/null +++ b/plugins/rollbar-backend/src/service/standaloneServer.ts @@ -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 { + 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); + }); +} diff --git a/plugins/rollbar-backend/src/setupTests.ts b/plugins/rollbar-backend/src/setupTests.ts new file mode 100644 index 0000000000..f7b6ca962d --- /dev/null +++ b/plugins/rollbar-backend/src/setupTests.ts @@ -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 {}; diff --git a/plugins/rollbar-backend/src/util/index.ts b/plugins/rollbar-backend/src/util/index.ts new file mode 100644 index 0000000000..e8b328d73d --- /dev/null +++ b/plugins/rollbar-backend/src/util/index.ts @@ -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('&'); +}; diff --git a/plugins/rollbar/.eslintrc.js b/plugins/rollbar/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/rollbar/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md new file mode 100644 index 0000000000..d0cfae00cc --- /dev/null +++ b/plugins/rollbar/README.md @@ -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] diff --git a/plugins/rollbar/dev/index.tsx b/plugins/rollbar/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/rollbar/dev/index.tsx @@ -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(); diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json new file mode 100644 index 0000000000..0645d62c9c --- /dev/null +++ b/plugins/rollbar/package.json @@ -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" + ] +} diff --git a/plugins/rollbar/src/api/RollbarApi.ts b/plugins/rollbar/src/api/RollbarApi.ts new file mode 100644 index 0000000000..c08bc08d48 --- /dev/null +++ b/plugins/rollbar/src/api/RollbarApi.ts @@ -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({ + id: 'plugin.rollbar.service', + description: + 'Used by the Rollbar plugin to make requests to accompanying backend', +}); + +export interface RollbarApi { + getAllProjects(): Promise; + getTopActiveItems( + project: string, + hours?: number, + ): Promise; + getProjectItems(project: string): Promise; +} diff --git a/plugins/rollbar/src/api/RollbarClient.ts b/plugins/rollbar/src/api/RollbarClient.ts new file mode 100644 index 0000000000..5cabbcbc24 --- /dev/null +++ b/plugins/rollbar/src/api/RollbarClient.ts @@ -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 { + const path = `/projects`; + + return await this.get(path); + } + + async getTopActiveItems( + project: string, + hours = 24, + environment = 'production', + ): Promise { + const path = `/projects/${project}/top_active_items?environment=${environment}&hours=${hours}`; + + return await this.get(path); + } + + async getProjectItems(project: string): Promise { + const path = `/projects/${project}/items`; + + return await this.get(path); + } + + private async get(path: string): Promise { + 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(); + } +} diff --git a/plugins/rollbar/src/api/RollbarMockClient.ts b/plugins/rollbar/src/api/RollbarMockClient.ts new file mode 100644 index 0000000000..cab9a0d23a --- /dev/null +++ b/plugins/rollbar/src/api/RollbarMockClient.ts @@ -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 { + 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 { + 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 { + return Promise.resolve({ + items: [], + page: 0, + totalCount: 0, + }); + } +} diff --git a/plugins/rollbar/src/api/types.ts b/plugins/rollbar/src/api/types.ts new file mode 100644 index 0000000000..de4cb43aef --- /dev/null +++ b/plugins/rollbar/src/api/types.ts @@ -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[]; +}; diff --git a/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx new file mode 100644 index 0000000000..7bda63ea5f --- /dev/null +++ b/plugins/rollbar/src/components/RollbarLayout/RollbarLayout.tsx @@ -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 ( + +
+ + + + Rollbar plugin allows you to preview issues and navigate to rollbar. + + + + {children} + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx new file mode 100644 index 0000000000..2b0268500d --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.test.tsx @@ -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 = { + getAllProjects: () => Promise.resolve(projects), + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + ), + ); + + it('should render rollbar landing page', async () => { + const rendered = renderWrapped(); + expect(rendered.getByText(/Rollbar/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx new file mode 100644 index 0000000000..ec07c0fa48 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarPage.tsx @@ -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 ( + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx new file mode 100644 index 0000000000..bb8299b57e --- /dev/null +++ b/plugins/rollbar/src/components/RollbarPage/RollbarProjectTable.tsx @@ -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) => ( + + {row.name} + + ), + }, + { + title: 'Status', + field: 'status', + type: 'string', + align: 'left', + }, +]; + +type Props = { + projects: RollbarProject[]; + loading: boolean; +}; + +export const RollbarProjectTable = ({ projects, loading }: Props) => { + return ( + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx new file mode 100644 index 0000000000..135075bf82 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx @@ -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 = { + getTopActiveItems: () => Promise.resolve(items), + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + ), + ); + + it('should render rollbar project page', async () => { + const rendered = renderWrapped(); + expect(rendered.getByText(/Top Active Items/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx new file mode 100644 index 0000000000..d5641e58ce --- /dev/null +++ b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx @@ -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 ( + + + + ); +}; diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx new file mode 100644 index 0000000000..724426ac8f --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx @@ -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(), + ); + expect(rendered.getByText(/No records to display/)).toBeInTheDocument(); + }); + + it('should display item attributes when loading has finished', async () => { + const rendered = render( + wrapInTestApp(), + ); + expect(rendered.getByText(/1234/)).toBeInTheDocument(); + expect(rendered.getByText(/Error in foo/)).toBeInTheDocument(); + expect(rendered.getByText(/critical/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx new file mode 100644 index 0000000000..44dd5ed3f0 --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx @@ -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 => ( + + ), + }, + { + 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 ( +
+ ); +}; diff --git a/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx new file mode 100644 index 0000000000..1f9d317e5b --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.test.tsx @@ -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( + , + ), + ); + expect(rendered).toBeTruthy(); + }); +}); diff --git a/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx new file mode 100644 index 0000000000..4f0b53eb4a --- /dev/null +++ b/plugins/rollbar/src/components/RollbarTrendGraph/RollbarTrendGraph.tsx @@ -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 ( + + + + ); +}; diff --git a/plugins/rollbar/src/index.ts b/plugins/rollbar/src/index.ts new file mode 100644 index 0000000000..3a8288a545 --- /dev/null +++ b/plugins/rollbar/src/index.ts @@ -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'; diff --git a/plugins/rollbar/src/plugin.test.ts b/plugins/rollbar/src/plugin.test.ts new file mode 100644 index 0000000000..44c2168c78 --- /dev/null +++ b/plugins/rollbar/src/plugin.test.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 { plugin } from './plugin'; + +describe('rollbar', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts new file mode 100644 index 0000000000..d682750a83 --- /dev/null +++ b/plugins/rollbar/src/plugin.ts @@ -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); + }, +}); diff --git a/plugins/rollbar/src/routes.ts b/plugins/rollbar/src/routes.ts new file mode 100644 index 0000000000..ecf20f3167 --- /dev/null +++ b/plugins/rollbar/src/routes.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 { createRouteRef } from '@backstage/core'; + +export const rootRoute = createRouteRef({ + path: '/rollbar', + title: 'Rollbar Home', +}); + +export const rootProjectRoute = createRouteRef({ + path: '/rollbar/:componentId/*', + title: 'Rollbar', +}); diff --git a/plugins/rollbar/src/setupTests.ts b/plugins/rollbar/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/rollbar/src/setupTests.ts @@ -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(); diff --git a/yarn.lock b/yarn.lock index 0641f5ed77..29fe56eb44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"