diff --git a/packages/app/package.json b/packages/app/package.json index 58ed20bf42..82b25f58e8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,6 +15,7 @@ "@backstage/plugin-tech-radar": "^0.1.1-alpha.6", "@backstage/plugin-welcome": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", + "@backstage/plugin-sentry": "^0.1.1-alpha.5", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 021005a97a..25a5bcfc9d 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -22,3 +22,4 @@ export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; export { plugin as Explore } from '@backstage/plugin-explore'; export { plugin as Circleci } from '@backstage/plugin-circleci'; export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; +export { plugin as Sentry } from '@backstage/plugin-sentry'; diff --git a/plugins/sentry/.eslintrc.js b/plugins/sentry/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/sentry/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md new file mode 100644 index 0000000000..9004fd91c7 --- /dev/null +++ b/plugins/sentry/README.md @@ -0,0 +1,13 @@ +# sentry + +Welcome to the sentry plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/sentry](http://localhost:3000/sentry). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json new file mode 100644 index 0000000000..dde234e901 --- /dev/null +++ b/plugins/sentry/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-sentry", + "version": "0.1.1-alpha.5", + "main": "dist/index.esm.js", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "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.5", + "@backstage/theme": "^0.1.1-alpha.5", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-sparklines": "^1.7.0", + "react-use": "^14.2.0", + "timeago.js": "^4.0.2" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.5", + "@backstage/dev-utils": "^0.1.1-alpha.5", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "@types/jest": "^25.2.1", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "^5.0.4", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx new file mode 100644 index 0000000000..0daa36f32b --- /dev/null +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { ErrorCell } from './ErrorCell'; +import React from 'react'; +import { render } from '@testing-library/react'; +import mockIssue from '../../data/sentry-issue-mock.json'; +import { ThemeProvider } from '@material-ui/styles'; +import { lightTheme } from '@backstage/theme'; + +describe('Sentry error cell component', () => { + it('should render a link that lead to Sentry', async () => { + const testIssue = { + ...mockIssue, + metadata: { + type: 'Exception', + value: 'exception was thrown', + }, + count: '1', + userCount: 2, + permalink: 'http://example.com', + }; + const cell = render( + + + , + ); + const errorType = await cell.findByText('Exception'); + expect(errorType.closest('a')).toHaveAttribute( + 'href', + 'http://example.com', + ); + }); +}); diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx new file mode 100644 index 0000000000..42ab27e839 --- /dev/null +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -0,0 +1,53 @@ +import React, { FC } from 'react'; +import { SentryIssue } from '../../data/sentry-issue'; +import { Link, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; + +function stripText(text: string, maxLength: number) { + return text.length > maxLength ? `${text.substr(0, maxLength)}...` : text; +} +const useStyles = makeStyles(theme => ({ + root: { + minWidth: 260, + position: 'relative', + '&::before': { + left: -16, + position: 'absolute', + width: '4px', + height: '100%', + content: '""', + backgroundColor: theme.palette.status.error, + borderRadius: 2, + }, + }, + text: { + marginBottom: 0, + }, +})); + +export const ErrorCell: FC<{ sentryIssue: SentryIssue }> = ({ + sentryIssue, +}) => { + const classes = useStyles(); + return ( +
+ + + {sentryIssue.metadata.type + ? stripText(sentryIssue.metadata.type, 28) + : '[No type]'} + + + + {sentryIssue.metadata.value && + stripText(sentryIssue.metadata.value, 48)} + +
+ ); +}; diff --git a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx new file mode 100644 index 0000000000..7643c5ac6e --- /dev/null +++ b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.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, { FC } from 'react'; +import { SentryIssue } from '../../data/sentry-issue'; +import { Sparklines, SparklinesBars } from 'react-sparklines'; + +export const ErrorGraph: FC<{ sentryIssue: SentryIssue }> = ({ + sentryIssue, +}) => { + const data = + '12h' in sentryIssue.stats + ? sentryIssue.stats['12h'] + : sentryIssue.stats['24h']; + + return ( + val)} svgHeight={48} margin={4}> + + + ); +}; diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx new file mode 100644 index 0000000000..08159b16fc --- /dev/null +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 SentryIssuesTable from './SentryIssuesTable'; +import { SentryIssue } from '../../data/sentry-issue'; +import mockIssue from '../../data/sentry-issue-mock.json'; +import { ThemeProvider } from '@material-ui/styles'; +import { lightTheme } from '@backstage/theme'; + +describe('SentryIssuesTable', () => { + it('should render headers in a table', async () => { + const issues: SentryIssue[] = [ + { + ...mockIssue, + metadata: { + type: 'Exception', + value: 'exception was thrown', + }, + count: '1', + userCount: 2, + }, + ]; + const table = await render( + + + , + ); + expect(await table.findByText('Error')).toBeInTheDOM(); + expect(await table.findByText('Graph')).toBeInTheDOM(); + expect(await table.findByText('First seen')).toBeInTheDOM(); + expect(await table.findByText('Last seen')).toBeInTheDOM(); + expect(await table.findByText('Events')).toBeInTheDOM(); + expect(await table.findByText('Users')).toBeInTheDOM(); + }); + it('should render values in a table', async () => { + const issues: SentryIssue[] = [ + { + ...mockIssue, + metadata: { + type: 'Exception', + value: 'exception was thrown', + }, + count: '101', + userCount: 202, + }, + ]; + const table = await render( + + + , + ); + expect(await table.findByText('Exception')).toBeInTheDOM(); + expect(await table.findByText('exception was thrown')).toBeInTheDOM(); + expect(await table.findByText('101')).toBeInTheDOM(); + expect(await table.findByText('202')).toBeInTheDOM(); + }); +}); diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx new file mode 100644 index 0000000000..3e5ed98dba --- /dev/null +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { Table, TableColumn } from '@backstage/core'; +import { SentryIssue } from '../../data/sentry-issue'; +import { format } from 'timeago.js'; +import { ErrorCell } from '../ErrorCell/ErrorCell'; +import { ErrorGraph } from '../ErrorGraph/ErrorGraph'; + +const columns: TableColumn[] = [ + { + title: 'Error', + render: (data) => , + }, + { + title: 'Graph', + render: (data) => , + }, + { + title: 'First seen', + field: 'firstSeen', + render: (data) => { + const { firstSeen } = data as SentryIssue; + return format(firstSeen); + }, + }, + { + title: 'Last seen', + field: 'lastSeen', + render: (data) => { + const { lastSeen } = data as SentryIssue; + return format(lastSeen); + }, + }, + { + title: 'Events', + field: 'count', + }, + { + title: 'Users', + field: 'userCount', + }, +]; + +type SentryIssuesTableProps = { + sentryIssues: SentryIssue[]; +}; + +const SentryIssuesTable: FC = ({ sentryIssues }) => { + return ( + + ); +}; + +export default SentryIssuesTable; diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx new file mode 100644 index 0000000000..cb22885861 --- /dev/null +++ b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 mockFetch from 'jest-fetch-mock'; +import SentryPluginPage from './SentryPluginPage'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +describe('SentryPluginPage', () => { + it('should render header and time switched', () => { + mockFetch.mockResponse(() => new Promise(() => {})); + const rendered = render( + + + , + ); + expect(rendered.getByText('Sentry issues')).toBeInTheDocument(); + expect(rendered.getByText('24H')).toBeInTheDocument(); + expect(rendered.getByText('12H')).toBeInTheDocument(); + }); +}); diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx new file mode 100644 index 0000000000..d7bf731a93 --- /dev/null +++ b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx @@ -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. + */ + +import React, { FC, useState } from 'react'; +import { Grid } from '@material-ui/core'; +import { + Header, + Page, + pageTheme, + Content, + ContentHeader, + SupportButton, +} from '@backstage/core'; +import SentryPluginWidget from '../SentryPluginWidget/SentryPluginWidget'; +import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'; + +const SentryPluginPage: FC<{}> = () => { + const [statsFor, setStatsFor] = useState<'12h' | '24h'>('12h'); + const toggleStatsFor = () => + statsFor === '12h' ? setStatsFor('24h') : setStatsFor('12h'); + return ( + +
+ + + 24H + + + 12H + + +
+ + + + Sentry plugin allows you to preview issues and navigate to sentry. + + + + + + + + +
+ ); +}; + +export default SentryPluginPage; diff --git a/plugins/sentry/src/components/SentryPluginPage/index.ts b/plugins/sentry/src/components/SentryPluginPage/index.ts new file mode 100644 index 0000000000..67b34db517 --- /dev/null +++ b/plugins/sentry/src/components/SentryPluginPage/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 { default } from './SentryPluginPage'; diff --git a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx new file mode 100644 index 0000000000..9b5131d01e --- /dev/null +++ b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useEffect } from 'react'; +import { + ErrorApi, + errorApiRef, + InfoCard, + Progress, + useApi, +} from '@backstage/core'; +import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; +import { useAsync } from 'react-use'; +import { sentryApiFactory } from '../../data/api-factory'; + +const api = sentryApiFactory('spotify'); + +const SentryPluginWidget: FC<{ + sentryProjectId: string; + statsFor: '24h' | '12h'; +}> = ({ sentryProjectId, statsFor }) => { + const errorApi = useApi(errorApiRef); + + const { loading, value, error } = useAsync( + () => api.fetchIssues(sentryProjectId, statsFor), + [statsFor, sentryProjectId], + ); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error]); + + if (loading) { + return ( + + + + ); + } + + return ; +}; + +export default SentryPluginWidget; diff --git a/plugins/sentry/src/data/api-factory.ts b/plugins/sentry/src/data/api-factory.ts new file mode 100644 index 0000000000..32ff4bde5c --- /dev/null +++ b/plugins/sentry/src/data/api-factory.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. + */ +import { SentryApi } from './sentry-api'; +import { MockSentryApi } from './mock-api'; +import { ProductionSentryApi } from './production-api'; + +export function sentryApiFactory(organization: string): SentryApi { + if (process.env.NODE_ENV === 'production') { + return new ProductionSentryApi(organization); + } + return new MockSentryApi(); +} diff --git a/plugins/sentry/src/data/mock-api.ts b/plugins/sentry/src/data/mock-api.ts new file mode 100644 index 0000000000..2eab8e30df --- /dev/null +++ b/plugins/sentry/src/data/mock-api.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SentryIssue } from './sentry-issue'; +import { SentryApi } from './sentry-api'; +import mockData from './sentry-issue-mock.json'; +function getMockIssue(): SentryIssue { + const randomizedStats = { + '12h': new Array(12) + .fill(0) + .map(() => [0, Math.floor(Math.random() * 100)]), + }; + return { + ...mockData, + userCount: Math.floor(Math.random() * 1000), + stats: randomizedStats, + }; +} +function getMockIssues(number: number): SentryIssue[] { + return new Array(number).fill(0).map(getMockIssue); +} +export class MockSentryApi implements SentryApi { + fetchIssues(project: string, statsFor: string): Promise { + console.info('Fetching mock responses for', project, statsFor); + return new Promise((resolve) => { + setTimeout(() => resolve(getMockIssues(14)), 800); + }); + } +} diff --git a/plugins/sentry/src/data/production-api.ts b/plugins/sentry/src/data/production-api.ts new file mode 100644 index 0000000000..0e8f22452f --- /dev/null +++ b/plugins/sentry/src/data/production-api.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { SentryIssue } from './sentry-issue'; +import { SentryApi } from './sentry-api'; + +const API_BASE_URL = 'http://localhost:7000/sentry/api/0/projects/'; + +export class ProductionSentryApi implements SentryApi { + private organization: string; + + constructor(organization: string) { + this.organization = organization; + } + + async fetchIssues(project: string, statsFor: string): Promise { + try { + const response = await fetch( + `${API_BASE_URL}/${this.organization}/${project}/issues/?statsFor=${statsFor}`, + ); + + if (response.status >= 400 && response.status < 600) { + throw new Error('Failed fetching Sentry issues'); + } + + return (await response.json()) as SentryIssue[]; + } catch (exception) { + if (exception.detail) { + return exception; + } + throw new Error('Unknown error'); + } + } +} diff --git a/plugins/sentry/src/data/sentry-api.ts b/plugins/sentry/src/data/sentry-api.ts new file mode 100644 index 0000000000..538900b738 --- /dev/null +++ b/plugins/sentry/src/data/sentry-api.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. + */ +import { SentryIssue } from './sentry-issue'; + +export interface SentryApi { + fetchIssues(project: string, statsFor: string): Promise; +} diff --git a/plugins/sentry/src/data/sentry-issue-mock.json b/plugins/sentry/src/data/sentry-issue-mock.json new file mode 100644 index 0000000000..115be77b19 --- /dev/null +++ b/plugins/sentry/src/data/sentry-issue-mock.json @@ -0,0 +1,61 @@ +{ + "platform": "javascript", + "lastSeen": "2020-05-15T09:17:17.384804Z", + "numComments": 0, + "userCount": 0, + "stats": { + "12h": [ + [1589450400, 7], + [1589454000, 2], + [1589457600, 6], + [1589461200, 8], + [1589464800, 9], + [1589468400, 11], + [1589472000, 4], + [1589475600, 19], + [1589479200, 3], + [1589482800, 24], + [1589486400, 8], + [1589490000, 5], + [1589493600, 5], + [1589497200, 10], + [1589500800, 16], + [1589504400, 24], + [1589508000, 24], + [1589511600, 54], + [1589515200, 4], + [1589518800, 7], + [1589522400, 4], + [1589526000, 4], + [1589529600, 13], + [1589533200, 1] + ] + }, + "culprit": "https://www.example.com/de/account//", + "title": "TypeError: Failed to fetch", + "id": "991214716", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { "type": "TypeError", "value": "Failed to fetch" }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": true, + "shortId": "example-slug-21", + "shareId": null, + "firstSeen": "2019-04-18T23:36:40.988000Z", + "count": "169815", + "permalink": "https://sentry.io/organizations/example/issues/99176416/", + "level": "error", + "isSubscribed": false, + "isBookmarked": false, + "project": { + "platform": "javascript-react", + "slug": "example-slug", + "id": "1282343", + "name": "example-slug" + }, + "statusDetails": {} +} diff --git a/plugins/sentry/src/data/sentry-issue.ts b/plugins/sentry/src/data/sentry-issue.ts new file mode 100644 index 0000000000..14621bf629 --- /dev/null +++ b/plugins/sentry/src/data/sentry-issue.ts @@ -0,0 +1,69 @@ +/* + * 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. + */ +type SentryPlatform = 'javascript' | 'javascript-react' | string; + +type EventPoint = number[]; + +type SentryProject = { + platform: SentryPlatform; + slug: string; + id: string; + name: string; +}; + +type SentryIssueMetadata = { + function?: string; + type?: string; + value?: string; + filename?: string; +}; + +export type SentryIssue = { + platform: SentryPlatform; + lastSeen: string; + numComments: number; + userCount: number; + stats: { + '24h'?: EventPoint[]; + '12h'?: EventPoint[]; + }; + culprit: string; + title: string; + id: string; + assignedTo: any; + logger: any; + type: string; + annotations: any[]; + metadata: SentryIssueMetadata; + status: string; + subscriptionDetails: any; + isPublic: boolean; + hasSeen: boolean; + shortId: string; + shareId: string | null; + firstSeen: string; + count: string; + permalink: string; + level: string; + isSubscribed: boolean; + isBookmarked: boolean; + project: SentryProject; + statusDetails: any; +}; + +export type SentryApiError = { + detail: string; +}; diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts new file mode 100644 index 0000000000..350b8fd668 --- /dev/null +++ b/plugins/sentry/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 { plugin } from './plugin'; +export { default as SentryIssuesWidget } from './components/SentryPluginWidget/SentryPluginWidget'; diff --git a/plugins/sentry/src/plugin.test.ts b/plugins/sentry/src/plugin.test.ts new file mode 100644 index 0000000000..8f24236586 --- /dev/null +++ b/plugins/sentry/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('sentry', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts new file mode 100644 index 0000000000..6b4080dd2b --- /dev/null +++ b/plugins/sentry/src/plugin.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. + */ + +import { createPlugin } from '@backstage/core'; +import SentryPluginPage from './components/SentryPluginPage'; + +export const plugin = createPlugin({ + id: 'sentry', + register({ router }) { + router.registerRoute('/sentry', SentryPluginPage); + }, +}); diff --git a/plugins/sentry/src/setupTests.ts b/plugins/sentry/src/setupTests.ts new file mode 100644 index 0000000000..1a907ab8e6 --- /dev/null +++ b/plugins/sentry/src/setupTests.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. + */ + +import '@testing-library/jest-dom/extend-expect'; +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/sentry/tsconfig.json b/plugins/sentry/tsconfig.json new file mode 100644 index 0000000000..b663b01fa2 --- /dev/null +++ b/plugins/sentry/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "dev"], + "compilerOptions": {} +} diff --git a/yarn.lock b/yarn.lock index 0d2cda2b05..0e0edd4bf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19958,6 +19958,11 @@ tildify@2.0.0: resolved "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== +timeago.js@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz#724e8c8833e3490676c7bb0a75f5daf20e558028" + integrity sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w== + timed-out@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"