├── .nvmrc ├── .prettierignore ├── src ├── setupTests.ts ├── index.ts ├── routes.ts ├── plugin.test.ts ├── plugin.ts ├── helper.ts └── components │ ├── ChartTitle.test.tsx │ ├── ChartTitle.tsx │ ├── AtAGlance.tsx │ └── Charts.tsx ├── CODEOWNERS ├── .gitignore ├── .eslintrc.js ├── screenshots ├── ranked │ ├── tab.png │ ├── teamView.png │ ├── atAGlance.png │ └── atAGlance_hover.png ├── trend │ ├── tab.png │ ├── atAGlance.png │ ├── teamView.png │ ├── tabIndividual.png │ ├── atAGlanceIndividual.png │ └── teamViewIndividual.png └── dora-backstage-plugin-architecture.drawio.png ├── .husky └── pre-commit ├── dev └── index.ts ├── renovate.json ├── .github ├── workflows │ ├── pull_request.yml │ ├── codeql.yml │ ├── release.yml │ └── code_validation.yml ├── ISSUE_TEMPLATE │ ├── doc-improvement.md │ ├── config.yaml │ ├── feature-request.md │ └── bug-report.md └── PULL_REQUEST_TEMPLATE ├── tsconfig.json ├── catalog-info.yaml ├── dependencies ├── promtail-config.yaml ├── .env ├── loki-config.yaml ├── docker-compose.yml └── otelcol-config.yml ├── .cspell.yaml ├── config.d.ts ├── .releaserc ├── .pre-commit-config.yaml ├── CONTRIBUTING ├── package.json ├── CHANGELOG.md ├── LICENSE └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.18.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | CODEOWNERS @liatrio/backstage-foundations 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist-types 4 | dist 5 | .env 6 | coverage 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This is our entry point 2 | export { EntityDORAAtAGlance, EntityDORACharts } from './plugin'; 3 | -------------------------------------------------------------------------------- /screenshots/ranked/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/ranked/tab.png -------------------------------------------------------------------------------- /screenshots/trend/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/tab.png -------------------------------------------------------------------------------- /screenshots/ranked/teamView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/ranked/teamView.png -------------------------------------------------------------------------------- /screenshots/trend/atAGlance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/atAGlance.png -------------------------------------------------------------------------------- /screenshots/trend/teamView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/teamView.png -------------------------------------------------------------------------------- /screenshots/ranked/atAGlance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/ranked/atAGlance.png -------------------------------------------------------------------------------- /screenshots/trend/tabIndividual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/tabIndividual.png -------------------------------------------------------------------------------- /screenshots/ranked/atAGlance_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/ranked/atAGlance_hover.png -------------------------------------------------------------------------------- /screenshots/trend/atAGlanceIndividual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/atAGlanceIndividual.png -------------------------------------------------------------------------------- /screenshots/trend/teamViewIndividual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/trend/teamViewIndividual.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | 3 | set -e 4 | 5 | yarn lint:all 6 | 7 | if command -v pre-commit &> /dev/null; then 8 | pre-commit run 9 | fi 10 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'liatrio-dora', 5 | }); 6 | -------------------------------------------------------------------------------- /screenshots/dora-backstage-plugin-architecture.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/HEAD/screenshots/dora-backstage-plugin-architecture.drawio.png -------------------------------------------------------------------------------- /dev/index.ts: -------------------------------------------------------------------------------- 1 | import { createDevApp } from '@backstage/dev-utils'; 2 | import { DORAMetricsPlugin } from '../src/plugin'; 3 | 4 | createDevApp().registerPlugin(DORAMetricsPlugin).render(); 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "labels": ["dependencies"], 5 | "updateLockFiles": false 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | code-validation: 10 | uses: ./.github/workflows/code_validation.yml 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": ["src", "dev"], 4 | "compilerOptions": { 5 | "outDir": "dist-types", 6 | "declaration": true, 7 | "incremental": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: backstage-dora-plugin 5 | annotations: 6 | github.com/project-slug: liatrio/backstage-dora-plugin 7 | spec: 8 | type: other 9 | lifecycle: unknown 10 | owner: backstage-foundations 11 | -------------------------------------------------------------------------------- /dependencies/promtail-config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 9080 3 | grpc_listen_port: 0 4 | 5 | positions: 6 | filename: /tmp/positions.yaml 7 | 8 | clients: 9 | - url: http://loki:3100/loki/api/v1/push 10 | 11 | scrape_configs: 12 | - job_name: system 13 | static_configs: 14 | - targets: 15 | - localhost 16 | labels: 17 | job: varlogs 18 | __path__: /var/log/*log 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/doc-improvement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Improvement 3 | about: Suggest improvements or report issues with the documentation 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | **Describe the issue with the documentation.** 10 | 11 | A clear and concise description of the documentation issue. 12 | 13 | **Suggested improvements.** 14 | 15 | Provide suggestions on how to improve the documentation. 16 | 17 | **Additional context.** 18 | 19 | Include any other details or links that might be useful. 20 | -------------------------------------------------------------------------------- /.cspell.yaml: -------------------------------------------------------------------------------- 1 | language: en_US 2 | words: 3 | - Liatrio 4 | - testid 5 | - uuidv 6 | dictionaries: 7 | - aws 8 | - companies 9 | - css 10 | - docker 11 | - html 12 | - k8s 13 | - markdown 14 | - misc 15 | - npm 16 | - softwareTerms 17 | - useCompounds 18 | ignorePaths: 19 | - .gitignore 20 | - .dockerignore 21 | - .cspell.yaml 22 | - .git/ 23 | - CHANGELOG.md 24 | - package.json 25 | - stories/ 26 | - node_modules/ 27 | features: 28 | weighted-suggestions: true 29 | allowCompoundWords: true 30 | useGitignore: true 31 | -------------------------------------------------------------------------------- /src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DORAMetricsPlugin, 3 | EntityDORAAtAGlance, 4 | EntityDORACharts, 5 | } from './plugin'; 6 | 7 | describe('DORAMetricsPlugin', () => { 8 | it('should have the correct id', () => { 9 | expect(DORAMetricsPlugin.getId()).toBe('dora-metrics'); 10 | }); 11 | 12 | it('should provide EntityDORAAtAGlance component', () => { 13 | expect(EntityDORAAtAGlance).toBeDefined(); 14 | }); 15 | 16 | it('should provide EntityDORACharts component', () => { 17 | expect(EntityDORACharts).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /dependencies/.env: -------------------------------------------------------------------------------- 1 | LOKI_URL="http://loki:3100/loki/api/v1/query_range" 2 | 3 | # LOG_LEVEL=warn 4 | 5 | PORT=3030 6 | 7 | GITHUB_ORG= 8 | GITHUB_USER= 9 | GITHUB_TOKEN= 10 | 11 | LOKI_DAYS_BATCH_SIZE=5 12 | 13 | OTEL_COLLECTOR_HOST=otelcol 14 | OTEL_COLLECTOR_PORT_GRPC=4317 15 | OTEL_COLLECTOR_PORT_HTTP=4318 16 | OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 17 | 18 | OTEL_RESOURCE_ATTRIBUTES=service.name=backstage,service.version=1.0,deployment.environment=local 19 | OTEL_EXPORTER_OTLP_PROTOCOL=grpc 20 | 21 | OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a Bug 4 | url: https://github.com/liatrio/backstage-dora-plugin/issues/new?template=bug-report.md 5 | about: Report a bug to help us improve the project. 6 | - name: Request a Feature 7 | url: https://github.com/liatrio/backstage-dora-plugin/issues/new?template=feature_request.md 8 | about: Suggest a new feature or enhancement for the project. 9 | - name: Requst Documentation Improvements 10 | url: https://github.com/liatrio/backstage-dora-plugin/issues/new?doc-improvements.md 11 | about: Request improvements or additions to the documentation. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of the problem or issue. 12 | 13 | **Describe the solution you'd like.** 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered.** 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context.** 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Hey, I just made a Pull Request! 2 | 3 | 5 | 6 | ### Description 7 | 8 | 9 | 10 | ### Related Issue 11 | 12 | 13 | 14 | ### Checklist 15 | 16 | - [ ] Code is formatted with `prettier` 17 | - [ ] Code is linted with `eslint` 18 | - [ ] Tests have been added or updated 19 | - [ ] Documentation has been updated (if applicable) 20 | 21 | ### Additional Notes 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL Scanner 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze (javascript-typescript) 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | packages: read 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v3 23 | with: 24 | languages: javascript-typescript 25 | build-mode: none 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v3 29 | with: 30 | category: '/language:javascript-typescript' 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the Bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected Behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Anonymized Data** 31 | 32 | If possible, please provide any relevant data that has been anonymized to help us replicate the issue. 33 | 34 | **Additional Context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createComponentExtension, 3 | createPlugin, 4 | createRouteRef, 5 | } from '@backstage/core-plugin-api'; 6 | 7 | export const entityContentRouteRef = createRouteRef({ 8 | id: 'dora-metrics', 9 | }); 10 | 11 | export const DORAMetricsPlugin = createPlugin({ 12 | id: 'dora-metrics', 13 | routes: { 14 | entityContent: entityContentRouteRef, 15 | }, 16 | }); 17 | 18 | export const EntityDORAAtAGlance = DORAMetricsPlugin.provide( 19 | createComponentExtension({ 20 | name: 'EntityDORAAtAGlance', 21 | component: { 22 | lazy: () => import('./components/AtAGlance').then(m => m.AtAGlance), 23 | }, 24 | }), 25 | ); 26 | 27 | export const EntityDORACharts = DORAMetricsPlugin.provide( 28 | createComponentExtension({ 29 | name: 'EntityDORACharts', 30 | component: { 31 | lazy: () => import('./components/Charts').then(m => m.Charts), 32 | }, 33 | }), 34 | ); 35 | -------------------------------------------------------------------------------- /config.d.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | /** 3 | * Frontend root URL 4 | * NOTE: Visibility applies to only this field 5 | * @deepVisibility frontend 6 | */ 7 | dora: { 8 | dataEndpoint: string; 9 | serviceListEndpoint: string; 10 | daysToFetch: number; 11 | includeWeekends?: boolean; 12 | showDetails?: boolean; 13 | services?: string[]; 14 | showTrendGraph?: boolean; 15 | showIndividualTrends?: boolean; 16 | rankThresholds?: { 17 | deployment_frequency?: { 18 | elite?: number; 19 | high?: number; 20 | medium?: number; 21 | }; 22 | recover_time?: { 23 | elite?: number; 24 | high?: number; 25 | medium?: number; 26 | }; 27 | change_lead_time?: { 28 | elite?: number; 29 | high?: number; 30 | medium?: number; 31 | }; 32 | change_failure_rate?: { 33 | elite?: number; 34 | high?: number; 35 | medium?: number; 36 | }; 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - 'CHANGELOG.md' 8 | 9 | jobs: 10 | code-validation: 11 | uses: ./.github/workflows/code_validation.yml 12 | 13 | release: 14 | needs: [code-validation] 15 | runs-on: ubuntu-latest 16 | concurrency: release 17 | 18 | permissions: 19 | id-token: write 20 | contents: write 21 | 22 | env: 23 | HUSKY: 0 24 | 25 | steps: 26 | - name: Check out the repository 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version-file: '.nvmrc' 35 | cache: 'yarn' 36 | 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Build Typescript Definitions 41 | run: yarn tsc 42 | 43 | - name: Build 44 | run: yarn build 45 | 46 | - name: Bump and Release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.TAGGING_TOKEN }} 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | run: npx semantic-release 51 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "preset": "conventionalcommits", 6 | "tagFormat": "v${version}", 7 | "plugins": [ 8 | [ 9 | "@semantic-release/commit-analyzer", 10 | { 11 | "preset": "conventionalcommits", 12 | "releaseRules": [ 13 | { 14 | "type": "docs", 15 | "release": "patch" 16 | }, 17 | { 18 | "type": "refactor", 19 | "release": "patch" 20 | }, 21 | { 22 | "type": "style", 23 | "release": "patch" 24 | } 25 | ], 26 | "parserOpts": { 27 | "noteKeywords": [ 28 | "BREAKING CHANGE", 29 | "BREAKING CHANGES" 30 | ] 31 | } 32 | } 33 | ], 34 | "@semantic-release/release-notes-generator", 35 | [ 36 | "@semantic-release/changelog", 37 | { 38 | "changelogFile": "CHANGELOG.md" 39 | } 40 | ], 41 | [ 42 | "@semantic-release/git", 43 | { 44 | "assets": [ 45 | "CHANGELOG.md", 46 | "package.json" 47 | ] 48 | } 49 | ], 50 | "@semantic-release/github", 51 | "@semantic-release/npm" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /dependencies/loki-config.yaml: -------------------------------------------------------------------------------- 1 | auth_enabled: false 2 | 3 | server: 4 | # log_level: warn 5 | http_listen_port: 3100 6 | grpc_listen_port: 9096 7 | 8 | common: 9 | instance_addr: 127.0.0.1 10 | path_prefix: /tmp/loki 11 | storage: 12 | filesystem: 13 | chunks_directory: /tmp/loki/chunks 14 | rules_directory: /tmp/loki/rules 15 | replication_factor: 1 16 | ring: 17 | kvstore: 18 | store: inmemory 19 | 20 | query_range: 21 | results_cache: 22 | cache: 23 | embedded_cache: 24 | enabled: true 25 | max_size_mb: 100 26 | 27 | schema_config: 28 | configs: 29 | - from: 2020-10-24 30 | store: tsdb 31 | object_store: filesystem 32 | schema: v13 33 | index: 34 | prefix: index_ 35 | period: 24h 36 | 37 | limits_config: 38 | split_queries_by_interval: 24h 39 | max_query_length: 0 40 | allow_structured_metadata: true 41 | 42 | ruler: 43 | alertmanager_url: http://localhost:9093 44 | # By default, Loki will send anonymous, but uniquely-identifiable usage and configuration 45 | # analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/ 46 | # 47 | # Statistics help us better understand how Loki is used, and they show us performance 48 | # levels for most users. This helps us prioritize features and documentation. 49 | # For more information on what's sent, look at 50 | # https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go 51 | # Refer to the buildReport method to see what goes into a report. 52 | # 53 | # If you would like to disable reporting, uncomment the following lines: 54 | #analytics: 55 | # reporting_enabled: false 56 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import { useApi, identityApiRef } from '@backstage/core-plugin-api'; 2 | 3 | export const COLOR_GREEN = '#24ae1d'; 4 | export const COLOR_DARK = '#000'; 5 | export const COLOR_LIGHT = '#FFF'; 6 | 7 | export const getRepositoryName = (e: any): string => { 8 | if ('github.com/project-slug' in e.entity.metadata.annotations) { 9 | return e.entity.metadata.annotations['github.com/project-slug'].split( 10 | '/', 11 | )[1]; 12 | } 13 | 14 | return ''; 15 | }; 16 | 17 | export const useAuthHeaderValueLookup = () => { 18 | const identityApi = useApi(identityApiRef); 19 | 20 | return async () => { 21 | const obj = await identityApi.getCredentials(); 22 | 23 | if (obj.token) { 24 | return `Bearer ${obj.token}`; 25 | } 26 | 27 | return undefined; 28 | }; 29 | }; 30 | 31 | export const fetchServices = async ( 32 | url: string, 33 | getAuthHeaderValue: () => Promise, 34 | onSuccess: (data: any) => void, 35 | onFailure?: (data: any) => void, 36 | ) => { 37 | if (!url) { 38 | return; 39 | } 40 | 41 | let headers = {}; 42 | 43 | if (getAuthHeaderValue) { 44 | headers = { 45 | 'Content-Type': 'application/json', 46 | Authorization: await getAuthHeaderValue(), 47 | }; 48 | } else { 49 | headers = { 50 | 'Content-Type': 'application/json', 51 | }; 52 | } 53 | 54 | const options = { 55 | method: 'GET', 56 | headers: headers, 57 | }; 58 | 59 | try { 60 | const response = await fetch(url, options); 61 | const json = await response.text(); 62 | 63 | const parsedData = JSON.parse(json); 64 | 65 | onSuccess(parsedData); 66 | } catch (error) { 67 | if (onFailure) { 68 | onFailure(error); 69 | } 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, pre-push, commit-msg] 2 | # See https://pre-commit.com for more information 3 | # See https://pre-commit.com/hooks.html for more hooks 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.6.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-merge-conflict 10 | - id: check-symlinks 11 | - id: check-yaml 12 | args: 13 | - '--allow-multiple-documents' 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | - id: trailing-whitespace 17 | - repo: https://github.com/igorshubovych/markdownlint-cli 18 | rev: v0.41.0 19 | hooks: 20 | - id: markdownlint 21 | exclude: CHANGELOG.md 22 | args: 23 | - '--fix' # Automatically fix issues 24 | - '--disable=MD013' # Ignore Line length 25 | - '--disable=MD033' # Allow Inline HTML 26 | - '--disable=MD034' # Allow bare URLs 27 | - '--ignore=./.github/ISSUE_TEMPLATE' # Ignore issue templates 28 | - repo: https://github.com/commitizen-tools/commitizen 29 | rev: v3.29.0 30 | hooks: 31 | - id: commitizen 32 | - id: commitizen-branch 33 | stages: [push] 34 | - repo: https://github.com/adrienverge/yamllint 35 | rev: v1.35.1 36 | hooks: 37 | - id: yamllint 38 | args: 39 | - '-d' 40 | - '{extends: relaxed, rules: {line-length: {max: 120}}}' 41 | - repo: https://github.com/streetsidesoftware/cspell-cli 42 | rev: v8.13.3 43 | hooks: 44 | - id: cspell 45 | - repo: local 46 | hooks: 47 | - id: prettier 48 | name: Prettier via npm 49 | entry: npm run prettier:fix 50 | language: node 51 | types_or: [css, javascript, ts, tsx] 52 | -------------------------------------------------------------------------------- /.github/workflows/code_validation.yml: -------------------------------------------------------------------------------- 1 | name: Code Validation 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | lint: 8 | name: Lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Setup node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | cache: 'yarn' 18 | 19 | - name: Install dependencies 20 | run: yarn install 21 | 22 | - name: backstage lint 23 | run: yarn lint 24 | 25 | format: 26 | name: Formatting 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Setup node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version-file: '.nvmrc' 35 | cache: 'yarn' 36 | 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Check Format 41 | run: yarn prettier:check 42 | 43 | check-types: 44 | name: Check Typescript Types 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Setup node 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version-file: '.nvmrc' 53 | cache: 'yarn' 54 | 55 | - name: Install dependencies 56 | run: yarn install 57 | 58 | - name: Check Types 59 | run: yarn tsc:full 60 | 61 | test: 62 | name: Unit Tests 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - name: Setup node 68 | uses: actions/setup-node@v4 69 | with: 70 | node-version-file: '.nvmrc' 71 | cache: 'yarn' 72 | 73 | - name: Install dependencies 74 | run: yarn install 75 | 76 | - name: Unit Tests 77 | run: yarn test 78 | -------------------------------------------------------------------------------- /dependencies/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | x-default-logging: &logging 4 | driver: 'json-file' 5 | options: 6 | max-size: '5m' 7 | max-file: '2' 8 | 9 | services: 10 | liatrio-dora-api: 11 | image: ghcr.io/liatrio/liatrio-dora-api:v1.1.13 12 | platform: linux/amd64 13 | ports: 14 | - '${PORT:-3030}:3030' 15 | env_file: .env 16 | 17 | loki: 18 | image: grafana/loki:3.0.1 19 | platform: linux/amd64 20 | ports: 21 | - '3100:3100' 22 | volumes: 23 | - ./loki-config.yaml:/etc/loki/local-config.yaml 24 | - loki_data:/loki 25 | environment: 26 | LOKI_STORAGE_PATH: /loki 27 | 28 | promtail: 29 | image: grafana/promtail:2.9.1 30 | platform: linux/amd64 31 | volumes: 32 | - /var/log:/var/log 33 | - ./promtail-config.yaml:/etc/promtail/config.yaml 34 | command: -config.file=/etc/promtail/config.yaml 35 | 36 | otelcol: 37 | image: ghcr.io/liatrio/liatrio-otel-collector:0.61.0-arm64 38 | deploy: 39 | resources: 40 | limits: 41 | memory: 125M 42 | env_file: .env 43 | restart: unless-stopped 44 | command: ['--config=/etc/otelcol-config.yml'] 45 | volumes: 46 | - ./otelcol-config.yml:/etc/otelcol-config.yml 47 | ports: 48 | - '4317:4317' # OTLP over gRPC receiver 49 | - '4318:4318' # OTLP over HTTP receiver 50 | - '8088:8088' # OTLP over HTTP receiver 51 | logging: *logging 52 | 53 | grafana: 54 | image: grafana/grafana-enterprise 55 | container_name: grafana 56 | restart: unless-stopped 57 | # if you are running as root then set it to 0 58 | # else find the right id with the id -u command 59 | user: '0' 60 | ports: 61 | - '3000:3000' 62 | # adding the mount volume point which we create earlier 63 | volumes: 64 | - '$PWD/data:/var/lib/grafana' 65 | 66 | volumes: 67 | loki_data: 68 | -------------------------------------------------------------------------------- /src/components/ChartTitle.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { ChartTitle, Props } from './ChartTitle'; 4 | import { Theme } from '@liatrio/react-dora-charts'; 5 | import { COLOR_DARK, COLOR_GREEN, COLOR_LIGHT } from '../helper'; 6 | 7 | describe('ChartTitle', () => { 8 | const defaultProps: Props = { 9 | title: 'Test Title', 10 | info: 'Test Info', 11 | theme: Theme.Light, 12 | }; 13 | 14 | it('renders the title and info correctly', () => { 15 | render(); 16 | expect(screen.getByText('Test Title:')).toBeInTheDocument(); 17 | expect(screen.getByTestId('metric_tooltip')).toHaveAttribute( 18 | 'data-tooltip-content', 19 | 'Test Info', 20 | ); 21 | }); 22 | 23 | it('applies the correct color based on theme', () => { 24 | const { rerender } = render(); 25 | let tooltipPaths = screen 26 | .getByTestId('metric_tooltip') 27 | .querySelectorAll('path'); 28 | 29 | expect(tooltipPaths[0]).toHaveAttribute('fill', COLOR_GREEN); 30 | expect(tooltipPaths[1]).toHaveAttribute('fill', COLOR_DARK); 31 | expect(tooltipPaths[2]).toHaveAttribute('fill', COLOR_DARK); 32 | expect(tooltipPaths[3]).toHaveAttribute('fill', COLOR_DARK); 33 | 34 | rerender(); 35 | tooltipPaths = screen 36 | .getByTestId('metric_tooltip') 37 | .querySelectorAll('path'); 38 | expect(tooltipPaths[0]).toHaveAttribute('fill', COLOR_GREEN); 39 | expect(tooltipPaths[1]).toHaveAttribute('fill', COLOR_LIGHT); 40 | expect(tooltipPaths[2]).toHaveAttribute('fill', COLOR_LIGHT); 41 | expect(tooltipPaths[3]).toHaveAttribute('fill', COLOR_LIGHT); 42 | }); 43 | 44 | it('displays the score display if provided', () => { 45 | render(); 46 | expect(screen.getByText('85')).toBeInTheDocument(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | # Contributing to the Backstage Dora Plugin 2 | 3 | Thank you for considering contributing to the Backstage Dora Plugin! We're excited to have you on board. 4 | 5 | ## Code of Conduct 6 | 7 | We follow the standard open-source code of conduct. Please be respectful and considerate in your interactions with the community. 8 | 9 | ## Getting Started 10 | 11 | Before you start contributing, please make sure you have the following: 12 | 13 | * Node.js 14 | * npm or yarn 15 | * A code editor or IDE of your choice 16 | 17 | ## Coding Style and Practices 18 | 19 | We follow standard JavaScript and React coding practices. Please make sure to: 20 | 21 | * Use consistent indentation and spacing. 22 | * Use clear and descriptive variable names. 23 | * Use functions and modules to organize your code. 24 | * Keep your code concise and readable. 25 | * Use JSDoc comments to document your code. 26 | 27 | ### Using `pre-commit` 28 | 29 | We use [pre-commit](https://pre-commit.com/) to ensure that our code is formatted consistently and follows good coding practices. Run `pre-commit install` to install the pre-commit hooks. After installation, `pre-commit` will run automatically against your changes on every commit. You can also run `pre-commit run --all-files` to manually run the hooks on all files. 30 | 31 | ## Testing 32 | 33 | We use Jest for testing. Please make sure to write tests for any new features or bug fixes you contribute. 34 | 35 | ## Opening Pull Requests 36 | 37 | To contribute to the codebase, please follow these steps: 38 | 39 | 1. Fork the repository and create a new branch for your feature or bug fix. 40 | 2. Run `npm install` or `yarn install` to install the dependencies. 41 | 3. Run `pre-commit install` to install the pre-commit hooks. 42 | 4. Use [Conventional Commits](https://www.conventionalcommits.org) to format your commit messages. 43 | 5. Open a pull request to the main repository, targeting the `main` branch. 44 | 45 | ## Review Process 46 | 47 | Once you've opened a pull request, it will be reviewed by the maintainers. We'll provide feedback and guidance to help you improve your contribution. 48 | 49 | Thank you again for contributing to the Backstage Dora Plugin! 50 | -------------------------------------------------------------------------------- /src/components/ChartTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import { Theme } from '@liatrio/react-dora-charts'; 4 | import { COLOR_DARK, COLOR_GREEN, COLOR_LIGHT } from '../helper'; 5 | 6 | export interface Props { 7 | title: string; 8 | info: string; 9 | color?: string; 10 | scoreDisplay?: string; 11 | theme?: Theme; 12 | } 13 | 14 | const useStyles = makeStyles(() => ({ 15 | chartHeader: { 16 | display: 'flex', 17 | justifyContent: 'space-between', 18 | alignItems: 'center', 19 | alignContent: 'center', 20 | }, 21 | })); 22 | 23 | export const ChartTitle = (props: Props) => { 24 | const classes = useStyles(); 25 | 26 | const color = props.theme !== Theme.Dark ? COLOR_DARK : COLOR_LIGHT; 27 | 28 | return ( 29 | <> 30 |
31 | 32 | {props.title}:{' '} 33 | {props.scoreDisplay ?? ''} 34 | 35 | 43 | 59 | 60 | 64 | 68 | 72 | 76 | 77 | 78 | 79 |
80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liatrio/backstage-dora-plugin", 3 | "version": "0.0.37", 4 | "main": "./dist/index.esm.js", 5 | "types": "./dist/index.d.ts", 6 | "license": "Apache-2.0", 7 | "description": "A Backstage plugin for DORA metrics", 8 | "keywords": [ 9 | "backstage", 10 | "dora", 11 | "plugin", 12 | "metrics" 13 | ], 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "backstage": { 18 | "role": "frontend-plugin", 19 | "pluginId": "dora-metrics", 20 | "pluginPackages": [ 21 | "@liatrio/backstage-dora-plugin" 22 | ] 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/liatrio/backstage-dora-plugin.git" 27 | }, 28 | "configSchema": "config.d.ts", 29 | "sideEffects": false, 30 | "scripts": { 31 | "start": "backstage-cli package start", 32 | "build": "backstage-cli package build", 33 | "lint": "backstage-cli package lint", 34 | "lint:all": "yarn lint && yarn prettier:check", 35 | "test": "backstage-cli package test", 36 | "clean": "backstage-cli package clean", 37 | "prepack": "backstage-cli package prepack", 38 | "postpack": "backstage-cli package postpack", 39 | "prettier:check": "npx --yes prettier --check .", 40 | "prettier:fix": "npx --yes prettier --write .", 41 | "tsc:full": "tsc --skipLibCheck true --incremental false", 42 | "prepare": "husky" 43 | }, 44 | "dependencies": { 45 | "@backstage/core-components": "^0.14.3", 46 | "@backstage/core-plugin-api": "^1.9.1", 47 | "@backstage/plugin-catalog-react": "^1.12.3", 48 | "@backstage/theme": "^0.5.2", 49 | "@liatrio/react-dora-charts": "^1.2.0", 50 | "@material-ui/core": "^4.9.13", 51 | "@material-ui/icons": "^4.9.1", 52 | "@material-ui/lab": "^4.0.0-alpha.61", 53 | "@mui/material": "^6.1.0", 54 | "react-datepicker": "^7.3.0", 55 | "react-dropdown": "^1.11.0", 56 | "react-tooltip": "^5.28.0", 57 | "react-use": "^17.2.4", 58 | "recharts": "^2.12.7" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16.13.1 || ^17.0.0 || ^18.0.0" 62 | }, 63 | "devDependencies": { 64 | "@backstage/cli": "^0.26.0", 65 | "@backstage/core-app-api": "^1.12.3", 66 | "@backstage/dev-utils": "^1.0.26", 67 | "@backstage/test-utils": "^1.5.0", 68 | "@semantic-release/changelog": "^6.0.3", 69 | "@semantic-release/commit-analyzer": "^10.0.4", 70 | "@semantic-release/git": "^10.0.1", 71 | "@semantic-release/github": "^9.2.6", 72 | "@semantic-release/npm": "^11.0.3", 73 | "@semantic-release/release-notes-generator": "^12.1.0", 74 | "@spotify/prettier-config": "^15.0.0", 75 | "@testing-library/jest-dom": "^6.0.0", 76 | "@testing-library/react": "^15.0.0", 77 | "@testing-library/user-event": "^14.0.0", 78 | "conventional-changelog-conventionalcommits": "^7.0.2", 79 | "husky": "^9.1.6", 80 | "msw": "^1.0.0", 81 | "pinst": "^3.0.0", 82 | "react": "^18.0.2", 83 | "react-dom": "^18.0.2", 84 | "react-router-dom": "^6.3.0", 85 | "semantic-release": "^22.0.12" 86 | }, 87 | "files": [ 88 | "dist", 89 | "dist-types", 90 | "config.d.ts", 91 | "LICENSE" 92 | ], 93 | "module": "./dist/index.esm.js", 94 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610", 95 | "prettier": "@spotify/prettier-config" 96 | } 97 | -------------------------------------------------------------------------------- /src/components/AtAGlance.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useTheme } from '@mui/material/styles'; 3 | import { InfoCard } from '@backstage/core-components'; 4 | import { Box } from '@material-ui/core'; 5 | import { 6 | TrendGraph, 7 | Board, 8 | MetricThresholdSet, 9 | getDateDaysInPastUtc, 10 | fetchData, 11 | Theme, 12 | } from '@liatrio/react-dora-charts'; 13 | import { useEntity } from '@backstage/plugin-catalog-react'; 14 | import { useApi, configApiRef } from '@backstage/core-plugin-api'; 15 | import { 16 | COLOR_DARK, 17 | COLOR_LIGHT, 18 | useAuthHeaderValueLookup, 19 | getRepositoryName, 20 | } from '../helper'; 21 | import { ChartTitle } from './ChartTitle'; 22 | import { Tooltip } from 'react-tooltip'; 23 | 24 | export const AtAGlance = () => { 25 | const entity = useEntity(); 26 | const configApi = useApi(configApiRef); 27 | const backendUrl = configApi.getString('backend.baseUrl'); 28 | const dataEndpoint = configApi.getString('dora.dataEndpoint'); 29 | const daysToFetch = configApi.getNumber('dora.daysToFetch'); 30 | const includeWeekends = configApi.getOptionalBoolean('dora.includeWeekends'); 31 | const showDetails = configApi.getOptionalBoolean('dora.showDetails'); 32 | const rankThresholds = configApi.getOptional( 33 | 'dora.rankThresholds', 34 | ) as MetricThresholdSet; 35 | const showTrendGraph = configApi.getOptionalBoolean('dora.showTrendGraph'); 36 | const showIndividualTrends = configApi.getOptionalBoolean( 37 | 'dora.showIndividualTrends', 38 | ); 39 | 40 | const [data, setData] = useState(); 41 | const [loading, setLoading] = useState(false); 42 | const backstageTheme = useTheme(); 43 | const theme = 44 | backstageTheme.palette.mode === 'dark' ? Theme.Dark : Theme.Light; 45 | 46 | const getAuthHeaderValue = useAuthHeaderValueLookup(); 47 | 48 | const apiUrl = `${backendUrl}/api/proxy/dora/api/${dataEndpoint}`; 49 | const repositoryName = getRepositoryName(entity); 50 | const startDate = getDateDaysInPastUtc(31); 51 | const endDate = getDateDaysInPastUtc(0); 52 | const message = ''; 53 | 54 | useEffect(() => { 55 | if (!repositoryName) { 56 | return; 57 | } 58 | 59 | const fetch = async () => { 60 | const fetchOptions: any = { 61 | api: apiUrl, 62 | getAuthHeaderValue: getAuthHeaderValue, 63 | start: getDateDaysInPastUtc(daysToFetch), 64 | end: getDateDaysInPastUtc(0), 65 | repositories: [repositoryName], 66 | }; 67 | 68 | setLoading(true); 69 | 70 | await fetchData( 71 | fetchOptions, 72 | (respData: any) => { 73 | setData(respData); 74 | setLoading(false); 75 | }, 76 | _ => { 77 | setLoading(false); 78 | }, 79 | ); 80 | }; 81 | 82 | fetch(); 83 | // eslint-disable-next-line react-hooks/exhaustive-deps 84 | }, []); 85 | 86 | const tTitle = ( 87 | 92 | ); 93 | const bTitle = ( 94 | 99 | ); 100 | 101 | return ( 102 | 103 | 116 | 117 | 118 | {repositoryName === '' ? ( 119 |
120 | DORA Metrics are not available for Non-GitHub repos currently 121 |
122 | ) : ( 123 |
124 | {showTrendGraph ? ( 125 | 135 | ) : ( 136 | 147 | )} 148 |
149 | )} 150 |
151 |
152 |
153 | ); 154 | }; 155 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.1.4](https://github.com/liatrio/backstage-dora-plugin/compare/v1.1.3...v1.1.4) (2025-05-29) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * updating react-dora-charts dependency to most recent, non-deprec… ([#63](https://github.com/liatrio/backstage-dora-plugin/issues/63)) ([4a836ee](https://github.com/liatrio/backstage-dora-plugin/commit/4a836ee7ed4a3edb8dc1f9cc4337db40bbf3b3ac)) 7 | 8 | ## [1.1.3](https://github.com/liatrio/backstage-dora-plugin/compare/v1.1.2...v1.1.3) (2025-04-03) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * update dora react charts version ([#60](https://github.com/liatrio/backstage-dora-plugin/issues/60)) ([37f7d71](https://github.com/liatrio/backstage-dora-plugin/commit/37f7d71bddda7d236700a8e33fd3be5ae3d433a1)) 14 | 15 | ## [1.1.2](https://github.com/liatrio/backstage-dora-plugin/compare/v1.1.1...v1.1.2) (2025-04-01) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * add fallback for service selection ([#59](https://github.com/liatrio/backstage-dora-plugin/issues/59)) ([acd6172](https://github.com/liatrio/backstage-dora-plugin/commit/acd6172079454e0fa34cde33afd4a91cd178c708)) 21 | 22 | ## [1.1.1](https://github.com/liatrio/backstage-dora-plugin/compare/v1.1.0...v1.1.1) (2025-04-01) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * update config and readme ([#58](https://github.com/liatrio/backstage-dora-plugin/issues/58)) ([094fd47](https://github.com/liatrio/backstage-dora-plugin/commit/094fd47342672407fec7d0f54f9a8aa2cd7ad471)) 28 | 29 | ## [1.1.0](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.9...v1.1.0) (2025-04-01) 30 | 31 | 32 | ### Features 33 | 34 | * change team to service ([#56](https://github.com/liatrio/backstage-dora-plugin/issues/56)) ([dcb7c4a](https://github.com/liatrio/backstage-dora-plugin/commit/dcb7c4ade419698b86678f774fb106655bbd556c)) 35 | 36 | ## [1.0.9](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.8...v1.0.9) (2025-02-06) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **deps:** update dependency @liatrio/react-dora-charts to v1.1.10 ([#47](https://github.com/liatrio/backstage-dora-plugin/issues/47)) ([d7d4fc5](https://github.com/liatrio/backstage-dora-plugin/commit/d7d4fc5bbfc533ddd18589e4fdbd3bea722bb7eb)) 42 | 43 | ## [1.0.8](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.7...v1.0.8) (2024-09-20) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * address infinite rerender bug ([#27](https://github.com/liatrio/backstage-dora-plugin/issues/27)) ([659ffb4](https://github.com/liatrio/backstage-dora-plugin/commit/659ffb43183fed5eba7d15ed18d5b9792992facb)) 49 | 50 | ## [1.0.7](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.6...v1.0.7) (2024-09-17) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * tweaks to the readme ([#25](https://github.com/liatrio/backstage-dora-plugin/issues/25)) ([06fbcce](https://github.com/liatrio/backstage-dora-plugin/commit/06fbcce638f0041fcd7086ad63d4261aa26c7044)) 56 | 57 | ## [1.0.6](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.5...v1.0.6) (2024-09-17) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * add husky pre-commit ([#22](https://github.com/liatrio/backstage-dora-plugin/issues/22)) ([389235b](https://github.com/liatrio/backstage-dora-plugin/commit/389235bfb8e2f13cb5a36b2ab39288ac50846410)) 63 | 64 | ## [1.0.5](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.4...v1.0.5) (2024-09-16) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * added LICENSE, build, and language badge to repo ([#24](https://github.com/liatrio/backstage-dora-plugin/issues/24)) ([69aafc6](https://github.com/liatrio/backstage-dora-plugin/commit/69aafc6bdc6bc173b2669cec956ef38b551acbef)) 70 | 71 | ## [1.0.4](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.3...v1.0.4) (2024-09-16) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * updated references in the readme ([#21](https://github.com/liatrio/backstage-dora-plugin/issues/21)) ([f966f4e](https://github.com/liatrio/backstage-dora-plugin/commit/f966f4eebb1b9a4a0791c00e1f919ff517a61c9e)) 77 | 78 | ## [1.0.3](https://github.com/liatrio/backstage-dora-plugin/compare/v1.0.2...v1.0.3) (2024-09-16) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * add code validation to workflows ([#18](https://github.com/liatrio/backstage-dora-plugin/issues/18)) ([252edba](https://github.com/liatrio/backstage-dora-plugin/commit/252edba3f3608aca2a53304b522266df3ccda85e)) 84 | 85 | ## v1.0.2 (2024-09-12) 86 | 87 | ### Fix 88 | 89 | * fix: address styling bugs (#19) ([`7bc5b9b`](https://github.com/liatrio/backstage-dora-plugin/commit/7bc5b9b3c8696df27bc92662a42120ccd0170372)) 90 | 91 | ## v1.0.1 (2024-09-11) 92 | 93 | ### Chore 94 | 95 | * chore: add issue templates (#12) ([`e2ae0c0`](https://github.com/liatrio/backstage-dora-plugin/commit/e2ae0c09a438643fc1a84c886ff749cdb7e62984)) 96 | 97 | * chore: Update Dependencies (#9) 98 | 99 | * Create .env 100 | 101 | * chore: update readme ([`77cd7d7`](https://github.com/liatrio/backstage-dora-plugin/commit/77cd7d7ccecc78a444c2f3e029c1dbe10859de2b)) 102 | 103 | ### Fix 104 | 105 | * fix: update project to use npm project version (#16) ([`b5a1c9c`](https://github.com/liatrio/backstage-dora-plugin/commit/b5a1c9cfe44ea0165c0e54515d0622d0005693fa)) 106 | 107 | ### Unknown 108 | 109 | * Add a link to a DORA guide about the metrics (#11) 110 | 111 | Signed-off-by: Nathen Harvey <nathen.harvey@gmail.com> ([`a3b08c3`](https://github.com/liatrio/backstage-dora-plugin/commit/a3b08c34c2230ed657c9653a589bd541a579d6bb)) 112 | 113 | * Update README.md (#15) 114 | 115 | Small spelling fix for the readme ([`316e449`](https://github.com/liatrio/backstage-dora-plugin/commit/316e449ce62c16d9169a8b0abf795da08a0e2eb2)) 116 | 117 | * It's "Backstage", not "BackStage" (#13) ([`abfc914`](https://github.com/liatrio/backstage-dora-plugin/commit/abfc914135b9699dc21845a7f2129c9111ceb514)) 118 | 119 | * Update README.md (#10) ([`ae51aca`](https://github.com/liatrio/backstage-dora-plugin/commit/ae51aca02941c8c36d885b0f6173ae767ccde55f)) 120 | 121 | ## v1.0.0 (2024-08-28) 122 | 123 | ### Breaking 124 | 125 | * feat!: Initial Commit ([`741a1c0`](https://github.com/liatrio/backstage-dora-plugin/commit/741a1c06f0bafc5a3c873d2b6cdb15888473cef9)) 126 | 127 | ### Unknown 128 | 129 | * Initial commit ([`4ab6624`](https://github.com/liatrio/backstage-dora-plugin/commit/4ab6624fd2eb2d121d023fa8ca42ee4110df7b04)) 130 | -------------------------------------------------------------------------------- /dependencies/otelcol-config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extensions: 3 | health_check: 4 | 5 | pprof: 6 | endpoint: 0.0.0.0:1777 7 | 8 | zpages: 9 | endpoint: 0.0.0.0:55679 10 | 11 | receivers: 12 | ## Webhookevent receiver is used to connect to a GitHub App and receive json event logs 13 | ## The processors are used to extract/filter all the meaningful data from those logs 14 | webhookevent: 15 | endpoint: 0.0.0.0:8088 16 | path: /events 17 | health_path: /healthcheck 18 | 19 | processors: 20 | memory_limiter: 21 | check_interval: 1s 22 | limit_percentage: 75 23 | spike_limit_percentage: 15 24 | 25 | batch: 26 | send_batch_size: 100 27 | timeout: 10s 28 | 29 | transform/timestamp: 30 | error_mode: propagate 31 | log_statements: 32 | - context: log 33 | statements: 34 | - set(time_unix_nano, UnixNano(Time(attributes["created_at"], "%FT%TZ"))) where attributes["created_at"] != nil 35 | 36 | transform/team_name: 37 | log_statements: 38 | - context: log 39 | statements: 40 | - set(attributes["team_name"], body["repository"]["custom_properties"]["team_name"]) where body["repository"]["custom_properties"]["team_name"] != nil 41 | 42 | ############################################ 43 | # GitHub Issue Events 44 | ############################################ 45 | transform/body: 46 | log_statements: 47 | - context: log 48 | statements: 49 | - set(body, ParseJSON(body)) where body != nil 50 | 51 | transform/issue: 52 | log_statements: 53 | - context: log 54 | statements: 55 | - keep_keys(body, ["issue", "action", "resources", "instrumentation_scope", "repository"]) 56 | - keep_keys(body["repository"], ["name", "full_name", "owner", "topics", "custom_properties"]) where body["repository"] != nil 57 | - keep_keys(body["repository"]["owner"], ["login"]) where body["repository"]["owner"] != nil 58 | - keep_keys(body["issue"], ["created_at", "closed_at", "labels", "number", "repository_url", "state"]) where body["issue"] != nil 59 | - set(attributes["repository_name"], body["repository"]["name"]) where body["repository"]["name"] != nil 60 | - set(attributes["repository_owner"], body["repository"]["owner"]["login"]) where body["repository"]["owner"]["login"] != nil 61 | - set(attributes["action"], body["action"]) where body["action"] != nil 62 | - set(attributes["created_at"], body["issue"]["created_at"]) where body["issue"]["created_at"] != nil 63 | - set(attributes["closed_at"], body["issue"]["closed_at"]) where body["issue"]["closed_at"] != nil 64 | - set(attributes["topics"], body["repository"]["topics"]) where body["repository"]["topics"] != nil 65 | - set(attributes["environment_name"], body["deployment"]["environment"]) where body["deployment"]["environment"] != nil 66 | 67 | transform/issue-closed: 68 | log_statements: 69 | - context: log 70 | statements: 71 | - set(attributes["created_at"], attributes["closed_at"]) where attributes["closed_at"] != nil 72 | 73 | attributes/issues: 74 | actions: 75 | - action: upsert 76 | key: loki.attribute.labels 77 | value: action, issue_labels, topics, team_name, repository_name 78 | 79 | filter/issues: 80 | error_mode: ignore 81 | logs: 82 | log_record: 83 | - 'not IsMatch(body["issue"], ".*")' 84 | - 'IsMatch(body["action"], "closed")' 85 | 86 | filter/issues-closed: 87 | error_mode: ignore 88 | logs: 89 | log_record: 90 | - 'not IsMatch(body["issue"], ".*")' 91 | - 'not IsMatch(body["action"], "closed")' 92 | 93 | ############################################ 94 | # GitHub Action Deployment Events 95 | ############################################ 96 | transform/deployments: 97 | log_statements: 98 | - context: log 99 | statements: 100 | - keep_keys(body, ["deployment", "deployment_status", "workflow", "workflow_run", "repository"]) 101 | - keep_keys(body["deployment"], ["url", "id", "task", "environment", "created_at", "updated_at", "sha", "ref"]) where body["deployment"] != nil 102 | - keep_keys(body["deployment_status"], ["state", "url", "environment", "created_at"]) where body["deployment_status"] != nil 103 | - keep_keys(body["workflow"], ["name", "path", "url"]) where body["workflow"] != nil 104 | - keep_keys(body["workflow_run"], ["head_branch", "head_sha", "display_title", "run_number", "status", "workflow_id", "url", "id", "html_url"]) where body["workflow_run"] != nil 105 | - keep_keys(body["repository"], ["name", "full_name", "owner", "topics", "custom_properties"]) where body["repository"] != nil 106 | - keep_keys(body["repository"]["owner"], ["login"]) where body["repository"]["owner"] != nil 107 | - set(attributes["repository_name"], body["repository"]["name"]) where body["repository"]["name"] != nil 108 | - set(attributes["repository_owner"], body["repository"]["owner"]["login"]) where body["repository"]["owner"]["login"] != nil 109 | - set(attributes["topics"], body["repository"]["topics"]) where body["repository"]["topics"] != nil 110 | - set(attributes["created_at"], body["deployment_status"]["created_at"]) where body["deployment_status"]["created_at"] != nil 111 | - set(attributes["deployment_state"], body["deployment_status"]["state"]) where body["deployment_status"]["state"] != nil 112 | - set(attributes["deployment_environment"], body["deployment"]["environment"]) where body["deployment"]["environment"] != nil 113 | - set(attributes["environment_name"], body["deployment"]["environment"]) where body["deployment"]["environment"] != nil 114 | - set(body["deployment"]["created_at"], body["deployment_status"]["created_at"]) where body["deployment_status"]["created_at"] != nil 115 | 116 | attributes/deployments: 117 | actions: 118 | - action: upsert 119 | key: loki.attribute.labels 120 | value: deployment_state, deployment_environment, topics, team_name, repository_name, environment_name 121 | 122 | filter/deployments: 123 | error_mode: ignore 124 | logs: 125 | log_record: 126 | - 'not IsMatch(body["deployment"], ".*")' 127 | 128 | ############################################ 129 | # GitHub Pull Request Events 130 | ############################################ 131 | 132 | transform/pull_requests: 133 | log_statements: 134 | - context: log 135 | statements: 136 | - keep_keys(body, ["action", "pull_request", "number", "resources", "instrumentation_scope", "repository"]) 137 | - keep_keys(body["pull_request"], ["title", "user", "created_at", "merged_at", "merged", "merge_commit_sha"]) where body["pull_request"] != nil 138 | - keep_keys(body["pull_request"]["user"], ["login"]) where body["pull_request"]["user"] != nil 139 | - keep_keys(body["repository"], ["name", "full_name", "owner", "topics", "custom_properties"]) where body["repository"] != nil 140 | - set(attributes["repository_name"], body["repository"]["name"]) where body["repository"]["name"] != nil 141 | - set(attributes["action"], body["action"]) where body["action"] != nil 142 | - set(attributes["created_at"], body["pull_request"]["merged_at"]) where body["pull_request"]["merged_at"] != nil 143 | - set(attributes["merged_at"], body["pull_request"]["merged_at"]) where body["pull_request"]["merged_at"] != nil 144 | - set(attributes["merge_sha"], body["pull_request"]["merge_commit_sha"]) where body["pull_request"]["merge_commit_sha"] != nil 145 | - set(attributes["topics"], body["repository"]["topics"]) where body["repository"]["topics"] != nil 146 | - set(attributes["environment_name"], body["deployment"]["environment"]) where body["deployment"]["environment"] != nil 147 | - set(body["pull_request"]["created_at"], body["deployment_status"]["merged_at"]) where body["deployment_status"]["merged_at"] != nil 148 | 149 | filter/pull_requests: 150 | error_mode: ignore 151 | logs: 152 | log_record: 153 | - 'not IsMatch(body["pull_request"], ".*")' 154 | 155 | attributes/pull_requests: 156 | actions: 157 | - action: upsert 158 | key: loki.attribute.labels 159 | # value: action, created_at, merged_at, merge_sha, topics, team_name, repository_name 160 | value: action, merged_at, topics, repository_name, team_name 161 | 162 | exporters: 163 | debug: 164 | verbosity: detailed 165 | sampling_initial: 2 166 | sampling_thereafter: 500 167 | 168 | otlphttp: 169 | endpoint: http://loki:3100/otlp 170 | tls: 171 | insecure: true 172 | 173 | loki: 174 | endpoint: 'http://loki:3100/loki/api/v1/push' 175 | 176 | service: 177 | telemetry: 178 | logs: 179 | level: debug 180 | 181 | extensions: 182 | - health_check 183 | - pprof 184 | - zpages 185 | 186 | pipelines: 187 | logs/issues: 188 | receivers: 189 | - webhookevent 190 | processors: 191 | - transform/body 192 | - filter/issues 193 | - transform/issue 194 | - transform/team_name 195 | - attributes/issues 196 | - transform/timestamp 197 | exporters: 198 | - debug 199 | - otlphttp 200 | - loki 201 | 202 | logs/issues-closed: 203 | receivers: 204 | - webhookevent 205 | processors: 206 | - transform/body 207 | - filter/issues-closed 208 | - transform/issue 209 | - transform/issue-closed 210 | - transform/team_name 211 | - attributes/issues 212 | - transform/timestamp 213 | exporters: 214 | - debug 215 | - otlphttp 216 | - loki 217 | 218 | logs/gha-deployments: 219 | receivers: 220 | - webhookevent 221 | processors: 222 | - transform/body 223 | - filter/deployments 224 | - transform/deployments 225 | - transform/team_name 226 | - attributes/deployments 227 | - transform/timestamp 228 | exporters: 229 | - debug 230 | - otlphttp 231 | - loki 232 | 233 | logs/pull-requests: 234 | receivers: 235 | - webhookevent 236 | processors: 237 | - transform/body 238 | - filter/pull_requests 239 | - transform/pull_requests 240 | - transform/team_name 241 | - attributes/pull_requests 242 | - transform/timestamp 243 | exporters: 244 | - debug 245 | - otlphttp 246 | - loki 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backstage DORA Plugin 2 | 3 | [![CodeQL](https://github.com/liatrio/backstage-liatrio-dora-plugin/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/liatrio/backstage-liatrio-dora-plugin/actions/workflows/codeql.yml) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Release](https://github.com/liatrio/backstage-dora-plugin/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/liatrio/backstage-dora-plugin/actions/workflows/release.yml) ![GitHub top language](https://img.shields.io/github/languages/top/liatrio/backstage-dora-plugin) 4 | 5 | This is a plugin for the [Backstage](https://backstage.io/) Project that provides a seamless way to display [DORA Metrics](https://dora.dev/guides/dora-metrics-four-keys/) in your developer portals. 6 | 7 | Our goal is to provide an Open Source plugin that works with the Open Telemetry backend collecting your DORA metrics in a non-opinionated manner. 8 | 9 | 10 | 11 | **This plugin is currently loosely tied to GitHub and Loki DB, we plan to expand to GitLab and other platforms in the future** 12 | 13 | 14 | ## Plugin Architecture 15 | 16 | ![Dora Backstage Plugin Architecture](./screenshots/dora-backstage-plugin-architecture.drawio.png) 17 | 18 | ### Links to Modules 19 | 20 | - [liatrio-otel-collector](https://github.com/liatrio/liatrio-otel-collector) 21 | - [liatrio-dora-api](https://github.com/liatrio/liatrio-dora-api) 22 | - [backstage-dora-plugin](https://github.com/liatrio/backstage-dora-plugin) 23 | - [react-dora-charts](https://github.com/liatrio/react-dora-charts) 24 | 25 | ## Components 26 | 27 | ### `At A Glance` 28 | 29 | This offers you a quick view of the state of a component or service. 30 | 31 | Depending on how you have set up your configuration for this plugin, it will show: 32 | 33 | - The individual DORA Metrics for the last 30 days 34 | - `Deployment Frequency`- The average how often you are deploying, failed or successful 35 | - Weekends, unless included, and holidays, if set, are subtracted from timespans that go over them 36 | - `Change Lead Time` - The average of time merged to `main` to deployment success 37 | - Weekends, unless included, and holidays, if set, are subtracted from timespans that go over them 38 | - `Change Failure Rate` - The average number of changes that result in a failed deployment 39 | - `Recovery Time` - The average of a failed deployment to the next successful deployment 40 | - Regardless of configuration, includes weekends and does not subtract the holidays if set. 41 | - The DORA Metrics overall trend over the last 30 days 42 | - The Trend is calculated on a per-week basis 43 | - If a component has gone stale or is too new, you will see a note about there not being enough data to render a trend. 44 | - There is an option to also show each DORA Metric as a line on the graph, a legend will appear in this case 45 | 46 | Here are some examples: 47 | 48 | | Metric View with Details Always Showing | Metric View with Details on Hover | 49 | | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | 50 | | ![Metrics](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/ranked/atAGlance.png 'Metrics') | ![Metrics](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/ranked/atAGlance_hover.png?raw=true 'Metrics') | 51 | 52 | | Overall Trend View | Overall Trend View with Individual Metric Trends | 53 | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | 54 | | ![Trend](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/trend/atAGlance.png?raw=true 'Trend') | ![Trend](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/trend/atAGlanceIndividual.png?raw=true 'Trend') | 55 | 56 | ### `Charts` 57 | 58 | This is a set of charts that for the DORA metrics. 59 | 60 | It has two different modes `Service View` and `Component View`: 61 | 62 | - `Component View` - You will see this when you access a specific Component in the Catalog. 63 | - `Service View` - You will see this when you access the `DORA Metrics` sidebar navigation. 64 | 65 | Here are some examples: 66 | 67 | | Component View | Service View | 68 | | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | 69 | | ![Metrics](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/ranked/tab.png?raw=true 'Metrics') | ![Trend](https://raw.githubusercontent.com/liatrio/backstage-dora-plugin/main/screenshots/trend/teamView.png?raw=true 'Trend') | 70 | 71 | ## Dependencies 72 | 73 | This plugin relies on the following dependencies: 74 | 75 | - [Liatrio DORA React Components](https://github.com/liatrio/react-dora-charts) 76 | - [Liatrio OTel Collector](https://github.com/liatrio/liatrio-otel-collector) 77 | - An instance of Loki DB 78 | - **You can swap out for any Time Series DB, but you will need to fork and modify the [Liatrio DORA API](https://github.com/liatrio/liatrio-dora-api) to do so** 79 | - A GitHub Organization hosting your repositories 80 | - **We will expand this to more platforms in the future** 81 | 82 | ## Installation of Dependencies 83 | 84 | ### Docker Compose 85 | 86 | In the `dependencies` folder, you will find a docker-compose file. Using this will spin up the following in docker containers: 87 | 88 | - An instance of Loki DB with persistent storage 89 | - An instance of Promtail, which is required by Loki DB 90 | - An instance of an OTel Collector configured to accept events from GitHub 91 | - An instance of the [Liatrio DORA API](https://github.com/liatrio/liatrio-dora-api), which this plugin can call to get the data it needs 92 | 93 | You will need to update the `.env` file with your `GitHub Org`, `User` and `PAT` (with full repo access) for the API to be able to return a list of services 94 | 95 | ### Kubernetes 96 | 97 | If you have a Kubernetes Cluster, we have a quick start guide that installs `Loki DB` and [Liatrio OTel Collector](https://github.com/liatrio/liatrio-otel-collector) (among a few other tools) that can be found [here](https://github.com/liatrio/tag-o11y-quick-start-manifests) 98 | 99 | This quick start manifest does not set up the [Liatrio DORA API](https://github.com/liatrio/liatrio-dora-api) or any other API which you would need to sit between this plugin and the Loki DB. 100 | 101 | ### Configuring GitHub 102 | 103 | Once you have the dependencies configured and running, you will need to update your GitHub Organization to send events to the OTel Collector. 104 | 105 | You can do this by setting up a new `Webhook` and configuring the `Webhook` to send the following events: 106 | 107 | - Deployments 108 | - Issues 109 | - Pull Requests 110 | - Deployment Statuses 111 | 112 | ## Installation into Backstage 113 | 114 | To Install this plugin you'll need to do the following: 115 | 116 | 1. Install the `@liatrio/backstage-dora-plugin` package into the `/packages/app` folder 117 | 118 | ```shell 119 | npm install @liatrio/backstage-dora-plugin 120 | 121 | yarn add @liatrio/backstage-dora-plugin 122 | ``` 123 | 124 | 2. Update the `/packages/app/src/App.tsx` file: 125 | 126 | - Add this to your imports: 127 | 128 | ```typescript 129 | import { EntityDORACharts } from '@liatrio/backstage-dora-plugin'; 130 | ``` 131 | 132 | - Add this into the `FlatRoutes` element as a child: 133 | 134 | ```typescript 135 | } /> 136 | ``` 137 | 138 | 3. Update the `/packages/app/src/components/catalog/EntityPage.tsx` file: 139 | 140 | - Add this to your imports: 141 | 142 | ```typescript 143 | import { 144 | EntityDORACharts, 145 | EntityDORAAtAGlance, 146 | } from '@liatrio/backstage-dora-plugin'; 147 | ``` 148 | 149 | - Define this constant: 150 | 151 | ```typescript 152 | const doraContent = ( 153 | 154 | {entityWarningContent} 155 | 156 | 157 | ); 158 | ``` 159 | 160 | - Add this into the `serviceEntityPage`, `websiteEntityPage`, `defaultEntityPage` `EntityLayoutWrapper` elements: 161 | 162 | ```typescript 163 | 164 | {doraContent} 165 | 166 | ``` 167 | 168 | - Add this into the `overviewContent` `Grid`: 169 | 170 | ```typescript 171 | 172 | 173 | 174 | ``` 175 | 176 | 4. Update the `app-config.yaml`: 177 | 178 | - Add this to the `proxy.endpoints` and use the correct URL for your API: 179 | 180 | ```yaml 181 | /dora/api: 182 | target: [URL_TO_DORA_API] 183 | ``` 184 | 185 | - Add this root property `dora` to the file and then add the following under it: 186 | 187 | - Required: 188 | 189 | - `dataEndpoint`: This the endpoint on the proxy that provides the deployment data. If you are using the `liatrio-dora-api` this will be `data` 190 | - `serviceListEndpoint`: This the endpoint on the proxy that provides the service and repo ownership data. If you are using the `liatrio-dora-api` this will be `services` 191 | - `daysToFetch`: This is the number of days worth of data that will be fetched for the charts to have available for display 192 | 193 | - Optional: 194 | 195 | - `showWeekends`: This boolean will toggle the `Deployment Frequency Chart` to hide weekends or show them. The default is to hide them. 196 | - `includeWeekends`: This boolean will toggle whether weekends are included in scoring your `Deployment Frequency` and `Change Lead Time`. The default is to exclude them. 197 | - `showDetails`: This boolean will toggle whether or not the `DORA At a Glance` shows the exact scores on hover or as static text. The default is to show them on hover. 198 | - `showTrendGraph`: Enabling this field will change the `DORA At a Glance` to be a Trend Graph rather than have Metric indicators 199 | - `showIndividualTrends`: Enabling this field will add individual Metric Trends to the Trend Graph in the `DORA At a Glance` component 200 | - `rankThresholds`: This is an object to override the default rank thresholds for DORA Score Board and is fully optional all the way down to the individual ranks. 201 | 202 | There are 4 scores, all are optional: 203 | 204 | - `deployment_frequency` measured in hours 205 | - `change_lead_time` measured in hours 206 | - `change_failure_rate` measured as a percentage 207 | - `recover_time` measured in hours 208 | 209 | Each score has the following rank options: 210 | 211 | - `elite` 212 | - `high` 213 | - `medium` 214 | 215 | **Note: Anything outside `medium` is considered `low`** 216 | 217 | The default rank thresholds are: 218 | 219 | - deployment_frequency 220 | - elite: 24 (1 day or less) 221 | - high: 168 (1 week or less) 222 | - medium: 720 (1 month or less) 223 | - change_lead_time 224 | - elite: 24 (1 day or less) 225 | - high: 168 (1 week or less) 226 | - medium: 720 (1 month or less) 227 | - change_failure_rate 228 | - elite: 5 229 | - high: 10 230 | - medium: 45 231 | - recover_time 232 | - elite: 1 (1 hr or less) 233 | - high: 24 (1 day or less) 234 | - medium: 168 (1 week or less) 235 | 236 | ## Contributing 237 | 238 | See [Contributing](./CONTRIBUTING) to Backstage Dora Plugin 239 | -------------------------------------------------------------------------------- /src/components/Charts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useTheme } from '@mui/material/styles'; 3 | import { InfoCard } from '@backstage/core-components'; 4 | import { Box, Grid } from '@material-ui/core'; 5 | import { Tooltip } from 'react-tooltip'; 6 | import { 7 | RecoverTimeGraph, 8 | ChangeFailureRateGraph, 9 | ChangeLeadTimeGraph, 10 | DeploymentFrequencyGraph, 11 | Board, 12 | TrendGraph, 13 | fetchData, 14 | getDateDaysInPast, 15 | buildDoraStateForPeriod, 16 | MetricThresholdSet, 17 | DoraState, 18 | getDateDaysInPastUtc, 19 | DoraRecord, 20 | Theme, 21 | } from '@liatrio/react-dora-charts'; 22 | import { useEntity } from '@backstage/plugin-catalog-react'; 23 | import { useApi, configApiRef } from '@backstage/core-plugin-api'; 24 | import { 25 | COLOR_DARK, 26 | COLOR_LIGHT, 27 | useAuthHeaderValueLookup, 28 | getRepositoryName, 29 | } from '../helper'; 30 | import { makeStyles } from '@material-ui/core/styles'; 31 | import DatePicker from 'react-datepicker'; 32 | import 'react-datepicker/dist/react-datepicker.css'; 33 | import { ChartTitle } from './ChartTitle'; 34 | import Dropdown from 'react-dropdown'; 35 | import 'react-dropdown/style.css'; 36 | 37 | const useStyles = makeStyles(theme => ({ 38 | doraCalendar: { 39 | '& .react-datepicker__header': { 40 | backgroundColor: theme.palette.background.default, 41 | }, 42 | '& .react-datepicker__month-container': { 43 | backgroundColor: theme.palette.background.default, 44 | }, 45 | '& .react-datepicker__current-month': { 46 | color: theme.palette.text.primary, 47 | }, 48 | '& .react-datepicker__day': { 49 | backgroundColor: theme.palette.background.default, 50 | color: theme.palette.text.primary, 51 | '&:hover': { 52 | backgroundColor: 'rgb(92, 92, 92)', 53 | }, 54 | }, 55 | '& .react-datepicker__day-name': { 56 | color: theme.palette.text.primary, 57 | }, 58 | '& .react-datepicker__day--in-range': { 59 | backgroundColor: 'green', 60 | '&:hover': { 61 | backgroundColor: 'rgb(0, 161, 0)', 62 | }, 63 | }, 64 | '& .react-datepicker__input-container input': { 65 | backgroundColor: theme.palette.background.default, 66 | color: theme.palette.text.primary, 67 | padding: '10px', 68 | }, 69 | '& .react-datepicker': { 70 | borderWidth: '2px', 71 | }, 72 | }, 73 | doraContainer: { 74 | '& .doraCard > :first-child': { 75 | padding: '6px 16px 6px 20px', 76 | }, 77 | '& .doraGrid': { 78 | paddingBottom: '0px', 79 | }, 80 | '& .Dropdown-root': { 81 | width: '50%', 82 | }, 83 | '& .Dropdown-control': { 84 | backgroundColor: theme.palette.background.default, 85 | color: theme.palette.text.primary, 86 | }, 87 | '& .Dropdown-option is-selected': { 88 | backgroundColor: 'green', 89 | color: theme.palette.text.primary, 90 | }, 91 | '& .Dropdown-option': { 92 | backgroundColor: theme.palette.background.default, 93 | color: theme.palette.text.primary, 94 | }, 95 | '& .Dropdown-option:hover': { 96 | backgroundColor: 'green', 97 | color: theme.palette.text.primary, 98 | }, 99 | '& .Dropdown-menu': { 100 | backgroundColor: theme.palette.background.default, 101 | }, 102 | '& .doraOptions': { 103 | overflow: 'visible', 104 | }, 105 | }, 106 | pageView: { 107 | padding: '10px', 108 | }, 109 | })); 110 | 111 | export interface ChartProps { 112 | showServiceSelection?: boolean; 113 | } 114 | 115 | const defaultMetric = { 116 | average: 0, 117 | display: '', 118 | color: '', 119 | trend: 0, 120 | rank: 0, 121 | }; 122 | 123 | const defaultMetrics: DoraState = { 124 | deploymentFrequency: defaultMetric, 125 | changeLeadTime: defaultMetric, 126 | changeFailureRate: defaultMetric, 127 | recoverTime: defaultMetric, 128 | }; 129 | 130 | export const Charts = (props: ChartProps) => { 131 | // Always call useEntity unconditionally 132 | const entityContext = useEntity(); 133 | // Then conditionally use the result 134 | const entity = props.showServiceSelection ? null : entityContext; 135 | const configApi = useApi(configApiRef); 136 | const backendUrl = configApi.getString('backend.baseUrl'); 137 | const dataEndpoint = configApi.getString('dora.dataEndpoint'); 138 | const serviceListEndpoint = configApi.getString('dora.serviceListEndpoint'); 139 | const includeWeekends = configApi.getOptionalBoolean('dora.includeWeekends'); 140 | const showDetails = configApi.getOptionalBoolean('dora.showDetails'); 141 | const servicesList = configApi.getOptional('dora.services') as string[]; 142 | const showTrendGraph = configApi.getOptionalBoolean('dora.showTrendGraph'); 143 | const showIndividualTrends = configApi.getOptionalBoolean( 144 | 'dora.showIndividualTrends', 145 | ); 146 | const daysToFetch = configApi.getNumber('dora.daysToFetch'); 147 | const rankThresholds = configApi.getOptional( 148 | 'dora.rankThresholds', 149 | ) as MetricThresholdSet; 150 | 151 | const getAuthHeaderValue = useAuthHeaderValueLookup(); 152 | 153 | const apiUrl = `${backendUrl}/api/proxy/dora/api/${dataEndpoint}`; 154 | const serviceListUrl = `${backendUrl}/api/proxy/dora/api/${serviceListEndpoint}`; 155 | 156 | const [serviceIndex, setServiceIndex] = useState(0); 157 | const [services, setServices] = useState([ 158 | { 159 | value: '', 160 | label: 'Please Select', 161 | }, 162 | ]); 163 | const [repository, setRepository] = useState(''); 164 | const [data, setData] = useState([]); 165 | const [startDate, setStartDate] = useState(getDateDaysInPast(30)); 166 | const [endDate, setEndDate] = useState(getDateDaysInPast(0)); 167 | const [calendarStartDate, setCalendarStartDate] = useState( 168 | getDateDaysInPast(30), 169 | ); 170 | const [calendarEndDate, setCalendarEndDate] = useState( 171 | getDateDaysInPast(0), 172 | ); 173 | const [loading, setLoading] = useState(true); 174 | const [metrics, setMetrics] = useState({ ...defaultMetrics }); 175 | const [message, setMessage] = useState(''); 176 | 177 | const classes = useStyles(); 178 | const backstageTheme = useTheme(); 179 | const theme = 180 | backstageTheme.palette.mode === 'dark' ? Theme.Dark : Theme.Light; 181 | 182 | const getMetrics = (respData: any) => { 183 | if (!respData || respData.length === 0) { 184 | setMetrics({ ...defaultMetrics }); 185 | return; 186 | } 187 | 188 | const metricsData = buildDoraStateForPeriod( 189 | { 190 | data: [], 191 | metricThresholdSet: rankThresholds, 192 | holidays: [], 193 | includeWeekendsInCalculations: includeWeekends, 194 | graphEnd: endDate, 195 | graphStart: startDate, 196 | }, 197 | respData, 198 | startDate, 199 | endDate, 200 | ); 201 | 202 | setMetrics(metricsData); 203 | }; 204 | 205 | const updateData = ( 206 | respData: any, 207 | start?: Date, 208 | end?: Date, 209 | msg?: string, 210 | ) => { 211 | if (!respData || respData.length < 1) { 212 | setData([]); 213 | setMetrics({ ...defaultMetrics }); 214 | setMessage(''); 215 | } else { 216 | setData(respData); 217 | } 218 | 219 | getMetrics(respData); 220 | 221 | if (msg !== undefined) { 222 | setMessage(msg); 223 | } 224 | 225 | if (start) { 226 | setStartDate(start); 227 | } 228 | 229 | if (end) { 230 | setEndDate(end); 231 | } 232 | }; 233 | 234 | const makeFetchOptions = (service?: string, repositories?: string[]) => { 235 | const fetchOptions: any = { 236 | api: apiUrl, 237 | getAuthHeaderValue: getAuthHeaderValue, 238 | start: getDateDaysInPast(daysToFetch), 239 | end: getDateDaysInPastUtc(0), 240 | }; 241 | 242 | if (!props.showServiceSelection) { 243 | fetchOptions.repositories = repositories!; 244 | } else { 245 | fetchOptions.service = service; 246 | } 247 | 248 | return fetchOptions; 249 | }; 250 | 251 | const fetchServicesData = async ( 252 | url: string, 253 | getAuthHeader: () => string | Promise, 254 | onSuccess: (data: any) => void, 255 | onError: (error: any) => void, 256 | ) => { 257 | try { 258 | const authHeader = await Promise.resolve(getAuthHeader()); 259 | const response = await fetch(url, { 260 | headers: { 261 | Authorization: authHeader || '', 262 | }, 263 | }); 264 | 265 | if (!response.ok) { 266 | throw new Error(`Error fetching services: ${response.statusText}`); 267 | } 268 | 269 | const responseData = await response.json(); 270 | onSuccess(responseData); 271 | } catch (error) { 272 | onError(error); 273 | } 274 | }; 275 | 276 | const callFetchData = async (idx: number, repo: string) => { 277 | const fetchOptions = makeFetchOptions(services[idx]?.value, [repo]); 278 | 279 | setLoading(true); 280 | 281 | await fetchData( 282 | fetchOptions, 283 | (respData: any) => { 284 | updateData(respData, undefined, undefined, ''); 285 | setLoading(false); 286 | }, 287 | _ => { 288 | setLoading(false); 289 | }, 290 | ); 291 | }; 292 | 293 | const updateService = async (value: any) => { 294 | const newIndex = services.findIndex( 295 | (range: { value: string; label: string }) => range.label === value.label, 296 | ); 297 | 298 | setServiceIndex(newIndex); 299 | 300 | if (!startDate || !endDate) { 301 | return; 302 | } 303 | 304 | if (newIndex === 0) { 305 | updateData(null, undefined, undefined, 'Please select a Service'); 306 | return; 307 | } 308 | 309 | setMessage(''); 310 | 311 | await callFetchData(newIndex, repository); 312 | }; 313 | 314 | const updateDateRange = async (dates: any) => { 315 | const [newStartDate, newEndDate] = dates; 316 | 317 | setCalendarStartDate(newStartDate); 318 | setCalendarEndDate(newEndDate); 319 | 320 | if ( 321 | !newStartDate || 322 | !newEndDate || 323 | (props.showServiceSelection && serviceIndex === 0) 324 | ) { 325 | return; 326 | } 327 | 328 | setStartDate(newStartDate); 329 | setCalendarEndDate(newEndDate); 330 | }; 331 | 332 | useEffect(() => { 333 | setLoading(true); 334 | 335 | let repositoryName = ''; 336 | 337 | if (!props.showServiceSelection) { 338 | repositoryName = getRepositoryName(entity); 339 | setRepository(repositoryName); 340 | 341 | if (!repositoryName) { 342 | setLoading(false); 343 | return; 344 | } 345 | } 346 | 347 | const fetch = props.showServiceSelection 348 | ? async () => { 349 | if (servicesList && servicesList.length > 0) { 350 | const serviceEntries = [ 351 | { 352 | value: '', 353 | label: 'Please Select', 354 | }, 355 | ]; 356 | 357 | for (const service of servicesList) { 358 | serviceEntries.push({ 359 | value: service, 360 | label: service, 361 | }); 362 | } 363 | 364 | setMessage('Please select a Service'); 365 | setLoading(false); 366 | setServices(serviceEntries); 367 | } else { 368 | fetchServicesData( 369 | serviceListUrl, 370 | getAuthHeaderValue, 371 | (services_data: any) => { 372 | const newList: any[] = [{ label: 'Please Select', value: '' }]; 373 | 374 | for (const entry of services_data.services) { 375 | const newEntry = { 376 | label: entry, 377 | value: entry, 378 | }; 379 | 380 | newList.push(newEntry); 381 | } 382 | 383 | setServices(newList); 384 | setLoading(false); 385 | }, 386 | _ => { 387 | setLoading(false); 388 | }, 389 | ); 390 | } 391 | } 392 | : async () => { 393 | callFetchData(serviceIndex, repositoryName); 394 | }; 395 | 396 | fetch(); 397 | // eslint-disable-next-line react-hooks/exhaustive-deps 398 | }, []); 399 | 400 | if (repository === '' && !props.showServiceSelection) { 401 | return ( 402 |
DORA Metrics are not available for Non-GitHub repos currently
403 | ); 404 | } 405 | 406 | const tTitle = ( 407 | 412 | ); 413 | const bTitle = ( 414 | 419 | ); 420 | const dfTitle = ( 421 | 428 | ); 429 | const cfrTitle = ( 430 | 437 | ); 438 | const cltTitle = ( 439 | 446 | ); 447 | const rtTitle = ( 448 | 455 | ); 456 | 457 | const containerClass = props.showServiceSelection 458 | ? `${classes.doraContainer} ${classes.pageView}` 459 | : classes.doraContainer; 460 | 461 | return ( 462 |
463 | 476 | 482 | 487 | 488 | 489 | 495 | 501 |
502 | 511 |
512 | {props.showServiceSelection && ( 513 |
521 | 522 | Select Service: 523 | 524 | 529 |
530 | )} 531 |
532 |
533 |
534 |
535 | 536 | 541 | 542 | 543 |
550 | {showTrendGraph ? ( 551 | 561 | ) : ( 562 | 573 | )} 574 |
575 |
576 |
577 |
578 |
579 |
580 | 586 | 587 | 588 | 589 | 590 |
591 | 600 |
601 |
602 |
603 |
604 |
605 | 606 | 607 | 608 | 609 |
610 | 619 |
620 |
621 |
622 |
623 |
624 | 625 | 626 | 627 | 628 |
629 | 638 |
639 |
640 |
641 |
642 |
643 | 644 | 645 | 646 | 647 |
648 | 657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 | ); 665 | }; 666 | --------------------------------------------------------------------------------